├── .gitignore ├── package.json ├── README.md ├── lib ├── main.js ├── jquery.timer.js └── time-series-annotator.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.vscode 2 | *.DS_Store 3 | node_modules/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "time-series-annotator", 3 | "version": "1.0.0", 4 | "description": "A time series annotation interface for classification", 5 | "main": "lib/main.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/CrowdCurio/time-series-annotator.git" 15 | }, 16 | "keywords": [ 17 | "crowdcurio", 18 | "time", 19 | "series", 20 | "annotation" 21 | ], 22 | "author": "Mike Schaekermann", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/CrowdCurio/time-series-annotator/issues" 26 | }, 27 | "homepage": "https://github.com/CrowdCurio/time-series-annotator#readme", 28 | "dependencies": { 29 | "bootbox": "^4.4.0", 30 | "bootstrap": "^3.3.7", 31 | "crowdcurio-client": "0.0.7", 32 | "highcharts": "^6.0.3", 33 | "highcharts-annotations": "^1.3.1", 34 | "jquery": "^3.2.1", 35 | "jquery-ui-browserify": "^1.11.0-pre-seelio", 36 | "yt-player": "^2.5.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CrowdCurio Time Series Annotator Library 2 | 3 | The CrowdCurio Time Series Annotation Library implements classification tasks for time series. 4 | 5 | ![A screenshot of the Time Series Annotator.](https://s3.amazonaws.com/curio-media/github-media/time-series-annotator.png) 6 | 7 | ## Features 8 | - Support for feature annotation tasks. 9 | - Support for interactive practice tasks. 10 | - Support for multivariate time series. 11 | - Support for medical time series in EDF format. 12 | - Integrated support for CrowdCurio. 13 | 14 | ## Build Process 15 | We use Browserify, Wachify and Uglify in our build processes. All three tools can be installed with NPM. 16 | 17 | >npm install -g browserify 18 | 19 | >npm install -g watchify 20 | 21 | >npm install -g uglify-js 22 | 23 | To build the script bundle *without* minification, run: 24 | >browserify lib/main.js -o bundle.js 25 | 26 | To build *with* minification, run: 27 | >browserify lib/main.js | uglifyjs bundle.js 28 | 29 | To watch for file changes and automatically bundle *without* minification, run: 30 | >watchify lib/main.js -o bundle.js 31 | 32 | ## Contact 33 | Mike Schaekermann, University of Waterloo -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | // file: main.js 2 | // author: Mike Schaekermann 3 | // desc: root file for bundling the time series annotator 4 | var CrowdCurioClient = require('crowdcurio-client'); 5 | require('./time-series-annotator'); 6 | 7 | global.csrftoken = $("[name='csrfmiddlewaretoken']").val(); 8 | 9 | // set UI vars 10 | var DEV = window.DEV; 11 | var task = window.task || -1; 12 | var user = window.user || -1; 13 | var experiment = window.experiment || -1; 14 | var condition = window.condition || -1; 15 | var containerId = window.container || 'task-container'; 16 | var containerElement = $('#' + containerId); 17 | 18 | var config = convertKeysFromUnderscoreToCamelCase(window.config); 19 | var task_config = convertKeysFromUnderscoreToCamelCase(window.task_config); 20 | var apiClient = new CrowdCurioClient(); 21 | var apiClientConfig = { 22 | user: user, 23 | task: task, 24 | } 25 | if (experiment != -1) { 26 | apiClientConfig.experiment = experiment; 27 | } 28 | if (condition != -1) { 29 | apiClientConfig.condition = condition; 30 | } 31 | apiClient.init(apiClientConfig); 32 | task_config.apiClient = apiClient; 33 | function byId(a, b) { 34 | return (a.id - b.id); 35 | } 36 | apiClient.getNextTask('required', function(data) { 37 | if (!data || !data.id) { 38 | if (experiment != -1) { 39 | var workflowNextUrl = '/experiments/' + experiment + '/workflow/next/'; 40 | $.ajax({ 41 | url: workflowNextUrl, 42 | type: 'POST', 43 | data: { 44 | csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value 45 | }, 46 | dataType: 'json', 47 | success: function() { 48 | window.location.reload(); 49 | }, 50 | error: function(error) { 51 | window.location.reload(); 52 | }, 53 | }); 54 | } 55 | return; 56 | } 57 | apiClient.setData(data.id); 58 | var dataTaskConfig = convertKeysFromUnderscoreToCamelCase(data.content.task_config); 59 | $.extend(true, task_config, dataTaskConfig); 60 | containerElement.TimeSeriesAnnotator(task_config); 61 | }) 62 | 63 | function convertKeysFromUnderscoreToCamelCase(a) { 64 | return JSON.parse(JSON.stringify(a, function (key, value) { 65 | if (value && typeof value === 'object' && !(value instanceof Array)) { 66 | var replacement = {}; 67 | for (var k in value) { 68 | if (Object.hasOwnProperty.call(value, k)) { 69 | replacement[underscoreToCamelCase(k)] = value[k]; 70 | } 71 | } 72 | return replacement; 73 | } 74 | return value; 75 | })); 76 | function underscoreToCamelCase(string) { 77 | return string.replace(/(\_\w)/g, function(m){ 78 | return m[1].toUpperCase(); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | td { 2 | padding: 5px; 3 | } 4 | 5 | .graph_container { 6 | padding: 0 20px; 7 | } 8 | 9 | .graph_footer { 10 | margin-top: 10px; 11 | } 12 | 13 | .graph { 14 | margin: 20px auto; 15 | } 16 | 17 | .graph_control { 18 | height: 36px; 19 | } 20 | 21 | .gainUp, .gainDown, .gainReset { 22 | float: left; 23 | } 24 | 25 | .selected { 26 | font-weight: bold; 27 | text-decoration: underline; 28 | } 29 | 30 | .panel-heading a:after { 31 | font-family:'Glyphicons Halflings'; 32 | content:"\e114"; 33 | color: grey; 34 | } 35 | 36 | .panel-heading a.collapsed:after { 37 | content:"\e080"; 38 | } 39 | 40 | .feature_panel { 41 | float: left; 42 | display: inline; 43 | } 44 | 45 | .artifact_panel, .sleep_stage_panel, .submit_response_for_data_panel { 46 | display: inline; 47 | } 48 | 49 | .submit_response_for_data_panel { 50 | margin-left: 20px; 51 | } 52 | 53 | .phase-indicator { 54 | margin: 0; 55 | } 56 | 57 | .navigation_panel, .examples_panel { 58 | float: right; 59 | display: inline; 60 | } 61 | 62 | .main_container { 63 | padding: 10px; 64 | padding-top: 0; 65 | text-align: center; 66 | } 67 | 68 | .experiment_container { 69 | padding-top: 0; 70 | } 71 | 72 | .keyboardShortcuts { 73 | margin-left: 30px; 74 | } 75 | 76 | .complete-condition { 77 | margin-right: -15px; 78 | } 79 | 80 | .classification.active { 81 | background-color: lightgrey; 82 | } 83 | 84 | .highcharts-annotation { 85 | cursor: pointer; 86 | } 87 | 88 | .highcharts-annotation.saved { 89 | cursor: auto; 90 | } 91 | 92 | .confidence-buttons { 93 | width: 60px; 94 | height: auto; 95 | background-color: rgba(0, 0, 0, 0); 96 | } 97 | 98 | .highcharts-annotation.saved .toolbar { 99 | display: none; 100 | } 101 | 102 | .highcharts-annotation.saved:hover .toolbar { 103 | display: block; 104 | } 105 | 106 | .confidence-buttons input { 107 | display: none; 108 | } 109 | 110 | .confidence-buttons .btn { 111 | width: 20px !important; 112 | height: 20px !important; 113 | padding: 0 !important; 114 | margin: 0 !important; 115 | border-radius: 0 !important; 116 | display: inline-block !important; 117 | } 118 | 119 | .confidence-buttons .btn.active { 120 | border: 3px solid #000000; 121 | } 122 | 123 | .highcharts-annotation .comment { 124 | width: 240px; 125 | height: auto; 126 | background-color: rgba(0, 0, 0, 0); 127 | } 128 | 129 | .highcharts-annotation .comment button { 130 | position: static; 131 | height: 20px; 132 | width: 60px; 133 | font-size: 12px; 134 | line-height: 0; 135 | padding: 0; 136 | border-radius: 0; 137 | } 138 | 139 | .highcharts-annotation .comment input { 140 | height: 21px !important; 141 | width: 180px !important; 142 | vertical-align: middle !important; 143 | font-size: 13px !important; 144 | background-color: #ffffff !important; 145 | margin: 0 !important; 146 | } 147 | 148 | .annotationTime { 149 | position: relative; 150 | } 151 | 152 | .annotation-time-container { 153 | display: none; 154 | position: absolute; 155 | top: 33px; 156 | right: 0; 157 | z-index: 9999999999; 158 | background-color: #ffffff; 159 | padding: 10px 0 5px 10px; 160 | } 161 | 162 | .annotationTime:hover .annotation-time-container, .annotationTime:focus .annotation-time-container { 163 | display: block; 164 | } 165 | 166 | .annotation-time-container .time { 167 | margin-left: 20px; 168 | display: inline-block; 169 | width: auto; 170 | } 171 | 172 | .examples { 173 | margin-top: 60px; 174 | margin-bottom: 60px; 175 | } 176 | 177 | .examples-title { 178 | height: 34px; 179 | display: inline-block; 180 | line-height: 34px; 181 | margin-right: 15px; 182 | font-size: 20px; 183 | font-weight: bold; 184 | } 185 | 186 | .progress { 187 | width: 30%; 188 | margin: 0px 2px; 189 | display: inline-block; 190 | height: 34px; 191 | } 192 | 193 | .correct-answer-explanation { 194 | display: inline-block; 195 | } 196 | 197 | .correct-answer-explanation .label { 198 | display: inline-block; 199 | height: 34px; 200 | font-size: 18px; 201 | line-height: 34px; 202 | padding-top: 0; 203 | padding-bottom: 0; 204 | } 205 | 206 | .highcharts-container { 207 | outline: 1px solid rgba(134, 142, 149, 0.4); 208 | } 209 | 210 | .shortcut-key { 211 | font-weight: bold; 212 | text-decoration: underline; 213 | } 214 | 215 | .hidden { 216 | display: none; 217 | } -------------------------------------------------------------------------------- /lib/jquery.timer.js: -------------------------------------------------------------------------------- 1 | /*global define:false */ 2 | /* 3 | * ======================= 4 | * jQuery Timer Plugin 5 | * ======================= 6 | * Start/Stop/Resume a time in any HTML element 7 | */ 8 | 9 | (function(root, factory) { 10 | if (typeof define === 'function' && define.amd) { 11 | define(['jquery'], factory); 12 | } else { 13 | factory(root.jQuery); 14 | } 15 | }(window, function($) { 16 | // PRIVATE 17 | var options = { 18 | seconds: 0, // default seconds value to start timer from 19 | editable: false, // this will let users make changes to the time 20 | restart: false, // this will enable stop or continue after a timer callback 21 | duration: null, // duration to run callback after 22 | // callback to run after elapsed duration 23 | callback: function() { 24 | alert('Time up!'); 25 | }, 26 | startTimer: function() {}, 27 | pauseTimer: function() {}, 28 | resumeTimer: function() {}, 29 | resetTimer: function() {}, 30 | removeTimer: function() {}, 31 | repeat: false, // this will repeat callback every n times duration is elapsed 32 | countdown: false, // if true, this will render the timer as a countdown if duration > 0 33 | format: null, // this sets the format in which the time will be printed 34 | updateFrequency: 1000, // How often should timer display update (default 500ms) 35 | state: 'running' 36 | }, 37 | display = 'html', // to be used as $el.html in case of div and $el.val in case of input type text 38 | // Constants for various states of the timer 39 | TIMER_STOPPED = 'stopped', 40 | TIMER_RUNNING = 'running', 41 | TIMER_PAUSED = 'paused'; 42 | 43 | /** 44 | * Common function to start or resume a timer interval 45 | */ 46 | function startTimerInterval(timer) { 47 | var element = timer.element; 48 | $(element).data('intr', setInterval(incrementSeconds.bind(timer), timer.options.updateFrequency)); 49 | $(element).data('isTimerRunning', true); 50 | } 51 | 52 | /** 53 | * Common function to stop timer interval 54 | */ 55 | function stopTimerInterval(timer) { 56 | clearInterval($(timer.element).data('intr')); 57 | $(timer.element).data('isTimerRunning', false); 58 | } 59 | 60 | /** 61 | * Increment total seconds by subtracting startTime from the current unix timestamp in seconds 62 | * and call render to display pretty time 63 | */ 64 | function incrementSeconds() { 65 | $(this.element).data('totalSeconds', getUnixSeconds() - $(this.element).data('startTime')); 66 | render(this); 67 | 68 | // Check if totalSeconds is equal to duration if any 69 | if ($(this.element).data('duration') && 70 | $(this.element).data('totalSeconds') % $(this.element).data('duration') === 0) { 71 | 72 | // If 'repeat' is not requested then disable the duration 73 | if (!this.options.repeat) { 74 | $(this.element).data('duration', null); 75 | this.options.duration = null; 76 | } 77 | 78 | // If this is a countdown, then end it as duration has completed 79 | if (this.options.countdown) { 80 | stopTimerInterval(this); 81 | this.options.countdown = false; 82 | $(this.element).data('state', TIMER_STOPPED); 83 | } 84 | 85 | // Run the default callback 86 | this.options.callback(); 87 | } 88 | } 89 | 90 | /** 91 | * Render pretty time 92 | */ 93 | function render(timer) { 94 | var element = timer.element, 95 | sec = $(element).data('totalSeconds'); 96 | 97 | if (timer.options.countdown && ($(element).data('duration') > 0)) { 98 | sec = $(element).data('duration') - $(element).data('totalSeconds'); 99 | } 100 | 101 | $(element)[display](secondsToTime(sec, timer)); 102 | $(element).data('seconds', sec); 103 | } 104 | 105 | /** 106 | * Method to make timer field editable 107 | * This method hard binds focus & blur events to pause & resume 108 | * and recognizes built-in pretty time (for eg 12 sec OR 3:34 min) 109 | * It won't recognize user created formats. 110 | * Users may not always want this hard bound. In such a case, 111 | * do not use the editable property. Instead bind custom functions 112 | * to blur and focus. 113 | */ 114 | function makeEditable(timer) { 115 | var element = timer.element; 116 | $(element).on('focus', function() { 117 | pauseTimer(timer); 118 | }); 119 | 120 | $(element).on('blur', function() { 121 | // eg. 12 sec 3:34 min 12:30 min 122 | var val = $(element)[display](), valArr; 123 | 124 | if (val.indexOf('sec') > 0) { 125 | // sec 126 | $(element).data('totalSeconds', Number(val.replace(/\ssec/g, ''))); 127 | } else if (val.indexOf('min') > 0) { 128 | // min 129 | val = val.replace(/\smin/g, ''); 130 | valArr = val.split(':'); 131 | $(element).data('totalSeconds', Number(valArr[0] * 60) + Number(valArr[1])); 132 | } else if (val.match(/\d{1,2}:\d{2}:\d{2}/)) { 133 | // hrs 134 | valArr = val.split(':'); 135 | $(element).data('totalSeconds', Number(valArr[0] * 3600) + Number(valArr[1] * 60) + Number(valArr[2])); 136 | } 137 | 138 | resumeTimer(timer); 139 | }); 140 | } 141 | 142 | /** 143 | * Get the current unix timestamp in seconds 144 | * @return {Number} [unix timestamp in seconds] 145 | */ 146 | function getUnixSeconds() { 147 | return Math.round(new Date().getTime() / 1000); 148 | } 149 | 150 | /** 151 | * Convert a number of seconds into an object of hours, minutes and seconds 152 | * @param {Number} sec [Number of seconds] 153 | * @return {Object} [An object with hours, minutes and seconds representation of the given seconds] 154 | */ 155 | function sec2TimeObj(sec) { 156 | var hours = 0, minutes = Math.floor(sec / 60), seconds; 157 | 158 | // Hours 159 | if (sec >= 3600) { 160 | hours = Math.floor(sec / 3600); 161 | } 162 | 163 | // Minutes 164 | if (sec >= 3600) { 165 | minutes = Math.floor(sec % 3600 / 60); 166 | } 167 | // Prepend 0 to minutes under 10 168 | if (minutes < 10 && hours > 0) { 169 | minutes = '0' + minutes; 170 | } 171 | // Seconds 172 | seconds = sec % 60; 173 | // Prepend 0 to seconds under 10 174 | if (seconds < 10 && (minutes > 0 || hours > 0)) { 175 | seconds = '0' + seconds; 176 | } 177 | 178 | return { 179 | hours: hours, 180 | minutes: minutes, 181 | seconds: seconds 182 | }; 183 | } 184 | 185 | /** 186 | * Convert the given seconds to an object made up of hours, minutes and seconds and return a pretty display 187 | * @param {Number} sec [Second to display as pretty time] 188 | * @return {String} [Pretty time] 189 | */ 190 | function secondsToTime(sec, timer) { 191 | var time = '', 192 | timeObj = sec2TimeObj(sec); 193 | 194 | if (timer.options.format) { 195 | var formatDef = [ 196 | {identifier: '%h', value: timeObj.hours, pad: false}, 197 | {identifier: '%m', value: timeObj.minutes, pad: false}, 198 | {identifier: '%s', value: timeObj.seconds, pad: false}, 199 | {identifier: '%H', value: parseInt(timeObj.hours), pad: true}, 200 | {identifier: '%M', value: parseInt(timeObj.minutes), pad: true}, 201 | {identifier: '%S', value: parseInt(timeObj.seconds), pad: true} 202 | ]; 203 | time = timer.options.format; 204 | 205 | formatDef.forEach(function(format) { 206 | time = time.replace( 207 | new RegExp(format.identifier.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'), 208 | (format.pad) ? ((format.value < 10) ? '0' + format.value : format.value) : format.value 209 | ); 210 | }); 211 | } else { 212 | if (timeObj.hours) { 213 | time = timeObj.hours + ':' + timeObj.minutes + ':' + timeObj.seconds; 214 | } else { 215 | if (timeObj.minutes) { 216 | time = timeObj.minutes + ':' + timeObj.seconds + ' min'; 217 | } else { 218 | time = timeObj.seconds + ' sec'; 219 | } 220 | } 221 | } 222 | return time; 223 | } 224 | 225 | /** 226 | * Convert a string time like 5m30s to seconds 227 | * If a number (eg 300) is provided, then return as is 228 | * @param {Number|String} time [The human time to convert to seconds] 229 | * @return {Number} [Number of seconds] 230 | */ 231 | function timeToSeconds(time) { 232 | // In case the passed arg is a number, then use that as number of seconds 233 | if (!isNaN(Number(time))) { 234 | return time; 235 | } 236 | 237 | var hMatch = time.match(/\d{1,2}h/), 238 | mMatch = time.match(/\d{1,2}m/), 239 | sMatch = time.match(/\d{1,2}s/), 240 | seconds = 0; 241 | 242 | time = time.toLowerCase(); 243 | 244 | // @todo: throw an error in case of faulty time value like 5m61s or 61m 245 | if (hMatch) { 246 | seconds += Number(hMatch[0].replace('h', '')) * 3600; 247 | } 248 | 249 | if (mMatch) { 250 | seconds += Number(mMatch[0].replace('m', '')) * 60; 251 | } 252 | 253 | if (sMatch) { 254 | seconds += Number(sMatch[0].replace('s', '')); 255 | } 256 | 257 | return seconds; 258 | } 259 | 260 | // TIMER INTERFACE 261 | function startTimer(timer) { 262 | var element = timer.element; 263 | if (!$(element).data('isTimerRunning')) { 264 | render(timer); 265 | startTimerInterval(timer); 266 | $(element).data('state', TIMER_RUNNING); 267 | timer.options.startTimer.bind(timer).call(); 268 | } 269 | } 270 | 271 | function pauseTimer(timer) { 272 | var element = timer.element; 273 | if ($(element).data('isTimerRunning')) { 274 | stopTimerInterval(timer); 275 | $(element).data('state', TIMER_PAUSED); 276 | timer.options.pauseTimer.bind(timer).call(); 277 | } 278 | } 279 | 280 | function resumeTimer(timer) { 281 | var element = timer.element; 282 | if (!$(element).data('isTimerRunning')) { 283 | $(element).data('startTime', getUnixSeconds() - $(element).data('totalSeconds')); 284 | startTimerInterval(timer); 285 | $(element).data('state', TIMER_RUNNING); 286 | timer.options.resumeTimer.bind(timer).call(); 287 | } 288 | } 289 | 290 | function resetTimer(timer) { 291 | var element = timer.element; 292 | $(element).data('startTime', 0); 293 | $(element).data('totalSeconds', 0); 294 | $(element).data('seconds', 0); 295 | $(element).data('state', TIMER_STOPPED); 296 | $(element).data('duration', timer.options.duration); 297 | timer.options.resetTimer.bind(timer).call(); 298 | } 299 | 300 | function removeTimer(timer) { 301 | var element = timer.element; 302 | stopTimerInterval(timer); 303 | timer.options.removeTimer.bind(timer).call(); 304 | $(element).data('plugin_' + pluginName, null); 305 | $(element).data('seconds', null); 306 | $(element).data('state', null); 307 | $(element)[display](''); 308 | } 309 | 310 | // TIMER PROTOTYPE 311 | var Timer = function(element, userOptions) { 312 | var elementType; 313 | 314 | this.options = options = $.extend(this.options, options, userOptions); 315 | this.element = element; 316 | 317 | // Setup total seconds from options.seconds (if any) 318 | $(element).data('totalSeconds', options.seconds); 319 | 320 | // Setup start time if seconds were provided 321 | $(element).data('startTime', getUnixSeconds() - $(element).data('totalSeconds')); 322 | 323 | $(element).data('seconds', $(element).data('totalSeconds')); 324 | $(element).data('state', TIMER_STOPPED); 325 | 326 | // Check if this is a input/textarea element or not 327 | elementType = $(element).prop('tagName').toLowerCase(); 328 | if (elementType === 'input' || elementType === 'textarea') { 329 | display = 'val'; 330 | } 331 | 332 | if (this.options.duration) { 333 | $(element).data('duration', timeToSeconds(this.options.duration)); 334 | this.options.duration = timeToSeconds(this.options.duration); 335 | } 336 | 337 | if (this.options.editable) { 338 | makeEditable(this); 339 | } 340 | 341 | }; 342 | 343 | /** 344 | * Initialize the plugin with public methods 345 | */ 346 | Timer.prototype = { 347 | start: function() { 348 | startTimer(this); 349 | }, 350 | 351 | pause: function() { 352 | pauseTimer(this); 353 | }, 354 | 355 | resume: function() { 356 | resumeTimer(this); 357 | }, 358 | 359 | reset: function() { 360 | resetTimer(this); 361 | }, 362 | 363 | remove: function() { 364 | removeTimer(this); 365 | } 366 | }; 367 | 368 | // INITIALIZE THE PLUGIN 369 | var pluginName = 'timer'; 370 | $.fn[pluginName] = function(options) { 371 | options = options || 'start'; 372 | 373 | return this.each(function() { 374 | /** 375 | * Allow the plugin to be initialized on an element only once 376 | * This way we can call the plugin's internal function 377 | * without having to reinitialize the plugin all over again. 378 | */ 379 | if (!($.data(this, 'plugin_' + pluginName) instanceof Timer)) { 380 | 381 | /** 382 | * Create a new data attribute on the element to hold the plugin name 383 | * This way we can know which plugin(s) is/are initialized on the element later 384 | */ 385 | $.data(this, 'plugin_' + pluginName, new Timer(this, options)); 386 | 387 | } 388 | 389 | /** 390 | * Use the instance of this plugin derived from the data attribute for this element 391 | * to conduct whatever action requested as a string parameter. 392 | */ 393 | var instance = $.data(this, 'plugin_' + pluginName); 394 | 395 | /** 396 | * Provision for calling a function from this plugin 397 | * without initializing it all over again 398 | */ 399 | if (typeof options === 'string') { 400 | if (typeof instance[options] === 'function') { 401 | /* 402 | Pass in 'instance' to provide for the value of 'this' in the called function 403 | */ 404 | instance[options].call(instance); 405 | } 406 | } 407 | 408 | /** 409 | * Allow passing custom options object 410 | */ 411 | if (typeof options === 'object') { 412 | if (instance.options.state === TIMER_RUNNING) { 413 | instance.start.call(instance); 414 | } else { 415 | render(instance); 416 | } 417 | } 418 | }); 419 | }; 420 | 421 | })); -------------------------------------------------------------------------------- /lib/time-series-annotator.js: -------------------------------------------------------------------------------- 1 | var jQuery = require('jquery'); 2 | var $ = jQuery; 3 | require('jquery-ui-browserify'); 4 | global.bootbox = require('bootbox'); 5 | var YTPlayer = require('yt-player'); 6 | require('bootstrap'); 7 | require('./jquery.timer'); 8 | var Highcharts = require('highcharts'); 9 | var HighchartsAnnotations = require('highcharts-annotations')(Highcharts); 10 | 11 | var frequencyBandVisualizationDefault = { 12 | bands: [ 13 | // { 14 | // name: 'Delta', 15 | // frequency: { 16 | // min: 1, 17 | // max: 4, 18 | // }, 19 | // color: '#25C700', // green 20 | // }, 21 | { 22 | name: 'Mixed Freq.', 23 | frequency: { 24 | min: 4, 25 | max: 8, 26 | }, 27 | color: '#F77C00', // orange 28 | 29 | }, 30 | { 31 | name: 'Alpha', 32 | frequency: { 33 | min: 8, 34 | max: 13, 35 | }, 36 | color: '#1968FF', // blue 37 | }, 38 | // { 39 | // name: 'Beta', 40 | // frequency: { 41 | // min: 13, 42 | // max: 30, 43 | // }, 44 | // color: '#000000', // black 45 | // }, 46 | ], 47 | }; 48 | 49 | $.widget('crowdcurio.TimeSeriesAnnotator', { 50 | 51 | options: { 52 | optionsURLParameter: 'annotatorOptions', 53 | apiClient: undefined, 54 | projectUUID: undefined, 55 | requireConsent: false, 56 | trainingVideo: { 57 | forcePlay: false, 58 | blockInteraction: true, 59 | vimeoId: '169158678', 60 | }, 61 | payment: 0.00, 62 | showConfirmationCode: false, 63 | confirmationCode: undefined, 64 | recordingName: undefined, 65 | channelsDisplayed: [0, 1, 2, 3, 4, 6, 7], 66 | channelGainAdjustmentEnabled: true, 67 | keyboardInputEnabled: true, 68 | isReadOnly: false, 69 | startTime: 0, 70 | visibleRegion: { 71 | start: undefined, 72 | end: undefined, 73 | showProgress: true, 74 | hitModeEnabled: true, 75 | training: { 76 | enabled: true, 77 | isTrainingOnly: false, 78 | numberOfInitialWindowsUsedForTraining: 0, 79 | windows: [], 80 | } 81 | }, 82 | graph: { 83 | channelSpacing: 400, 84 | width: undefined, 85 | height: 600, 86 | marginTop: 10, 87 | marginBottom: 30, 88 | marginLeft: 90, 89 | marginRight: 30, 90 | backgroundColor: '#ffffff', 91 | }, 92 | marginTop: null, 93 | marginBottom: null, 94 | windowSizeInSeconds: 30, 95 | windowJumpSizeFastForwardBackward: 10, 96 | numberOfForwardWindowsToPrefetch: 3, 97 | numberOfFastForwardWindowsToPrefetch: 3, 98 | numberOfBackwardWindowsToPrefetch: 3, 99 | numberOfFastBackwardWindowsToPrefetch: 3, 100 | relativeGainChangePerStep: 0.25, 101 | idleTimeThresholdSeconds: 300, 102 | experiment: {}, 103 | showArtifactButtons: false, 104 | showSleepStageButtons: false, 105 | showNavigationButtons: true, 106 | showBackToLastActiveWindowButton: true, 107 | showFastBackwardButton: true, 108 | showBackwardButton: true, 109 | showForwardButton: true, 110 | showFastForwardButton: true, 111 | showShortcuts: false, 112 | showLogoutButton: false, 113 | showAnnotationTime: false, 114 | showReferenceLines: true, 115 | showTimeLabels: true, 116 | showChannelNames: true, 117 | frequencyBandVisualizationPerChannel: [ 118 | frequencyBandVisualizationDefault, 119 | frequencyBandVisualizationDefault, 120 | frequencyBandVisualizationDefault, 121 | undefined, 122 | undefined, 123 | undefined, 124 | undefined, 125 | ], 126 | instructionSlidesUrl: undefined, 127 | features: { 128 | examplesModeEnabled: false, 129 | examples: [{ 130 | recording: '1209153-1_P', 131 | channels_displayed: [0], 132 | channels: 0, 133 | type: 'sleep_spindle', 134 | start: 1925, 135 | end: 1928, 136 | annotator_experience: 1, 137 | confidence: 1, 138 | comment: 'Interesting!', 139 | }, { 140 | recording: '1209153-1_P', 141 | channels_displayed: [0], 142 | channels: 0, 143 | type: 'sleep_spindle', 144 | start: 2030, 145 | end: 2035, 146 | annotator_experience: 1, 147 | confidence: 1, 148 | comment: 'Interesting!', 149 | }], 150 | cheatSheetsEnabled: false, 151 | openCheatSheetOnPageLoad: true, 152 | scrollThroughExamplesAutomatically: true, 153 | scrollThroughExamplesSpeedInSeconds: 5, 154 | showUserAnnotations: true, 155 | order: ['sleep_spindle', 'k_complex', 'rem', 'vertex_wave'], 156 | options: { 157 | 'sleep_spindle': { 158 | name: 'Spindle', 159 | annotation: { 160 | red: 86, 161 | green: 186, 162 | blue: 219, 163 | alpha: { 164 | min: 0.22, 165 | max: 0.45 166 | } 167 | }, 168 | answer: { 169 | red: 0, 170 | green: 0, 171 | blue: 0, 172 | alpha: { 173 | min: 0.1, 174 | max: 0.25 175 | } 176 | }, 177 | training: { 178 | windows: [ 179 | { 180 | recordingName: '1209056-1_P', 181 | timeStart: 1920, 182 | windowSizeInSeconds: 30, 183 | }, 184 | { 185 | recordingName: '1209056-1_P', 186 | timeStart: 14790, 187 | windowSizeInSeconds: 30, 188 | }, 189 | { 190 | recordingName: '1209056-1_P', 191 | timeStart: 1980, 192 | windowSizeInSeconds: 30, 193 | }, 194 | { 195 | recordingName: '1209056-1_P', 196 | timeStart: 16680, 197 | windowSizeInSeconds: 30, 198 | }, 199 | { 200 | recordingName: '1209056-1_P', 201 | timeStart: 2010, 202 | windowSizeInSeconds: 30, 203 | }, 204 | { 205 | recordingName: '1209056-1_P', 206 | timeStart: 2040, 207 | windowSizeInSeconds: 30, 208 | }, 209 | { 210 | recordingName: '1209056-1_P', 211 | timeStart: 2070, 212 | windowSizeInSeconds: 30, 213 | }, 214 | { 215 | recordingName: '1209056-1_P', 216 | timeStart: 2130, 217 | windowSizeInSeconds: 30, 218 | }, 219 | { 220 | recordingName: '1209056-1_P', 221 | timeStart: 17130, 222 | windowSizeInSeconds: 30, 223 | }, 224 | { 225 | recordingName: '1209056-1_P', 226 | timeStart: 2160, 227 | windowSizeInSeconds: 30, 228 | }, 229 | ], 230 | }, 231 | }, 232 | 'k_complex': { 233 | name: 'K-Complex', 234 | annotation: { 235 | red: 195, 236 | green: 123, 237 | blue: 225, 238 | alpha: { 239 | min: 0.18, 240 | max: 0.35 241 | } 242 | }, 243 | answer: { 244 | red: 0, 245 | green: 0, 246 | blue: 0, 247 | alpha: { 248 | min: 0.1, 249 | max: 0.25 250 | } 251 | }, 252 | training: { 253 | windows: [ 254 | { 255 | recordingName: '1209056-1_P', 256 | timeStart: 1590, 257 | windowSizeInSeconds: 30, 258 | }, 259 | { 260 | recordingName: '1209056-1_P', 261 | timeStart: 600, 262 | windowSizeInSeconds: 30, 263 | }, 264 | { 265 | recordingName: '1209056-1_P', 266 | timeStart: 1620, 267 | windowSizeInSeconds: 30, 268 | }, 269 | { 270 | recordingName: '1209056-1_P', 271 | timeStart: 900, 272 | windowSizeInSeconds: 30, 273 | }, 274 | { 275 | recordingName: '1209056-1_P', 276 | timeStart: 1650, 277 | windowSizeInSeconds: 30, 278 | }, 279 | { 280 | recordingName: '1209056-1_P', 281 | timeStart: 2700, 282 | windowSizeInSeconds: 30, 283 | }, 284 | { 285 | recordingName: '1209056-1_P', 286 | timeStart: 1800, 287 | windowSizeInSeconds: 30, 288 | }, 289 | { 290 | recordingName: '1209056-1_P', 291 | timeStart: 3300, 292 | windowSizeInSeconds: 30, 293 | }, 294 | { 295 | recordingName: '1209056-1_P', 296 | timeStart: 1860, 297 | windowSizeInSeconds: 30, 298 | }, 299 | { 300 | recordingName: '1209056-1_P', 301 | timeStart: 1200, 302 | windowSizeInSeconds: 30, 303 | }, 304 | ], 305 | }, 306 | }, 307 | 'rem': { 308 | name: 'REM', 309 | annotation: { 310 | red: 238, 311 | green: 75, 312 | blue: 38, 313 | alpha: { 314 | min: 0.18, 315 | max: 0.35 316 | } 317 | }, 318 | answer: { 319 | red: 0, 320 | green: 0, 321 | blue: 0, 322 | alpha: { 323 | min: 0.1, 324 | max: 0.25 325 | } 326 | }, 327 | training: { 328 | windows: [ 329 | { 330 | recordingName: '1209056-1_P', 331 | timeStart: 5250, 332 | windowSizeInSeconds: 30, 333 | }, 334 | { 335 | recordingName: '1209056-1_P', 336 | timeStart: 600, 337 | windowSizeInSeconds: 30, 338 | }, 339 | { 340 | recordingName: '1209056-1_P', 341 | timeStart: 5400, 342 | windowSizeInSeconds: 30, 343 | }, 344 | { 345 | recordingName: '1209056-1_P', 346 | timeStart: 900, 347 | windowSizeInSeconds: 30, 348 | }, 349 | { 350 | recordingName: '1209056-1_P', 351 | timeStart: 9810, 352 | windowSizeInSeconds: 30, 353 | }, 354 | { 355 | recordingName: '1209056-1_P', 356 | timeStart: 2700, 357 | windowSizeInSeconds: 30, 358 | }, 359 | { 360 | recordingName: '1209056-1_P', 361 | timeStart: 9960, 362 | windowSizeInSeconds: 30, 363 | }, 364 | { 365 | recordingName: '1209056-1_P', 366 | timeStart: 3300, 367 | windowSizeInSeconds: 30, 368 | }, 369 | { 370 | recordingName: '1209056-1_P', 371 | timeStart: 10230, 372 | windowSizeInSeconds: 30, 373 | }, 374 | { 375 | recordingName: '1209056-1_P', 376 | timeStart: 1200, 377 | windowSizeInSeconds: 30, 378 | }, 379 | ], 380 | }, 381 | }, 382 | 'vertex_wave': { 383 | name: 'Vertex Wave', 384 | annotation: { 385 | red: 0, 386 | green: 0, 387 | blue: 0, 388 | alpha: { 389 | min: 0.18, 390 | max: 0.35 391 | } 392 | }, 393 | answer: { 394 | red: 0, 395 | green: 0, 396 | blue: 0, 397 | alpha: { 398 | min: 0.1, 399 | max: 0.25 400 | } 401 | }, 402 | training: { 403 | windows: [ 404 | { 405 | recordingName: '1209056-1_P', 406 | timeStart: 600, 407 | windowSizeInSeconds: 30, 408 | }, 409 | { 410 | recordingName: '1209056-1_P', 411 | timeStart: 17550, 412 | windowSizeInSeconds: 30, 413 | }, 414 | { 415 | recordingName: '1209056-1_P', 416 | timeStart: 5400, 417 | windowSizeInSeconds: 30, 418 | }, 419 | { 420 | recordingName: '1209056-1_P', 421 | timeStart: 900, 422 | windowSizeInSeconds: 30, 423 | }, 424 | { 425 | recordingName: '1209056-1_P', 426 | timeStart: 9810, 427 | windowSizeInSeconds: 30, 428 | }, 429 | { 430 | recordingName: '1209056-1_P', 431 | timeStart: 12360, 432 | windowSizeInSeconds: 30, 433 | }, 434 | { 435 | recordingName: '1209056-1_P', 436 | timeStart: 9960, 437 | windowSizeInSeconds: 30, 438 | }, 439 | { 440 | recordingName: '1209056-1_P', 441 | timeStart: 3300, 442 | windowSizeInSeconds: 30, 443 | }, 444 | { 445 | recordingName: '1209056-1_P', 446 | timeStart: 4230, 447 | windowSizeInSeconds: 30, 448 | }, 449 | { 450 | recordingName: '1209056-1_P', 451 | timeStart: 1200, 452 | windowSizeInSeconds: 30, 453 | }, 454 | ], 455 | }, 456 | }, 457 | 'delta_wave': { 458 | name: 'Delta Wave', 459 | annotation: { 460 | red: 20, 461 | green: 230, 462 | blue: 30, 463 | alpha: { 464 | min: 0.18, 465 | max: 0.35 466 | } 467 | }, 468 | answer: { 469 | red: 0, 470 | green: 0, 471 | blue: 0, 472 | alpha: { 473 | min: 0.1, 474 | max: 0.25 475 | } 476 | }, 477 | training: { 478 | windows: [], 479 | }, 480 | } 481 | }, 482 | }, 483 | }, 484 | 485 | _create: function() { 486 | var that = this; 487 | that._initializeVariables(); 488 | $(that.element).addClass(that.vars.uniqueClass); 489 | that._fetchOptionsFromURLParameter(); 490 | that._createHTMLContent(); 491 | that._loadPreferences(function() { 492 | if (that.options.requireConsent) { 493 | that._showConsentForm(); 494 | } 495 | if (that.options.trainingVideo.forcePlay) { 496 | that._forcePlayTrainingVideo(); 497 | } 498 | that._setupHITMode(); 499 | if (that.options.features.examplesModeEnabled) { 500 | that._setupExamplesMode(); 501 | return; 502 | } 503 | if (that.options.experiment.running) { 504 | that._setupExperiment(); 505 | that._setup(); 506 | } 507 | else { 508 | var recordingNameFromGetParameter = that._getUrlParameter('recording_name'); 509 | if (recordingNameFromGetParameter) { 510 | that.options.recordingName = recordingNameFromGetParameter; 511 | } 512 | that._setup(); 513 | } 514 | }); 515 | }, 516 | 517 | _initializeVariables: function() { 518 | var that = this; 519 | that.vars = { 520 | uniqueClass: that._getUUID(), 521 | activeFeatureType: 0, 522 | chart: null, 523 | activeAnnotations: [], 524 | annotationsLoaded: false, 525 | selectedChannelIndex: undefined, 526 | currentWindowData: null, 527 | currentWindowStart: null, 528 | lastActiveWindowStart: null, 529 | initialChannelGains: [], 530 | currentChannelGainAdjustments: [], 531 | forwardEnabled: undefined, 532 | fastForwardEnabled: undefined, 533 | backwardEnabled: undefined, 534 | fastBackwardEnabled: undefined, 535 | numberOfAnnotationsInCurrentWindow: 0, 536 | specifiedTrainingWindows: undefined, 537 | currentTrainingWindowIndex: 0, 538 | cheatSheetOpenedBefore: false, 539 | scrollThroughExamplesIntervalId: undefined, 540 | taskDataConfiguration: undefined, 541 | windowsCache: {}, 542 | // windowCache is an object, keeping track of data that is loaded in the background: 543 | // 544 | // { 545 | // 'window_identifier_key_1': undefined, // <-- data for this window is not available, but can be requested 546 | // 'window_identifier_key_2': false, // <-- this window does not contain valid data 547 | // 'window_identifier_key_3': { // <-- data for this window has been requested, but not been returned so far 548 | // request: jqXHRObject, 549 | // data: undefined 550 | // }, 551 | // 'window_identifier_key_4': { // <-- data for this window is available 552 | // request: jqXHRObject, 553 | // data: dataObject 554 | // }, 555 | // } 556 | annotationsCache: {}, 557 | // annotationsCache is an object, keeping track of annotations loaded from the server: 558 | // 559 | // { 560 | // 'start_end_answer': undefined, // <-- data for this window is not available, but can be requested 561 | // 'start_end_answer': {}, // <-- data for this window has been requested already 562 | // } 563 | } 564 | }, 565 | 566 | _shouldBeMergedDeeply: function(objectA) { 567 | if (!objectA) return false; 568 | if (typeof objectA == 'number') return false; 569 | if (typeof objectA == 'string') return false; 570 | if (typeof objectA == 'boolean') return false; 571 | if (objectA instanceof Array) return false; 572 | return true; 573 | }, 574 | 575 | _mergeObjectsDeeply: function(target) { 576 | var that = this; 577 | var sources = [].slice.call(arguments, 1); 578 | sources.forEach(function (source) { 579 | for (var prop in source) { 580 | if (that._shouldBeMergedDeeply(target[prop]) && that._shouldBeMergedDeeply(source[prop])) { 581 | target[prop] = that._mergeObjectsDeeply(target[prop], source[prop]); 582 | } 583 | else { 584 | target[prop] = source[prop]; 585 | } 586 | } 587 | }); 588 | return target; 589 | }, 590 | 591 | _fetchOptionsFromURLParameter: function() { 592 | var that = this; 593 | if (!that.options.optionsURLParameter) return; 594 | var optionsStringFromURL = that._getUrlParameter(that.options.optionsURLParameter); 595 | if (!optionsStringFromURL) return; 596 | try { 597 | var optionsFromURL = JSON.parse(optionsStringFromURL); 598 | that._mergeObjectsDeeply(that.options, optionsFromURL); 599 | } 600 | catch (e) { 601 | console.log('The following options string does not have valid JSON syntax:', optionsStringFromURL); 602 | } 603 | }, 604 | 605 | _createHTMLContent: function() { 606 | var that = this; 607 | var content = ' \ 608 |
\ 609 |
\ 610 |
\ 611 |
\ 612 |
\ 613 |
\ 614 |
\ 615 |
\ 616 |
\ 617 |
\ 618 |
\ 619 |
\ 620 |
\ 621 | \ 622 | \ 623 | \ 624 | \ 625 |
\ 626 |
\ 627 | \ 628 | \ 629 |
\ 630 |
\ 631 | \ 632 |

