feat: update web Interface
This commit is contained in:
parent
d5d633b6c7
commit
f9844dba10
1111 changed files with 171093 additions and 0 deletions
624
WebInterface/Front-end/assets/js/custom.js
Executable file
624
WebInterface/Front-end/assets/js/custom.js
Executable file
|
@ -0,0 +1,624 @@
|
|||
/* 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";
|
||||
}
|
325
WebInterface/Front-end/assets/js/debugout.js
Normal file
325
WebInterface/Front-end/assets/js/debugout.js
Normal file
|
@ -0,0 +1,325 @@
|
|||
/*
|
||||
|
||||
debugout.js
|
||||
by @inorganik
|
||||
|
||||
*/
|
||||
|
||||
// save all the console.logs
|
||||
function debugout() {
|
||||
var self = this;
|
||||
|
||||
// OPTIONS
|
||||
self.realTimeLoggingOn = true; // log in real time (forwards to console.log)
|
||||
self.useTimestamps = false; // insert a timestamp in front of each log
|
||||
self.useLocalStorage = false; // store the output using window.localStorage() and continuously add to the same log each session
|
||||
self.recordLogs = true; // set to false after you're done debugging to avoid the log eating up memory
|
||||
self.autoTrim = true; // to avoid the log eating up potentially endless memory
|
||||
self.maxLines = 5000; // if autoTrim is true, this many most recent lines are saved
|
||||
self.tailNumLines = 100; // how many lines tail() will retrieve
|
||||
self.logFilename = 'debugout'; // filename of log downloaded with downloadLog()
|
||||
self.maxDepth = 25; // max recursion depth for logged objects
|
||||
|
||||
// vars
|
||||
self.depth = 0;
|
||||
self.parentSizes = [0];
|
||||
self.currentResult = '';
|
||||
self.startTime = new Date();
|
||||
self.output = '';
|
||||
|
||||
this.version = function() { return '0.5.0' }
|
||||
|
||||
/*
|
||||
USER METHODS
|
||||
*/
|
||||
this.getLog = function() {
|
||||
var retrievalTime = new Date();
|
||||
// if recording is off, so dev knows why they don't have any logs
|
||||
if (!self.recordLogs) {
|
||||
self.log('[debugout.js] log recording is off.');
|
||||
}
|
||||
// if using local storage, get values
|
||||
if (self.useLocalStorage) {
|
||||
var saved = window.localStorage.getItem('debugout.js');
|
||||
if (saved) {
|
||||
saved = JSON.parse(saved);
|
||||
self.startTime = new Date(saved.startTime);
|
||||
self.output = saved.log;
|
||||
retrievalTime = new Date(saved.lastLog);
|
||||
}
|
||||
}
|
||||
return self.output +
|
||||
'\n---- Log retrieved: ' + retrievalTime + ' ----\n' +
|
||||
self.formatSessionDuration(self.startTime, retrievalTime);
|
||||
}
|
||||
// accepts optional number or uses the default for number of lines
|
||||
this.tail = function(numLines) {
|
||||
var numLines = numLines || self.tailLines;
|
||||
return self.trimLog(self.getLog(), numLines);
|
||||
}
|
||||
// accepts a string to search for
|
||||
this.search = function(string) {
|
||||
var lines = self.output.split('\n');
|
||||
var rgx = new RegExp(string);
|
||||
var matched = [];
|
||||
// can't use a simple Array.prototype.filter() here
|
||||
// because we need to add the line number
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var addr = '[' + i + '] ';
|
||||
if (lines[i].match(rgx)) {
|
||||
matched.push(addr + lines[i]);
|
||||
}
|
||||
}
|
||||
var result = matched.join('\n');
|
||||
if (result.length == 0) result = 'Nothing found for "' + string + '".';
|
||||
return result
|
||||
}
|
||||
// accepts the starting line and how many lines after the starting line you want
|
||||
this.getSlice = function(lineNumber, numLines) {
|
||||
var lines = self.output.split('\n');
|
||||
var segment = lines.slice(lineNumber, lineNumber + numLines);
|
||||
return segment.join('\n');
|
||||
}
|
||||
// immediately downloads the log - for desktop browser use
|
||||
this.downloadLog = function() {
|
||||
var logFile = self.getLog();
|
||||
var blob = new Blob([logFile], { type: 'data:text/plain;charset=utf-8' });
|
||||
var a = document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.target = '_blank';
|
||||
date = new Date();
|
||||
a.download = self.logFilename + date.getTime() + '.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(a.href);
|
||||
}
|
||||
// clears the log
|
||||
this.clear = function() {
|
||||
var clearTime = new Date();
|
||||
self.output = '---- Log cleared: ' + clearTime + ' ----\n';
|
||||
if (self.useLocalStorage) {
|
||||
// local storage
|
||||
var saveObject = {
|
||||
startTime: self.startTime,
|
||||
log: self.output,
|
||||
lastLog: clearTime
|
||||
}
|
||||
saveObject = JSON.stringify(saveObject);
|
||||
window.localStorage.setItem('debugout.js', saveObject);
|
||||
}
|
||||
if (self.realTimeLoggingOn) console.log('[debugout.js] clear()');
|
||||
}
|
||||
// records a log
|
||||
this.log = function(obj) {
|
||||
// log in real time
|
||||
if (self.realTimeLoggingOn) console.log(obj);
|
||||
// record log
|
||||
var type = self.determineType(obj);
|
||||
if (type != null && self.recordLogs) {
|
||||
var addition = self.formatType(type, obj);
|
||||
// timestamp, formatted for brevity
|
||||
if (self.useTimestamps) {
|
||||
var logTime = new Date();
|
||||
self.output += self.formatTimestamp(logTime);
|
||||
}
|
||||
self.output += addition + '\n';
|
||||
if (self.autoTrim) self.output = self.trimLog(self.output, self.maxLines);
|
||||
// local storage
|
||||
if (self.useLocalStorage) {
|
||||
var last = new Date();
|
||||
var saveObject = {
|
||||
startTime: self.startTime,
|
||||
log: self.output,
|
||||
lastLog: last
|
||||
}
|
||||
saveObject = JSON.stringify(saveObject);
|
||||
window.localStorage.setItem('debugout.js', saveObject);
|
||||
}
|
||||
}
|
||||
self.depth = 0;
|
||||
self.parentSizes = [0];
|
||||
self.currentResult = '';
|
||||
}
|
||||
/*
|
||||
METHODS FOR CONSTRUCTING THE LOG
|
||||
*/
|
||||
|
||||
// like typeof but classifies objects of type 'object'
|
||||
// kept separate from formatType() so you can use at your convenience!
|
||||
this.determineType = function(object) {
|
||||
if (object != null) {
|
||||
var typeResult;
|
||||
var type = typeof object;
|
||||
if (type == 'object') {
|
||||
var len = object.length;
|
||||
if (len == null) {
|
||||
if (typeof object.getTime == 'function') {
|
||||
typeResult = 'Date';
|
||||
} else if (typeof object.test == 'function') {
|
||||
typeResult = 'RegExp';
|
||||
} else {
|
||||
typeResult = 'Object';
|
||||
}
|
||||
} else {
|
||||
typeResult = 'Array';
|
||||
}
|
||||
} else {
|
||||
typeResult = type;
|
||||
}
|
||||
return typeResult;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// format type accordingly, recursively if necessary
|
||||
this.formatType = function(type, obj) {
|
||||
if (self.maxDepth && self.depth >= self.maxDepth) {
|
||||
return '... (max-depth reached)';
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'Object':
|
||||
self.currentResult += '{\n';
|
||||
self.depth++;
|
||||
self.parentSizes.push(self.objectSize(obj));
|
||||
var i = 0;
|
||||
for (var prop in obj) {
|
||||
self.currentResult += self.indentsForDepth(self.depth);
|
||||
self.currentResult += prop + ': ';
|
||||
var subtype = self.determineType(obj[prop]);
|
||||
var subresult = self.formatType(subtype, obj[prop]);
|
||||
if (subresult) {
|
||||
self.currentResult += subresult;
|
||||
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ',';
|
||||
self.currentResult += '\n';
|
||||
} else {
|
||||
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ',';
|
||||
self.currentResult += '\n';
|
||||
}
|
||||
i++;
|
||||
}
|
||||
self.depth--;
|
||||
self.parentSizes.pop();
|
||||
self.currentResult += self.indentsForDepth(self.depth);
|
||||
self.currentResult += '}';
|
||||
if (self.depth == 0) return self.currentResult;
|
||||
break;
|
||||
case 'Array':
|
||||
self.currentResult += '[';
|
||||
self.depth++;
|
||||
self.parentSizes.push(obj.length);
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
var subtype = self.determineType(obj[i]);
|
||||
if (subtype == 'Object' || subtype == 'Array') self.currentResult += '\n' + self.indentsForDepth(self.depth);
|
||||
var subresult = self.formatType(subtype, obj[i]);
|
||||
if (subresult) {
|
||||
self.currentResult += subresult;
|
||||
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ', ';
|
||||
if (subtype == 'Array') self.currentResult += '\n';
|
||||
} else {
|
||||
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ', ';
|
||||
if (subtype != 'Object') self.currentResult += '\n';
|
||||
else if (i == self.parentSizes[self.depth] - 1) self.currentResult += '\n';
|
||||
}
|
||||
}
|
||||
self.depth--;
|
||||
self.parentSizes.pop();
|
||||
self.currentResult += ']';
|
||||
if (self.depth == 0) return self.currentResult;
|
||||
break;
|
||||
case 'function':
|
||||
obj += '';
|
||||
var lines = obj.split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].match(/\}/)) self.depth--;
|
||||
self.currentResult += self.indentsForDepth(self.depth);
|
||||
if (lines[i].match(/\{/)) self.depth++;
|
||||
self.currentResult += lines[i] + '\n';
|
||||
}
|
||||
return self.currentResult;
|
||||
break;
|
||||
case 'RegExp':
|
||||
return '/' + obj.source + '/';
|
||||
break;
|
||||
case 'Date':
|
||||
case 'string':
|
||||
if (self.depth > 0 || obj.length == 0) {
|
||||
return '"' + obj + '"';
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
case 'boolean':
|
||||
if (obj) return 'true';
|
||||
else return 'false';
|
||||
case 'number':
|
||||
return obj + '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.indentsForDepth = function(depth) {
|
||||
var str = '';
|
||||
for (var i = 0; i < depth; i++) {
|
||||
str += '\t';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
this.trimLog = function(log, maxLines) {
|
||||
var lines = log.split('\n');
|
||||
if (lines.length > maxLines) {
|
||||
lines = lines.slice(lines.length - maxLines);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
this.lines = function() {
|
||||
return self.output.split('\n').length;
|
||||
}
|
||||
// calculate testing time
|
||||
this.formatSessionDuration = function(startTime, endTime) {
|
||||
var msec = endTime - startTime;
|
||||
var hh = Math.floor(msec / 1000 / 60 / 60);
|
||||
var hrs = ('0' + hh).slice(-2);
|
||||
msec -= hh * 1000 * 60 * 60;
|
||||
var mm = Math.floor(msec / 1000 / 60);
|
||||
var mins = ('0' + mm).slice(-2);
|
||||
msec -= mm * 1000 * 60;
|
||||
var ss = Math.floor(msec / 1000);
|
||||
var secs = ('0' + ss).slice(-2);
|
||||
msec -= ss * 1000;
|
||||
return '---- Session duration: ' + hrs + ':' + mins + ':' + secs + ' ----'
|
||||
}
|
||||
this.formatTimestamp = function(timestamp) {
|
||||
var year = timestamp.getFullYear();
|
||||
var date = timestamp.getDate();
|
||||
var month = ('0' + (timestamp.getMonth() + 1)).slice(-2);
|
||||
var hrs = Number(timestamp.getHours());
|
||||
var mins = ('0' + timestamp.getMinutes()).slice(-2);
|
||||
var secs = ('0' + timestamp.getSeconds()).slice(-2);
|
||||
return '[' + year + '-' + month + '-' + date + ' ' + hrs + ':' + mins + ':' + secs + ']: ';
|
||||
}
|
||||
this.objectSize = function(obj) {
|
||||
var size = 0,
|
||||
key;
|
||||
for (key in obj) {
|
||||
if (obj.hasOwnProperty(key)) size++;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/*
|
||||
START/RESUME LOG
|
||||
*/
|
||||
if (self.useLocalStorage) {
|
||||
var saved = window.localStorage.getItem('debugout.js');
|
||||
if (saved) {
|
||||
saved = JSON.parse(saved);
|
||||
self.output = saved.log;
|
||||
var start = new Date(saved.startTime);
|
||||
var end = new Date(saved.lastLog);
|
||||
self.output += '\n---- Session end: ' + saved.lastLog + ' ----\n';
|
||||
self.output += self.formatSessionDuration(start, end);
|
||||
self.output += '\n\n';
|
||||
}
|
||||
}
|
||||
self.output += '---- Session started: ' + self.startTime + ' ----\n\n';
|
||||
}
|
115
WebInterface/Front-end/assets/js/demo_survey.js
Executable file
115
WebInterface/Front-end/assets/js/demo_survey.js
Executable file
|
@ -0,0 +1,115 @@
|
|||
var demoSurvey = {
|
||||
maybeLoadSurvey: function(config) {
|
||||
if (config.advanced.includeDemographicSurvey) {
|
||||
console.log("loading demo survey");
|
||||
$('#demo-survey').load("assets/html/demo_survey.html");
|
||||
$('#demo-survey').hide();
|
||||
$('#feedback-field').hide();
|
||||
}
|
||||
},
|
||||
hideSurvey: function() {
|
||||
$('#demo-survey').hide();
|
||||
},
|
||||
showTask: function() {
|
||||
|
||||
// make sure to hide experiment: use the appropriate div references (or add div wrappers) to hide the previous task elements
|
||||
//$('#custom-experiment').hide();
|
||||
$(".subtask").hide();
|
||||
// -----------------------
|
||||
|
||||
$('#demo-survey').show();
|
||||
|
||||
// Rules for collecting demographic survey data
|
||||
$('#survey-form')
|
||||
.form({
|
||||
fields: {
|
||||
gender: {
|
||||
identifier: 'gender',
|
||||
rules: [{
|
||||
type: 'checked',
|
||||
prompt: 'Please select a gender'
|
||||
}]
|
||||
},
|
||||
ageGroup: {
|
||||
identifier: 'ageGroup',
|
||||
rules: [{
|
||||
type: 'checked',
|
||||
prompt: 'Please select an age group'
|
||||
}]
|
||||
},
|
||||
ethnicity: {
|
||||
identifier: 'ethnicity',
|
||||
rules: [{
|
||||
type: 'checked',
|
||||
prompt: 'Please select an ethnicity'
|
||||
}]
|
||||
},
|
||||
education: {
|
||||
identifier: 'education',
|
||||
rules: [{
|
||||
type: 'checked',
|
||||
prompt: 'Please select an education level'
|
||||
}]
|
||||
},
|
||||
vizExperience: {
|
||||
identifier: 'vizExperience',
|
||||
rules: [{
|
||||
type: 'checked',
|
||||
prompt: 'Please select your experience with visualizations'
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$("input:checkbox[name=ethnicity]").change(function() {
|
||||
var unspecified = $("#ethnicUnspecified").is(":checked");
|
||||
if (unspecified) {
|
||||
$("input:checkbox[name=ethnicity]").not("#ethnicUnspecified")
|
||||
.prop("checked", false);
|
||||
$(".ethnicityOption").addClass("disabled");
|
||||
} else {
|
||||
$(".ethnicityOption").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
},
|
||||
collectData: function() {
|
||||
var gender = $("input[type=radio][name=gender]:checked").val();
|
||||
var ageGroup = $("input[type=radio][name=ageGroup]:checked").val();
|
||||
var ethnicity = $("input[type=checkbox][name=ethnicity]:checked").val();
|
||||
var education = $("input[type=radio][name=education]:checked").val();
|
||||
var vizExperience = $("input[type=radio][name=vizExperience]:checked").val();
|
||||
var feedback = htmlEscape($("textarea[name=feedback]").val());
|
||||
|
||||
var data = {
|
||||
gender: gender,
|
||||
ageGroup: ageGroup,
|
||||
ethnicity: ethnicity,
|
||||
education: education,
|
||||
vizExperience: vizExperience,
|
||||
feedback: feedback
|
||||
};
|
||||
|
||||
return {
|
||||
survey_data: data
|
||||
};
|
||||
},
|
||||
validateTask: function() {
|
||||
console.log("validating demographic survey");
|
||||
$('#survey-form').form('validate form');
|
||||
// falsey value indicates no error...
|
||||
if (!$('#survey-form').form('is valid')) {
|
||||
return {errorMessage: ""}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function htmlEscape(str) {
|
||||
/* Html-escape a sensitive string. */
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
500
WebInterface/Front-end/assets/js/main.js
Executable file
500
WebInterface/Front-end/assets/js/main.js
Executable file
|
@ -0,0 +1,500 @@
|
|||
var qa_counter = 1;
|
||||
var reco_flag = 0;
|
||||
|
||||
var config = {};
|
||||
|
||||
var state = {
|
||||
taskIndex: gup("skipto") ? parseInt(gup("skipto")) : 0,
|
||||
taskInputs: {},
|
||||
taskOutputs: [],
|
||||
//assignmentId: gup("assignmentId"),
|
||||
assignmentId: Math.floor(Math.random() * 1000),
|
||||
//workerId: gup("workerId"),
|
||||
workerId: Math.floor(Math.random() * 1000),
|
||||
hitId: gup("hitId")
|
||||
};
|
||||
|
||||
// Debug log
|
||||
var bugout = new debugout();
|
||||
var rectBugout = new debugout();
|
||||
|
||||
/* HELPERS */
|
||||
function saveTaskData() {
|
||||
var data;
|
||||
if (isDemoSurvey()) {
|
||||
data = demoSurvey.collectData();
|
||||
} else {
|
||||
data = custom.collectData(getTaskInputs(state.taskIndex), state.taskIndex, getTaskOutputs(state.taskIndex));
|
||||
}
|
||||
if (config.meta.aggregate) {
|
||||
$.extend(state.taskOutputs, data);
|
||||
} else {
|
||||
state.taskOutputs[state.taskIndex] = data;
|
||||
}
|
||||
}
|
||||
|
||||
function getTaskInputs(i) {
|
||||
return config.meta.aggregate ? state.taskInputs : state.taskInputs[i];
|
||||
}
|
||||
``
|
||||
|
||||
function getTaskOutputs(i) {
|
||||
return config.meta.aggregate ? state.taskOutputs : state.taskOutputs[i];
|
||||
}
|
||||
|
||||
function updateTask() {
|
||||
//console.log(state.taskIndex);
|
||||
if (config.advanced.hideIfNotAccepted && hideIfNotAccepted()) {
|
||||
return;
|
||||
}
|
||||
$("#progress-bar").progress("set progress", state.taskIndex + 1);
|
||||
if (isDemoSurvey()) {
|
||||
demoSurvey.showTask();
|
||||
} else {
|
||||
// show the user's task
|
||||
demoSurvey.hideSurvey();
|
||||
$('#custom-experiment').show();
|
||||
custom.showTask(getTaskInputs(state.taskIndex), state.taskIndex, getTaskOutputs(state.taskIndex));
|
||||
}
|
||||
if (state.taskIndex == config.meta.numSubtasks + config.advanced.includeDemographicSurvey - 1) {
|
||||
// last page
|
||||
$("#next-button").hide();
|
||||
$("#qa-button").hide();
|
||||
$('#nq-button').hide();
|
||||
$("#reco-button").hide();
|
||||
if (state.taskIndex != 0) {
|
||||
$("#prev-button").removeClass("disabled");
|
||||
} else {
|
||||
$("#prev-button").addClass("disabled");
|
||||
}
|
||||
$("#submit-button").removeClass("disabled");
|
||||
$("#disclaimer").show();
|
||||
$("#final-task-fields").css("display", "block"); // added this to custom.js only on the last page (last subtask) of the last task
|
||||
// NOTE: comments in the above 2 lines only refer to the case where demographic survey is not shown
|
||||
} else if (state.taskIndex == 0) {
|
||||
// first page
|
||||
$("#next-button").removeClass("disabled");
|
||||
$("#prev-button").addClass("disabled");
|
||||
$("#submit-button").addClass("disabled");
|
||||
$("#final-task-fields").css("display", "none");
|
||||
$("#disclaimer").hide();
|
||||
} else {
|
||||
// intermediate page
|
||||
$("#next-button").removeClass("disabled");
|
||||
$("#prev-button").removeClass("disabled");
|
||||
$("#submit-button").addClass("disabled");
|
||||
$("#final-task-fields").css("display", "none");
|
||||
$("#disclaimer").hide();
|
||||
}
|
||||
}
|
||||
|
||||
function nextTask() {
|
||||
spanPosition();
|
||||
dd = new Date();
|
||||
bugout.log('next button clicked for task:' + state.taskIndex + ', at: ' + dd);
|
||||
bugout.log(dd.getTime());
|
||||
console.log("moving to next task");
|
||||
if (qa_counter == 5) {
|
||||
custom.updateAnswers(qa_counter);
|
||||
qa_counter = 1;
|
||||
}
|
||||
if (state.taskIndex == 79) {
|
||||
custom.recoAnswers();
|
||||
}
|
||||
if (state.taskIndex < (config.meta.numSubtasks + config.advanced.includeDemographicSurvey) - 1) {
|
||||
saveTaskData();
|
||||
|
||||
var failedValidation;
|
||||
if (isDemoSurvey()) {
|
||||
failedValidation = demoSurvey.validateTask();
|
||||
} else {
|
||||
failedValidation = custom.validateTask(getTaskInputs(state.taskIndex), state.taskIndex, getTaskOutputs(state.taskIndex));
|
||||
}
|
||||
|
||||
if (failedValidation == false) {
|
||||
state.taskIndex++;
|
||||
updateTask();
|
||||
clearMessage();
|
||||
console.log("Current collected data", state.taskOutputs);
|
||||
} else {
|
||||
generateMessage("negative", failedValidation.errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prevTask() {
|
||||
if (state.taskIndex > 0) {
|
||||
saveTaskData();
|
||||
state.taskIndex--;
|
||||
updateTask();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleInstructions() {
|
||||
dd = new Date();
|
||||
bugout.log('Entering recall stage: ' + dd);
|
||||
bugout.log(dd.getTime());
|
||||
rectBugout.log("id x y width height top right bottom left");
|
||||
|
||||
if ($("#experiment").css("display") == "none") {
|
||||
$("#experiment").css("display", "flex");
|
||||
$("#instructions").css("display", "none");
|
||||
$("#disclaimer").hide();
|
||||
updateTask();
|
||||
} else {
|
||||
saveTaskData();
|
||||
$("#experiment").css("display", "none");
|
||||
$("#instructions").css("display", "flex");
|
||||
$("#disclaimer").show();
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessage() {
|
||||
$("#message-field").html("");
|
||||
}
|
||||
|
||||
function generateMessage(cls, header) {
|
||||
clearMessage();
|
||||
if (!header) return;
|
||||
var messageStr = "<div class='ui message " + cls + "'>";
|
||||
messageStr += "<i class='close icon'></i>";
|
||||
messageStr += "<div class='header'>" + header + "</div></div>";
|
||||
|
||||
var newMessage = $(messageStr);
|
||||
$("#message-field").append(newMessage);
|
||||
newMessage.click(function() {
|
||||
$(this).closest(".message").transition("fade");
|
||||
});
|
||||
}
|
||||
|
||||
function addHiddenField(form, name, value) {
|
||||
// form is a jQuery object, name and value are strings
|
||||
var input = $("<input type='hidden' name='" + name + "' value=''>");
|
||||
input.val(value);
|
||||
form.append(input);
|
||||
}
|
||||
|
||||
function submitHIT() {
|
||||
console.log("submitting");
|
||||
|
||||
$("#copy-key-button").click(function() {
|
||||
selectText('submit-code');
|
||||
});
|
||||
|
||||
saveTaskData();
|
||||
clearMessage();
|
||||
$("#submit-button").addClass("loading");
|
||||
for (var i = 0; i < config.meta.numSubtasks; i++) {
|
||||
var failedValidation = custom.validateTask(getTaskInputs(i), i, getTaskOutputs(i));
|
||||
if (failedValidation) {
|
||||
cancelSubmit(failedValidation.errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (config.advanced.includeDemographicSurvey) {
|
||||
var failedValidation = demoSurvey.validateTask();
|
||||
if (failedValidation) {
|
||||
cancelSubmit(failedValidation.errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var results = custom.getUploadPayload(state.taskOutputs);
|
||||
var payload = {
|
||||
'assignmentId': state.assignmentId,
|
||||
'workerId': state.workerId,
|
||||
'hitId': state.hitId,
|
||||
//'tag': gup('tag'),
|
||||
'origin': state.origin,
|
||||
'results': results
|
||||
}
|
||||
|
||||
var submitUrl;
|
||||
if (config.advanced.externalSubmit) {
|
||||
submitUrl = config.advanced.externalSubmitUrl;
|
||||
externalSubmit(submitUrl, payload);
|
||||
} else {
|
||||
submitUrl = decodeURIComponent(gup("turkSubmitTo")) + "/mturk/externalSubmit";
|
||||
mturkSubmit(submitUrl, payload);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelSubmit(err) {
|
||||
console.log("cancelling submit");
|
||||
$("#submit-button").removeClass("loading");
|
||||
generateMessage("negative", err);
|
||||
}
|
||||
|
||||
function gup(name) {
|
||||
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];
|
||||
}
|
||||
|
||||
/* SETUP FUNCTIONS */
|
||||
function populateMetadata(config) {
|
||||
$(".meta-title").html(config.meta.title);
|
||||
$(".meta-desc").html(config.meta.description);
|
||||
$(".instructions-simple").html(config.instructions.simple);
|
||||
for (var i = 0; i < config.instructions.steps.length; i++) {
|
||||
$(".instructions-steps").append($("<li>" + config.instructions.steps[i] + "</li>"));
|
||||
}
|
||||
$(".disclaimer-text").html(config.meta.disclaimer);
|
||||
if (config.instructions.images.length > 0) {
|
||||
$("#sample-task").css("display", "block");
|
||||
var instructionsIndex = Math.floor(Math.random() * config.instructions.images.length);
|
||||
var imgEle = "<img class='instructions-img' src='";
|
||||
imgEle += config.instructions.images[instructionsIndex] + "'></img>";
|
||||
$("#instructions-demo").append($(imgEle));
|
||||
|
||||
}
|
||||
$("#progress-bar").progress({
|
||||
total: config.meta.numSubtasks + config.advanced.includeDemographicSurvey,
|
||||
});
|
||||
}
|
||||
|
||||
function setupButtons() {
|
||||
$("#next-button").click(nextTask);
|
||||
$("#prev-button").click(prevTask);
|
||||
$(".instruction-button").click(toggleInstructions);
|
||||
$("#submit-button").click(submitHIT);
|
||||
$("#qa-button").click(qaSubmit);
|
||||
$("#nq-button").click(qaSubmit);
|
||||
$("#reco-button").click(recognition_stage);
|
||||
if (state.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE") {
|
||||
$("#submit-button").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function recognition_stage() {
|
||||
spanPosition();
|
||||
dd = new Date();
|
||||
bugout.log('Entering recognition stage: ' + dd);
|
||||
bugout.log(dd.getTime());
|
||||
|
||||
if (qa_counter == 5) {
|
||||
custom.updateAnswers(qa_counter);
|
||||
qa_counter = 1;
|
||||
}
|
||||
$('#question-answer-subtask').hide();
|
||||
$('#show-image-subtask').hide();
|
||||
$('#reco-subtask').show();
|
||||
$('#qa-button').hide();
|
||||
$('#next-button').show();
|
||||
$('#nq-button').hide();
|
||||
$("#reco-button").hide();
|
||||
}
|
||||
|
||||
function updateQA() {
|
||||
custom.updateAnswers(qa_counter)
|
||||
$('#question').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].question);
|
||||
$('label[for=A]').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].A);
|
||||
$('label[for=B]').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].B);
|
||||
$('label[for=C]').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].C);
|
||||
}
|
||||
|
||||
function spanPosition() {
|
||||
// Span postion of answer
|
||||
var allQuestion = document.getElementById("question");
|
||||
var span = allQuestion.getElementsByTagName("span");
|
||||
var qSkip = true;
|
||||
for (j of span) {
|
||||
var rectQuestion = j.getBoundingClientRect();
|
||||
var rectQ = j.id + " ";
|
||||
for (var key in rectQuestion) {
|
||||
var item = rectQuestion[key];
|
||||
if (!isNaN(item) && item != 0) {
|
||||
rectQ = rectQ + item.toString() + " ";
|
||||
} else if (item == 0) {
|
||||
qSkip = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip recording data when the "next" button is clicked during encoding interface
|
||||
if (qSkip) {
|
||||
rectBugout.log(rectQ);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Span position of answer
|
||||
var allSelection = document.getElementsByClassName("selection");
|
||||
aSkip = true;
|
||||
for (i of allSelection) {
|
||||
var span = i.getElementsByTagName("span");
|
||||
for (j of span) {
|
||||
var rectAnswer = j.getBoundingClientRect();
|
||||
var rectA = j.id + " ";
|
||||
for (var key in rectAnswer) {
|
||||
var item = rectAnswer[key];
|
||||
if (!isNaN(item) && item != 0) {
|
||||
rectA = rectA + item.toString() + " ";
|
||||
} else if (item == 0) {
|
||||
aSkip = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip recording data when the "next" button is clicked during encoding interface
|
||||
if (aSkip) {
|
||||
rectBugout.log(rectA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function qaSubmit() {
|
||||
spanPosition();
|
||||
dd = new Date();
|
||||
bugout.log('QA button clicked, question No' + qa_counter + ': ' + dd);
|
||||
bugout.log(dd.getTime());
|
||||
if (state.taskIndex < TARGET_NUM && state.taskIndex % 4 > 1) {
|
||||
if (qa_counter <= 5) {
|
||||
qa_counter = qa_counter + 1;
|
||||
updateQA()
|
||||
$('#question-answer-subtask').show();
|
||||
$('#qa-button').hide();
|
||||
$("#reco-button").hide();
|
||||
if (qa_counter === 5) {
|
||||
if (state.taskIndex === TARGET_NUM - 1) {
|
||||
$('#next-button').hide();
|
||||
$('#nq-button').hide();
|
||||
$('#reco-button').show();
|
||||
} else {
|
||||
$('#next-button').show();
|
||||
$('#nq-button').hide();
|
||||
}
|
||||
} else {
|
||||
$('#next-button').hide();
|
||||
$('#nq-button').show();
|
||||
}
|
||||
$('#show-image-subtask').show();
|
||||
}
|
||||
} else {
|
||||
nextTask()
|
||||
}
|
||||
$('#remembered-char-subtask').hide();
|
||||
}
|
||||
/* USEFUL HELPERS */
|
||||
|
||||
function isDemoSurvey() {
|
||||
var useSurvey = config.advanced.includeDemographicSurvey;
|
||||
var lastTask = state.taskIndex == config.meta.numSubtasks + config.advanced.includeDemographicSurvey - 1;
|
||||
return useSurvey && lastTask;
|
||||
}
|
||||
|
||||
// Hides the task UI if the user is working within an MTurk iframe and has not accepted the task
|
||||
// Returns true if the task was hidden, false otherwise
|
||||
function hideIfNotAccepted() {
|
||||
if (state.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE") {
|
||||
console.log("Hiding if not accepted");
|
||||
$('#experiment').hide();
|
||||
$("#hit-not-accepted").show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Code to show the user's validation code; only used if task is configured as an external link
|
||||
function showSubmitKey(key) {
|
||||
$('#submit-code').text(key);
|
||||
$('#experiment').hide();
|
||||
$('#succesful-submit').show();
|
||||
selectText('submit-code');
|
||||
}
|
||||
|
||||
// highlights/selects text within an html element
|
||||
// copied from:
|
||||
// https://stackoverflow.com/questions/985272/selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
|
||||
function selectText(node) {
|
||||
node = document.getElementById(node);
|
||||
|
||||
if (document.body.createTextRange) {
|
||||
const range = document.body.createTextRange();
|
||||
range.moveToElementText(node);
|
||||
range.select();
|
||||
document.execCommand("copy");
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.");
|
||||
}
|
||||
}
|
||||
|
||||
/* SUBMIT FUNCTIONS */
|
||||
|
||||
// submit to MTurk as a back-end. MTurk only accepts form submissions and frowns
|
||||
// upon async POSTs.
|
||||
function mturkSubmit(submitUrl, results) {
|
||||
var form = $("#submit-form");
|
||||
addHiddenField(form, 'assignmentId', state.assignmentId);
|
||||
addHiddenField(form, 'workerId', state.workerId);
|
||||
addHiddenField(form, 'results', JSON.stringify(results));
|
||||
addHiddenField(form, 'feedback', $("#feedback-input").val());
|
||||
|
||||
$("#submit-form").attr("action", submitUrl);
|
||||
$("#submit-form").attr("method", "POST");
|
||||
// if (DEBUG) {
|
||||
// return;
|
||||
// }
|
||||
$("#submit-form").submit();
|
||||
$("#submit-button").removeClass("loading");
|
||||
generateMessage("positive", "Thanks! Your task was submitted successfully.");
|
||||
$("#submit-button").addClass("disabled");
|
||||
}
|
||||
|
||||
// submit to a customized back-end.
|
||||
function externalSubmit(submitUrl, results) {
|
||||
dd = new Date();
|
||||
bugout.log('Submitting study:' + dd);
|
||||
bugout.log(dd.getTime());
|
||||
bugout.log(results);
|
||||
console.log("payload", results);
|
||||
console.log("submitUrl", submitUrl);
|
||||
bugout.downloadLog();
|
||||
rectBugout.downloadLog();
|
||||
|
||||
$.ajax({
|
||||
url: submitUrl,
|
||||
type: 'POST',
|
||||
data: JSON.stringify(results),
|
||||
dataType: 'json'
|
||||
}).then(function(response) {
|
||||
showSubmitKey(response['key']);
|
||||
}).catch(function(error) {
|
||||
// This means there was an error connecting to the DEVELOPER'S
|
||||
// data collection server.
|
||||
// even if there is a bug/connection problem at this point,
|
||||
// we want people to be paid.
|
||||
// use a consistent prefix so we can pick out problem cases,
|
||||
// and include their worker id so we can figure out what happened
|
||||
console.log("ERROR", error);
|
||||
key = "mturk_key_" + state.workerId + "_" + state.assignmentId;
|
||||
showSubmitKey(key);
|
||||
})
|
||||
}
|
||||
|
||||
/* MAIN */
|
||||
$(document).ready(function() {
|
||||
$.getJSON("config.json").done(function(data) {
|
||||
config = data;
|
||||
config.meta.aggregate = true;
|
||||
state.taskOutputs = {};
|
||||
custom.loadTasks().done(function(taskInputData) {
|
||||
config.meta.numSubtasks = taskInputData[1];
|
||||
state.taskInputs = taskInputData[0];
|
||||
populateMetadata(config);
|
||||
demoSurvey.maybeLoadSurvey(config);
|
||||
setupButtons(config);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue