Monster commit
- New analysis functions - Scene rotation and scale done server side - PGSQL files are now launchable - New sql queries - Removed dependancy from ip
This commit is contained in:
parent
58f6caca40
commit
e5eacddc58
|
@ -55,8 +55,8 @@ if (!Array.prototype.reduce) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var pg = require('pg');
|
var pg = require('pg');
|
||||||
var pgc = require('../../private.js');
|
var pgc = require('../private.js');
|
||||||
var Log = require('../../lib/NodeLog.js');
|
var Log = require('../lib/NodeLog.js');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
function Vector3(x, y, z) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRecommendation(position, target) {
|
||||||
|
return {
|
||||||
|
position: position,
|
||||||
|
target: target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.createPeachRecommendations = function(width, height, rec) {
|
||||||
|
var recos = [];
|
||||||
|
|
||||||
|
recos.push(
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-4.318087280217455,2.8007613084859253,1.5193437897009336),
|
||||||
|
new Vector3(19.04561491034525,-11.893857635144567,-27.432436709124897)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-6.257935852456958,2.093463399444844,-7.017904350052701),
|
||||||
|
new Vector3(25.88235261144178,-14.928107421416371,-23.669270187358173)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(9.807915641060413,1.599662719763407,1.3278972044453563),
|
||||||
|
new Vector3(-16.404678696813406,-19.467671402046243,-20.330065097629618)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(8.593027849546461,2.341563400341173,-10.381814971692629),
|
||||||
|
new Vector3(-23.363783342561,-18.42997444113019,1.755130036517576)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(6.422879729593868,3.06821771913114,-4.664407524854438),
|
||||||
|
new Vector3(-15.171947266786782,-24.05662912371069,-24.6119921091785)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(10.155340138717236,6.631665534350463,-5.574670324070963),
|
||||||
|
new Vector3(-20.721131232754608,-9.966488352174423,-24.839789145555535)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-6.548087435820877,6.193523907010158,-3.627483164733988),
|
||||||
|
new Vector3(16.752484674681824,-11.466024392567634,-30.926268727065203)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return recos;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.createBobombRecommendations = function(width, height) {
|
||||||
|
var recos = [];
|
||||||
|
|
||||||
|
recos.push(
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(22.81974561274774,23.728166674516967,-23.50757340835654),
|
||||||
|
new Vector3(27.45807332015761,4.665400463440239,11.350666083340474)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(4.512241856806823,19.542184465749266,-21.6277607809511),
|
||||||
|
new Vector3(-16.322542559288507,6.552211144388629,9.95027512132075)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(3.7236872166568786,11.547542009941035,7.743737673292326),
|
||||||
|
new Vector3(11.778234958188895,3.590700880634021,46.107951987185814)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(17.51280189401515,22.651733665113007,32.1344270612909),
|
||||||
|
new Vector3(-17.09689080040822,6.202382514300329,20.663244981189692)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-12.00887621348721,25.979463024729398,37.05007506157123),
|
||||||
|
new Vector3(-6.018501236275041,9.054329353511584,1.3057712098552159)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-9.467050533255307,30.088761873923442,28.727671886170505),
|
||||||
|
new Vector3(-39.96888839418932,10.735797300746938,11.549178083317258)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-30.2051081707108,44.36298906887656,35.77746943907231),
|
||||||
|
new Vector3(-16.54652438711394,19.924260316887796,7.208401795672)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-52.44058113318328,27.688845222097196,28.78379753054363),
|
||||||
|
new Vector3(-21.760754138048632,11.37128676599093,8.972550684871294)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-32.51800140864256,30.21720398723899,-2.2695677339908484),
|
||||||
|
new Vector3(-4.161205509090522,12.002869652965245,-23.813247806588592)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-24.869080810307878,24.29489455015078,-48.36061039882109),
|
||||||
|
new Vector3(-16.792809571743753,4.99108388972596,-14.270483721620096)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(24.213548666073923,19.67561630411922,-34.50857509027397),
|
||||||
|
new Vector3(35.82557966946029,-3.7247748037464845,-4.21695195820471)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return recos;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.createWhompRecommendations = function(width, height) {
|
||||||
|
var recos = [];
|
||||||
|
|
||||||
|
recos.push(
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-9.183036772081453,3.0766349039394916,-10.631680881366988),
|
||||||
|
new Vector3(23.306020365359252,-17.647069934844886,0.09162197153512075)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-11.38099373489364,4.5301496570861906,-8.680448599715064),
|
||||||
|
new Vector3(14.218919789700848,-9.33335658285769,18.75033014002037)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-2.989815984700766,4.808626217924975,-10.034026966216151),
|
||||||
|
new Vector3(10.476586340125928,-16.676909597940817,20.90183828968142)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(8.739544533019469,4.57426117700506,-10.246457362075027),
|
||||||
|
new Vector3(-7.420839007222124,-3.599225856368915,25.419157921381895)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(11.215995865644405,5.100092599462174,5.157320142222007),
|
||||||
|
new Vector3(-17.739835597264776,-0.18398638725505378,-21.92843872759245)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-7.511384733151988,6.569117611729606,13.141669794236272),
|
||||||
|
new Vector3(11.160164249947218,-9.709441800002363,-18.26504544391685)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(0.6846182375474082,13.717750177060871,-3.878598405225172),
|
||||||
|
new Vector3(14.749877291524962,-2.4709024675402205,29.886709431324352)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-5.628153398727744,10.292624364958618,-0.15423059405658932),
|
||||||
|
new Vector3(21.830921092510273,-1.2953399806023977,26.523818630177338)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-3.2817952119549387,8.014848779391615,-6.822708271111021),
|
||||||
|
new Vector3(13.01307852868053,-12.339101451861252,23.511988031315184)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(7.805400745480024,9.185305503970957,11.919240783005307),
|
||||||
|
new Vector3(-9.777424733344784,-5.603738432878275,-20.8241314870455)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return recos;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.createMountainRecommendations = function(width, height) {
|
||||||
|
var recos = [];
|
||||||
|
|
||||||
|
recos.push(
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-32.55470573684094,29.55322138048939,-17.59574199842915),
|
||||||
|
new Vector3(-2.6530082773148784,13.825746134447998,3.8176886333992925)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(12.100158831224025,26.077021046580555,-23.46706423961512),
|
||||||
|
new Vector3(-13.67308964482135,11.574392013301521,3.4664356093669397)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(16.801072439731502,20.09189357317027,14.011145351254608),
|
||||||
|
new Vector3(-13.195470192683612,-4.443428210365667,4.1002717732066145)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-16.879597154353956,28.027328987174787,23.2120994633039),
|
||||||
|
new Vector3(-6.922498345966725,7.02598138495819,-9.342463691665415)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(24.007103291390404,-10.579535956547192,-30.14734612569218),
|
||||||
|
new Vector3(5.7117612503958135,-23.76440846717267,2.8895967789043198)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-12.257327932010769,-12.526038797341444,-36.05191812094985),
|
||||||
|
new Vector3(0.19983861525745894,-20.375474197075437,1.1395508675026633)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(16.426221516558684,4.064315972012067,-19.84262328062327),
|
||||||
|
new Vector3(-16.71831968665397,-6.887503610208118,-0.3106741646994493)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(44.96685545730114,-6.205815468014633,-0.5730193999373548),
|
||||||
|
new Vector3(7.154826082461277,-13.661034435943513,10.135395267812534)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-33.00196818869413,20.41721604790279,38.566026084656386),
|
||||||
|
new Vector3(-11.64931778228043,-1.846673249080439,13.102649364489118)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(-53.183958472088925,-8.39869666868559,28.102017801758063),
|
||||||
|
new Vector3(-15.679778341058253,-11.462793205152831,14.53559656716515)
|
||||||
|
),
|
||||||
|
createRecommendation(
|
||||||
|
new Vector3(27.528666741865862,-9.63536430265764,46.43021804402408),
|
||||||
|
new Vector3(1.1519844626168592,-18.896564555304533,17.820765028981576)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return recos;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/node
|
||||||
|
var lib = require('./lib.js');
|
||||||
|
var r = require('./initScene.js');
|
||||||
|
|
||||||
|
var reco = [
|
||||||
|
r.createPeachRecommendations(),
|
||||||
|
r.createBobombRecommendations(),
|
||||||
|
r.createMountainRecommendations(),
|
||||||
|
r.createWhompRecommendations()
|
||||||
|
];
|
||||||
|
|
||||||
|
function distanceBetweenPoints(pt1, pt2) {
|
||||||
|
return (
|
||||||
|
Math.sqrt(
|
||||||
|
(pt2.x - pt1.x) * (pt2.x - pt1.x) +
|
||||||
|
(pt2.y - pt1.y) * (pt2.y - pt1.y) +
|
||||||
|
(pt2.z - pt1.z) * (pt2.z - pt1.z)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(path) {
|
||||||
|
|
||||||
|
var db = lib.loadFromFile(path);
|
||||||
|
var groups = lib.makeGroups(db);
|
||||||
|
|
||||||
|
// Erase groups that are not usable
|
||||||
|
var invalid = 0;
|
||||||
|
groups = groups.filter(function(elt) {
|
||||||
|
|
||||||
|
// An elt is valid if it contains at least 2 exp, BaseRecommendation included
|
||||||
|
if (elt.length > 1 && elt.find(function(e) { return e.recommendation_style[4] === 'B'; }) !== undefined) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
invalid++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var percentSum = 0;
|
||||||
|
var eltNum = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < db.experiments.length; i++) {
|
||||||
|
|
||||||
|
var exp = db.experiments[i];
|
||||||
|
|
||||||
|
if (exp.coinCombination.scene_id === 1 || exp.recommendation_style[4] === 'B' || !exp.finished || lib.coinsGot(exp) < 6) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
eltNum++;
|
||||||
|
|
||||||
|
var distance = 0;
|
||||||
|
var distanceWithReco = 0;
|
||||||
|
|
||||||
|
|
||||||
|
var j = 0;
|
||||||
|
|
||||||
|
while (exp.elements.events[j].position === undefined) { j++; };
|
||||||
|
|
||||||
|
var startPosition = exp.elements.events[j].position;
|
||||||
|
j++;
|
||||||
|
|
||||||
|
while (j < exp.elements.events.length) {
|
||||||
|
|
||||||
|
var nextPosition, evt = exp.elements.events[j];
|
||||||
|
|
||||||
|
if (evt.position === undefined && evt.type !== 'arrow') { j++; continue; }
|
||||||
|
|
||||||
|
if (evt.type === 'arrow') {
|
||||||
|
nextPosition = reco[exp.coinCombination.scene_id - 1][evt.id].position;
|
||||||
|
} else {
|
||||||
|
nextPosition = evt.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp = distanceBetweenPoints(startPosition, nextPosition);
|
||||||
|
|
||||||
|
if (evt.type === 'arrow') {
|
||||||
|
distanceWithReco += tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
distance += tmp;
|
||||||
|
|
||||||
|
startPosition = nextPosition;
|
||||||
|
|
||||||
|
j++;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
percentSum += 100 * distanceWithReco / distance;
|
||||||
|
console.log(exp.id + ' -> ' + Math.floor(100 * distanceWithReco / distance) + '%');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Mean : ' + percentSum / eltNum);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.length !== 3) {
|
||||||
|
process.stderr.write('Error : please give me a JSON file to work on\n');
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main(process.argv[2])
|
|
@ -46,7 +46,17 @@ Lib.experimentDuration = function(exp) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastCoin === null) {
|
||||||
|
console.log(exp.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Lib.coinsGot(exp) === 8)
|
||||||
return Lib.timeDifference(exp.elements.events[0].time, lastCoin.time);
|
return Lib.timeDifference(exp.elements.events[0].time, lastCoin.time);
|
||||||
|
else
|
||||||
|
return Lib.timeDifference(
|
||||||
|
exp.elements.events[0].time,
|
||||||
|
exp.elements.events[exp.elements.events.length-1].time
|
||||||
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,6 +98,21 @@ Lib.timeToString = function(_time) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Lib.coinsGot = function(exp) {
|
||||||
|
var counter = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < exp.elements.events.length; i++) {
|
||||||
|
|
||||||
|
if (exp.elements.events[i].type === 'coin') {
|
||||||
|
|
||||||
|
counter ++;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
Lib.makeGroups = function(db) {
|
Lib.makeGroups = function(db) {
|
||||||
|
|
||||||
var elements = [];
|
var elements = [];
|
||||||
|
@ -96,8 +121,18 @@ Lib.makeGroups = function(db) {
|
||||||
|
|
||||||
if (db.experiments[i].coinCombination.scene_id !== 1 && db.experiments[i].elements.events.length !== 0) {
|
if (db.experiments[i].coinCombination.scene_id !== 1 && db.experiments[i].elements.events.length !== 0) {
|
||||||
|
|
||||||
|
if (db.experiments[i].finished === true && Lib.coinsGot(db.experiments[i]) > 5) {
|
||||||
|
|
||||||
|
if (db.experiments[i].user === undefined) {
|
||||||
|
db.experiments[i].user = {};
|
||||||
|
}
|
||||||
|
|
||||||
elements.push(db.experiments[i]);
|
elements.push(db.experiments[i]);
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -126,6 +161,19 @@ Lib.toMatlabArray = function(name, array) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Lib.toLaTeXCoordinate = function(name, array) {
|
||||||
|
var str = '\\addplot coordinates {';
|
||||||
|
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
|
||||||
|
str += '(' + i + ',' + array[i] + ') ';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return str + '};\n';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/3895478/
|
// http://stackoverflow.com/questions/3895478/
|
||||||
Lib.range = function(start, stop, step, computation) {
|
Lib.range = function(start, stop, step, computation) {
|
||||||
|
|
||||||
|
@ -150,7 +198,10 @@ Lib.range = function(start, stop, step, computation) {
|
||||||
|
|
||||||
var a = [];
|
var a = [];
|
||||||
while (start < stop) {
|
while (start < stop) {
|
||||||
a.push(computation(start));
|
var e = computation(start);
|
||||||
|
if (e === undefined)
|
||||||
|
e = NaN;
|
||||||
|
a.push(e);
|
||||||
start += step;
|
start += step;
|
||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
|
@ -198,3 +249,18 @@ Lib.durationBetweenCoins = function(exp) {
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Lib.toLaTeXMatrix = function(mat) {
|
||||||
|
var str = 'x,y,r\n';
|
||||||
|
|
||||||
|
for (var i = 0; i < mat.length; i++) {
|
||||||
|
|
||||||
|
for (var j = 0; j < mat[i].length; j++) {
|
||||||
|
str += i + ',' + j + ',' + mat[i][j] + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// str += i === mat.length - 1 ? '' : '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
|
@ -5,3 +5,5 @@ plot(X, Y2, 'red');
|
||||||
hold on;
|
hold on;
|
||||||
plot(X, Y3, 'green');
|
plot(X, Y3, 'green');
|
||||||
legend('Without recommendation', 'Worst with recommendation', 'Best with recommendation');
|
legend('Without recommendation', 'Worst with recommendation', 'Best with recommendation');
|
||||||
|
xlabel('Group id');
|
||||||
|
ylabel('Number of interaction received by the server');
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
timecoins;
|
time;
|
||||||
plot(X,Y1);
|
plot(X,Y1);
|
||||||
hold on;
|
hold on;
|
||||||
names = who('Y*');
|
plot(X, Y2, 'red');
|
||||||
|
hold on;
|
||||||
N = size(names, 1);
|
plot(X, Y3, 'green');
|
||||||
|
legend('Without recommendation', 'Worst with recommendation', 'Best with recommendation');
|
||||||
for i = 1:N,
|
xlabel('Group id');
|
||||||
name = names(i)
|
ylabel('Time to get the last coin');
|
||||||
name = name{1};
|
|
||||||
plot(X, eval(name));
|
|
||||||
pause
|
|
||||||
clf
|
|
||||||
end
|
|
||||||
|
|
||||||
close all;
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
timecoins;
|
||||||
|
plot(X,Y1);
|
||||||
|
hold on;
|
||||||
|
names = who('Y*');
|
||||||
|
|
||||||
|
N = size(names, 1);
|
||||||
|
|
||||||
|
for i = 1:N,
|
||||||
|
name = names(i)
|
||||||
|
name = name{1};
|
||||||
|
plot(X, eval(name));
|
||||||
|
pause
|
||||||
|
clf
|
||||||
|
end
|
||||||
|
|
||||||
|
close all;
|
|
@ -19,22 +19,76 @@ function main(path) {
|
||||||
|
|
||||||
groups.forEach(function(elt) {
|
groups.forEach(function(elt) {
|
||||||
elt.sort(lib.compareRecommendationStyle);
|
elt.sort(lib.compareRecommendationStyle);
|
||||||
|
|
||||||
|
if (elt.length === 2 && elt[1].recommendation_style[4] === 'V') {
|
||||||
|
elt[2] = elt[1];
|
||||||
|
elt[1] = undefined;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(lib.toMatlabArray('X', lib.range(0, groups.length)));
|
groups.sort(function(elt1, elt2) {
|
||||||
|
|
||||||
console.log(lib.toMatlabArray('Y1', lib.range(0, groups.length, function(i) {
|
el1 = [];
|
||||||
|
el2 = [];
|
||||||
|
|
||||||
|
if (elt1[0] === undefined) el1[0] = {user:{}}; else el1[0] = elt1[0];
|
||||||
|
if (elt1[1] === undefined) el1[1] = {user:{}}; else el1[1] = elt1[1];
|
||||||
|
if (elt1[2] === undefined) el1[2] = {user:{}}; else el1[2] = elt1[2];
|
||||||
|
|
||||||
|
if (elt2[0] === undefined) el2[0] = {user:{}}; else el2[0] = elt2[0];
|
||||||
|
if (elt2[1] === undefined) el2[1] = {user:{}}; else el2[1] = elt2[1];
|
||||||
|
if (elt2[2] === undefined) el2[2] = {user:{}}; else el2[2] = elt2[2];
|
||||||
|
|
||||||
|
|
||||||
|
var r1 = el1[0].user.rating || el1[1].user.rating;
|
||||||
|
var r2 = el2[0].user.rating || el2[1].user.rating;
|
||||||
|
|
||||||
|
return r1 - r2;;
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(lib.toMatlabArray('X', lib.range(0, groups.length)));
|
||||||
|
|
||||||
|
var header =
|
||||||
|
'\\begin{axis}[\n'
|
||||||
|
+ ' ybar,\n'
|
||||||
|
+ ' enlargelimits=0.05,\n'
|
||||||
|
+ ' legend style={at={(0.5,-0.15)},\n'
|
||||||
|
+ ' anchor=north,legend columns=-1},\n'
|
||||||
|
+ ' ylabel={Number of interactions},\n'
|
||||||
|
+ ' xlabel={Groups sharing the same coin combination},\n'
|
||||||
|
+ ' symbolic x coords={ ';
|
||||||
|
|
||||||
|
for (var i = 0; i < groups.length; i++) {
|
||||||
|
header += i + (i === groups.length -1 ? '' : ',');
|
||||||
|
}
|
||||||
|
1,2,3,4,5,6,7,8,9,10
|
||||||
|
|
||||||
|
header += '},\n'
|
||||||
|
+ ' xtick=data,\n'
|
||||||
|
+ ' nodes near coords,\n'
|
||||||
|
+ ' width=32cm,\n'
|
||||||
|
+ ' height=10cm,\n'
|
||||||
|
+ ' % nodes near coords align5={vertical},\n'
|
||||||
|
+ ']\n';
|
||||||
|
|
||||||
|
console.log(header);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(lib.toLaTeXCoordinate('Y1', lib.range(0, groups.length, function(i) {
|
||||||
return lib.numberOfInteraction(groups[i][0]);
|
return lib.numberOfInteraction(groups[i][0]);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
console.log(lib.toMatlabArray('Y2', lib.range(0, groups.length, function(i) {
|
console.log(lib.toLaTeXCoordinate('Y2', lib.range(0, groups.length, function(i) {
|
||||||
return lib.max(lib.numberOfInteraction(groups[i][1]), lib.numberOfInteraction(groups[i][2]));
|
return lib.max(lib.numberOfInteraction(groups[i][1]), 0);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
console.log(lib.toMatlabArray('Y3', lib.range(0, groups.length, function(i) {
|
console.log(lib.toLaTeXCoordinate('Y3', lib.range(0, groups.length, function(i) {
|
||||||
return lib.min(lib.numberOfInteraction(groups[i][1]), lib.numberOfInteraction(groups[i][2]));
|
return lib.max(0, lib.numberOfInteraction(groups[i][2]));
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
console.log('\\legend{Without reco, With viewports, With arrows}');
|
||||||
|
console.log('\\end{axis}');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv.length !== 3) {
|
if (process.argv.length !== 3) {
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/node
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
var lib = require('./lib.js');
|
||||||
|
|
||||||
|
function zeros(lines, columns) {
|
||||||
|
|
||||||
|
if (columns === undefined)
|
||||||
|
columns = lines;
|
||||||
|
|
||||||
|
var ret = [];
|
||||||
|
|
||||||
|
for(var i=0; i<lines; i++) {
|
||||||
|
ret[i] = [];
|
||||||
|
for(var j=0; j<columns; j++) {
|
||||||
|
ret[i][j] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(mat) {
|
||||||
|
// Normalize the matrices
|
||||||
|
var max = 0;
|
||||||
|
for (var i = 0; i < mat.length; i++) {
|
||||||
|
for (var j = 0; j < mat[i].length; j++) {
|
||||||
|
if (mat[i][j] > max)
|
||||||
|
max = mat[i][j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mat.map(function(line) { return line.map(function(num) { return num / (2 * max) })});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(path) {
|
||||||
|
|
||||||
|
// Generated with ./test.pgsql | tail -n+3 | head -n-2 | cut -d '|' -f 2 | sort -g | tr '\n' ' ' | tr -s ' ' | tr ' ' ','
|
||||||
|
var recoExps = [10,27,28,57,68,83,127,129,145,192,205,206,209,210,212,214,236,240];
|
||||||
|
|
||||||
|
var db = lib.loadFromFile(path);
|
||||||
|
var mat1 = zeros(12); // Bombomb
|
||||||
|
var mat2 = zeros(12); // Mountain
|
||||||
|
var mat3 = zeros(11); // Whomp
|
||||||
|
|
||||||
|
var matt1 = zeros(12); // Bombomb
|
||||||
|
var matt2 = zeros(12); // Mountain
|
||||||
|
var matt3 = zeros(11); // Whomp
|
||||||
|
|
||||||
|
for (var expIndex = 0; expIndex < db.experiments.length; expIndex++) {
|
||||||
|
|
||||||
|
var exp = db.experiments[expIndex];
|
||||||
|
var coinCombination = db.coinCombinations[exp.coin_combination_id - 1];
|
||||||
|
let mat, matt;
|
||||||
|
|
||||||
|
switch (coinCombination.scene_id) {
|
||||||
|
|
||||||
|
case 1: continue; // Continue the loop
|
||||||
|
case 2: mat = mat1; matt = matt1; break;
|
||||||
|
case 3: mat = mat2; matt = matt2; break;
|
||||||
|
case 4: mat = mat3; matt = matt3; break;
|
||||||
|
default: continue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var prev = 0; // 0 is the reset camera
|
||||||
|
var next = null;
|
||||||
|
|
||||||
|
for (var evtIndex = 0; evtIndex < exp.elements.events.length; evtIndex++) {
|
||||||
|
|
||||||
|
var evt = exp.elements.events[evtIndex];
|
||||||
|
|
||||||
|
if (evt.type === 'reset') {
|
||||||
|
prev = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.type === 'arrow') {
|
||||||
|
next = evt.id + 1;
|
||||||
|
if (prev !== next) {
|
||||||
|
mat[prev][next]++;
|
||||||
|
if (recoExps.indexOf(exp.id) !== -1) {
|
||||||
|
matt[prev][next]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Update prev
|
||||||
|
prev = next;
|
||||||
|
next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mat1 = normalize(mat1);
|
||||||
|
mat2 = normalize(mat2);
|
||||||
|
mat3 = normalize(mat3);
|
||||||
|
|
||||||
|
fs.writeFile('mat1.dat', lib.toLaTeXMatrix(mat1), function(e) {});
|
||||||
|
fs.writeFile('mat2.dat', lib.toLaTeXMatrix(mat2), function(e) {});
|
||||||
|
fs.writeFile('mat3.dat', lib.toLaTeXMatrix(mat3), function(e) {});
|
||||||
|
|
||||||
|
matt1 = normalize(matt1);
|
||||||
|
matt2 = normalize(matt2);
|
||||||
|
matt3 = normalize(matt3);
|
||||||
|
|
||||||
|
fs.writeFile('matt1.dat', lib.toLaTeXMatrix(matt1), function(e) {});
|
||||||
|
fs.writeFile('matt2.dat', lib.toLaTeXMatrix(matt2), function(e) {});
|
||||||
|
fs.writeFile('matt3.dat', lib.toLaTeXMatrix(matt3), function(e) {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.length !== 3) {
|
||||||
|
process.stderr.write('Error : please give me a JSON file to work on\n');
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main(process.argv[2])
|
|
@ -1,12 +1,72 @@
|
||||||
var Log = require('../lib/NodeLog.js');
|
var Log = require('../lib/NodeLog.js');
|
||||||
|
|
||||||
|
function clone(vec) {
|
||||||
|
return {x : vec.x, y : vec.y, z : vec.z};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotation(vec1, x, y, z) {
|
||||||
|
|
||||||
|
var cos = Math.cos(z);
|
||||||
|
var sin = Math.sin(z);
|
||||||
|
|
||||||
|
var newVec = {x:0, y:0, z:0};
|
||||||
|
oldVec = clone(vec1);
|
||||||
|
|
||||||
|
newVec.x = cos * oldVec.x - sin * oldVec.y;
|
||||||
|
newVec.y = sin * oldVec.x + cos * oldVec.y;
|
||||||
|
newVec.z = oldVec.z;
|
||||||
|
|
||||||
|
oldVec = clone(newVec);
|
||||||
|
|
||||||
|
cos = Math.cos(y);
|
||||||
|
sin = Math.sin(y);
|
||||||
|
|
||||||
|
newVec.x = cos * oldVec.x + sin * oldVec.z;
|
||||||
|
newVec.y = oldVec.y;
|
||||||
|
newVec.z = - sin * oldVec.x + cos * oldVec.z;
|
||||||
|
|
||||||
|
cos = Math.cos(x);
|
||||||
|
sin = Math.sin(x);
|
||||||
|
|
||||||
|
oldVec = clone(newVec);
|
||||||
|
|
||||||
|
newVec.x = oldVec.x;
|
||||||
|
newVec.y = oldVec.y * cos - oldVec.z * sin;
|
||||||
|
newVec.z = oldVec.y * sin + oldVec.z * cos;
|
||||||
|
|
||||||
|
return clone(newVec);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTransformation(vector, transfo) {
|
||||||
|
|
||||||
|
var ret = rotation(vector, transfo.rotation.x, transfo.rotation.y, transfo.rotation.z);
|
||||||
|
var scale = transfo.scale || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: (ret.x + transfo.translation.x) * scale,
|
||||||
|
y: (ret.y + transfo.translation.y) * scale,
|
||||||
|
z: (ret.z + transfo.translation.z) * scale
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a mesh. All meshes are loaded once in geo.availableMesh to avoid
|
* Represents a mesh. All meshes are loaded once in geo.availableMesh to avoid
|
||||||
* loading at each mesh request
|
* loading at each mesh request
|
||||||
* @constructor
|
* @constructor
|
||||||
* @memberOf geo
|
* @memberOf geo
|
||||||
*/
|
*/
|
||||||
geo.MeshContainer = function(path, callback) {
|
geo.MeshContainer = function(path, transfo, callback) {
|
||||||
|
|
||||||
|
if (callback === undefined && typeof transfo === 'function') {
|
||||||
|
callback = transfo;
|
||||||
|
transfo = {translation: {x:0,y:0,z:0}, rotation: {x:0,y:0,z:0}};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transfo === undefined) {
|
||||||
|
transfo = {translation: {x:0,y:0,z:0}, rotation: {x:0,y:0,z:0}};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* array of each part of the mesh
|
* array of each part of the mesh
|
||||||
|
@ -44,6 +104,8 @@ geo.MeshContainer = function(path, callback) {
|
||||||
*/
|
*/
|
||||||
this.numberOfFaces = 0;
|
this.numberOfFaces = 0;
|
||||||
|
|
||||||
|
this.transfo = transfo;
|
||||||
|
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
||||||
if (path !== undefined) {
|
if (path !== undefined) {
|
||||||
|
@ -92,6 +154,12 @@ geo.MeshContainer.prototype.loadFromFile = function(path) {
|
||||||
|
|
||||||
// Just a simple vertex
|
// Just a simple vertex
|
||||||
var vertex = new geo.Vertex(line);
|
var vertex = new geo.Vertex(line);
|
||||||
|
var vertexTransformed = applyTransformation(vertex, self.transfo);
|
||||||
|
|
||||||
|
vertex.x = vertexTransformed.x;
|
||||||
|
vertex.y = vertexTransformed.y;
|
||||||
|
vertex.z = vertexTransformed.z;
|
||||||
|
|
||||||
vertex.index = self.vertices.length;
|
vertex.index = self.vertices.length;
|
||||||
self.vertices.push(vertex);
|
self.vertices.push(vertex);
|
||||||
|
|
||||||
|
@ -152,7 +220,7 @@ geo.MeshContainer.prototype.loadFromFile = function(path) {
|
||||||
function trySetLoaded() {
|
function trySetLoaded() {
|
||||||
for (var name in availableMeshNames) {
|
for (var name in availableMeshNames) {
|
||||||
|
|
||||||
if (availableMeshNames[name] === false) {
|
if (availableMeshNames[name].done === false) {
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -160,15 +228,54 @@ function trySetLoaded() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.ready("All meshes are ready");
|
Log.ready("Meshes loaded in " + (Date.now() - start) + 'ms');
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableMeshNames = {
|
var availableMeshNames = {
|
||||||
'/static/data/castle/princess peaches castle (outside).obj':false,
|
'/static/data/castle/princess peaches castle (outside).obj': {
|
||||||
'/static/data/mountain/coocoolmountain.obj':false,
|
done: false
|
||||||
'/static/data/whomp/Whomps Fortress.obj':false,
|
},
|
||||||
'/static/data/bobomb/bobomb battlefeild.obj':false,
|
'/static/data/mountain/coocoolmountain.obj': {
|
||||||
'/static/data/sponza/sponza.obj':false
|
done: false
|
||||||
|
},
|
||||||
|
|
||||||
|
'/static/data/whomp/Whomps Fortress.obj': {
|
||||||
|
done: false,
|
||||||
|
transfo: {
|
||||||
|
rotation: {
|
||||||
|
x: -Math.PI / 2,
|
||||||
|
y: 0,
|
||||||
|
z: Math.PI / 2
|
||||||
|
},
|
||||||
|
translation: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0
|
||||||
|
},
|
||||||
|
scale: 0.1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'/static/data/bobomb/bobomb battlefeild.obj': {
|
||||||
|
done: false,
|
||||||
|
transfo: {
|
||||||
|
rotation: {
|
||||||
|
x: 0,
|
||||||
|
y: Math.PI - 0.27,
|
||||||
|
z: 0
|
||||||
|
},
|
||||||
|
translation: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'/static/data/sponza/sponza.obj': {
|
||||||
|
done: false
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var i = 1; i < 26; i++) {
|
for (var i = 1; i < 26; i++) {
|
||||||
|
@ -179,12 +286,18 @@ for (var i = 1; i < 26; i++) {
|
||||||
|
|
||||||
geo.availableMeshes = {};
|
geo.availableMeshes = {};
|
||||||
|
|
||||||
|
var start = Date.now();
|
||||||
|
|
||||||
function pushMesh(name) {
|
function pushMesh(name) {
|
||||||
|
|
||||||
geo.availableMeshes[name] = new geo.MeshContainer(name.substring(1, name.length), function() {
|
geo.availableMeshes[name] = new geo.MeshContainer(
|
||||||
availableMeshNames[name] = true;
|
name.substring(1, name.length),
|
||||||
|
availableMeshNames[name].transfo,
|
||||||
|
function() {
|
||||||
|
availableMeshNames[name].done = true;
|
||||||
trySetLoaded();
|
trySetLoaded();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -180,8 +180,8 @@ L3D.initBobombScene = function(scene, collidableObjects, recommendation, clickab
|
||||||
);
|
);
|
||||||
|
|
||||||
loader.load();
|
loader.load();
|
||||||
var theta = 0.27;
|
// var theta = 0.27;
|
||||||
loader.obj.rotation.y = Math.PI - theta;
|
// loader.obj.rotation.y = Math.PI - theta;
|
||||||
|
|
||||||
loader.obj.up = new THREE.Vector3(0,0,1);
|
loader.obj.up = new THREE.Vector3(0,0,1);
|
||||||
collidableObjects.push(loader.obj);
|
collidableObjects.push(loader.obj);
|
||||||
|
@ -377,9 +377,9 @@ L3D.initWhompScene = function(scene, collidableObjects, recommendation, clickabl
|
||||||
|
|
||||||
loader.load();
|
loader.load();
|
||||||
|
|
||||||
loader.obj.rotation.x = -Math.PI/2;
|
// loader.obj.rotation.x = -Math.PI/2;
|
||||||
loader.obj.rotation.z = Math.PI/2;
|
// loader.obj.rotation.z = Math.PI/2;
|
||||||
loader.obj.scale.set(0.1,0.1,0.1);
|
// loader.obj.scale.set(0.1,0.1,0.1);
|
||||||
|
|
||||||
// loader.getRecommendation = function() {
|
// loader.getRecommendation = function() {
|
||||||
// var ret = loader.recommendation.toList();
|
// var ret = loader.recommendation.toList();
|
||||||
|
|
|
@ -104,7 +104,8 @@ app.use(function(req, res) {
|
||||||
var serverPort, serverIpAddress;
|
var serverPort, serverIpAddress;
|
||||||
if ( isDev ) {
|
if ( isDev ) {
|
||||||
serverPort = 4000;
|
serverPort = 4000;
|
||||||
serverIpAddress = require('ip').address();
|
// serverIpAddress = require('ip').address();
|
||||||
|
serverIpAddress = '0.0.0.0';
|
||||||
} else {
|
} else {
|
||||||
// Openhift conf
|
// Openhift conf
|
||||||
serverPort = process.env.OPENSHIFT_NODEJS_PORT || 8080;
|
serverPort = process.env.OPENSHIFT_NODEJS_PORT || 8080;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
psql interface 3dinterface << E_O_SQL
|
||||||
|
|
||||||
-- Clear database from previous tables (just in case...)
|
-- Clear database from previous tables (just in case...)
|
||||||
DROP TABLE IF EXISTS Users CASCADE;
|
DROP TABLE IF EXISTS Users CASCADE;
|
||||||
DROP TABLE IF EXISTS Arrowclicked CASCADE;
|
DROP TABLE IF EXISTS Arrowclicked CASCADE;
|
||||||
|
@ -154,3 +158,5 @@ CREATE TABLE SwitchedLockOption(
|
||||||
time TIMESTAMP DEFAULT NOW(),
|
time TIMESTAMP DEFAULT NOW(),
|
||||||
locked BOOLEAN
|
locked BOOLEAN
|
||||||
);
|
);
|
||||||
|
|
||||||
|
E_O_SQL
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
psql interface 3dinterface << E_O_SQL
|
||||||
|
|
||||||
-- Checks that each user has never twice either the same scene or the same recommendation style
|
-- Checks that each user has never twice either the same scene or the same recommendation style
|
||||||
SELECT count(*) = 0
|
SELECT count(*) = 0
|
||||||
FROM
|
FROM
|
||||||
|
@ -32,3 +36,5 @@ FROM (
|
||||||
GROUP BY Experiment.coin_combination_id
|
GROUP BY Experiment.coin_combination_id
|
||||||
HAVING count(CoinCombination.id) != 1
|
HAVING count(CoinCombination.id) != 1
|
||||||
) AS T;
|
) AS T;
|
||||||
|
|
||||||
|
E_O_SQL
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
psql interface 3dinterface << E_O_SQL
|
||||||
|
|
||||||
|
ALTER TABLE Scene ADD recommendation_number INTEGER;
|
||||||
|
|
||||||
|
UPDATE SCENE SET recommendation_number = 11 WHERE id = 2;
|
||||||
|
UPDATE SCENE SET recommendation_number = 11 WHERE id = 3;
|
||||||
|
UPDATE SCENE SET recommendation_number = 10 WHERE id = 4;
|
||||||
|
|
||||||
|
E_O_SQL
|
|
@ -0,0 +1,33 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
psql interface 3dinterface << E_O_SQL
|
||||||
|
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT Users.id AS user_id,
|
||||||
|
Experiment.id AS exp_id,
|
||||||
|
Scene.name AS scene_name,
|
||||||
|
Scene.recommendation_number AS reco_total,
|
||||||
|
count(DISTINCT ArrowClicked.arrow_id) AS reco_clicked,
|
||||||
|
-- Scene.recommendation_number - 2 <= count(DISTINCT ArrowClicked.arrow_id) AS reco_clicker
|
||||||
|
100 * count(DISTINCT ArrowClicked.arrow_id) / Scene.recommendation_number AS reco_percent
|
||||||
|
|
||||||
|
FROM Users, Experiment, CoinCombination, ArrowClicked, Scene
|
||||||
|
-- JOIN conditions
|
||||||
|
WHERE Experiment.user_id = Users.id AND
|
||||||
|
CoinCombination.id = Experiment.coin_combination_id AND
|
||||||
|
ArrowClicked.exp_id = Experiment.id AND
|
||||||
|
Scene.id = CoinCombination.scene_id AND
|
||||||
|
|
||||||
|
-- other conditions
|
||||||
|
Experiment.finished AND
|
||||||
|
CoinCombination.scene_id != 1 AND
|
||||||
|
Users.valid
|
||||||
|
|
||||||
|
GROUP BY Users.id, Experiment.id, Scene.name, Scene.recommendation_number
|
||||||
|
) T
|
||||||
|
WHERE reco_percent > 75
|
||||||
|
ORDER BY reco_percent
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
E_O_SQL
|
Loading…
Reference in New Issue