\ 633 |
\ 634 | \ 662 |
\ 663 |
\ 664 |
\ 665 |
\ 666 |
\ 667 |
\ 668 |
\ 669 | \ 688 |
\ 689 | '; 690 | $(that.element).html(content); 691 | }, 692 | 693 | _adaptContent: function() { 694 | var that = this; 695 | if (!that.options.channelGainAdjustmentEnabled) { 696 | $(that.element).find('.adjustment_buttons').hide(); 697 | } 698 | if (!that.options.showArtifactButtons) { 699 | $(that.element).find('.artifact_panel').hide(); 700 | } 701 | if (!that.options.showSleepStageButtons) { 702 | $(that.element).find('.sleep_stage_panel').hide(); 703 | } 704 | if (!that.options.showNavigationButtons) { 705 | $(that.element).find('.navigation_panel').hide(); 706 | } 707 | if (!that.options.showBackToLastActiveWindowButton) { 708 | $(that.element).find('.backToLastActiveWindow').hide(); 709 | } 710 | if (!that.options.showFastBackwardButton) { 711 | $(that.element).find('.fastBackward').hide(); 712 | } 713 | if (!that.options.showBackwardButton) { 714 | $(that.element).find('.backward').hide(); 715 | } 716 | if (!that.options.showForwardButton) { 717 | $(that.element).find('.forward').hide(); 718 | } 719 | if (!that.options.showFastForwardButton) { 720 | $(that.element).find('.fastForward').hide(); 721 | } 722 | if (!that.options.showShortcuts) { 723 | $(that.element).find('.keyboardShortcuts').hide(); 724 | } 725 | if (!that.options.showAnnotationTime) { 726 | $(that.element).find('.annotationTime').hide(); 727 | } 728 | if (!that.options.showLogoutButton) { 729 | $(that.element).find('.logout').hide(); 730 | } 731 | if (!that._isHITModeEnabled()) { 732 | $(that.element).find('.progress').hide(); 733 | } 734 | $(that.element).css({ 735 | marginTop: that.options.marginTop, 736 | marginBottom: that.options.marginBottom, 737 | }) 738 | }, 739 | 740 | _forcePlayTrainingVideo: function() { 741 | var that = this; 742 | var videoBox = bootbox.dialog({ 743 | title: 'Training Video (PLEASE TURN UP YOUR SOUND VOLUME)', 744 | onEscape: false, 745 | backdrop: false, 746 | closeButton: false, 747 | animate: true, 748 | message: '
', 749 | size: 'large', 750 | }); 751 | videoBox.appendTo(that.element); 752 | videoBox.css({ 753 | backgroundColor: 'rgba(0, 0, 0, 1)', 754 | zIndex: 999999, 755 | }); 756 | if (that.options.trainingVideo.blockInteraction) { 757 | videoBox.find('.interaction-blocker').css({ 758 | position: 'fixed', 759 | width: '100%', 760 | height: '100%', 761 | left: 0, 762 | top: 0, 763 | }); 764 | } 765 | var videoContainer = videoBox.find('.training-video'); 766 | var videoId = that.options.trainingVideo.vimeoId; 767 | var aspectRatio = 513 / 287 768 | var width = Math.round(videoContainer.width()); 769 | var height = Math.round(width / aspectRatio); 770 | var playerId = that._getUUID(); 771 | $.getJSON('http://www.vimeo.com/api/oembed.json?url=' + encodeURIComponent('http://vimeo.com/' + videoId) + '&title=0&byline=0&portrait=0&badge=0&loop=0&autoplay=1&width=' + width + '&height=' + height + '&api=1&player_id=' + playerId + '&callback=?', function(data) { 772 | var playerIFrame = $(data.html).attr('id', playerId).appendTo(videoContainer); 773 | var player = $f(playerIFrame[0]); 774 | player.addEvent('ready', function() { 775 | player.addEvent('finish',function() { 776 | videoBox.remove(); 777 | }); 778 | }); 779 | }); 780 | }, 781 | 782 | _showConsentForm: function() { 783 | var that = this; 784 | var confirmationCodeInfo = ''; 785 | if (that.options.showConfirmationCode && that.options.confirmationCode) { 786 | confirmationCodeInfo = '. For the payment to be processed correctly you need to enter the confirmation code presented to you at the end of the task into the corresponding input field in the instructions panel on Mechanical Turk'; 787 | } 788 | bootbox.dialog({ 789 | onEscape: false, 790 | backdrop: false, 791 | closeButton: false, 792 | animate: true, 793 | title: 'Information Consent', 794 | message: ' \ 795 |
\ 796 | You are invited to participate in a research study conducted by Mike Schaekermann under the supervision of Professor Edith Law of the University of Waterloo, Canada. The objectives of the research study are to develop a low cost crowdsourcing system for EEG analysis for use in the third world.

