/* 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('
'); $("#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"; }