├── .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 | 
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 |
\
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 | I do not see any features \
845 | Submit features \
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 | \
1044 | Open Cheat Sheet \
1045 | \
1046 | \
1047 | \
1048 | \
1049 | \
1050 | \
1051 | \
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 = $('' + feature_name + ' ').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 = $('