\ 797 | If you decide to participate, you will be asked to complete a 20-30 minute online EEG analysis task, as described on the task listing. Participation in this study is voluntary. You may decline to answer any questions that you do not wish to answer and you can withdraw your participation at any time by closing this browser tab or window. You will be paid $' + that.options.payment.toFixed(2) + ' upon completion of the task' + confirmationCodeInfo + '. Unfortunately we are unable to pay participants who do not complete the task. There are no known or anticipated risks from participating in this study.

\ 798 | It is important for you to know that any information that you provide will be confidential. All of the data will be summarized and no individual could be identified from these summarized results. Furthermore, the web site is programmed to collect responses alone and will not collect any information that could potentially identify you (such as machine identifiers). The data collected from this study will be maintained on a password-protected computer database in a restricted access area of the university. As well, the data will be electronically archived after completion of the study and maintained for eight years and then erased.

\ 799 | This survey uses Mechanical Turk which is a United States of America company. Consequently, USA authorities under provisions of the Patriot Act may access this survey data. If you prefer not to submit your data through Mechanical Turk, please do not participate.

\ 800 | Note that the remuneration you receive may be taxable income. You are responsible for reporting this income for tax purposes. Should you have any questions about the study, please contact either Mike Schaekermann (mschaeke@uwaterloo.ca) or Edith Law (edith.law@uwaterloo.ca). Further, if you would like to receive a copy of the results of this study, please contact either investigator.

\ 801 | I would like to assure you that this study has been reviewed and received ethics clearance through a University of Waterloo Research Ethics Committee. However, the final decision about participation is yours. Should you have any comments or concerns resulting about your participation in this study, please contact Dr. Maureen Nummelin in the Office of Research Ethics at 1-519-888-4567, Ext. 36005 or maureen.nummelin@uwaterloo.ca.

