624 lines
22 KiB
JavaScript
624 lines
22 KiB
JavaScript
|
/* UI Parameters *******************************************************************/
|
||
|
|
||
|
const PARAMS = {
|
||
|
ENABLE_MSEC_SET_URL: false, // enables setting image display time in the url like so: '?msecImg=1000'
|
||
|
NUM_MSEC_CROSS: 750, // how long the fixation cross is shown for
|
||
|
NUM_MSEC_IMAGE: 20000, // num milliseconds to show each image for
|
||
|
NUM_MSEC_SENTINEL: 750, // how long a sentinel image is shown for
|
||
|
NUM_MSEC_CHAR: 400, // how long the code chart is shown
|
||
|
|
||
|
IMG_WIDTH: 1600, // max img width
|
||
|
IMG_HEIGHT: 800, // max img height
|
||
|
|
||
|
N_BUCKETS: 1,
|
||
|
N_SUBJ_FILES: 1,
|
||
|
|
||
|
MAX_INVALID_ALLOWED_TUTORIAL: 1,
|
||
|
MAX_INCORRECT_SENTINELS_ALLOWED_TUTORIAL: 0,
|
||
|
|
||
|
GIVE_FEEDBACK: true, // should feedback be given during the tutorial
|
||
|
NUM_MSEC_FEEDBACK: 2000, // how long does the feedback stay on the screen
|
||
|
}
|
||
|
|
||
|
// messages shown if feedback is given
|
||
|
const POSITIVE_MESSAGE = "Keep up the good work!";
|
||
|
const NEGATIVE_MESSAGE = "Please type the triplet you see when the image vanishes.";
|
||
|
|
||
|
// path to the task data to use
|
||
|
const DATA_BASE_PATH = "assets/task_data/";
|
||
|
// base path relative to which image paths are defined
|
||
|
const IMAGE_BASE_PATH = "assets/"
|
||
|
const SUBJECT_FILES_BASE_PATH = DATA_BASE_PATH + "full_subject_files/";
|
||
|
const FIXATION_CROSS = DATA_BASE_PATH + "fixation-cross.jpg"
|
||
|
const TARGET_NUM = 40
|
||
|
|
||
|
/* Global vars **********************************************************************/
|
||
|
|
||
|
// variables we want to save to store to outputs
|
||
|
var SUBJ_ID;
|
||
|
var BUCKET_NUM;
|
||
|
var SUBJ_FILE_PATH;
|
||
|
var OPEN_TIME; // when the url is first loaded
|
||
|
var START_TIME; // when they first click the "continue" button
|
||
|
var LOAD_TIME; // when the last image loads and the task is ready to go
|
||
|
|
||
|
// normal images
|
||
|
var IMAGES = new Array(); // preload the images
|
||
|
|
||
|
var CHECKED_TUTORIAL_FLAG = false;
|
||
|
var IS_TUTORIAL = true;
|
||
|
var MESSAGE = "";
|
||
|
var MESSAGE_IS_POSITIVE = true;
|
||
|
|
||
|
// during the task, keeps track of how the participant is doing
|
||
|
// this count is only valid up until you hit the submit button
|
||
|
var SCORES = {
|
||
|
SENTINEL_CORRECT: 0,
|
||
|
SENTINEL_TOTAL: 0,
|
||
|
IMAGE_CORRECT: 0,
|
||
|
IMAGE_TOTAL: 0,
|
||
|
INVALID: 0
|
||
|
}
|
||
|
|
||
|
// The answers picked by user
|
||
|
var ANSWERS = []
|
||
|
var ANSWER_TMP = []
|
||
|
var RECO_ANSWERS = []
|
||
|
|
||
|
|
||
|
/* End vars ************************************************************/
|
||
|
|
||
|
var custom = {
|
||
|
loadTasks: function() {
|
||
|
|
||
|
/*
|
||
|
* Loads data needed to run the task and does some one-time setup, such as:
|
||
|
* - timestamping the start of the task
|
||
|
* - selecting a subject file to use and loading it
|
||
|
* - preloading images
|
||
|
*
|
||
|
* returns [obj, int]: A length-two list where the first element is the loaded task data
|
||
|
* and the second element is the number of trails (number of images) in the task.
|
||
|
*/
|
||
|
|
||
|
OPEN_TIME = new Date();
|
||
|
DEBUG = gup("debug") == "true";
|
||
|
|
||
|
$(".instruction-button").click(function() {
|
||
|
START_TIME = new Date();
|
||
|
$(this).unbind();
|
||
|
})
|
||
|
|
||
|
//hide all subtasks to begin with
|
||
|
$(".subtask").hide();
|
||
|
|
||
|
// set the size of the images
|
||
|
$("img.img-main").css({
|
||
|
"width": "100%",
|
||
|
"height": "100%",
|
||
|
"objectFit": "contain"
|
||
|
});
|
||
|
$("img.img-blur").css({
|
||
|
"width": "100%",
|
||
|
"height": "100%",
|
||
|
"objectFit": "contain"
|
||
|
});
|
||
|
|
||
|
BUCKET_NUM = gupOrRandom("bucket", PARAMS.N_BUCKETS);
|
||
|
SUBJ_ID = gupOrRandom("subj", PARAMS.N_SUBJ_FILES);
|
||
|
console.log("Bucket", BUCKET_NUM, "subjId", SUBJ_ID);
|
||
|
SUBJ_FILE_PATH = SUBJECT_FILES_BASE_PATH + "bucket" + BUCKET_NUM + "/subject_file_" + SUBJ_ID + ".json";
|
||
|
|
||
|
return $.get(SUBJ_FILE_PATH).then(function(tasks) {
|
||
|
console.log(tasks)
|
||
|
/*if (DEBUG) {
|
||
|
if (tasks.length > 1) {
|
||
|
tasks = tasks.slice(0, 1);
|
||
|
}
|
||
|
}*/
|
||
|
// pre-load all the images
|
||
|
preloadImages(tasks);
|
||
|
|
||
|
// set the correct image exposure
|
||
|
if (PARAMS.ENABLE_MSEC_SET_URL) {
|
||
|
var urlMsecImg = gup('msecImg');
|
||
|
if (urlMsecImg.length > 0) {
|
||
|
PARAMS.NUM_MSEC_IMAGE = parseInt(urlMsecImg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log("Trial start:", new Date());
|
||
|
console.log("Image exposure time:", PARAMS.NUM_MSEC_IMAGE);
|
||
|
console.log("CC exposure time:", PARAMS.NUM_MSEC_CHAR);
|
||
|
console.log("S exposure time:", PARAMS.NUM_MSEC_SENTINEL);
|
||
|
|
||
|
return [tasks, tasks.length];
|
||
|
});
|
||
|
|
||
|
},
|
||
|
|
||
|
showTask: function(taskInput, taskIndex, taskOutput) {
|
||
|
/*
|
||
|
* Shows the next trial of the experiment (fix cross, image, code chart, and character input.)
|
||
|
*
|
||
|
* taskInput - the task data returned from loadTasks
|
||
|
* taskIndex - the number of the current subtask (image)
|
||
|
* taskOutput - a partially filled out object containing the results (so far) of the task.
|
||
|
*
|
||
|
* returns: None
|
||
|
*/
|
||
|
var nMsecFeedback = (PARAMS.GIVE_FEEDBACK && MESSAGE && IS_TUTORIAL) ? PARAMS.NUM_MSEC_FEEDBACK : 0;
|
||
|
var messageClass = MESSAGE_IS_POSITIVE ? "positive" : "negative";
|
||
|
|
||
|
// terminate task early if they do not have required performance on tutorial
|
||
|
IS_TUTORIAL = isTutorial(taskInput[taskIndex]);
|
||
|
if (IS_TUTORIAL || didEndTutorial(taskInput, taskIndex, taskOutput)) {
|
||
|
if (!passedTutorial()) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$(".subtask").hide();
|
||
|
$('#next-button').hide(); // Hide the next button; we will handle control flow for this task
|
||
|
$('#qa-button').hide();
|
||
|
$('#nq-button').hide();
|
||
|
$("#reco-button").hide();
|
||
|
|
||
|
hideIfNotAccepted();
|
||
|
|
||
|
if (nMsecFeedback > 0) {
|
||
|
var feedbackMessageElt = $("#feedback-message");
|
||
|
feedbackMessageElt.empty();
|
||
|
feedbackMessageElt.append('<div class="ui message ' + messageClass + '"><div class="header">' + MESSAGE + '</div></div>');
|
||
|
$("#feedback-subtask").show();
|
||
|
}
|
||
|
setTimeout(showFixationCross.bind(this, taskInput, taskIndex, taskOutput), nMsecFeedback);
|
||
|
|
||
|
},
|
||
|
|
||
|
collectData: function(taskInput, taskIndex, taskOutput) {
|
||
|
/*
|
||
|
* Records the experimental output for the current subtask (image).
|
||
|
*
|
||
|
* taskInput - the task data returned from loadTasks
|
||
|
* taskIndex - the number of the current subtask (image)
|
||
|
* taskOutput - a partially filled out object containing the results (so far) of the task.
|
||
|
*
|
||
|
* returns: the new taskOutput object containing the data for the current subtask.
|
||
|
*/
|
||
|
var rememberedCode = $("#remembered-char").val().toUpperCase();
|
||
|
var isValidCode = _includes(taskInput[taskIndex].codes, rememberedCode);
|
||
|
//var isValidCode = _isCodePresent(rememberedCode, taskInput[taskIndex].codes);
|
||
|
var coord = isValidCode ? taskInput[taskIndex].coordinates[rememberedCode] : false;
|
||
|
|
||
|
taskOutput[taskIndex] = {
|
||
|
rememberedCode: rememberedCode,
|
||
|
isValidCode: isValidCode,
|
||
|
coordinate: coord
|
||
|
};
|
||
|
|
||
|
|
||
|
return taskOutput;
|
||
|
},
|
||
|
|
||
|
validateTask: function(taskInput, taskIndex, taskOutput) {
|
||
|
/*
|
||
|
* Reports whether the data corresponding to the current
|
||
|
* subtask (image) is valid (e.g. fully filled out)
|
||
|
*
|
||
|
* taskInput - the task data returned from loadTasks
|
||
|
* taskIndex - the number of the current subtask (image)
|
||
|
* taskOutput - a partially filled out object containing the results (so far) of the task.
|
||
|
*
|
||
|
* returns: falsey value if validation passed for the taskIndex-th subjtask.
|
||
|
* Truthy value if validation failed. To display a specific error message,
|
||
|
* return an object of the form {errorMessage: ""}
|
||
|
*/
|
||
|
|
||
|
// Answer rejection
|
||
|
/*if(ANSWERS.length>=5){
|
||
|
for(var i = 0; i < ANSWERS.length-5; i++) {
|
||
|
if(ANSWERS[i]==ANSWERS[i+1]&ANSWERS[i]==ANSWERS[i+3]&ANSWERS[i]==ANSWERS[i+2]&ANSWERS[i]==ANSWERS[i+4]){
|
||
|
return {errorMessage: "Sorry, you are not allowed to continue."};
|
||
|
}
|
||
|
}
|
||
|
}*/
|
||
|
|
||
|
// keep track of scores
|
||
|
var validCode = taskOutput[taskIndex].isValidCode;
|
||
|
SCORES.INVALID += !validCode;
|
||
|
var correctTrial;
|
||
|
if (isSentinel(taskInput[taskIndex])) {
|
||
|
SCORES.SENTINEL_TOTAL += 1;
|
||
|
var codeEntered = taskOutput[taskIndex].rememberedCode;
|
||
|
var correctCodes = taskInput[taskIndex].correct_codes;
|
||
|
if (!correctCodes) throw new Error("Correct codes were not provided in the subject file!");
|
||
|
var gotSentinel = _includes(correctCodes, codeEntered);
|
||
|
SCORES.SENTINEL_CORRECT += gotSentinel;
|
||
|
correctTrial = gotSentinel;
|
||
|
} else {
|
||
|
SCORES.IMAGE_TOTAL += 1;
|
||
|
SCORES.IMAGE_CORRECT += validCode;
|
||
|
correctTrial = validCode;
|
||
|
}
|
||
|
MESSAGE = correctTrial ? POSITIVE_MESSAGE : NEGATIVE_MESSAGE;
|
||
|
MESSAGE_IS_POSITIVE = correctTrial;
|
||
|
console.log('Invalid scores answered so far:', SCORES.INVALID);
|
||
|
|
||
|
return false; // we'll allow the task to continue either way but we'll remember if an invalid code was entered
|
||
|
},
|
||
|
|
||
|
getUploadPayload: function(taskOutputs) {
|
||
|
/*
|
||
|
* Returns the final output object to be saved from the task.
|
||
|
*
|
||
|
* taskInput - the task data returned from loadTasks
|
||
|
* taskOutput - a fully filled out object containing the results of the task.
|
||
|
*
|
||
|
* returns: all the data you want to be stored from the task.
|
||
|
*/
|
||
|
|
||
|
var endTime = new Date();
|
||
|
|
||
|
// compile timing data
|
||
|
var timing = {
|
||
|
openTime: OPEN_TIME,
|
||
|
loadTime: LOAD_TIME,
|
||
|
startTime: START_TIME,
|
||
|
endTime: endTime,
|
||
|
timeToCompleteFromOpenMsec: endTime - OPEN_TIME,
|
||
|
timeToLoadMsec: LOAD_TIME - OPEN_TIME,
|
||
|
timeToCompleteFromStartMsec: endTime - START_TIME
|
||
|
}
|
||
|
|
||
|
// put the survey data under a separate key
|
||
|
var surveyData = taskOutputs.survey_data;
|
||
|
delete taskOutputs.survey_data;
|
||
|
|
||
|
// task outputs to store, including timing data
|
||
|
var new_taskoutputs = {};
|
||
|
for (var i = 0; i < TARGET_NUM; i++) {
|
||
|
new_taskoutputs[i] = taskOutputs[i];
|
||
|
}
|
||
|
var outputs = {
|
||
|
timing: timing,
|
||
|
surveyData: surveyData,
|
||
|
tasks: new_taskoutputs,
|
||
|
qa_answers: ANSWERS,
|
||
|
re_answers: RECO_ANSWERS
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
'outputs': outputs
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
getPayload: function(taskInputs, taskOutputs) {
|
||
|
/*
|
||
|
* Returns the final output object to be saved from the task.
|
||
|
*
|
||
|
* taskInput - the task data returned from loadTasks
|
||
|
* taskOutput - a fully filled out object containing the results of the task.
|
||
|
*
|
||
|
* returns: all the data you want to be stored from the task.
|
||
|
*/
|
||
|
|
||
|
// delete codes and coordinates from taskInputs to save space
|
||
|
taskInputs.forEach(function(elt, i) {
|
||
|
delete elt.codes;
|
||
|
delete elt.coordinates;
|
||
|
})
|
||
|
|
||
|
// task inputs to store, including parameters used
|
||
|
var inputs = {
|
||
|
params: PARAMS,
|
||
|
tasks: taskInputs,
|
||
|
subjId: SUBJ_ID,
|
||
|
bucketNum: BUCKET_NUM,
|
||
|
subjFilePath: SUBJ_FILE_PATH
|
||
|
}
|
||
|
|
||
|
var endTime = new Date();
|
||
|
|
||
|
// compile timing data
|
||
|
var timing = {
|
||
|
openTime: OPEN_TIME,
|
||
|
loadTime: LOAD_TIME,
|
||
|
startTime: START_TIME,
|
||
|
endTime: endTime,
|
||
|
timeToCompleteFromOpenMsec: endTime - OPEN_TIME,
|
||
|
timeToLoadMsec: LOAD_TIME - OPEN_TIME,
|
||
|
timeToCompleteFromStartMsec: endTime - START_TIME
|
||
|
}
|
||
|
|
||
|
// put the survey data under a separate key
|
||
|
var surveyData = taskOutputs.survey_data;
|
||
|
delete taskOutputs.survey_data;
|
||
|
|
||
|
// task outputs to store, including timing data
|
||
|
var new_taskoutputs = {};
|
||
|
for (var i = 0; i < TARGET_NUM; i++) {
|
||
|
new_taskoutputs[i] = taskOutputs[i];
|
||
|
}
|
||
|
var outputs = {
|
||
|
timing: timing,
|
||
|
surveyData: surveyData,
|
||
|
tasks: new_taskoutputs,
|
||
|
qa_answers: ANSWERS,
|
||
|
re_answers: RECO_ANSWERS
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
'inputs': inputs,
|
||
|
'outputs': outputs
|
||
|
}
|
||
|
|
||
|
},
|
||
|
updateAnswers: function(qa_counter) {
|
||
|
if (qa_counter == 1) {
|
||
|
ANSWER_TMP = [];
|
||
|
}
|
||
|
console.log(ANSWER_TMP);
|
||
|
var ans = $('input:radio[name="answer"]:checked').val();
|
||
|
console.log(ans);
|
||
|
ANSWER_TMP.push(ans);
|
||
|
if (qa_counter == 5) {
|
||
|
ANSWERS = ANSWER_TMP;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
recoAnswers: function() {
|
||
|
console.log(RECO_ANSWERS);
|
||
|
var ans = $('input:radio[name="re-answer"]:checked').val();
|
||
|
console.log(ans);
|
||
|
if (ans == "1" || ans == "2") {
|
||
|
RECO_ANSWERS.push(ans);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function passedTutorial() {
|
||
|
/* Returns false if the subject has failed the tutorial, else true.*/
|
||
|
var failedValidCheck = SCORES.INVALID > PARAMS.MAX_INVALID_ALLOWED_TUTORIAL;
|
||
|
var failedSentinelCheck = (SCORES.SENTINEL_TOTAL - SCORES.SENTINEL_CORRECT) > PARAMS.MAX_INCORRECT_SENTINELS_ALLOWED_TUTORIAL;
|
||
|
// check accuracy
|
||
|
if (failedValidCheck || failedSentinelCheck) {
|
||
|
$('.subtask').hide();
|
||
|
$('#next-button').hide();
|
||
|
$('#qa-button').hide();
|
||
|
$('#nq-button').hide();
|
||
|
$("#reco-button").hide();
|
||
|
$("#accuracy-error-message").show();
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function showFixationCross(taskInput, taskIndex, taskOutput) {
|
||
|
/* Displays the fixation cross on the screen and queues the next target image. */
|
||
|
$('.subtask').hide();
|
||
|
$("#show-cross-subtask").show();
|
||
|
setTimeout(showImage.bind(this, taskInput, taskIndex, taskOutput), PARAMS.NUM_MSEC_CROSS);
|
||
|
}
|
||
|
|
||
|
function showImage(taskInput, taskIndex, taskOutput) {
|
||
|
/* Displays a target image on the screen and queues the corresponding code chart. */
|
||
|
console.log(taskIndex);
|
||
|
//console.log(ANSWERS);
|
||
|
$('#recall-subtask').hide();
|
||
|
$('.subtask').hide();
|
||
|
var imgFile = IMAGES[taskIndex].src;
|
||
|
if (!imgFile.includes('blur')) {
|
||
|
$("#img-blur-box").hide();
|
||
|
$("#img-main-box").show();
|
||
|
$("#img-main").attr("src", imgFile);
|
||
|
} else {
|
||
|
$("#img-blur-box").show();
|
||
|
$("#img-main-box").hide();
|
||
|
$("#img-blur").attr("src", imgFile);
|
||
|
}
|
||
|
|
||
|
$("#show-image-subtask").show();
|
||
|
|
||
|
var nSecs = isSentinel(taskInput[taskIndex]) ? PARAMS.NUM_MSEC_SENTINEL : PARAMS.NUM_MSEC_IMAGE;
|
||
|
if (taskIndex < TARGET_NUM) {
|
||
|
if (taskIndex === TARGET_NUM - 1) {
|
||
|
PARAMS.NUM_MSEC_IMAGE = 2000; //for recall stage
|
||
|
}
|
||
|
if (!imgFile.includes('blur'))
|
||
|
setTimeout(function() {
|
||
|
$(".subtask").hide();
|
||
|
$("#next-button").show()
|
||
|
}, nSecs);
|
||
|
// load QA
|
||
|
else {
|
||
|
$('#question-answer-subtask').show();
|
||
|
$('#qa-button').hide();
|
||
|
$('#next-button').hide();
|
||
|
$("#reco-button").hide();
|
||
|
$('#show-image-subtask').show();
|
||
|
$('#nq-button').show();
|
||
|
|
||
|
$('#question').html(taskInput[taskIndex].QA.Q1.question);
|
||
|
$('label[for=A]').html(taskInput[taskIndex].QA.Q1.A);
|
||
|
$('label[for=B]').html(taskInput[taskIndex].QA.Q1.B);
|
||
|
$('label[for=C]').html(taskInput[taskIndex].QA.Q1.C);
|
||
|
}
|
||
|
} else {
|
||
|
setTimeout(function() { recall_QA(taskInput, taskIndex, taskOutput); }, PARAMS.NUM_MSEC_IMAGE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function recall_QA(taskInput, taskIndex, taskOutput) {
|
||
|
console.log("Recall_QA start");
|
||
|
|
||
|
$('#question-answer-subtask').hide();
|
||
|
$('#qa-button').hide();
|
||
|
$('#show-image-subtask').hide();
|
||
|
$('#nq-button').hide();
|
||
|
$('#next-button').show();
|
||
|
$('#recall-subtask').show();
|
||
|
custom.recoAnswers();
|
||
|
}
|
||
|
|
||
|
function preloadImages(data) {
|
||
|
/*
|
||
|
* Loads all images for the task so they are ready to go when the user starts.
|
||
|
*
|
||
|
* Shows a progress bar and disables the button to start the task until all
|
||
|
* images are loaded.
|
||
|
*
|
||
|
* data: task data loaded from a subject file.
|
||
|
*/
|
||
|
var continueButton = $(".instruction-button");
|
||
|
_disable(continueButton);
|
||
|
|
||
|
var cross = new Image(); // fixation cross image
|
||
|
|
||
|
// populates arrays to store the Image elements
|
||
|
data.forEach(function(elt, i) {
|
||
|
IMAGES.push(new Image());
|
||
|
});
|
||
|
|
||
|
|
||
|
// callback for when all images have loaded
|
||
|
var imLoadCallback = function() {
|
||
|
console.log("done loading");
|
||
|
_enable(continueButton);
|
||
|
|
||
|
// Once you have loaded the images and know the size, set the correct display dimensions.
|
||
|
// Assumes all images have the same height/width
|
||
|
var shouldConstrainHeight = IMAGES[0].height / IMAGES[0].width > PARAMS.IMG_HEIGHT / PARAMS.IMG_WIDTH;
|
||
|
if (shouldConstrainHeight) {
|
||
|
$(".img-box").height(PARAMS.IMG_HEIGHT);
|
||
|
} else {
|
||
|
$(".img-box").width(PARAMS.IMG_WIDTH);
|
||
|
}
|
||
|
$(".img-blur-box").height(PARAMS.IMG_HEIGHT / 2);
|
||
|
}
|
||
|
// callback for every time a single image loads
|
||
|
var imProgressCallback = function(imsLoaded, totalImsToLoad) {
|
||
|
$("#im-load-progress").progress({ 'percent': imsLoaded / totalImsToLoad * 100 })
|
||
|
}
|
||
|
onAllImagesLoaded(IMAGES.concat([cross]), imProgressCallback, imLoadCallback);
|
||
|
|
||
|
// start images loading
|
||
|
cross.src = FIXATION_CROSS;
|
||
|
$("#img-cross").attr("src", FIXATION_CROSS);
|
||
|
data.forEach(function(elt, i) {
|
||
|
IMAGES[i].src = IMAGE_BASE_PATH + elt.image;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function gupOrRandom(name, num) {
|
||
|
/* Returns the value for the key `name` in the querystring if it exists,
|
||
|
* else an int n s.t. 0 <= n < num. */
|
||
|
var qs = gup(name);
|
||
|
return qs.length > 0 ? parseInt(qs) : Math.floor(Math.random() * num);
|
||
|
}
|
||
|
|
||
|
function gup(name) {
|
||
|
/* Searches for a querystring with key name. Returns the value or "". */
|
||
|
var regexS = "[\\?&]" + name + "=([^&#]*)";
|
||
|
var regex = new RegExp(regexS);
|
||
|
var tmpURL = window.location.href;
|
||
|
var results = regex.exec(tmpURL);
|
||
|
if (results == null) return "";
|
||
|
else return results[1];
|
||
|
}
|
||
|
|
||
|
function clickButtonOnEnter(inputElt, buttonToClick) {
|
||
|
/* Set up a binding between the enter key and a specific button.
|
||
|
*
|
||
|
* If the user enters the enter key into `inputElt`, `buttonToClick`
|
||
|
* will be clicked.
|
||
|
*/
|
||
|
inputElt.unbind();
|
||
|
inputElt.focus();
|
||
|
|
||
|
// wait a little bit before re-adding the callback, otherwise
|
||
|
// the callback could fire twice
|
||
|
setTimeout(function() {
|
||
|
inputElt.keyup(function(event) {
|
||
|
if (event.keyCode === 13) { // 13 is the enter key
|
||
|
buttonToClick.click();
|
||
|
}
|
||
|
});
|
||
|
}, 500);
|
||
|
}
|
||
|
|
||
|
function onAllImagesLoaded(imgs, progressCallback, callback) {
|
||
|
/*
|
||
|
* Registers callbacks for when certain images are fully and partially loaded.
|
||
|
*
|
||
|
* This must be called BEFORE setting the `src` elements on imgs are set,
|
||
|
* else the callback could be lost.
|
||
|
*
|
||
|
* imgs: an array of Image objects to watch. They should not have already started
|
||
|
* loading, i.e. the `src` attribute should not yet be set.
|
||
|
* progressCallback: Callback to be called every time an image loads. Takes two args:
|
||
|
* the first is the number of images that have already loaded, the second is the total
|
||
|
* number of images that will be loaded.
|
||
|
* callback: Callback to be called when all images are loaded. Takes no args.
|
||
|
*
|
||
|
* returns: null
|
||
|
*/
|
||
|
var imsToLoad = imgs.length;
|
||
|
var numImsLoaded = 0;
|
||
|
|
||
|
var incrementProgress = function() {
|
||
|
numImsLoaded++;
|
||
|
progressCallback(numImsLoaded, imsToLoad);
|
||
|
if (numImsLoaded == imsToLoad) {
|
||
|
callback();
|
||
|
LOAD_TIME = new Date();
|
||
|
console.log("Time to load secs", (LOAD_TIME - OPEN_TIME) / 1000)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var successHandler = function() {
|
||
|
console.log("loaded an image");
|
||
|
incrementProgress();
|
||
|
}
|
||
|
|
||
|
var errorHandler = function(event) {
|
||
|
console.log("Error!");
|
||
|
}
|
||
|
|
||
|
imgs.forEach(function(elt, i) {
|
||
|
elt.onload = successHandler;
|
||
|
elt.onerror = errorHandler;
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function _includes(arr, elt) {
|
||
|
/* Checks if array `arr` contains element `elt`. */
|
||
|
var idx = $.inArray(elt, arr);
|
||
|
return idx != -1;
|
||
|
}
|
||
|
|
||
|
function _disable(button) {
|
||
|
/* Disables button `button`. */
|
||
|
button.addClass('disabled');
|
||
|
}
|
||
|
|
||
|
function _enable(button) {
|
||
|
/* Enables button `button`. */
|
||
|
button.removeClass('disabled');
|
||
|
}
|
||
|
|
||
|
function isTutorial(subtask) {
|
||
|
/* Checks if `subtask` is part of the tutorial or not. */
|
||
|
return subtask.flag == "tutorial_real" || subtask.flag == "tutorial_sentinel";
|
||
|
}
|
||
|
|
||
|
function didEndTutorial(taskInput, taskIndex, taskOutput) {
|
||
|
/* Checks if the tutorial just finished. */
|
||
|
return !isTutorial(taskInput[taskIndex]) && (taskIndex > 0 ? isTutorial(taskInput[taskIndex - 1]) : true);
|
||
|
}
|
||
|
|
||
|
function isSentinel(subtask) {
|
||
|
/* Checks if this subtask corresponds to a sentinel image. */
|
||
|
return subtask.flag == "tutorial_sentinel" || subtask.flag == "sentinel";
|
||
|
}
|