visrecall/WebInterface/Front-end/assets/js/custom.js

624 lines
No EOL
22 KiB
JavaScript
Executable file

/* 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";
}