\ 802 |
\ 803 | ', 804 | buttons: { 805 | consent: { 806 | label: 'I understand and accept the participant consent agreement', 807 | className: 'btn-success', 808 | } 809 | } 810 | }).css({ 811 | zIndex: 99999, 812 | }).appendTo(that.element); 813 | }, 814 | 815 | _isHITModeEnabled: function() { 816 | var that = this; 817 | return ( 818 | that._isVisibleRegionDefined() 819 | && that.options.visibleRegion.hitModeEnabled 820 | ); 821 | }, 822 | 823 | _isVisibleRegionDefined: function() { 824 | var that = this; 825 | return ( 826 | that.options.visibleRegion.start !== undefined 827 | && that.options.visibleRegion.end !== undefined 828 | ); 829 | }, 830 | 831 | _setupHITMode: function() { 832 | var that = this; 833 | if (!that._isHITModeEnabled()) return; 834 | 835 | that.options.showBackToLastActiveWindowButton = false; 836 | that.options.showFastBackwardButton = false; 837 | that.options.showBackwardButton = false; 838 | that.options.showForwardButton = false; 839 | that.options.showFastForwardButton = false; 840 | that.options.showShortcuts = false; 841 | that.options.showAnnotationTime = false; 842 | 843 | $(that.element).find('.graph_footer .middle').append(' \ 844 | \ 845 | \ 846 | \ 847 | '); 848 | 849 | $(that.element).find('.submit-annotations').click(function () { 850 | that._blockGraphInteraction(); 851 | if (that._isCurrentWindowTrainingWindow()) { 852 | that._revealCorrectAnnotations(); 853 | } 854 | // log this window as complete and 855 | // set bookmark to next window so that 856 | // on page load, the user cannot change 857 | // any annotations made before submitting 858 | that._saveUserEventWindowComplete(); 859 | that._savePreferences({ current_page_start: that.vars.currentWindowStart + that.options.windowSizeInSeconds }) 860 | $(that.element).find('.submit-annotations').prop('disabled', true); 861 | $(that.element).find('.next-window').prop('disabled', false); 862 | }); 863 | 864 | $(that.element).find('.next-window').click(function () { 865 | $(that.element).find('.no-features').prop('disabled', false); 866 | $(that.element).find('.submit-features').prop('disabled', true); 867 | $(that.element).find('.next-window').prop('disabled', true); 868 | if (that._isCurrentWindowLastTrainingWindow() && !that._isTrainingOnly()) { 869 | bootbox.alert({ 870 | closeButton: false, 871 | title: 'End of the Training Phase', 872 | message: 'You just completed the last window of the training phase. That means that, from now on, you will not be able to see the correct answer after submitting yours any longer. The examples panel below, however, will stay visible throughout the entire task. Hopefully, the training phase helped you learn more about the signal pattern we are looking for!', 873 | callback: function() { 874 | that._shiftChart(1); 875 | that._unblockGraphInteraction(); 876 | } 877 | }).appendTo(that.element); 878 | } 879 | else { 880 | that._shiftChart(1); 881 | that._unblockGraphInteraction(); 882 | } 883 | }); 884 | 885 | that._fetchOptionsFromURLParameter(); 886 | }, 887 | 888 | _getCurrentWindowIndexInVisibleRegion: function() { 889 | var that = this; 890 | if (!that._isHITModeEnabled()) return; 891 | var windowIndex = Math.floor((that.vars.currentWindowStart - that.options.visibleRegion.start) / that.options.windowSizeInSeconds); 892 | return windowIndex; 893 | }, 894 | 895 | _getNumberOfTrainingWindows: function() { 896 | var that = this; 897 | var training = that.options.visibleRegion.training; 898 | if (!that._isTrainingEnabled()) { 899 | return 0; 900 | } 901 | if (training.numberOfInitialWindowsUsedForTraining > 0) { 902 | return training.numberOfInitialWindowsUsedForTraining; 903 | } 904 | return that._getSpecifiedTrainingWindows().length; 905 | }, 906 | 907 | _areTrainingWindowsSpecified: function() { 908 | var that = this; 909 | that._getSpecifiedTrainingWindows(); 910 | return ( 911 | that.vars.specifiedTrainingWindows !== undefined 912 | && that.vars.specifiedTrainingWindows.length > 0 913 | ); 914 | }, 915 | 916 | _getCurrentTrainingWindow: function() { 917 | var that = this; 918 | if (!that._areTrainingWindowsSpecified()) { 919 | return; 920 | } 921 | var trainingWindows = that._getSpecifiedTrainingWindows(); 922 | var currentIndex = that.vars.currentTrainingWindowIndex; 923 | if (currentIndex > trainingWindows.length - 1) { 924 | return; 925 | } 926 | var trainingWindow = trainingWindows[currentIndex]; 927 | return trainingWindow; 928 | }, 929 | 930 | _isCurrentWindowSpecifiedTrainingWindow: function() { 931 | var that = this; 932 | if (!that._areTrainingWindowsSpecified()) return false; 933 | return that.vars.currentTrainingWindowIndex < that._getNumberOfTrainingWindows(); 934 | }, 935 | 936 | _getSpecifiedTrainingWindows: function() { 937 | var that = this; 938 | if (that.vars.specifiedTrainingWindows) { 939 | return that.vars.specifiedTrainingWindows; 940 | } 941 | var training = that.options.visibleRegion.training; 942 | if (!that._isTrainingEnabled() || training.numberOfInitialWindowsUsedForTraining > 0) { 943 | return []; 944 | } 945 | if (training.windows && training.windows.length > 0) { 946 | that.vars.specifiedTrainingWindows = training.windows; 947 | return that.vars.specifiedTrainingWindows; 948 | } 949 | var windows = []; 950 | var featureOrder = that.options.features.order; 951 | var featureOptions = that.options.features.options; 952 | for (f = 0; f < featureOrder.length; ++f) { 953 | var feature = featureOrder[f]; 954 | var featureTrainingWindows = featureOptions[feature].training.windows; 955 | if (featureTrainingWindows && featureTrainingWindows.length > 0) { 956 | windows.push.apply(windows, featureTrainingWindows); 957 | } 958 | } 959 | that.vars.specifiedTrainingWindows = windows; 960 | return that.vars.specifiedTrainingWindows; 961 | }, 962 | 963 | _isTrainingEnabled: function() { 964 | var that = this; 965 | return (that._isHITModeEnabled() && that.options.visibleRegion.training.enabled); 966 | }, 967 | 968 | _isTrainingOnly: function() { 969 | var that = this; 970 | return that.options.visibleRegion.training.isTrainingOnly; 971 | }, 972 | 973 | _isCurrentWindowTrainingWindow: function() { 974 | var that = this; 975 | if (!that._isHITModeEnabled()) return false; 976 | return that._getWindowIndexForTraining() <= that._getNumberOfTrainingWindows() - 1; 977 | }, 978 | 979 | _isCurrentWindowFirstTrainingWindow: function() { 980 | var that = this; 981 | if (!that._isHITModeEnabled()) return false; 982 | return ( 983 | that._getNumberOfTrainingWindows() > 0 984 | && that._getWindowIndexForTraining() === 0 985 | ); 986 | }, 987 | 988 | _isCurrentWindowLastTrainingWindow: function() { 989 | var that = this; 990 | if (!that._isHITModeEnabled()) return false; 991 | return that._getWindowIndexForTraining() == that._getNumberOfTrainingWindows() - 1; 992 | }, 993 | 994 | _getWindowIndexForTraining: function() { 995 | var that = this; 996 | if (!that._isHITModeEnabled()) return false; 997 | if (that._areTrainingWindowsSpecified()) { 998 | return that.vars.currentTrainingWindowIndex; 999 | } 1000 | else { 1001 | return that._getCurrentWindowIndexInVisibleRegion(); 1002 | } 1003 | }, 1004 | 1005 | _revealCorrectAnnotations: function() { 1006 | var that = this; 1007 | that._getAnnotations(that.vars.currentWindowRecording, that.vars.currentWindowStart, that.vars.currentWindowStart + that.options.windowSizeInSeconds, true); 1008 | }, 1009 | 1010 | _setupExamplesMode: function() { 1011 | var that = this; 1012 | var examples = that.options.features.examples; 1013 | 1014 | if (!examples || examples.length == 0) { 1015 | console.log('There are no examples for this viewer.'); 1016 | return; 1017 | } 1018 | examples.sort(function(a, b) { 1019 | return a.start - b.start; 1020 | }); 1021 | var firstExample = examples[0]; 1022 | var recordingName = firstExample.recording; 1023 | var channelsDisplayed = [firstExample.channels_displayed[firstExample.channels]]; 1024 | that.options.recordingName = recordingName; 1025 | that.options.channelsDisplayed = channelsDisplayed; 1026 | that.options.graph.height = 200; 1027 | that.options.features.showUserAnnotations = false; 1028 | that.options.features.order = [ firstExample.type ]; 1029 | that.options.isReadOnly = true; 1030 | that.options.channelGainAdjustmentEnabled = false; 1031 | that.options.keyboardInputEnabled = false; 1032 | that.options.showArtifactButtons = false; 1033 | that.options.showNavigationButtons = false; 1034 | that.options.showReferenceLines = false; 1035 | that.options.features.cheatSheetsEnabled = true; 1036 | that.options.features.openCheatSheetOnPageLoad = true; 1037 | that.options.showTimeLabels = false; 1038 | that._fetchOptionsFromURLParameter(); 1039 | 1040 | $(that.element).find('.button_container').prepend('Examples for:'); 1041 | $(that.element).find('.button_container').append(' \ 1042 |
\ 1043 | \ 1046 | \ 1049 | \ 1052 |
\ 1053 | '); 1054 | 1055 | if (!that.options.features.cheatSheetsEnabled) { 1056 | $(that.element).find('.open-cheat-sheet').remove(); 1057 | } 1058 | else { 1059 | $(that.element).find('.open-cheat-sheet').click(function() { 1060 | that._saveUserEvent('open_cheat_sheet', { 1061 | feature: firstExample.type, 1062 | }); 1063 | openCheatSheet(); 1064 | }); 1065 | if (that.options.features.openCheatSheetOnPageLoad) { 1066 | $(that.element).hover(function() { 1067 | if (that.vars.cheatSheetOpenedBefore) return; 1068 | openCheatSheet(); 1069 | }); 1070 | } 1071 | function openCheatSheet() { 1072 | that.vars.cheatSheetOpenedBefore = true; 1073 | bootbox.dialog({ 1074 | title: 'PLEASE READ CAREFULLY', 1075 | message: '', 1076 | buttons: { 1077 | close: { 1078 | label: 'Close', 1079 | } 1080 | }, 1081 | size: 'large', 1082 | }).appendTo(that.element); 1083 | } 1084 | } 1085 | 1086 | that.vars.currentExampleIndex = 0; 1087 | that.options.startTime = that._getWindowStartForTime(examples[that.vars.currentExampleIndex].start); 1088 | 1089 | $(that.element).find('.next-example').click(function() { 1090 | that._saveUserEvent('view_example_window', { 1091 | feature: firstExample.type, 1092 | direction: 'next', 1093 | }); 1094 | that._clearScrollThroughExamplesInterval(); 1095 | that._showNextExample(1); 1096 | }); 1097 | $(that.element).find('.previous-example').click(function() { 1098 | that._saveUserEvent('view_example_window', { 1099 | feature: firstExample.type, 1100 | direction: 'previous', 1101 | }); 1102 | that._clearScrollThroughExamplesInterval(); 1103 | that._showNextExample(-1); 1104 | }); 1105 | if (that.options.features.scrollThroughExamplesAutomatically) { 1106 | $(that.element).hover(function() { 1107 | if (that.vars.scrollThroughExamplesIntervalId !== undefined) return; 1108 | that.vars.scrollThroughExamplesIntervalId = window.setInterval(function() { 1109 | that._showNextExample(1); 1110 | }, that.options.features.scrollThroughExamplesSpeedInSeconds * 1000); 1111 | }); 1112 | } 1113 | 1114 | var wrapper = $('
').addClass('well'); 1115 | $(that.element).children().wrap(wrapper); 1116 | that.options.graph.backgroundColor = 'none'; 1117 | 1118 | that._setup(); 1119 | }, 1120 | 1121 | _clearScrollThroughExamplesInterval: function() { 1122 | var that = this; 1123 | if ( 1124 | that.vars.scrollThroughExamplesIntervalId !== undefined 1125 | && that.vars.scrollThroughExamplesIntervalId !== false 1126 | ) { 1127 | window.clearInterval(that.vars.scrollThroughExamplesIntervalId); 1128 | that.vars.scrollThroughExamplesIntervalId = false; 1129 | } 1130 | }, 1131 | 1132 | _showNextExample: function(stepLength) { 1133 | var that = this; 1134 | do { 1135 | that.vars.currentExampleIndex += stepLength; 1136 | that.vars.currentExampleIndex %= that.options.features.examples.length; 1137 | while (that.vars.currentExampleIndex < 0) { 1138 | that.vars.currentExampleIndex += that.options.features.examples.length; 1139 | } 1140 | var example = that.options.features.examples[that.vars.currentExampleIndex]; 1141 | var nextWindowStart = that._getWindowStartForTime(example.start); 1142 | } while (nextWindowStart == that.vars.currentWindowStart); 1143 | that._switchToWindow(that.options.recordingName, nextWindowStart, that.options.windowSizeInSeconds, that.options.graph.channelSpacing); 1144 | }, 1145 | 1146 | _getWindowStartForTime: function(time) { 1147 | var that = this; 1148 | var windowStart = Math.floor(time / that.options.windowSizeInSeconds); 1149 | windowStart *= that.options.windowSizeInSeconds; 1150 | return windowStart; 1151 | }, 1152 | 1153 | _setup: function() { 1154 | var that = this; 1155 | that._adaptContent(); 1156 | that._setupTimer(); 1157 | that._setupFeaturePanel(); 1158 | that._setupNavigationPanel(); 1159 | that._setupArtifactPanel(); 1160 | that._setupSleepStagePanel(); 1161 | that._setupTrainingPhase(); 1162 | that._getUserStatus(); 1163 | }, 1164 | 1165 | _getUrlParameter: function(sParam) { 1166 | var sPageURL = decodeURIComponent(window.location.search.substring(1)), 1167 | sURLVariables = sPageURL.split('&'), 1168 | sParameterName, 1169 | i; 1170 | 1171 | for (i = 0; i < sURLVariables.length; i++) { 1172 | sParameterName = sURLVariables[i].split('='); 1173 | 1174 | if (sParameterName[0] === sParam) { 1175 | return sParameterName[1] === undefined ? true : sParameterName[1]; 1176 | } 1177 | } 1178 | }, 1179 | 1180 | _setupExperiment: function() { 1181 | var that = this; 1182 | if (!that.options.experiment.running) return; 1183 | var temporalContextHint; 1184 | switch (that.options.experiment.current_condition.temporal_context) { 1185 | case 'continuous': 1186 | temporalContextHint = 'Continuous sequence of windows'; 1187 | break; 1188 | case 'shuffled': 1189 | temporalContextHint = 'Shuffled sequence of windows'; 1190 | break; 1191 | } 1192 | var hint = $('

