// jscs:disable
import Ember from 'ember';
// http://stackoverflow.com/a/12646864
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
/**
* Select the first matching session from an array of options, according to the specified rules
*
* @method getLastSession
* @param {Session[]} pastSessions An array of session records. This returns the first match, eg assumes newest-first sort order
* @return {Session} The model representing the last session in which the user participated
*/
function getLastSession(pastSessions) {
// Base randomization on the newest (last completed) session for which the participant got at
// least as far as recording data for a single video ID.
for (let i = 0; i < pastSessions.length; i++) {
let session = pastSessions[i];
// Frames might be numbered differently in different experiments... rather than check for a frame ID, check that at least one frame referencing the videos exists at all.
let expData = session.get('expData') || {};
let keys = Object.keys(expData);
for (let i = 0; i < keys.length; i++) {
let frameKeyName = keys[i];
let frameData = expData[frameKeyName];
if (frameKeyName.indexOf('pref-phys-videos') !== -1 && frameData && frameData.videoId) {
return session;
}
}
}
// If no match found, explicitly return null
return null;
}
function getConditions(lastSession, frameId) {
var startType, showStay, whichObjects;
// The last session payload refers to the frame we want by number (#-frameName), but frames aren't numbered until the sequence
// has been resolved (eg until we expand pref-phys-videos into a series of video frames, we won't know how many
// frames there are or in what order)
// To find the last conditions, we take the last (and presumably only) key of session.conditions that looks like
// the name (without the leading number part)
// This works insofar as this function only targets one sort of frame that we expect to occur only once in
// the pref-phys experiment. Otherwise this function would get confused.
let lastConditions = lastSession ? lastSession.get('conditions') : null;
let lastFrameConditions;
Object.keys(lastConditions || {}).forEach((keyName) => {
if (keyName.indexOf(frameId) !== -1) {
lastFrameConditions = lastConditions[keyName];
}
});
if (!lastFrameConditions) {
startType = Math.floor(Math.random() * 4);
showStay = Math.floor(Math.random() * 2);
var whichObjectG = Math.floor(Math.random() * 6);
var whichObjectI = Math.floor(Math.random() * 6);
var whichObjectS = Math.floor(Math.random() * 6);
var whichObjectC = Math.floor(Math.random() * 6);
whichObjects = [whichObjectG, whichObjectI, whichObjectS, whichObjectC];
} else {
startType = lastFrameConditions.startType;
startType++;
if (startType > 3) {
startType = 0;
}
showStay = lastFrameConditions.showStay;
//parseInt(prompt('Show support-stay (1) or support-fall (0) last session?', '0/1'));
showStay = 1 - showStay;
whichObjects = Ember.copy(lastFrameConditions.whichObjects);
for (var i = 0; i < 4; i++) {
whichObjects[i]++;
if (whichObjects[i] > 5) {
whichObjects[i] = 0;
}
}
}
return {
startType: startType,
showStay: showStay,
whichObjects: whichObjects
};
}
function assignVideos(startType, showStay, whichObjects, NPERTYPE) {
// Types of comparisons for each event type (gravity, inertia, support-fall, support-stay,
// control). Format [event, outcomeMoreProb, outcomeLessProb]
const comparisonsG = [
['ramp', 'down', 'up'],
['ramp', 'down', 'up'],
['toss', 'down', 'up']
];
// TODO: Is this one still used?
const comparisonsI = [ // jshint ignore:line
['stop', 'hand', 'nohand'],
['reverse', 'barrier', 'nobarrier']
];
const comparisonsSF = [
['fall', 'slightly-on', 'mostly-on'],
['fall', 'next-to', 'mostly-on'],
['fall', 'near', 'mostly-on'],
['fall', 'next-to', 'slightly-on'],
['fall', 'near', 'slightly-on'],
['fall', 'near', 'next-to']
];
const comparisonsSS = [
['stay', 'slightly-on', 'mostly-on'],
['stay', 'next-to', 'mostly-on'],
['stay', 'near', 'mostly-on'],
['stay', 'next-to', 'slightly-on'],
['stay', 'near', 'slightly-on'],
['stay', 'near', 'next-to']
];
const comparisonsC = [
['same', 'A', 'B'],
['salience', 'interesting', 'boring']
];
// const videotypes = ['gravity', 'inertia', 'support', 'control'];
// FOR PILOT ONLY:
const videotypes = ['gravity', 'stay', 'control', 'fall'];
var compTypes = [comparisonsG, comparisonsSS, comparisonsC, comparisonsSF];
// how many times does each comparison type listed need to be shown to get to NPERTYPE for that event type?
var nReps = [2, 1, 3, 1];
/*
// Choose which videos to show for support
if (showStay === 0) {
videotypes[1] = 'fall';
compTypes[1] = comparisonsSF;
} else if (showStay === 1) {
videotypes[1] = 'stay';
compTypes[1] = comparisonsSS;
} /* else {
alert('invalid value for showStay (should be '0' or '1'), using '0'');
videotypes[2] = 'fall';
compTypes[2] = comparisonsSF;
} */
// Objects to use: elements correspond to videotypes
const physicalObjects = [
['apple', 'cup', 'whiteball', 'lotion', 'spray', 'whiteball'],
['hammer', 'tissues', 'duck', 'book', 'shoe', 'brush'],
['box', 'funnel', 'eraser', 'scissors', 'spoon', 'wrench'],
['hammer', 'tissues', 'duck', 'book', 'shoe', 'brush']
];
// Options for videos, organized by event
const cameraAngles = {
table: ['c1', 'c2'],
ramp: ['c1', 'c2'],
toss: ['c1', 'c2'],
stop: ['c1', 'c2'],
reverse: ['c1', 'c2'],
fall: ['c2'],
stay: ['c2'],
same: ['c1'],
salience: ['c1'],
};
const backgrounds = {
table: ['b1', 'b2'],
ramp: ['b1', 'b2'],
toss: ['b1'],
stop: ['b1'],
reverse: ['b1'],
fall: ['green'],
stay: ['green'],
same: ['b1'],
salience: ['b1']
};
const flips = {
table: ['NR'],
ramp: ['NN', 'RR', 'NR', 'RN'],
toss: ['NN', 'RR'],
stop: ['NR'],
reverse: ['RN'],
fall: ['NN', 'NR', 'RN', 'RR'],
stay: ['NN', 'NR', 'RN', 'RR'],
same: ['NN', 'RR', 'NR', 'RN'],
salience: ['NN', 'NR', 'RN', 'RR'],
};
// Create list of TYPES (e.g. gravity, inertia, ...)
var typeOrder = videotypes.slice(startType, videotypes.length);
typeOrder = typeOrder.concat(videotypes.slice(0, startType));
var playlistsByType = {};
for (var iType = 0; iType < videotypes.length; iType++) { // for each video type
// make list of objects to use with canonically-ordered comparison types
var objList = physicalObjects[iType].slice(whichObjects[iType], physicalObjects[iType].length);
objList = objList.concat(physicalObjects[iType].slice(0, whichObjects[iType]));
// make canonical comparison type list
var eventTypeList = compTypes[iType];
for (var iRep = 1; iRep < nReps[iType]; iRep++) {
eventTypeList = eventTypeList.concat(compTypes[iType]);
}
// choose placement of more/less surprising outcomes (balanced)
var onLeft = ['moreProb', 'moreProb', 'moreProb', 'lessProb', 'lessProb', 'lessProb'];
onLeft = shuffleArray(onLeft);
// pair objects and comparison types
var events = [];
for (var iEvent = 0; iEvent < eventTypeList.length; iEvent++) {
var outcomeL, outcomeR;
if (onLeft[iEvent] === 'moreProb') {
outcomeL = eventTypeList[iEvent][1];
outcomeR = eventTypeList[iEvent][2];
} else {
outcomeL = eventTypeList[iEvent][2];
outcomeR = eventTypeList[iEvent][1];
}
// choose camera angle, background, and NN/NR/RN/RR randomly
var iCamera = Math.floor(Math.random() *
cameraAngles[eventTypeList[iEvent][0]].length);
var iBackground = Math.floor(Math.random() *
backgrounds[eventTypeList[iEvent][0]].length);
var iFlip = Math.floor(Math.random() *
flips[eventTypeList[iEvent][0]].length);
events.push({
compType: eventTypeList[iEvent][0],
outcomeL: outcomeL,
outcomeR: outcomeR,
object: objList[iEvent],
camera: cameraAngles[eventTypeList[iEvent][0]][iCamera],
background: backgrounds[eventTypeList[iEvent][0]][iBackground],
flip: flips[eventTypeList[iEvent][0]][iFlip]
});
}
// choose order of events randomly
events = shuffleArray(events);
playlistsByType[videotypes[iType]] = events;
}
// Put list together
var allEvents = [];
var filenames = [];
var eventNum = 1;
for (var nEvents = 0; nEvents < NPERTYPE; nEvents++) {
for (iType = 0; iType < typeOrder.length; iType++) {
var e = playlistsByType[typeOrder[iType]][nEvents];
var fname = `sbs_${e.compType}_${e.outcomeL}_${e.outcomeR}_${e.object}_${e.camera}_${e.background}_${e.flip}`;
filenames.push(fname);
var altName = `sbs_${e.compType}_${e.outcomeR}_${e.outcomeL}_${e.object}_${e.camera}_${e.background}_${e.flip}`;
e.fname = fname;
e.altName = altName;
e.index = eventNum;
allEvents.push(e);
eventNum++;
}
}
return [allEvents, filenames];
}
function parse_name(fname) {
var pieces = fname.split('_');
var features = {};
features.eventType = pieces[1];
features.leftEvent = pieces[2];
features.rightEvent = pieces[3];
features.object = pieces[4];
features.camera = pieces[5];
features.bg = pieces[6];
var variantExt = pieces[7];
features.variant = (variantExt.split('.'))[0];
//quick hack for dummy clips which have wrong names for some objects
// (so we can get a correct intro name)
switch (features.object) {
case 'A':
features.object = 'box';
break;
case 'B':
features.object = 'eraser';
break;
case 'C':
features.object = 'funnel';
break;
case 'D':
features.object = 'scissors';
break;
case 'E':
features.object = 'spoon';
break;
case 'F':
features.object = 'wrench';
break;
}
return features;
}
function audioSourceObjs(path, shortname) {
return [
{
'src': path + shortname + '.ogg',
'type': 'audio/ogg'
},
{
'src': path + shortname + '.mp3',
'type': 'audio/mp3'
}
];
}
function videoSourceObjs(path, shortname, organizedByType) {
if (!organizedByType) {
return [
{
'src': path + shortname + '.webm',
'type': 'video/webm'
},
{
'src': path + shortname + '.mp4',
'type': 'video/mp4'
}
];
} else {
return [
{
'src': path + 'webm/' + shortname + '.webm',
'type': 'video/webm'
},
{
'src': path + 'mp4/' + shortname + '.mp4',
'type': 'video/mp4'
}
];
}
}
function toFrames(frameId, eventVideos, BASE_DIR) {
var nVideos = eventVideos.length;
return eventVideos.map((e) => {
if (e.index === nVideos) {
return {
kind: 'exp-video-physics',
id: `${frameId}`,
autoplay: true,
isLast: true,
audioSources: audioSourceObjs(
BASE_DIR + 'audio/',
'all_done'),
attnSources: videoSourceObjs(
BASE_DIR + 'stimuli/attention/',
'attentiongrabber'),
};
}
var features = parse_name(e.fname);
var allMusic = ['music_01', 'music_02', 'music_03', 'music_04', 'music_06', 'music_07', 'music_09', 'music_10'];
var musicName = allMusic[Math.floor(Math.random() * allMusic.length)];
var returnFrame = {
kind: 'exp-video-physics',
id: `${frameId}`,
autoplay: true,
testLength: 20, // TODO: change to 20s for actual testing.
isLast: false,
audioSources: audioSourceObjs(
BASE_DIR + 'audio/',
'video_' + ('00' + (e.index)).slice(-2)),
musicSources: audioSourceObjs(
BASE_DIR + 'audio/',
musicName),
introSources: videoSourceObjs(
BASE_DIR + 'stimuli/intro/',
`cropped_${features.object}`),
attnSources: videoSourceObjs(
BASE_DIR + 'stimuli/attention/',
'attentiongrabber'),
sources: videoSourceObjs(
BASE_DIR + 'stimuli/' + features.eventType + '/',
e.fname, true),
altSources: videoSourceObjs(
BASE_DIR + 'stimuli/' + features.eventType + '/',
e.altName, true)
};
// FOR PILOT ONLY: replace fall videos with calibration
if (e.compType === 'fall') {
returnFrame.sources = videoSourceObjs(
BASE_DIR + 'stimuli/attention/',
'calibration');
returnFrame.altSources = videoSourceObjs(
BASE_DIR + 'stimuli/attention/',
'calibration');
}
return returnFrame;
});
}
var randomizer = function (frameId, frameConfig, pastSessions, resolveFrame) {
var MAX_VIDEOS = 24; // limit number of videos. Use 24 for actual study.
var BASE_DIR = 'https://s3.amazonaws.com/lookitcontents/exp-physics/';
pastSessions.sort(function (a, b) {
return a.get('createdOn') > b.get('createdOn') ? -1 : 1;
});
// TODO: In the future, we may want to identify the specific frame # to fetch instead of generic frame name
pastSessions = pastSessions.filter(function (session) {
return session.get('conditions');
});
let lastSession = getLastSession(pastSessions);
var conditions = getConditions(lastSession, frameId);
conditions.NPERTYPE = 6;
var {
startType,
showStay,
whichObjects,
NPERTYPE
} = conditions;
var [eventVideos, ] = assignVideos(startType, showStay, whichObjects, NPERTYPE);
eventVideos = eventVideos.slice(0, MAX_VIDEOS);
eventVideos.push({index: MAX_VIDEOS + 1});
// allEvents and filenames are a function of conditions (no need to store)
var resolved = [];
toFrames(frameId, eventVideos, BASE_DIR).forEach((frame) => {
return resolved.push(...resolveFrame(null, frame)[0]);
});
return [resolved, conditions];
};
export default randomizer;
// Export helper functions to support unit testing
export { getConditions, getLastSession };