').html(temporalContextHint); 1193 | $(that.element).find('.experiment_container .hints_container').append(hint); 1194 | }, 1195 | 1196 | _updateNavigationStatusForExperiment: function() { 1197 | var that = this; 1198 | if (!that.options.experiment.running) return; 1199 | var currentWindowIndex = that.options.experiment.current_condition.current_window_index; 1200 | var conditionWindows = that.options.experiment.current_condition.windows; 1201 | var lastWindowIndex = conditionWindows.length - 1; 1202 | var windowsRemaining = lastWindowIndex - currentWindowIndex; 1203 | that._setForwardEnabledStatus(windowsRemaining >= 1); 1204 | if (windowsRemaining < 1) { 1205 | that._lastWindowReached(); 1206 | } 1207 | that._setFastForwardEnabledStatus(windowsRemaining >= that.options.windowJumpSizeFastForwardBackward); 1208 | that._setBackwardEnabledStatus(currentWindowIndex >= 1); 1209 | that._setFastBackwardEnabledStatus(currentWindowIndex >= that.options.windowJumpSizeFastForwardBackward); 1210 | if (that.options.experiment.current_condition.temporal_context == 'shuffled') { 1211 | that._setFastForwardEnabledStatus(false); 1212 | that._setFastBackwardEnabledStatus(false); 1213 | $(that.element).find('.fastForward').hide(); 1214 | $(that.element).find('.fastBackward').hide(); 1215 | } 1216 | }, 1217 | 1218 | _setupTimer: function() { 1219 | var that = this; 1220 | that.vars.totalAnnotationTimeSeconds = 0 1221 | var preferences = {}; 1222 | if (that.vars.taskDataConfiguration) { 1223 | preferences = that.vars.taskDataConfiguration.configuration; 1224 | } 1225 | if (preferences.total_annotation_time_seconds) { 1226 | that.vars.totalAnnotationTimeSeconds = parseFloat(preferences.total_annotation_time_seconds); 1227 | } 1228 | that.vars.lastAnnotationTime = that._getCurrentServerTimeMilliSeconds(); 1229 | if (preferences.last_annotation_time) { 1230 | that.vars.lastAnnotationTime = parseInt(preferences.last_annotation_time); 1231 | } 1232 | var timerContainer = $(that.element).find('.annotation-time-container'); 1233 | var timeContainer = $('').addClass('time form-control'); 1234 | timerContainer.append(timeContainer); 1235 | that.vars.annotationTimeContainer = timeContainer; 1236 | that._setTotalAnnotationTimeSeconds(that.vars.totalAnnotationTimeSeconds); 1237 | }, 1238 | 1239 | _setTotalAnnotationTimeSeconds: function(timeSeconds) { 1240 | var that = this; 1241 | that.vars.totalAnnotationTimeSeconds = timeSeconds; 1242 | if (!that.vars.annotationTimeContainer) { 1243 | return; 1244 | } 1245 | that.vars.annotationTimeContainer 1246 | .timer('remove') 1247 | .timer({ 1248 | seconds: that.vars.totalAnnotationTimeSeconds, 1249 | format: '%H:%M:%S' 1250 | }) 1251 | .timer('pause'); 1252 | }, 1253 | 1254 | _updateLastAnnotationTime: function() { 1255 | var that = this; 1256 | var currentTime = that._getCurrentServerTimeMilliSeconds(); 1257 | var timeDifferenceSeconds = (currentTime - that.vars.lastAnnotationTime) / 1000; 1258 | that.vars.lastAnnotationTime = currentTime; 1259 | var preferencesUpdates = { 1260 | last_annotation_time: that.vars.lastAnnotationTime 1261 | } 1262 | if (timeDifferenceSeconds <= that.options.idleTimeThresholdSeconds) { 1263 | that.vars.totalAnnotationTimeSeconds += timeDifferenceSeconds; 1264 | that._setTotalAnnotationTimeSeconds(that.vars.totalAnnotationTimeSeconds); 1265 | preferencesUpdates.total_annotation_time_seconds = that.vars.totalAnnotationTimeSeconds; 1266 | } 1267 | that.vars.lastActiveWindowStart = that.vars.currentWindowStart; 1268 | that._savePreferences(preferencesUpdates); 1269 | }, 1270 | 1271 | _getCurrentServerTimeMilliSeconds: function() { 1272 | var today = new Date(); 1273 | var serverOffset = -5; 1274 | var date = new Date().getTime() + serverOffset * 3600 * 1000; 1275 | return date; 1276 | }, 1277 | 1278 | _setupFeaturePanel: function() { 1279 | var that = this; 1280 | $('[data-toggle="popover"]').popover({ trigger: 'hover' }); 1281 | 1282 | var firstFeature = that.options.features.order[0]; 1283 | that.vars.activeFeatureType = firstFeature; 1284 | 1285 | for (var i = 0; i < that.options.features.order.length; i++) { 1286 | var feature_key = that.options.features.order[i]; 1287 | var feature_name = that.options.features.options[feature_key].name; 1288 | var featureButton = $('').data('annotation-type', feature_key); 1289 | $(that.element).find('.feature_panel').append(featureButton); 1290 | $('').appendTo('head'); 1291 | } 1292 | $(that.element).find('.feature').click(function(event) { 1293 | that._selectFeatureClass($(this)); 1294 | }); 1295 | $(that.element).find('.feature.' + firstFeature) 1296 | .addClass('active') 1297 | .siblings() 1298 | .removeClass('active'); 1299 | }, 1300 | 1301 | _getUserStatus: function() { 1302 | var that = this; 1303 | if (that.options.experiment.running) { 1304 | that._updateNavigationStatusForExperiment(); 1305 | var currentWindowIndex = that.options.experiment.current_condition.current_window_index; 1306 | var conditionWindows = that.options.experiment.current_condition.windows; 1307 | that.options.recordingName = that.options.experiment.current_condition.recording_name; 1308 | var initialWindowStart = conditionWindows[currentWindowIndex]; 1309 | that.vars.lastActiveWindowStart = initialWindowStart; 1310 | that._switchToWindow(that.options.recordingName, initialWindowStart, that.options.windowSizeInSeconds, that.options.graph.channelSpacing); 1311 | } 1312 | else if (that._areTrainingWindowsSpecified()) { 1313 | var trainingWindow = that._getCurrentTrainingWindow(); 1314 | that._switchToWindow(trainingWindow.recordingName, trainingWindow.timeStart, trainingWindow.windowSizeInSeconds, that.options.graph.channelSpacing); 1315 | } 1316 | else if (that.options.recordingName) { 1317 | that.vars.lastActiveWindowStart = that.options.startTime; 1318 | that._switchToWindow(that.options.recordingName, that.vars.lastActiveWindowStart, that.options.windowSizeInSeconds, that.options.graph.channelSpacing); 1319 | } 1320 | else { 1321 | alert('Could not retrieve user data.'); 1322 | } 1323 | }, 1324 | 1325 | _setupArtifactPanel: function() { 1326 | var activeClass = 'teal darken-4'; 1327 | var that = this; 1328 | $(that.element).find('.artifact_panel button.artifact').click(function() { 1329 | var button = $(this); 1330 | var type = button.data('annotation-type'); 1331 | that._saveArtifactAnnotation(type); 1332 | button 1333 | .addClass(activeClass) 1334 | .siblings() 1335 | .removeClass(activeClass); 1336 | }); 1337 | }, 1338 | 1339 | _setupSleepStagePanel: function() { 1340 | var activeClass = 'teal'; 1341 | var inactiveClass = 'blue lighten-1'; 1342 | var that = this; 1343 | $(that.element).find('.sleep_stage_panel button.sleep_stage').click(function() { 1344 | $(that.element).find('.submit-response-for-data').prop('disabled', null); 1345 | var button = $(this); 1346 | var type = button.data('annotation-type'); 1347 | that._saveSleepStageAnnotation(type); 1348 | that.vars.sleepStageSelected = type; 1349 | button 1350 | .addClass(activeClass) 1351 | .removeClass(inactiveClass) 1352 | .siblings() 1353 | .removeClass(activeClass) 1354 | .addClass(inactiveClass); 1355 | }); 1356 | 1357 | if (that.options.instructionSlidesUrl) { 1358 | $('OPEN SLIDES FROM TRAINING VIDEO IN NEW TAB').insertBefore($(that.element).find('.phase-indicator')); 1359 | } 1360 | 1361 | if (!that.options.training.enabled) { 1362 | $(that.element).find('.phase-indicator').html('TESTING PHASE (NO FEEDBACK!)'); 1363 | } 1364 | else { 1365 | $(that.element).find('.phase-indicator').html('TRAINING PHASE (WITH FEEDBACK)'); 1366 | } 1367 | $(that.element).find('.submit-response-for-data').click(function() { 1368 | $(that.element).find('.sleep_stage_panel button.sleep_stage').prop('disabled', true); 1369 | $(that.element).find('.submit-response-for-data').prop('disabled', true); 1370 | function reloadPage() { 1371 | window.location.reload(); 1372 | } 1373 | if (!that.options.training.enabled) { 1374 | reloadPage(); 1375 | return; 1376 | } 1377 | var groundTruth = that.options.training.groundTruth || []; 1378 | var groundTruthForWindow = undefined; 1379 | groundTruth.forEach(function(g) { 1380 | if (g.start == that.vars.currentWindowStart) { 1381 | groundTruthForWindow = g; 1382 | } 1383 | }); 1384 | if (!groundTruthForWindow) { 1385 | reloadPage(); 1386 | return; 1387 | } 1388 | var makeSleepStageLabelHumanReadable = { 1389 | sleep_stage_wake: 'Wake', 1390 | sleep_stage_n1: 'N1 Sleep', 1391 | sleep_stage_n2: 'N2 Sleep', 1392 | sleep_stage_n3: 'N3 Sleep', 1393 | sleep_stage_rem: 'REM Sleep', 1394 | }; 1395 | if (that.vars.sleepStageSelected == groundTruthForWindow.label) { 1396 | var message = 'Congratulations, your answer is correct! This is indeed "' + makeSleepStageLabelHumanReadable[groundTruthForWindow.label] + '".'; 1397 | } 1398 | else { 1399 | var message = 'Unfortunately, your answer is wrong! You selected "' + makeSleepStageLabelHumanReadable[that.vars.sleepStageSelected] + '", but the correct answer is "' + makeSleepStageLabelHumanReadable[groundTruthForWindow.label] + '".'; 1400 | } 1401 | message += '\n\n'; 1402 | if (groundTruthForWindow.showExplanationVideo && groundTruthForWindow.explanationVideo && groundTruthForWindow.explanationVideo.id) { 1403 | message += 'Click OK to watch an expert discussion about this particular case. After watching the video, you will be automatically taken to the next example.' 1404 | } 1405 | else { 1406 | message += 'Click OK to go to the next step.' 1407 | } 1408 | alert(message); 1409 | if (!groundTruthForWindow.showExplanationVideo || !groundTruthForWindow.explanationVideo || !groundTruthForWindow.explanationVideo.id) { 1410 | reloadPage(); 1411 | return; 1412 | } 1413 | var videoWidth = 900; 1414 | var videoHeight = 570; 1415 | var videoBox = bootbox.dialog({ 1416 | title: 'EXPERT DISCUSSION (PLEASE TURN UP YOUR SOUND)', 1417 | onEscape: false, 1418 | backdrop: false, 1419 | closeButton: false, 1420 | animate: true, 1421 | message: '
', 1422 | size: 'large', 1423 | }); 1424 | videoBox.appendTo(that.element); 1425 | videoBox.find('.modal-content').css({ 1426 | padding: 0, 1427 | }) 1428 | videoBox.find('.modal-header').hide(); 1429 | videoBox.css({ 1430 | backgroundColor: 'rgba(0, 0, 0, 1)', 1431 | zIndex: 999999, 1432 | top: 70, 1433 | width: videoWidth + 20, 1434 | minHeight: videoHeight + 20, 1435 | border: '10px solid #000000', 1436 | }); 1437 | videoBox.find('.interaction-blocker').css({ 1438 | position: 'fixed', 1439 | width: '100%', 1440 | height: '100%', 1441 | left: 0, 1442 | top: 0, 1443 | }); 1444 | var player = new YTPlayer('#expert-discussion-player', { 1445 | captions: true, 1446 | controls: false, 1447 | fullscreen: true, 1448 | annotations: true, 1449 | modestBranding: true, 1450 | related: false, 1451 | info: false, 1452 | width: videoWidth, 1453 | height: videoHeight, 1454 | }) 1455 | player.load(groundTruthForWindow.explanationVideo.id); 1456 | player.seek(groundTruthForWindow.explanationVideo.start); 1457 | player.setVolume(100); 1458 | player.on('timeupdate', (seconds) => { 1459 | if (seconds >= groundTruthForWindow.explanationVideo.end) { 1460 | destroyVideoAndLoadNextStep(); 1461 | } 1462 | }) 1463 | player.on('ended', destroyVideoAndLoadNextStep); 1464 | function destroyVideoAndLoadNextStep() { 1465 | player.destroy(); 1466 | videoBox.remove(); 1467 | setTimeout(reloadPage, 1000); 1468 | } 1469 | }); 1470 | }, 1471 | 1472 | _setupNavigationPanel: function() { 1473 | var that = this; 1474 | that._setForwardEnabledStatus(false); 1475 | that._setFastForwardEnabledStatus(false); 1476 | that._setBackwardEnabledStatus(false); 1477 | that._setFastBackwardEnabledStatus(false); 1478 | 1479 | if (that.options.showBackToLastActiveWindowButton) { 1480 | $(that.element).find('.backToLastActiveWindow').click(function() { 1481 | that._switchBackToLastActiveWindow(); 1482 | }); 1483 | } 1484 | if (that.options.showForwardButton) { 1485 | $(that.element).find('.forward').click(function() { 1486 | that._shiftChart(1); 1487 | }); 1488 | } 1489 | if (that.options.showBackwardButton) { 1490 | $(that.element).find('.backward').click(function() { 1491 | that._shiftChart(-1); 1492 | }); 1493 | } 1494 | if (that.options.showFastForwardButton) { 1495 | $(that.element).find('.fastForward').click(function() { 1496 | that._shiftChart(that.options.windowJumpSizeFastForwardBackward); 1497 | }); 1498 | } 1499 | if (that.options.showFastBackwardButton) { 1500 | $(that.element).find('.fastBackward').click(function() { 1501 | that._shiftChart(-that.options.windowJumpSizeFastForwardBackward); 1502 | }); 1503 | } 1504 | $(that.element).find('.gainUp').click(function() { 1505 | that._updateChannelGain('step_increase'); 1506 | }); 1507 | $(that.element).find('.gainDown').click(function() { 1508 | that._updateChannelGain('step_decrease'); 1509 | }); 1510 | $(that.element).find('.gainReset').click(function() { 1511 | that._updateChannelGain('reset'); 1512 | }); 1513 | if (that.options.keyboardInputEnabled) { 1514 | // setup arrow key navigation 1515 | $(document).keydown(function(e) { 1516 | var keyCode = e.which; 1517 | var metaKeyPressed = e.metaKey; 1518 | if (keyCode == 82 && metaKeyPressed) { 1519 | // Suppress any action on CTRL+R / CMD+R page reload 1520 | return; 1521 | } 1522 | if(keyCode == 66 && that.options.showBackToLastActiveWindowButton) { 1523 | // back to last active window 1524 | that._switchBackToLastActiveWindow(); 1525 | return; 1526 | } else if((keyCode == 37 || keyCode == 65 || keyCode == 34) && that.options.showBackwardButton) { // left arrow, a, page down 1527 | // backward 1528 | e.preventDefault(); 1529 | that._shiftChart(-1); 1530 | return; 1531 | } else if ((keyCode == 39 || keyCode == 68 || keyCode == 33) && that.options.showForwardButton) { // right arrow, d, page up 1532 | // forward 1533 | e.preventDefault(); 1534 | that._shiftChart(1); 1535 | return; 1536 | } else if (keyCode == 38 && that.options.showFastForwardButton) { // up arrow 1537 | // fast foward 1538 | e.preventDefault(); 1539 | that._shiftChart(that.options.windowJumpSizeFastForwardBackward); 1540 | return; 1541 | } else if (keyCode == 40 && that.options.showFastBackwardButton) { // down arrow 1542 | // fast backward 1543 | e.preventDefault(); 1544 | that._shiftChart(-that.options.windowJumpSizeFastForwardBackward); 1545 | return; 1546 | } else if (that.options.showSleepStageButtons) { 1547 | var sleepStageShortCutPressed = false; 1548 | $(that.element).find('.sleep_stage_panel .shortcut-key').each(function() { 1549 | var character = $(this).text(); 1550 | var characterKeyCodeLowerCase = character.toLowerCase().charCodeAt(0); 1551 | var characterKeyCodeAlternative = character.toUpperCase().charCodeAt(0); 1552 | if (characterKeyCodeLowerCase >= 48 && characterKeyCodeLowerCase <= 57) { 1553 | characterKeyCodeAlternative = characterKeyCodeLowerCase + 48; 1554 | } 1555 | if (keyCode == characterKeyCodeLowerCase || keyCode == characterKeyCodeAlternative) { 1556 | sleepStageShortCutPressed = true; 1557 | var button = $(this).parents('.sleep_stage').first(); 1558 | button.click(); 1559 | } 1560 | }); 1561 | if (sleepStageShortCutPressed) { 1562 | return; 1563 | } 1564 | // make it possible to choose feature classificaiton using number keys 1565 | } else if (keyCode >= 49 && keyCode <= 57) { 1566 | e.preventDefault(); 1567 | var featureClassButton = $(that.element).find('.feature').eq(keyCode - 49) 1568 | if (featureClassButton) { 1569 | that._selectFeatureClass(featureClassButton); 1570 | } 1571 | return; 1572 | // separate case for the numpad keys, because javascript is a stupid language 1573 | } else if (keyCode >= 97 && keyCode <= 105) { 1574 | e.preventDefault(); 1575 | var featureClassButton = $(that.element).find('.feature').eq(keyCode - 97) 1576 | if (featureClassButton) { 1577 | that._selectFeatureClass(featureClassButton); 1578 | } 1579 | return; 1580 | } 1581 | }); 1582 | } 1583 | }, 1584 | 1585 | _setupTrainingPhase: function() { 1586 | var that = this; 1587 | if (!that._areTrainingWindowsSpecified()) return; 1588 | that._setForwardEnabledStatus(true); 1589 | that._getSpecifiedTrainingWindows(); 1590 | }, 1591 | 1592 | _setForwardEnabledStatus: function(status) { 1593 | var that = this; 1594 | var status = !!status; 1595 | 1596 | that.vars.forwardEnabled = status; 1597 | $(that.element).find('.forward').prop('disabled', !status); 1598 | }, 1599 | 1600 | _setFastForwardEnabledStatus: function(status) { 1601 | var that = this; 1602 | var status = !!status; 1603 | 1604 | that.vars.fastForwardEnabled = status; 1605 | $(that.element).find('.fastForward').prop('disabled', !status); 1606 | }, 1607 | 1608 | _setBackwardEnabledStatus: function(status) { 1609 | var that = this; 1610 | var status = !!status; 1611 | 1612 | that.vars.backwardEnabled = status; 1613 | $(that.element).find('.backward').prop('disabled', !status); 1614 | }, 1615 | 1616 | _setFastBackwardEnabledStatus: function(status) { 1617 | var that = this; 1618 | var status = !!status; 1619 | 1620 | that.vars.fastBackwardEnabled = status; 1621 | $(that.element).find('.fastBackward').prop('disabled', !status); 1622 | }, 1623 | 1624 | _selectFeatureClass: function(featureClassButton) { 1625 | /* called with the user clicks on one of the feature toggle buttons, or presses one of the 1626 | relevant number keys, this method updates the state of the toggle buttons, and sets the 1627 | feature type */ 1628 | var that = this; 1629 | featureClassButton.addClass('active'); 1630 | featureClassButton.siblings().removeClass('active'); 1631 | that.vars.activeFeatureType = featureClassButton.data('annotation-type'); 1632 | }, 1633 | 1634 | _shiftChart: function(windows) { 1635 | var that = this; 1636 | if (!that.vars.forwardEnabled && windows >= 1) return; 1637 | if (!that.vars.fastForwardEnabled && windows >= that.options.windowJumpSizeFastForwardBackward) return; 1638 | if (!that.vars.backwardEnabled && windows <= -1) return; 1639 | if (!that.vars.fastBackwardEnabled && windows <= -that.options.windowJumpSizeFastForwardBackward) return; 1640 | var nextRecordingName = that.options.recordingName; 1641 | var nextWindowSizeInSeconds = that.options.windowSizeInSeconds; 1642 | var nextWindowStart = that.options.currentWindowStart; 1643 | if (that.options.experiment.running) { 1644 | var currentWindowIndex = that.options.experiment.current_condition.current_window_index; 1645 | currentWindowIndex += windows; 1646 | that.options.experiment.current_condition.current_window_index = currentWindowIndex; 1647 | nextWindowStart = that.options.experiment.current_condition.windows[currentWindowIndex]; 1648 | that._updateNavigationStatusForExperiment(); 1649 | } 1650 | else if ( 1651 | that._areTrainingWindowsSpecified() 1652 | && that._isCurrentWindowTrainingWindow() 1653 | ) { 1654 | that.vars.currentTrainingWindowIndex += windows; 1655 | if (that._isCurrentWindowTrainingWindow()) { 1656 | var nextTrainingWindow = that._getCurrentTrainingWindow(); 1657 | nextRecordingName = nextTrainingWindow.recordingName; 1658 | nextWindowStart = nextTrainingWindow.timeStart; 1659 | nextWindowSizeInSeconds = nextTrainingWindow.windowSizeInSeconds; 1660 | } 1661 | else { 1662 | nextWindowStart = that.options.startTime; 1663 | nextWindowSizeInSeconds = that.options.windowSizeInSeconds; 1664 | } 1665 | that._flushAnnotations(); 1666 | } 1667 | else { 1668 | if (that._areTrainingWindowsSpecified()) { 1669 | that.vars.currentTrainingWindowIndex += windows; 1670 | } 1671 | nextWindowStart = Math.max(0, that.vars.currentWindowStart + that.options.windowSizeInSeconds * windows); 1672 | } 1673 | that._switchToWindow(nextRecordingName, nextWindowStart, nextWindowSizeInSeconds, that.options.graph.channelSpacing); 1674 | }, 1675 | 1676 | _switchToWindow: function (recording_name, start_time, window_length, channel_spacing) { 1677 | var that = this; 1678 | if (!that._isCurrentWindowSpecifiedTrainingWindow()) { 1679 | if (that.options.visibleRegion.start !== undefined) { 1680 | start_time = Math.max(that.options.visibleRegion.start, start_time); 1681 | start_time = window_length * Math.ceil(start_time / window_length); 1682 | that._setBackwardEnabledStatus(start_time - window_length >= that.options.visibleRegion.start); 1683 | that._setFastBackwardEnabledStatus(start_time - window_length * that.options.windowJumpSizeFastForwardBackward >= that.options.visibleRegion.start); 1684 | } 1685 | 1686 | if (that.options.visibleRegion.end !== undefined) { 1687 | start_time = Math.min(that.options.visibleRegion.end - window_length, start_time); 1688 | start_time = window_length * Math.floor(start_time / window_length); 1689 | var forwardEnabled = start_time + window_length <= that.options.visibleRegion.end - window_length; 1690 | that._setForwardEnabledStatus(forwardEnabled); 1691 | if (!forwardEnabled) { 1692 | that._lastWindowReached(); 1693 | } 1694 | that._setFastForwardEnabledStatus(start_time + window_length * that.options.windowJumpSizeFastForwardBackward < that.options.visibleRegion.end - window_length); 1695 | } 1696 | } 1697 | 1698 | if (that.vars.currentWindowStart != start_time) { 1699 | that._setNumberOfAnnotationsInCurrentWindow(0); 1700 | $(that.element).find('.correct-answer-explanation').children().remove(); 1701 | } 1702 | 1703 | that.vars.currentWindowStart = start_time; 1704 | that.vars.currentWindowRecording = recording_name; 1705 | 1706 | if (that._isVisibleRegionDefined()) { 1707 | var progress = that._getProgressInPercent(); 1708 | $(that.element).find('.progress-bar').css('width', progress + '%'); 1709 | } 1710 | 1711 | if (that._isCurrentWindowLastTrainingWindow() && that._isTrainingOnly()) { 1712 | that._lastWindowReached(); 1713 | } 1714 | 1715 | if (that._isCurrentWindowFirstTrainingWindow() && !that._isTrainingOnly()) { 1716 | bootbox.alert({ 1717 | closeButton: false, 1718 | title: 'Beginning of the Training Phase', 1719 | message: 'Welcome to our CrowdEEG experiment for scientific crowdsourcing!

This is the beginning of the training phase, meaning that, for the next ' + that._getNumberOfTrainingWindows() + ' window(s), we will show you the correct answer after you have submitted yours. The examples panel below will be visible throughout the entire task. We hope the training phase will help you learn more about the signal pattern we are looking for.', 1720 | callback: function() { 1721 | that._saveUserEventWindowBegin(); 1722 | }, 1723 | }).css({zIndex: 1}).appendTo(that.element); 1724 | } 1725 | else { 1726 | that._saveUserEventWindowBegin(); 1727 | } 1728 | 1729 | that._savePreferences({ current_page_start: start_time }) 1730 | 1731 | var windowsToRequest = [ 1732 | start_time 1733 | ]; 1734 | if ( 1735 | !that._isCurrentWindowSpecifiedTrainingWindow() 1736 | && !that.options.experiment.running 1737 | ) { 1738 | for (var i = 1; i <= that.options.numberOfForwardWindowsToPrefetch; ++i) { 1739 | windowsToRequest.push(start_time + i * window_length); 1740 | } 1741 | for (var i = 1; i <= that.options.numberOfFastForwardWindowsToPrefetch; ++i) { 1742 | windowsToRequest.push(start_time + i * that.options.windowJumpSizeFastForwardBackward * window_length); 1743 | } 1744 | for (var i = 1; i <= that.options.numberOfBackwardWindowsToPrefetch; ++i) { 1745 | windowsToRequest.push(start_time - i * window_length); 1746 | } 1747 | for (var i = 1; i <= that.options.numberOfFastBackwardWindowsToPrefetch; ++i) { 1748 | windowsToRequest.push(start_time - i * that.options.windowJumpSizeFastForwardBackward * window_length); 1749 | } 1750 | } 1751 | 1752 | for (var i = 0; i < windowsToRequest.length; ++i) { 1753 | (function (index) { 1754 | var windowStartTime = windowsToRequest[i]; 1755 | var options = { 1756 | recording_name: recording_name, 1757 | channels_displayed: that.options.channelsDisplayed, 1758 | start_time: windowStartTime, 1759 | window_length: window_length, 1760 | channel_spacing: channel_spacing, 1761 | channel_gains: that._getCurrentChannelGains() 1762 | }; 1763 | that._requestData(options, function(data, error) { 1764 | var windowAvailable = !error; 1765 | if (windowAvailable && windowStartTime == that.vars.currentWindowStart) { 1766 | that.vars.currentWindowData = data; 1767 | if (that.vars.initialChannelGains.length == 0) { 1768 | for (var i = 0; i < data.channels.length; ++i) { 1769 | that.vars.initialChannelGains.push(data.channels[i].gain); 1770 | } 1771 | } 1772 | if (that.vars.currentChannelGainAdjustments.length == 0) { 1773 | that.vars.currentChannelGainAdjustments = []; 1774 | for (var i = 0; i < data.channels.length; ++i) { 1775 | that.vars.currentChannelGainAdjustments.push(1); 1776 | } 1777 | } 1778 | that._populateGraph(that.vars.currentWindowData); 1779 | } 1780 | if (!that.options.experiment.running) { 1781 | switch (windowStartTime) { 1782 | case that.vars.currentWindowStart + window_length: 1783 | if (that.options.visibleRegion.end === undefined) { 1784 | that._setForwardEnabledStatus(windowAvailable); 1785 | if (!windowAvailable) { 1786 | that._lastWindowReached(); 1787 | } 1788 | } 1789 | break; 1790 | case that.vars.currentWindowStart + window_length * that.options.windowJumpSizeFastForwardBackward: 1791 | if (that.options.visibleRegion.end === undefined) { 1792 | that._setFastForwardEnabledStatus(windowAvailable); 1793 | } 1794 | break; 1795 | case that.vars.currentWindowStart - window_length: 1796 | if (that.options.visibleRegion.start === undefined) { 1797 | that._setBackwardEnabledStatus(windowAvailable); 1798 | } 1799 | break; 1800 | case that.vars.currentWindowStart - window_length * that.options.windowJumpSizeFastForwardBackward: 1801 | if (that.options.visibleRegion.start === undefined) { 1802 | that._setFastBackwardEnabledStatus(windowAvailable); 1803 | } 1804 | break; 1805 | } 1806 | } 1807 | }); 1808 | })(i); 1809 | } 1810 | }, 1811 | 1812 | _getProgressInPercent: function() { 1813 | var that = this; 1814 | if (!that._isVisibleRegionDefined()) return; 1815 | var windowSize = that.options.windowSizeInSeconds; 1816 | var start = windowSize * Math.ceil(that.options.visibleRegion.start / windowSize); 1817 | var end = windowSize * Math.floor((that.options.visibleRegion.end - windowSize) / windowSize); 1818 | if (!that._areTrainingWindowsSpecified()) { 1819 | var progress = (that.vars.currentWindowStart - start + windowSize) / (end - start + 2 * windowSize); 1820 | } 1821 | else { 1822 | var numberOfTrainingWindows = that._getNumberOfTrainingWindows(); 1823 | var numberOfWindowsInVisibleRegion = Math.floor((end - start) / windowSize); 1824 | var numberOfWindowsTotal = numberOfWindowsInVisibleRegion + numberOfTrainingWindows; 1825 | var currentWindowIndex = that.vars.currentTrainingWindowIndex; 1826 | var progress = (currentWindowIndex + 1) / (numberOfWindowsTotal + 2); 1827 | } 1828 | var progressInPercent = Math.ceil(progress * 100); 1829 | return progressInPercent; 1830 | }, 1831 | 1832 | _switchBackToLastActiveWindow: function() { 1833 | var that = this; 1834 | if (!that.vars.lastActiveWindowStart) { 1835 | that.vars.lastActiveWindowStart = 0; 1836 | } 1837 | that._switchToWindow(that.options.recordingName, that.vars.lastActiveWindowStart, that.options.windowSizeInSeconds, that.options.graph.channelSpacing); 1838 | }, 1839 | 1840 | _getCurrentChannelGains: function() { 1841 | var that = this; 1842 | var currentChannelGains = []; 1843 | for (var i = 0; i < that.vars.initialChannelGains.length; ++i) { 1844 | var currentChannelGain = that.vars.initialChannelGains[i]; 1845 | if (that.vars.currentChannelGainAdjustments[i] !== undefined) { 1846 | currentChannelGain *= that.vars.currentChannelGainAdjustments[i]; 1847 | } 1848 | currentChannelGains.push(currentChannelGain); 1849 | } 1850 | return currentChannelGains; 1851 | }, 1852 | 1853 | _getGainForChannelIndex: function(index) { 1854 | var that = this; 1855 | var initialGain = that.vars.initialChannelGains[index]; 1856 | var currentGainAdjustment = that.vars.currentChannelGainAdjustments[index]; 1857 | var currentGain = initialGain * currentGainAdjustment; 1858 | return currentGain; 1859 | }, 1860 | 1861 | _getOffsetForChannelIndex: function(index) { 1862 | var that = this; 1863 | var offset = (that.vars.currentWindowData.channels.length - 1 - index) * that.options.graph.channelSpacing; 1864 | return offset; 1865 | }, 1866 | 1867 | _requestData: function(options, callback) { 1868 | var that = this; 1869 | var identifierKey = that._getIdentifierKeyForDataRequest(options); 1870 | var noDataError = 'No data available for window with options ' + JSON.stringify(options); 1871 | 1872 | if (options.start_time < 0) { 1873 | that.vars.windowsCache[identifierKey] = false; 1874 | } 1875 | 1876 | if (that.vars.windowsCache[identifierKey] === false) { 1877 | if (callback) { 1878 | callback(null, noDataError); 1879 | } 1880 | return; 1881 | } 1882 | 1883 | if (that.vars.windowsCache[identifierKey]) { 1884 | if (that.vars.windowsCache[identifierKey].data && callback) { 1885 | callback(that.vars.windowsCache[identifierKey].data); 1886 | } 1887 | return; 1888 | } 1889 | 1890 | var url = '/data/edf/get/'; 1891 | if (that.options.experiment.running) { 1892 | url += '?experiment_id=' + that.options.experiment.id; 1893 | } 1894 | 1895 | var request = $.post(url, { options: JSON.stringify(options), csrfmiddlewaretoken: window.csrftoken }, function(data) { 1896 | if (!that._isDataValid(data)) { 1897 | that.vars.windowsCache[identifierKey] = false; 1898 | if (callback) { 1899 | callback(null, noDataError); 1900 | } 1901 | } 1902 | else { 1903 | that.vars.windowsCache[identifierKey].data = that._transformData(data); 1904 | if (callback) { 1905 | callback(that.vars.windowsCache[identifierKey].data); 1906 | } 1907 | } 1908 | }); 1909 | 1910 | that.vars.windowsCache[identifierKey] = { 1911 | request: request 1912 | }; 1913 | }, 1914 | 1915 | _transformData: function(input) { 1916 | var channels = []; 1917 | for (var i = 0; i < input.channel_order.length; ++i) { 1918 | var name = input.channel_order[i]; 1919 | var channel = { 1920 | name: name, 1921 | gain: input.channel_gains[i], 1922 | values: input.channel_values[name] 1923 | } 1924 | if (input.channel_stft_values_absolute && input.channel_stft_values_absolute[i]) { 1925 | channel.stftValuesAbsolute = input.channel_stft_values_absolute[i]; 1926 | } 1927 | channels.push(channel); 1928 | } 1929 | var output = { 1930 | channels: channels, 1931 | sampling_rate: input.sampling_rate, 1932 | stft_sample_frequencies: input.stft_sample_frequencies, 1933 | stft_segment_times: input.stft_segment_times, 1934 | } 1935 | return output; 1936 | }, 1937 | 1938 | _getIdentifierObjectForDataRequest: function(options) { 1939 | var options = options || {}; 1940 | var relevantOptions = [ 1941 | 'recording_name', 1942 | 'start_time', 1943 | 'window_length', 1944 | 'channel_spacing', 1945 | 'channel_gains', 1946 | ]; 1947 | var identifierObject = {}; 1948 | for (var i = 0; i < relevantOptions.length; ++i) { 1949 | identifierObject[relevantOptions[i]] = options[relevantOptions[i]]; 1950 | } 1951 | return identifierObject; 1952 | }, 1953 | 1954 | _getIdentifierKeyForDataRequest: function(options) { 1955 | var that = this; 1956 | var identifierKey = JSON.stringify(that._getIdentifierObjectForDataRequest(options)); 1957 | return identifierKey; 1958 | }, 1959 | 1960 | _isDataValid: function(data) { 1961 | if (!data) return false; 1962 | if (!data.sampling_rate) return false; 1963 | if (!data.channel_order) return false; 1964 | if (!data.channel_gains) return false; 1965 | if (!data.channel_values) return false; 1966 | return true; 1967 | }, 1968 | 1969 | _populateGraph: function(data) { 1970 | /* plot all of the points to the chart */ 1971 | var that = this; 1972 | // if the chart object does not yet exist, because the user is loading the page for the first time 1973 | // or refreshing the page, then it's necessary to initialize the plot area 1974 | if (!that.vars.chart) { 1975 | // if this is the first pageload, then we'll need to load the entire 1976 | that._initGraph(data); 1977 | // if the plot area has already been initialized, simply update the data displayed using AJAX calls 1978 | } else { 1979 | for(var ii=0; ii

'); 2018 | 2019 | that.vars.chart = new Highcharts.Chart({ 2020 | chart: { 2021 | animation: false, 2022 | renderTo: that.vars.graphID, 2023 | width: that.options.graph.width, 2024 | height: that.options.graph.height, 2025 | marginTop: that.options.graph.marginTop, 2026 | marginBottom: that.options.graph.marginBottom, 2027 | marginLeft: that.options.graph.marginLeft, 2028 | marginRight: that.options.graph.marginRight, 2029 | backgroundColor: that.options.graph.backgroundColor, 2030 | events: { 2031 | load: function(event) { 2032 | that._setupLabelHighlighting(); 2033 | }, 2034 | redraw: function(event) { 2035 | that._setupLabelHighlighting(); 2036 | that._setupYAxisLinesAndLabels(); 2037 | } 2038 | }, 2039 | }, 2040 | credits: { 2041 | enabled: false 2042 | }, 2043 | title: { 2044 | text: '' 2045 | }, 2046 | plotOptions: { 2047 | series: { 2048 | animation: false, 2049 | turboThreshold: 0, 2050 | lineWidth: 1, 2051 | enableMouseTracking: false, 2052 | color: 'black', 2053 | pointInterval: 1 / data.sampling_rate, 2054 | }, 2055 | line: { 2056 | marker: { 2057 | enabled: false, 2058 | } 2059 | }, 2060 | polygon: { 2061 | 2062 | } 2063 | }, 2064 | xAxis: { 2065 | gridLineWidth: 1, 2066 | labels: { 2067 | enabled: that.options.showTimeLabels, 2068 | crop: false, 2069 | step: 5, 2070 | formatter: function() { 2071 | // Format x-axis at HH:MM:SS 2072 | var s = this.value; 2073 | var h = Math.floor(s / 3600); 2074 | s -= h * 3600; 2075 | var m = Math.floor(s / 60); 2076 | s -= m * 60; 2077 | return h + ":" + (m < 10 ? '0' + m : m) + ":" + (s < 10 ? '0' + s : s); //zero padding on minutes and seconds 2078 | }, 2079 | }, 2080 | tickInterval: 1, 2081 | minorTickInterval: 0.5, 2082 | min: that.vars.currentWindowStart, 2083 | max: that.vars.currentWindowStart + that.options.windowSizeInSeconds, 2084 | unit: [ 2085 | ['second', 1] 2086 | ], 2087 | }, 2088 | yAxis: { 2089 | tickInterval: 100, 2090 | minorTickInterval: 50, 2091 | min: -0.75 * that.options.graph.channelSpacing * 0.75, 2092 | max: (channels.length - 0.25) * that.options.graph.channelSpacing, 2093 | gridLineWidth: 0, 2094 | minorGridLineWidth: 0, 2095 | labels: { 2096 | enabled: that.options.showChannelNames, 2097 | step: 1, 2098 | useHTML: true, 2099 | formatter: function() { 2100 | if ( 2101 | this.value < 0 2102 | || this.value > channels.length * that.options.graph.channelSpacing 2103 | || this.value % that.options.graph.channelSpacing !== 0 2104 | ) { 2105 | return null; 2106 | }; 2107 | var index = that._getChannelIndexFromY(this.value); 2108 | var channel = channels[index]; 2109 | var html = '' + channel.name + ""; 2110 | return html; 2111 | }, 2112 | }, 2113 | title: { 2114 | text: null 2115 | } 2116 | }, 2117 | legend: { 2118 | enabled: false 2119 | }, 2120 | series: that._initSeries(data), 2121 | annotationsOptions: { 2122 | enabledButtons: false, 2123 | } 2124 | }); 2125 | if (that.options.features.examplesModeEnabled) { 2126 | that._displayAnnotations(that._turnExamplesIntoAnnotations(that.options.features.examples)); 2127 | } 2128 | that._setupYAxisLinesAndLabels(); 2129 | that._setupAnnotationInteraction(); 2130 | }, 2131 | 2132 | _blockGraphInteraction: function() { 2133 | var that = this; 2134 | var container = $(that.element); 2135 | var graph = $('#' + that.vars.graphID); 2136 | var blocker = $('
') 2137 | .addClass('blocker') 2138 | .css({ 2139 | position: 'absolute', 2140 | left: 0, 2141 | width: '100%', 2142 | top: graph.offset().top, 2143 | height: graph.height(), 2144 | backgroundColor: 'rgba(0, 0, 0, 0)', 2145 | }) 2146 | .appendTo(container); 2147 | }, 2148 | 2149 | _unblockGraphInteraction: function() { 2150 | var that = this; 2151 | $(that.element).find('> .blocker').remove(); 2152 | }, 2153 | 2154 | _setupAnnotationInteraction: function() { 2155 | var that = this; 2156 | if (that.options.isReadOnly) return; 2157 | if (!that.options.features.order || !that.options.features.order.length) return; 2158 | var chart = that.vars.chart; 2159 | var container = chart.container; 2160 | 2161 | function drag(e) { 2162 | var annotation, 2163 | clickX = e.pageX - container.offsetLeft, 2164 | clickY = e.pageY - container.offsetTop; 2165 | if (!chart.isInsidePlot(clickX - chart.plotLeft, clickY - chart.plotTop)) { 2166 | return; 2167 | } 2168 | Highcharts.addEvent(document, 'mousemove', step); 2169 | Highcharts.addEvent(document, 'mouseup', drop); 2170 | var annotationId = undefined; 2171 | var clickXValue = that._convertPixelsToValue(clickX, 'x'); 2172 | var clickYValue = that._convertPixelsToValue(clickY, 'y'); 2173 | var channelIndex = that._getChannelIndexFromY(clickYValue); 2174 | var featureType = that.vars.activeFeatureType; 2175 | 2176 | annotation = that._addAnnotationBox(annotationId, clickXValue, channelIndex, featureType); 2177 | 2178 | function getAnnotationAttributes(e) { 2179 | var x = e.clientX - container.offsetLeft, 2180 | dx = x - clickX, 2181 | width = that._convertPixelsToValueLength(parseInt(dx, 10) + 1, 'x'); 2182 | 2183 | if (dx >= 0) { 2184 | var xValue = that._convertPixelsToValue(clickX, 'x'); 2185 | } 2186 | else { 2187 | var xValue = that._convertPixelsToValue(x, 'x'); 2188 | } 2189 | return { 2190 | xValue: xValue, 2191 | shape: { 2192 | params: { 2193 | width: width 2194 | } 2195 | } 2196 | }; 2197 | } 2198 | 2199 | function step(e) { 2200 | annotation.update(getAnnotationAttributes(e)); 2201 | } 2202 | 2203 | function drop(e) { 2204 | Highcharts.removeEvent(document, 'mousemove', step); 2205 | Highcharts.removeEvent(document, 'mouseup', drop); 2206 | var x = e.clientX - container.offsetLeft; 2207 | if (x == clickX) { 2208 | annotation.destroy(); 2209 | return; 2210 | } 2211 | if (annotation) { 2212 | annotation.update(getAnnotationAttributes(e)); 2213 | } 2214 | annotation.outsideClickHandler = function() { 2215 | annotation.destroy(); 2216 | } 2217 | $('html').on('mousedown', annotation.outsideClickHandler); 2218 | } 2219 | } 2220 | 2221 | Highcharts.addEvent(container, 'mousedown', drag); 2222 | }, 2223 | 2224 | _addAnnotationBox: function(annotationId, timeStart, channelIndex, featureType, timeEnd, confidence, comment, annotationData) { 2225 | var that = this; 2226 | var annotations = that.vars.chart.annotations.allItems; 2227 | var timeEnd = timeEnd !== undefined ? timeEnd : false; 2228 | var annotationData = annotationData !== undefined ? annotationData : {}; 2229 | var preliminary = timeEnd === false; 2230 | var shapeParams = { 2231 | height: that.options.graph.channelSpacing, 2232 | } 2233 | if (preliminary) { 2234 | shapeParams.width = 0; 2235 | shapeParams.fill = 'transparent'; 2236 | shapeParams.stroke = that._getFeatureColor(featureType, annotationData.is_answer); 2237 | shapeParams.strokeWidth = 10; 2238 | } 2239 | else { 2240 | shapeParams.width = timeEnd - timeStart; 2241 | shapeParams.fill = that._getFeatureColor(featureType, annotationData.is_answer, confidence); 2242 | shapeParams.stroke = 'transparent'; 2243 | shapeParams.strokeWidth = 0; 2244 | } 2245 | var channelTop = that._getBorderTopForChannelIndex(channelIndex); 2246 | that.vars.chart.addAnnotation({ 2247 | xValue: timeStart, 2248 | yValue: channelTop, 2249 | allowDragX: preliminary, 2250 | allowDragY: false, 2251 | anchorX: 'left', 2252 | anchorY: 'top', 2253 | shape: { 2254 | type: 'rect', 2255 | units: 'values', 2256 | params: shapeParams, 2257 | }, 2258 | events: { 2259 | dblclick: function(event) { 2260 | if (that.options.isReadOnly) return; 2261 | if (annotationData.is_answer) return; 2262 | event.preventDefault(); 2263 | var xMinFixed = that._getAnnotationXMinFixed(this); 2264 | var xMaxFixed = that._getAnnotationXMaxFixed(this); 2265 | var annotationId = annotation.metadata.id; 2266 | var channelIndex = annotation.metadata.channelIndex; 2267 | var channelsDisplayed = that.options.channelsDisplayed; 2268 | if (annotation.metadata.originalData) { 2269 | channelIndex = annotation.metadata.originalData.channels; 2270 | channelsDisplayed = annotation.metadata.originalData.channels_displayed; 2271 | } 2272 | that._deleteAnnotation(annotationId, that.vars.currentWindowRecording, xMinFixed, xMaxFixed, channelIndex, channelsDisplayed); 2273 | annotation.destroy(); 2274 | } 2275 | } 2276 | }); 2277 | var annotation = annotations[annotations.length - 1]; 2278 | if (!preliminary) { 2279 | var classString = $(annotation.group.element).attr('class'); 2280 | classString += ' saved'; 2281 | $(annotation.group.element).attr('class', classString); 2282 | } 2283 | $(annotation.group.element).on('mousedown', function(event) { 2284 | event.stopPropagation(); 2285 | }); 2286 | annotation.metadata = { 2287 | id: annotationId, 2288 | featureType: featureType, 2289 | channelIndex: channelIndex, 2290 | comment: '' 2291 | } 2292 | if (!preliminary) { 2293 | annotation.metadata.confidence = confidence; 2294 | annotation.metadata.comment = comment; 2295 | annotation.metadata.originalData = annotationData; 2296 | } 2297 | if (!that.options.isReadOnly && !annotationData.is_answer) { 2298 | that._addConfidenceLevelButtonsToAnnotationBox(annotation); 2299 | if (!preliminary) { 2300 | that._addCommentFormToAnnotationBox(annotation); 2301 | } 2302 | } 2303 | return annotation; 2304 | }, 2305 | 2306 | _addConfidenceLevelButtonsToAnnotationBox: function(annotation) { 2307 | var that = this; 2308 | var annotationElement = $(annotation.group.element); 2309 | // To learn more about the foreignObject tag, see: 2310 | // https://developer.mozilla.org/en/docs/Web/SVG/Element/foreignObject 2311 | var htmlContext = $(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')); 2312 | htmlContext 2313 | .attr({ 2314 | width: 70, 2315 | height: 25, 2316 | x: 0, 2317 | y: 0, 2318 | zIndex: 2, 2319 | }) 2320 | .mousedown(function(event) { 2321 | event.stopPropagation(); 2322 | }) 2323 | .click(function(event) { 2324 | event.stopPropagation(); 2325 | }) 2326 | .dblclick(function(event) { 2327 | event.stopPropagation(); 2328 | }); 2329 | var body = $(document.createElement('body')) 2330 | .addClass('toolbar confidence-buttons') 2331 | .attr('xmlns', 'http://www.w3.org/1999/xhtml'); 2332 | var buttonGroup = $('
') 2333 | var buttonDefinitions = [{ 2334 | confidence: 0, 2335 | class: 'red', 2336 | }, { 2337 | confidence: 0.5, 2338 | class: 'yellow', 2339 | }, { 2340 | confidence: 1, 2341 | class: 'light-green', 2342 | }] 2343 | for (var i = 0; i < buttonDefinitions.length; ++i) { 2344 | var buttonDefinition = buttonDefinitions[i]; 2345 | var buttonID = that._getUUID(); 2346 | var button = $('
') 2347 | .addClass('btn') 2348 | .addClass(buttonDefinition.class) 2349 | .addClass(buttonDefinition.confidence == annotation.metadata.confidence ? 'active' : '') 2350 | .data('confidence', buttonDefinition.confidence) 2351 | .click(function(event) { 2352 | $(this).addClass('active').siblings().removeClass('active'); 2353 | var confidence = $(this).data('confidence'); 2354 | annotation.metadata.confidence = confidence; 2355 | that._saveFeatureAnnotation(annotation); 2356 | $('html').off('mousedown', annotation.outsideClickHandler); 2357 | var classString = $(annotation.group.element).attr('class'); 2358 | classString += ' saved'; 2359 | $(annotation.group.element).attr('class', classString); 2360 | var newColor = that._getFeatureColor(annotation.metadata.featureType, false, confidence); 2361 | annotation.update({ 2362 | allowDragX: false, 2363 | shape: { 2364 | params: { 2365 | strokeWidth: 0, 2366 | stroke: 'transparent', 2367 | fill: newColor, 2368 | } 2369 | } 2370 | }); 2371 | that._addCommentFormToAnnotationBox(annotation); 2372 | }) 2373 | buttonGroup.append(button); 2374 | } 2375 | body.append(buttonGroup); 2376 | htmlContext.append(body); 2377 | annotationElement.append(htmlContext); 2378 | }, 2379 | 2380 | _addCommentFormToAnnotationBox: function(annotation) { 2381 | if (annotation.metadata.commentFormAdded) { 2382 | return; 2383 | } 2384 | var that = this; 2385 | var annotationElement = $(annotation.group.element); 2386 | // To learn more about the foreignObject tag, see: 2387 | // https://developer.mozilla.org/en/docs/Web/SVG/Element/foreignObject 2388 | var htmlContext = $(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')); 2389 | htmlContext 2390 | .attr({ 2391 | width: 250, 2392 | height: 25, 2393 | x: 0, 2394 | y: 20, 2395 | zIndex: 2, 2396 | }) 2397 | .mousedown(function(event) { 2398 | event.stopPropagation(); 2399 | }) 2400 | .click(function(event) { 2401 | event.stopPropagation(); 2402 | }) 2403 | .dblclick(function(event) { 2404 | event.stopPropagation(); 2405 | }); 2406 | var body = $('') 2407 | .addClass('toolbar comment') 2408 | .attr('xmlns', 'http://www.w3.org/1999/xhtml'); 2409 | var form = $(''); 2410 | var toggleButton = $(''); 2411 | form.append(toggleButton); 2412 | var comment = annotation.metadata.comment; 2413 | var input = $('') 2414 | .hide() 2415 | .keydown(function(event) { 2416 | event.stopPropagation(); 2417 | }); 2418 | form.submit(function(event) { 2419 | event.preventDefault(); 2420 | var collapsed = toggleButton.hasClass('fa-comment'); 2421 | if (collapsed) { 2422 | toggleButton 2423 | .removeClass('fa-comment') 2424 | .addClass('fa-floppy-o') 2425 | input.show().focus(); 2426 | } 2427 | else { 2428 | toggleButton 2429 | .removeClass('fa-floppy-o') 2430 | .addClass('fa-comment') 2431 | input.hide(); 2432 | var comment = input.val(); 2433 | toggleButton.focus(); 2434 | annotation.metadata.comment = comment; 2435 | that._saveFeatureAnnotation(annotation); 2436 | } 2437 | }); 2438 | form.append(input); 2439 | body.append(form); 2440 | htmlContext.append(body); 2441 | annotationElement.append(htmlContext); 2442 | annotation.metadata.commentFormAdded = true; 2443 | }, 2444 | 2445 | _saveFeatureAnnotation: function(annotation) { 2446 | var that = this; 2447 | var annotationId = annotation.metadata.id; 2448 | var type = annotation.metadata.featureType; 2449 | var time_start = that._getAnnotationXMinFixed(annotation); 2450 | var time_end = that._getAnnotationXMaxFixed(annotation); 2451 | var channel = annotation.metadata.channelIndex; 2452 | var confidence = annotation.metadata.confidence; 2453 | var comment = annotation.metadata.comment; 2454 | var metadata = {} 2455 | if (that._isHITModeEnabled()) { 2456 | metadata = { 2457 | visibleRegion: that.options.visibleRegion, 2458 | windowSizeInSeconds: that.options.windowSizeInSeconds, 2459 | isTrainingWindow: that._isCurrentWindowTrainingWindow(), 2460 | } 2461 | if (that.options.projectUUID) { 2462 | metadata.projectUUID = that.options.projectUUID; 2463 | } 2464 | } 2465 | that._saveAnnotation(annotationId, that.vars.currentWindowRecording, type, time_start, time_end, channel, confidence, comment, metadata, function(savedAnnotation, error) { 2466 | if (savedAnnotation) { 2467 | annotation.metadata.id = savedAnnotation.id; 2468 | } 2469 | }); 2470 | }, 2471 | 2472 | _saveFullWindowLabel: function(annotationCategory, label) { 2473 | var that = this; 2474 | var label = label; 2475 | var time_start = that.vars.currentWindowStart; 2476 | var time_end = time_start + that.options.windowSizeInSeconds; 2477 | var channel = undefined; 2478 | var confidence = 1; 2479 | var comment = ''; 2480 | var cacheKey = that._getAnnotationsCacheKey(that.vars.currentWindowRecording, time_start, time_end, false, annotationCategory); 2481 | var annotationId = that.vars.annotationsCache[cacheKey]; 2482 | that._saveAnnotation(annotationId, that.vars.currentWindowRecording, label, time_start, time_end, channel, confidence, comment, {}, function(savedAnnotation, error) { 2483 | if (savedAnnotation) { 2484 | that.vars.annotationsCache[cacheKey] = savedAnnotation.id; 2485 | } 2486 | }); 2487 | }, 2488 | 2489 | _saveArtifactAnnotation: function(type) { 2490 | var that = this; 2491 | that._saveFullWindowLabel('ARTIFACT', type); 2492 | }, 2493 | 2494 | _saveSleepStageAnnotation: function(type) { 2495 | var that = this; 2496 | that._saveFullWindowLabel('SLEEP_STAGE', type); 2497 | }, 2498 | 2499 | _getAnnotationXMinFixed: function(annotation) { 2500 | return parseFloat(annotation.options.xValue).toFixed(2); 2501 | }, 2502 | 2503 | _getAnnotationXMaxFixed: function(annotation) { 2504 | return parseFloat(annotation.options.xValue + annotation.options.shape.params.width).toFixed(2); 2505 | }, 2506 | 2507 | _getAxis: function(key) { 2508 | var that = this; 2509 | switch (key) { 2510 | case 'x': 2511 | case 'X': 2512 | var axis = that.vars.chart.xAxis[0]; 2513 | break; 2514 | default: 2515 | var axis = that.vars.chart.yAxis[0]; 2516 | break; 2517 | } 2518 | return axis; 2519 | }, 2520 | 2521 | _convertPixelsToValue: function(pixels, axisKey) { 2522 | var axis = this._getAxis(axisKey); 2523 | return axis.toValue(pixels); 2524 | }, 2525 | 2526 | _convertValueToPixels: function(value, axisKey) { 2527 | var axis = this._getAxis(axisKey); 2528 | return axis.toPixels(value); 2529 | }, 2530 | 2531 | _convertPixelsToValueLength: function(pixels, axisKey) { 2532 | var axis = this._getAxis(axisKey); 2533 | return Math.abs(axis.toValue(pixels) - axis.toValue(0)); 2534 | }, 2535 | 2536 | _convertValueToPixelsLength: function(value, axisKey) { 2537 | var axis = this._getAxis(axisKey); 2538 | return Math.abs(axis.toPixels(value) - axis.toPixels(0)); 2539 | }, 2540 | 2541 | _getChannelsAnnotated: function(yMin, yMax) { 2542 | var that = this; 2543 | var index1 = that._getChannelIndexFromY(yMin); 2544 | var index2 = that._getChannelIndexFromY(yMax); 2545 | var indexMin = Math.min(index1, index2); 2546 | var indexMax = Math.max(index1, index2); 2547 | var channels = []; 2548 | for (var i = indexMin; i <= indexMax; ++i) { 2549 | channels.push(i); 2550 | } 2551 | return channels; 2552 | }, 2553 | 2554 | _getChannelIndexFromY: function(value) { 2555 | var that = this; 2556 | var numberOfChannels = that.vars.currentWindowData.channels.length; 2557 | var indexFromEnd = Math.floor((value + that.options.graph.channelSpacing / 2) / that.options.graph.channelSpacing); 2558 | var index = numberOfChannels - 1 - indexFromEnd; 2559 | index = Math.min(numberOfChannels - 1, index); 2560 | index = Math.max(index, 0); 2561 | return index; 2562 | }, 2563 | 2564 | _setupLabelHighlighting: function() { 2565 | var that = this; 2566 | $(that.element).find('.channel-label').click(function(event) { 2567 | that.vars.selectedChannelIndex = $(this).data('index'); 2568 | $(that.element).find('.gain-button').prop('disabled', false); 2569 | $(that.element).find('.channel-label').removeClass('selected'); 2570 | $(this).addClass('selected'); 2571 | }); 2572 | }, 2573 | 2574 | _setupYAxisLinesAndLabels: function() { 2575 | var that = this; 2576 | var axis = that.vars.chart.yAxis[0]; 2577 | var channels = that.vars.currentWindowData.channels; 2578 | 2579 | for (var i = 0; i < channels.length; ++i) { 2580 | var channel = channels[i]; 2581 | var channelOffset = that._getOffsetForChannelIndex(i); 2582 | var channelUnit = that._getUnitForChannel(channel); 2583 | var zeroLineID = 'channel_' + i + '_zero'; 2584 | axis.removePlotLine(zeroLineID); 2585 | var zeroLineOptions = { 2586 | id: zeroLineID, 2587 | color: '#dddddd', 2588 | value: channelOffset, 2589 | width: 1 2590 | }; 2591 | axis.addPlotLine(zeroLineOptions); 2592 | 2593 | if (!that.options.showReferenceLines) continue; 2594 | 2595 | var referenceValue = that._getReferenceValueForChannel(channel); 2596 | var referenceValueWithGain = referenceValue * that._getGainForChannelIndex(i); 2597 | var polarities = [-1, 1]; 2598 | for (var j = 0; j < 2; ++j) { 2599 | var referenceLineID = 'channel_' + i + '_reference_' + j; 2600 | axis.removePlotLine(referenceLineID); 2601 | if (referenceValue == 0) { 2602 | continue; 2603 | } 2604 | var polarity = polarities[j]; 2605 | var flipFactor = that._getFlipFactorForChannel(channel); 2606 | var referenceLineOptions = { 2607 | id: referenceLineID, 2608 | color: '#ff0000', 2609 | dashStyle: 'dash', 2610 | value: channelOffset + polarity * referenceValueWithGain, 2611 | width: 1, 2612 | zIndex: 1, 2613 | label: { 2614 | text: (flipFactor * polarity * referenceValue) + ' ' + channelUnit, 2615 | textAlign: 'right', 2616 | verticalAlign: 'middle', 2617 | x: -15, 2618 | y: 3, 2619 | style: { 2620 | color: 'red', 2621 | marginRight: '50px', 2622 | } 2623 | } 2624 | }; 2625 | axis.addPlotLine(referenceLineOptions); 2626 | } 2627 | } 2628 | }, 2629 | 2630 | _getFlipFactorForChannel: function(channel) { 2631 | var that = this; 2632 | if (that._shouldChannelBeFlippedVertically(channel)) { 2633 | return -1; 2634 | } 2635 | return 1; 2636 | }, 2637 | 2638 | _shouldChannelBeFlippedVertically: function(channel) { 2639 | return true; 2640 | }, 2641 | 2642 | _getUnitForChannel: function(channel) { 2643 | // electrooculography 2644 | if ( 2645 | channel.name.indexOf('EOG') > -1 2646 | || channel.name.indexOf('LOC') > -1 2647 | || channel.name.indexOf('ROC') > -1 2648 | ) { 2649 | return 'μV'; 2650 | } 2651 | // electrocardiography 2652 | if (channel.name.indexOf('ECG') > -1) { 2653 | return 'mV'; 2654 | } 2655 | // electromygraphy 2656 | if ( 2657 | channel.name.indexOf('EMG') > -1 2658 | || channel.name.indexOf('Chin') > -1 2659 | ) { 2660 | return 'μV'; 2661 | } 2662 | // electroencephalography 2663 | // (default if no other channel 2664 | // type was recognized) 2665 | return 'μV'; 2666 | }, 2667 | 2668 | _getReferenceValueForChannel: function(channel) { 2669 | // electrooculography 2670 | if ( 2671 | channel.name.indexOf('EOG') > -1 2672 | || channel.name.indexOf('LOC') > -1 2673 | || channel.name.indexOf('ROC') > -1 2674 | ) { 2675 | return 0; 2676 | // return 37.5; 2677 | } 2678 | // electrocardiography 2679 | if (channel.name.indexOf('ECG') > -1) { 2680 | return 0; 2681 | // return 0.1; 2682 | } 2683 | // electromygraphy 2684 | if ( 2685 | channel.name.indexOf('EMG') > -1 2686 | || channel.name.indexOf('Chin') > -1 2687 | ) { 2688 | return 0; 2689 | // return 10; 2690 | } 2691 | // electroencephalography 2692 | // (default if no other channel 2693 | // type was recognized) 2694 | return 37.5; 2695 | }, 2696 | 2697 | _updateChannelGain: function(action) { 2698 | var that = this; 2699 | var previewDataAdjusted = []; 2700 | 2701 | if (action === "reset") { 2702 | that.vars.currentChannelGainAdjustments[that.vars.selectedChannelIndex] = 1; 2703 | } 2704 | else { 2705 | var gainChangeFactor = 1 + that.options.relativeGainChangePerStep; 2706 | if (action === "step_decrease") { 2707 | var gainChangeFactor = 1 / gainChangeFactor; 2708 | } 2709 | that.vars.currentChannelGainAdjustments[that.vars.selectedChannelIndex] *= gainChangeFactor; 2710 | } 2711 | 2712 | var channel = that.vars.currentWindowData.channels[that.vars.selectedChannelIndex]; 2713 | var currentGain = that._getGainForChannelIndex(that.vars.selectedChannelIndex); 2714 | var channelOffset = that._getOffsetForChannelIndex(that.vars.selectedChannelIndex); 2715 | 2716 | for (var ii = 0; ii < channel.values.length; ii++) { 2717 | previewDataAdjusted.push((channel.values[ii] - channelOffset) / channel.gain * currentGain + channelOffset); 2718 | } 2719 | 2720 | that.vars.chart.series[that.vars.selectedChannelIndex].update({ 2721 | pointStart: that.vars.currentWindowStart, 2722 | name: channel.name, 2723 | type: 'line', 2724 | data: previewDataAdjusted 2725 | }, false); 2726 | 2727 | that.vars.chart.redraw(); 2728 | 2729 | that._savePreferences({ 2730 | channel_gains: that._getCurrentChannelGains(), 2731 | channels_displayed: that.options.channelsDisplayed 2732 | }); 2733 | }, 2734 | 2735 | _initSeries: function(data) { 2736 | var that = this; 2737 | var channels = data.channels; 2738 | var series = []; 2739 | 2740 | for (var ii = 0; ii < channels.length; ++ii) { 2741 | var channel = { 2742 | pointStart: that.vars.currentWindowStart, 2743 | name: channels[ii].name, 2744 | type: 'line', 2745 | data: channels[ii].values 2746 | }; 2747 | var channelColor = that._getChannelColor(data, ii); 2748 | if (channelColor) { 2749 | channel.color = channelColor; 2750 | channel.enableMouseTracking = true; 2751 | channel.stickyTracking = false; 2752 | channel.tooltip = that._getChannelTooltip(data, ii); 2753 | } 2754 | series.push(channel); 2755 | } 2756 | return(series); 2757 | }, 2758 | 2759 | _getChannelColor: function(data, channelIndex) { 2760 | var that = this; 2761 | var frequencyBandVisualization = that.options.frequencyBandVisualizationPerChannel[channelIndex]; 2762 | if (!frequencyBandVisualization) { 2763 | return; 2764 | } 2765 | var bands = frequencyBandVisualization.bands; 2766 | var bandsFrequencyIndices = []; 2767 | bands.forEach(function(band) { 2768 | var frequencyIndexStart = 0; 2769 | var frequencyIndexEnd = data.stft_sample_frequencies.length - 1; 2770 | for (var f = 0; f < data.stft_sample_frequencies.length; ++f) { 2771 | if (data.stft_sample_frequencies[f] > band.frequency.min) { 2772 | frequencyIndexStart = f; 2773 | break; 2774 | } 2775 | } 2776 | for (var f = data.stft_sample_frequencies.length - 1; f >= 0; --f) { 2777 | if (data.stft_sample_frequencies[f] <= band.frequency.max) { 2778 | frequencyIndexEnd = f; 2779 | break; 2780 | } 2781 | } 2782 | bandsFrequencyIndices.push({ 2783 | start: frequencyIndexStart, 2784 | end: frequencyIndexEnd, 2785 | }); 2786 | }); 2787 | var stftValuesAbsolute = data.channels[channelIndex].stftValuesAbsolute; 2788 | var stops = stftValuesAbsolute.map(function(frequencySamples, t) { 2789 | var frequencyPowersAggregates = bandsFrequencyIndices.map(function(bandFrequencyIndices) { 2790 | var frequencyPowers = frequencySamples.slice(bandFrequencyIndices.start, bandFrequencyIndices.end + 1) 2791 | var frequencyPowersSum = frequencyPowers.reduce(function(a, b) { return a + b; }, 0); 2792 | return frequencyPowersSum / frequencyPowers.length; 2793 | }); 2794 | var dominantBandIndex = frequencyPowersAggregates.indexOf(Math.max.apply(Math, frequencyPowersAggregates)); 2795 | var dominantBand = bands[dominantBandIndex]; 2796 | var timeStepRelative = t / (stftValuesAbsolute.length - 1); 2797 | return [ timeStepRelative, dominantBand.color ]; 2798 | }); 2799 | var channelColor = { 2800 | linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 }, 2801 | stops: stops, 2802 | }; 2803 | return channelColor; 2804 | }, 2805 | 2806 | _getChannelTooltip: function(data, channelIndex) { 2807 | var that = this; 2808 | var frequencyBandVisualization = that.options.frequencyBandVisualizationPerChannel[channelIndex]; 2809 | if (!frequencyBandVisualization) { 2810 | return; 2811 | } 2812 | var bands = frequencyBandVisualization.bands; 2813 | var pointFormat = 'Rhythms:'; 2814 | bands.forEach(function(band) { 2815 | pointFormat += ' ' + band.name + ' (' + band.frequency.min + ' - ' + band.frequency.max + ' Hz)'; 2816 | }); 2817 | var tooltip = { 2818 | enabled: true, 2819 | split: true, 2820 | useHTML: true, 2821 | headerFormat: '', 2822 | pointFormat: pointFormat, 2823 | footerFormat: '', 2824 | }; 2825 | return tooltip; 2826 | }, 2827 | 2828 | _getFeatureColor: function(featureKey, isAnswer, confidence) { 2829 | var that = this; 2830 | var isAnswer = !!isAnswer; 2831 | var confidence = confidence !== undefined ? confidence : 1; 2832 | var feature = that.options.features.options[featureKey]; 2833 | if (!feature) { 2834 | console.error('Could not find feature', featureKey); 2835 | return 'rgba(255, 0, 0, 1)'; 2836 | } 2837 | if (isAnswer) { 2838 | var color = feature.answer; 2839 | } 2840 | else { 2841 | var color = feature.annotation; 2842 | } 2843 | var min = color.alpha.min; 2844 | var max = color.alpha.max; 2845 | var alpha = min + confidence * (max - min); 2846 | var colorValues = [color.red, color.green, color.blue, alpha]; 2847 | var colorString = 'rgba(' + colorValues.join(',') + ')'; 2848 | return colorString; 2849 | }, 2850 | 2851 | _getUUID: function() { 2852 | function s4() { 2853 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 2854 | } 2855 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 2856 | }, 2857 | 2858 | _getBorderTopForChannelIndex: function(index) { 2859 | var that = this; 2860 | var top = that._getOffsetForChannelIndex(index) + that.options.graph.channelSpacing / 2; 2861 | return top; 2862 | }, 2863 | 2864 | _getBorderBottomForChannelIndex: function(index) { 2865 | var that = this; 2866 | var bottom = that._getOffsetForChannelIndex(index) - that.options.graph.channelSpacing / 2; 2867 | return bottom; 2868 | }, 2869 | 2870 | _convertChannelsToNormalizedArray: function(channels) { 2871 | var that = this; 2872 | var numberOfChannels = that.vars.currentWindowData.channels.length; 2873 | if (channels == 'ALL') { 2874 | channels = []; 2875 | for (var i = 0; i < numberOfChannels; ++i) { 2876 | channels.push(i); 2877 | } 2878 | return channels; 2879 | } 2880 | if (!Array.isArray(channels)) { 2881 | channels = channels.toString().split(','); 2882 | } 2883 | for (var i = 0; i < channels.length; ++i) { 2884 | if (!that._isValidChannelIndex(channels[i])) { 2885 | throw new RangeError('Channel ' + channels + ' is not a valid channel from range [0..' + (numberOfChannels - 1) + '].'); 2886 | } 2887 | else { 2888 | channels[i] = Number(channels[i]); 2889 | } 2890 | } 2891 | channels.sort(); 2892 | return channels; 2893 | }, 2894 | 2895 | _convertChannelsToString: function(channels) { 2896 | var that = this; 2897 | if (typeof(channels) === 'string') { 2898 | return channels; 2899 | } 2900 | return that._convertChannelsToNormalizedArray(channels).join(','); 2901 | }, 2902 | 2903 | _isInt: function(value) { 2904 | var that = this; 2905 | return (value % 1 === 0); 2906 | }, 2907 | 2908 | _isValidChannelIndex: function(value) { 2909 | var that = this; 2910 | return (that._isInt(value) && value >= 0 && value <= that.vars.currentWindowData.channels.length - 1); 2911 | }, 2912 | 2913 | _loadPreferences: function(callback) { 2914 | var that = this; 2915 | // that._getApiClient().taskdataconfiguration().getForContext(function(configs, error) { 2916 | // if (configs && configs.length) { 2917 | // that.vars.taskDataConfiguration = configs[0]; 2918 | // } 2919 | // else { 2920 | // that.vars.taskDataConfiguration = that._getApiClient().taskdataconfiguration().init(); 2921 | // that.vars.taskDataConfiguration.configuration = {}; 2922 | // } 2923 | // if (that.vars.taskDataConfiguration.configuration.current_page_start !== undefined) { 2924 | // that.options.startTime = that.vars.taskDataConfiguration.configuration.current_page_start; 2925 | // } 2926 | // if (that.vars.taskDataConfiguration.configuration.channel_gains) { 2927 | // that.vars.initialChannelGains = that.vars.taskDataConfiguration.configuration.channel_gains; 2928 | // } 2929 | // callback && callback(); 2930 | // }); 2931 | if (that.options.channelGains) { 2932 | that.vars.initialChannelGains = that.options.channelGains; 2933 | } 2934 | callback && callback(); 2935 | }, 2936 | 2937 | _savePreferences: function(updates) { 2938 | var that = this; 2939 | if (that.options.isReadOnly) return; 2940 | if (!that.vars.taskDataConfiguration) return; 2941 | for (key in updates) { 2942 | that.vars.taskDataConfiguration.configuration[key] = updates[key]; 2943 | } 2944 | that.vars.taskDataConfiguration.save(); 2945 | }, 2946 | 2947 | _saveUserEvent: function(eventType, metadata) { 2948 | var that = this; 2949 | var eventType = eventType !== undefined ? eventType : 'undefined'; 2950 | var metadata = metadata !== undefined ? metadata : {}; 2951 | if (that.options.projectUUID) { 2952 | metadata.projectUUID = that.options.projectUUID; 2953 | } 2954 | var options = JSON.stringify({ 2955 | eventType: eventType, 2956 | metadata: metadata, 2957 | }); 2958 | console.error('Trying to save a user event, but this feature is not implemented yet.'); 2959 | }, 2960 | 2961 | _saveUserEventWindow: function(beginOrComplete) { 2962 | var that = this; 2963 | if (!that._isHITModeEnabled()) return; 2964 | that._saveUserEvent('window_' + beginOrComplete, { 2965 | recordingName: that.options.recordingName, 2966 | windowStart: that.vars.currentWindowStart, 2967 | windowSizeInSeconds: that.options.windowSizeInSeconds, 2968 | channelsDisplayed: that.options.channelsDisplayed, 2969 | features: that.options.features.order, 2970 | isTraining: that._isCurrentWindowTrainingWindow(), 2971 | }); 2972 | }, 2973 | 2974 | _saveUserEventWindowBegin: function() { 2975 | var that = this; 2976 | that._saveUserEventWindow('begin'); 2977 | }, 2978 | 2979 | _saveUserEventWindowComplete: function() { 2980 | var that = this; 2981 | that._saveUserEventWindow('complete'); 2982 | }, 2983 | 2984 | _getAnnotations: function(recording_name, window_start, window_end, correctAnswers) { 2985 | var that = this; 2986 | if (!that.options.features.showUserAnnotations) return; 2987 | 2988 | var cacheKey = that._getAnnotationsCacheKey(recording_name, window_start, window_end, correctAnswers); 2989 | if (that.vars.annotationsLoaded || that.vars.annotationsCache[cacheKey]) { 2990 | var data = that.vars.annotationsCache[cacheKey] || { 2991 | annotations: [], 2992 | }; 2993 | if (!correctAnswers) { 2994 | that._incrementNumberOfAnnotationsInCurrentWindow(that._getVisibleAnnotations(data.annotations).length); 2995 | } 2996 | that._displayArtifactsSelection(data.annotations); 2997 | that._displaySleepStageSelection(data.annotations); 2998 | return; 2999 | } 3000 | that.vars.annotationsCache[cacheKey] = { 3001 | annotations: [], 3002 | }; 3003 | that.vars.annotationsLoaded = true; 3004 | that._getApiClient().listAll('response', {}, function(annotations) { 3005 | if (!annotations) { 3006 | return; 3007 | } 3008 | // sort annotations by the date they were last updated, most recently updated first 3009 | annotations = annotations.sort(function(a, b) { 3010 | return (a.updated < b.updated) ? 1 : (a.updated > b.updated) ? -1 : 0; 3011 | }); 3012 | var annotations = annotations.map(function(annotation) { 3013 | var annotationFormatted = annotation.content 3014 | annotationFormatted.id = annotation.id; 3015 | return annotationFormatted; 3016 | }); 3017 | annotations.forEach(function(annotation) { 3018 | var annotationWindowStart = Math.floor(annotation.position.start / that.options.windowSizeInSeconds) * that.options.windowSizeInSeconds; 3019 | var annotationWindowEnd = annotationWindowStart + that.options.windowSizeInSeconds; 3020 | var annotationCacheKey = that._getAnnotationsCacheKey(recording_name, annotationWindowStart, annotationWindowEnd, correctAnswers); 3021 | if (!that.vars.annotationsCache[annotationCacheKey]) { 3022 | that.vars.annotationsCache[annotationCacheKey] = { 3023 | annotations: [], 3024 | }; 3025 | } 3026 | that.vars.annotationsCache[annotationCacheKey].annotations.push(annotation); 3027 | }); 3028 | var data = that.vars.annotationsCache[cacheKey] || { 3029 | annotations: [], 3030 | }; 3031 | that._displayArtifactsSelection(data.annotations); 3032 | that._displaySleepStageSelection(data.annotations); 3033 | that._displayAnnotations(data.annotations); 3034 | }); 3035 | }, 3036 | 3037 | _getVisibleAnnotations: function(annotations) { 3038 | var that = this; 3039 | var visibleAnnotations = annotations.filter(function(annotation) { 3040 | var isVisibleFeature = that.options.features.order.indexOf(annotation.label) > -1; 3041 | var isVisibleChannel = false; 3042 | var annotationChannels = annotation.position.channels; 3043 | if (annotationChannels) { 3044 | if (!annotationChannels.length) { 3045 | annotationChannels = [annotationChannels]; 3046 | } 3047 | for (var c = 0; c < annotationChannels.length; ++c) { 3048 | var channel = annotation.metadata.channels_displayed[annotationChannels[c]]; 3049 | if (that.options.channelsDisplayed.indexOf(channel) > -1) { 3050 | isVisibleChannel = true; 3051 | break; 3052 | } 3053 | } 3054 | } 3055 | return isVisibleFeature && isVisibleChannel; 3056 | }); 3057 | return visibleAnnotations; 3058 | }, 3059 | 3060 | _getAnnotationsCacheKey: function(recording_name, window_start, window_end, correctAnswers, cacheCategory) { 3061 | cacheCategory = cacheCategory || 'FEATURE'; 3062 | var key = recording_name + '_' + cacheCategory + '_' + window_start + '_' + window_end; 3063 | if (correctAnswers) { 3064 | key += '_correct_answers'; 3065 | } 3066 | return key; 3067 | }, 3068 | 3069 | _displayArtifactsSelection: function(annotations) { 3070 | var that = this; 3071 | var activeClass = 'teal darken-4'; 3072 | var validKeys = [ 3073 | 'artifacts_none', 3074 | 'artifacts_light', 3075 | 'artifacts_medium', 3076 | 'artifacts_strong', 3077 | ]; 3078 | var noArtifactAnnotation = true; 3079 | $(that.element).find('.artifact_panel button.artifact').removeClass(activeClass); 3080 | for (var i = 0; i < annotations.length; ++i) { 3081 | var annotation = annotations[i]; 3082 | if (validKeys.indexOf(annotation.label) > -1) { 3083 | var cacheKey = that._getAnnotationsCacheKey(that.vars.currentWindowRecording, annotation.position.start, annotation.position.end, false, 'ARTIFACT'); 3084 | that.vars.annotationsCache[cacheKey] = annotation.id; 3085 | $(that.element).find('.artifact_panel button.artifact[data-annotation-type="' + annotation.label + '"]').addClass(activeClass); 3086 | noArtifactAnnotation = false; 3087 | break; 3088 | } 3089 | } 3090 | if (noArtifactAnnotation) { 3091 | $(that.element).find('.artifact_panel button.artifact[data-annotation-type="artifacts_none"]').addClass(activeClass); 3092 | } 3093 | }, 3094 | 3095 | _displaySleepStageSelection: function(annotations) { 3096 | var that = this; 3097 | var activeClass = 'teal'; 3098 | var inactiveClass = 'blue lighten-1'; 3099 | var validKeys = [ 3100 | 'sleep_stage_wake', 3101 | 'sleep_stage_n1', 3102 | 'sleep_stage_n2', 3103 | 'sleep_stage_n3', 3104 | 'sleep_stage_rem', 3105 | 'sleep_stage_unknown', 3106 | ]; 3107 | var noSleepStageAnnotation = true; 3108 | $(that.element).find('.sleep_stage_panel button.sleep_stage').removeClass(activeClass).addClass(inactiveClass); 3109 | for (var i = 0; i < annotations.length; ++i) { 3110 | var annotation = annotations[i]; 3111 | if (validKeys.indexOf(annotation.label) > -1) { 3112 | var cacheKey = that._getAnnotationsCacheKey(that.vars.currentWindowRecording, annotation.position.start, annotation.position.end, false, 'SLEEP_STAGE'); 3113 | that.vars.annotationsCache[cacheKey] = annotation.id; 3114 | $(that.element).find('.sleep_stage_panel button.sleep_stage[data-annotation-type="' + annotation.label + '"]').addClass(activeClass).removeClass(inactiveClass); 3115 | noSleepStageAnnotation = false; 3116 | break; 3117 | } 3118 | } 3119 | }, 3120 | 3121 | _turnExampleIntoAnnotation: function(example) { 3122 | var that = this; 3123 | var annotation = { 3124 | id: that._getUUID(), 3125 | label: example.type, 3126 | confidence: example.confidence, 3127 | position: { 3128 | start: example.start, 3129 | end: example.end, 3130 | channels: example.channels, 3131 | }, 3132 | metadata: { 3133 | channels_displayed: example.channels_displayed, 3134 | comment: example.comment, 3135 | }, 3136 | }; 3137 | return annotation; 3138 | }, 3139 | 3140 | _turnExamplesIntoAnnotations: function(examples) { 3141 | var that = this; 3142 | var annotations = []; 3143 | examples.forEach(function(example) { 3144 | annotations.push(that._turnExampleIntoAnnotation(example)); 3145 | }); 3146 | return annotations; 3147 | }, 3148 | 3149 | _displayAnnotations: function(annotations) { 3150 | var that = this; 3151 | 3152 | for (var ii = 0; ii < annotations.length; ii++) { 3153 | var annotation = annotations[ii]; 3154 | var channelIndex = annotation.position.channels; 3155 | if (channelIndex === undefined) { 3156 | continue; 3157 | } 3158 | if (channelIndex.length) { 3159 | channelIndex = channelIndex[0]; 3160 | } 3161 | var channelIndexRecording = annotation.metadata.channels_displayed[channelIndex]; 3162 | var channelIndexMapped = that.options.channelsDisplayed.indexOf(channelIndexRecording); 3163 | if (channelIndexMapped < 0) { 3164 | continue; 3165 | } 3166 | 3167 | var annotationId = annotation.id; 3168 | var start_time = annotation.position.start; 3169 | var end_time = annotation.position.end; 3170 | var type = annotation.label; 3171 | var confidence = annotation.confidence; 3172 | var comment = annotation.metadata.comment; 3173 | var chart = that.vars.chart; 3174 | 3175 | if (chart != undefined && that.options.features.order.indexOf(type) > -1) { 3176 | that._addAnnotationBox(annotationId, start_time, channelIndexMapped, type, end_time, confidence, comment, annotation); 3177 | } 3178 | } 3179 | }, 3180 | 3181 | _flushAnnotations: function() { 3182 | var that = this; 3183 | annotations = that.vars.chart.annotations.allItems; 3184 | while (annotations && annotations.length > 0) { 3185 | annotations[0].destroy(); 3186 | } 3187 | that.vars.annotationsCache = {}; 3188 | }, 3189 | 3190 | _getApiClient: function() { 3191 | var that = this; 3192 | return that.options.apiClient; 3193 | }, 3194 | 3195 | _saveAnnotation: function(annotationId, recording_name, type, start, end, channels, confidence, comment, metadata, callback) { 3196 | 3197 | var that = this; 3198 | if (that.options.isReadOnly) return; 3199 | that._incrementNumberOfAnnotationsInCurrentWindow(1); 3200 | that._updateLastAnnotationTime(); 3201 | var metadata = metadata !== undefined ? metadata : {}; 3202 | if (channels && !channels.length) { 3203 | channels = [ channels ]; 3204 | } 3205 | var apiClient = that._getApiClient(); 3206 | var params = { 3207 | content: { 3208 | label: type, 3209 | confidence: confidence, 3210 | position: { 3211 | channels: channels, 3212 | start: parseFloat(start), 3213 | end: parseFloat(end), 3214 | }, 3215 | metadata: { 3216 | channels_displayed: that.options.channelsDisplayed, 3217 | comment: comment, 3218 | metadata: metadata, 3219 | }, 3220 | } 3221 | } 3222 | if (!annotationId) { 3223 | apiClient.create('response', params, updateCache); 3224 | } 3225 | else { 3226 | params.id = annotationId; 3227 | apiClient.partialUpdate('response', params, updateCache); 3228 | } 3229 | function updateCache(annotation) { 3230 | callback && callback(annotation, null); 3231 | var window_start = that.vars.currentWindowStart; 3232 | var window_end = window_start + that.options.windowSizeInSeconds; 3233 | var key = that._getAnnotationsCacheKey(recording_name, window_start, window_end); 3234 | var cacheEntry = that.vars.annotationsCache[key]; 3235 | if (!cacheEntry) { 3236 | cacheEntry = { 3237 | annotations: [] 3238 | } 3239 | } 3240 | cacheEntry.annotations.unshift(annotation.content); 3241 | that.vars.annotationsCache[key] = cacheEntry; 3242 | } 3243 | }, 3244 | 3245 | _deleteAnnotation: function(annotationId, recording_name, start, end, channels, channelsDisplayed) { 3246 | var that = this; 3247 | if (that.options.isReadOnly) return; 3248 | that._incrementNumberOfAnnotationsInCurrentWindow(-1); 3249 | that._updateLastAnnotationTime(); 3250 | // that._getApiClient().annotation().delete(annotationId, function(response, error) { 3251 | // if (error) { 3252 | // alert(error.message); 3253 | // return; 3254 | // } 3255 | // }); 3256 | }, 3257 | 3258 | _incrementNumberOfAnnotationsInCurrentWindow: function(increment) { 3259 | var that = this; 3260 | that._setNumberOfAnnotationsInCurrentWindow(that.vars.numberOfAnnotationsInCurrentWindow + increment); 3261 | }, 3262 | 3263 | _setNumberOfAnnotationsInCurrentWindow: function(value) { 3264 | var that = this; 3265 | that.vars.numberOfAnnotationsInCurrentWindow = Math.max(0, value); 3266 | var zeroAnnotations = that.vars.numberOfAnnotationsInCurrentWindow == 0 3267 | $(that.element).find('.no-features').prop('disabled', !zeroAnnotations); 3268 | $(that.element).find('.submit-features').prop('disabled', zeroAnnotations); 3269 | }, 3270 | 3271 | _lastWindowReached: function() { 3272 | var that = this; 3273 | if (!that.vars.lastWindowHasBeenReachedBefore) { 3274 | that._lastWindowReachedForTheFirstTime(); 3275 | } 3276 | that.vars.lastWindowHasBeenReachedBefore = true; 3277 | $(that.element).find('.next-window').hide(); 3278 | }, 3279 | 3280 | _lastWindowReachedForTheFirstTime: function() { 3281 | var that = this; 3282 | if (that.options.showConfirmationCode && that.options.confirmationCode) { 3283 | var code = that.options.confirmationCode; 3284 | var textCompletionButton = 'Show Confirmation Code' 3285 | var confirmationCodeInstructions = 'You need to copy and paste your confirmation code into the input field on the Mechanical Turk instructions page and submit in order to receive the full payment for this task.'; 3286 | if (!that._isHITModeEnabled()) { 3287 | bootbox.alert('
This is the last data window. Please annotate it just like you did with the others. After that, please click the green button saying "' + textCompletionButton + '" to get your confirmation code and finish the task.

Important: ' + confirmationCodeInstructions + '', showCompletionButton) 3288 | .appendTo(that.element); 3289 | } 3290 | else { 3291 | $(that.element).find('.submit-annotations').add('.next-window').click(function() { 3292 | showCompletionButton(); 3293 | showConfirmationCode(); 3294 | }); 3295 | } 3296 | 3297 | function showCompletionButton() { 3298 | $(that.element).find('.graph_footer .middle').children().hide(); 3299 | var taskCompletionCodeButton = $("