comparison interfaces/timeline.js @ 2484:83c1352ce756

Merge branch 'master' of https://github.com/BrechtDeMan/WebAudioEvaluationTool
author www-data <www-data@sucuk.dcs.qmul.ac.uk>
date Thu, 04 Aug 2016 12:20:54 +0100
parents 3c92c732fb05
children 03a9a90717ed c8e6991951ad
comparison
equal deleted inserted replaced
2478:1cc68e4ce885 2484:83c1352ce756
1 /**
2 * WAET Timeline
3 * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing.
4 */
5
6 // Once this is loaded and parsed, begin execution
7 loadInterface();
8
9 function loadInterface() {
10 // Use this to do any one-time page / element construction. For instance, placing any stationary text objects,
11 // holding div's, or setting up any nodes which are present for the entire test sequence
12
13 interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema
14
15 interfaceContext.insertPoint = document.getElementById("topLevelBody");
16 var testContent = document.createElement("div");
17
18 // Create the top div and Title element
19 var title = document.createElement("div");
20 title.className = "title";
21 title.align = "center";
22 var titleSpan = document.createElement("span");
23 titleSpan.id = "test-title";
24 titleSpan.textContent = "Listening Test";
25 title.appendChild(titleSpan);
26
27 var pagetitle = document.createElement("div");
28 pagetitle.className = "pageTitle";
29 pagetitle.align = "center";
30 titleSpan = document.createElement("span");
31 titleSpan.id = "page-title";
32 pagetitle.appendChild(titleSpan);
33
34 // Create Interface buttons
35 var interfaceButtons = document.createElement("div");
36 interfaceButtons.id = 'interface-buttons';
37 interfaceButtons.style.height = "25px";
38
39 // Create playback start/stop points
40 var playback = document.createElement("button");
41 playback.innerHTML = "Stop";
42 playback.id = "playback-button";
43 playback.onclick = function () {
44 if (audioEngineContext.status == 1) {
45 audioEngineContext.stop();
46 this.innerHTML = "Stop";
47 var time = audioEngineContext.timer.getTestTime();
48 console.log("Stopped at " + time);
49 }
50 };
51 // Create Submit (save) button
52 var submit = document.createElement("button");
53 submit.innerHTML = 'Next';
54 submit.onclick = buttonSubmitClick;
55 submit.id = 'submit-button';
56 submit.style.float = 'left';
57 // Append the interface buttons into the interfaceButtons object.
58 interfaceButtons.appendChild(playback);
59 interfaceButtons.appendChild(submit);
60
61 // Create outside reference holder
62 var outsideRef = document.createElement("div");
63 outsideRef.id = "outside-reference-holder";
64
65 // Create content point
66 var content = document.createElement("div");
67 content.id = "timeline-test-content";
68
69 //Inject
70 testContent.appendChild(title);
71 testContent.appendChild(pagetitle);
72 testContent.appendChild(interfaceButtons);
73 testContent.appendChild(outsideRef);
74 testContent.appendChild(content);
75 interfaceContext.insertPoint.appendChild(testContent);
76
77 // Load the full interface
78 testState.initialise();
79 testState.advanceState();
80 };
81
82 function loadTest(page) {
83 // Called each time a new test page is to be build. The page specification node is the only item passed in
84 var content = document.getElementById("timeline-test-content");
85 content.innerHTML = "";
86 var interfaceObj = page.interfaces;
87 if (interfaceObj.length > 1) {
88 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
89 }
90 interfaceObj = interfaceObj[0];
91
92 //Set the page title
93 if (typeof page.title == "string" && page.title.length > 0) {
94 document.getElementById("test-title").textContent = page.title;
95 }
96
97 if (interfaceObj.title != null) {
98 document.getElementById("page-title").textContent = interfaceObj.title;
99 }
100
101 // Delete outside reference
102 var outsideReferenceHolder = document.getElementById("outside-reference-holder");
103 outsideReferenceHolder.innerHTML = "";
104
105 var commentBoxPrefix = "Comment on track";
106 if (interfaceObj.commentBoxPrefix != undefined) {
107 commentBoxPrefix = interfaceObj.commentBoxPrefix;
108 }
109
110 $(page.audioElements).each(function (index, element) {
111 var audioObject = audioEngineContext.newTrack(element);
112 if (page.audioElements.type == 'outside-reference') {
113 var refNode = interfaceContext.outsideReferenceDOM(audioObject, index, outsideReferenceHolder);
114 audioObject.bindInterface(orNode);
115 } else {
116 switch (audioObject.specification.parent.label) {
117 case "none":
118 label = "";
119 break;
120 case "letter":
121 label = String.fromCharCode(97 + index);
122 break;
123 case "capital":
124 label = String.fromCharCode(65 + index);
125 break;
126 default:
127 label = "" + index;
128 break;
129 }
130 var node = new interfaceObject(audioObject, label);
131
132 content.appendChild(node.DOM);
133 audioObject.bindInterface(node);
134 }
135 });
136
137 resizeWindow();
138 }
139
140 function interfaceObject(audioObject, labelstr) {
141 // Each audio object has a waveform guide and self-generated comments
142 this.parent = audioObject;
143 this.DOM = document.createElement("div");
144 this.DOM.className = "timeline-element";
145 this.DOM.id = audioObject.specification.id;
146
147 var root = document.createElement("div");
148 root.className = "timeline-element-content";
149 this.DOM.appendChild(root);
150
151 var label = document.createElement("div");
152 label.style.textAlign = "center";
153 var labelSpan = document.createElement("span");
154 labelSpan.textContent = "Fragment " + labelstr;
155 label.appendChild(labelSpan);
156 root.appendChild(label);
157
158 var canvasHolder = document.createElement("div");
159 canvasHolder.className = "timeline-element-canvas-holder";
160 var buttonHolder = document.createElement("div");
161 buttonHolder.className = "timeline-element-button-holder";
162 var commentHolder = document.createElement("div");
163 commentHolder.className = "timeline-element-comment-holder";
164
165 root.appendChild(canvasHolder);
166 root.appendChild(buttonHolder);
167 root.appendChild(commentHolder);
168
169 this.comments = {
170 parent: this,
171 list: [],
172 Comment: function (parent, time, str) {
173 this.parent = parent;
174 this.time = time;
175 this.DOM = document.createElement("div");
176 this.DOM.className = "comment-entry";
177 var titleHolder = document.createElement("div");
178 titleHolder.className = "comment-entry-header";
179 this.title = document.createElement("span");
180 if (str != undefined) {
181 this.title.textContent = str;
182 } else {
183 this.title.textContent = "Time: " + time.toFixed(2) + "s";
184 }
185 titleHolder.appendChild(this.title);
186 this.textarea = document.createElement("textarea");
187 this.textarea.className = "comment-entry-text";
188 this.DOM.appendChild(titleHolder);
189 this.DOM.appendChild(this.textarea);
190
191 this.clear = {
192 DOM: document.createElement("button"),
193 parent: this,
194 handleEvent: function () {
195 this.parent.parent.deleteComment(this.parent);
196 }
197 }
198 this.clear.DOM.textContent = "Delete";
199 this.clear.DOM.addEventListener("click", this.clear);
200 titleHolder.appendChild(this.clear.DOM);
201
202 this.resize = function () {
203 var w = window.innerWidth;
204 w = Math.min(w, 800);
205 w = Math.max(w, 200);
206 var elem_w = w / 2.5;
207 elem_w = Math.max(elem_w, 190);
208 this.DOM.style.width = elem_w + "px";
209 this.textarea.style.width = (elem_w - 5) + "px";
210 }
211 this.buildXML = function (root) {
212 //storage.document.createElement();
213 var node = storage.document.createElement("comment");
214 var question = storage.document.createElement("question");
215 var comment = storage.document.createElement("response");
216 node.setAttribute("time", this.time);
217 question.textContent = this.title.textContent;
218 comment.textContent = this.textarea.value;
219 node.appendChild(question);
220 node.appendChild(comment);
221 root.appendChild(node);
222 }
223 this.resize();
224 },
225 newComment: function (time) {
226 var node = new this.Comment(this, time);
227 this.list.push(node);
228 commentHolder.appendChild(node.DOM);
229 return node;
230 },
231 deleteComment: function (comment) {
232 var index = this.list.findIndex(function (element, index, array) {
233 if (element == comment) {
234 return true;
235 }
236 return false;
237 }, comment);
238 if (index == -1) {
239 return false;
240 }
241 var node = this.list.splice(index, 1);
242 comment.DOM.remove();
243 this.parent.canvas.drawMarkers();
244 return true;
245 },
246 clearList: function () {
247 while (this.list.length > 0) {
248 this.deleteComment(this.list[0]);
249 }
250 }
251 }
252
253 this.canvas = {
254 parent: this,
255 comments: this.comments,
256 layer1: document.createElement("canvas"),
257 layer2: document.createElement("canvas"),
258 layer3: document.createElement("canvas"),
259 layer4: document.createElement("canvas"),
260 resize: function (w) {
261 this.layer1.width = w;
262 this.layer2.width = w;
263 this.layer3.width = w;
264 this.layer4.width = w;
265 this.layer1.style.width = w + "px";
266 this.layer2.style.width = w + "px";
267 this.layer3.style.width = w + "px";
268 this.layer4.style.width = w + "px";
269 this.drawWaveform();
270 this.drawMarkers();
271 },
272 handleEvent: function (event) {
273 switch (event.currentTarget) {
274 case this.layer1:
275 switch (event.type) {
276 case "mousemove":
277 this.drawMouse(event);
278 break;
279 case "mouseleave":
280 this.clearCanvas(this.layer1);
281 break;
282 case "click":
283 var rect = this.layer1.getBoundingClientRect();
284 var pixX = event.clientX - rect.left;
285 var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width;
286 this.comments.newComment(pixX * tpp);
287 this.drawMarkers();
288 break;
289 }
290 break;
291 }
292 },
293 drawWaveform: function () {
294 if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
295 return;
296 }
297 var buffer = this.parent.parent.buffer.buffer;
298 var context = this.layer4.getContext("2d");
299 context.lineWidth = 1;
300 context.strokeStyle = "#888";
301 context.clearRect(0, 0, this.layer4.width, this.layer4.height);
302 var data = buffer.getChannelData(0);
303 var t_per_pixel = buffer.duration / this.layer4.width;
304 var s_per_pixel = data.length / this.layer4.width;
305 var pixX = 0;
306 while (pixX < this.layer4.width) {
307 var start = Math.floor(s_per_pixel * pixX);
308 var end = Math.min(Math.ceil(s_per_pixel * (pixX + 1)), data.length);
309 var frame = data.subarray(start, end);
310 var min = frame[0];
311 var max = min;
312 for (var n = 0; n < frame.length; n++) {
313 if (frame[n] < min) {
314 min = frame[n];
315 }
316 if (frame[n] > max) {
317 max = frame[n];
318 }
319 }
320 // Assuming min/max normalised between [-1, 1] to map to [150, 0]
321 context.beginPath();
322 context.moveTo(pixX + 0.5, (min + 1) * -75 + 150);
323 context.lineTo(pixX + 0.5, (max + 1) * -75 + 150);
324 context.stroke();
325 pixX++;
326 }
327 },
328 drawMouse: function (event) {
329 var context = this.layer1.getContext("2d");
330 context.clearRect(0, 0, this.layer1.width, this.layer1.height);
331 var rect = this.layer1.getBoundingClientRect();
332 var pixX = event.clientX - rect.left;
333 pixX = Math.floor(pixX) - 0.5;
334 context.strokeStyle = "#800";
335 context.beginPath();
336 context.moveTo(pixX, 0);
337 context.lineTo(pixX, this.layer1.height);
338 context.stroke();
339 },
340 drawTicker: function () {
341 var context = this.layer2.getContext("2d");
342 context.clearRect(0, 0, this.layer2.width, this.layer2.height);
343 var time = this.parent.parent.getCurrentPosition();
344 var ratio = time / this.parent.parent.buffer.buffer.duration;
345 var pixX = Math.floor(ratio * this.layer2.width) + 0.5;
346 context.strokeStyle = "#080";
347 context.beginPath();
348 context.moveTo(pixX, 0);
349 context.lineTo(pixX, this.layer2.height);
350 context.stroke();
351 },
352 drawMarkers: function () {
353 if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
354 return;
355 }
356 var context = this.layer3.getContext("2d");
357 context.clearRect(0, 0, this.layer3.width, this.layer3.height);
358 context.strokeStyle = "#008";
359 var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width;
360 for (var i = 0; i < this.comments.list.length; i++) {
361 var comment = this.comments.list[i];
362 var pixX = Math.floor(comment.time / tpp) + 0.5;
363 context.beginPath();
364 context.moveTo(pixX, 0);
365 context.lineTo(pixX, this.layer3.height);
366 context.stroke();
367 }
368 },
369 clearCanvas: function (canvas) {
370 var context = canvas.getContext("2d");
371 context.clearRect(0, 0, canvas.width, canvas.height);
372 }
373 }
374 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled";
375 this.canvas.layer2.className = "timeline-element-canvas canvas-layer2";
376 this.canvas.layer3.className = "timeline-element-canvas canvas-layer3";
377 this.canvas.layer4.className = "timeline-element-canvas canvas-layer3";
378 this.canvas.layer1.height = "150";
379 this.canvas.layer2.height = "150";
380 this.canvas.layer3.height = "150";
381 this.canvas.layer4.height = "150";
382 canvasHolder.appendChild(this.canvas.layer1);
383 canvasHolder.appendChild(this.canvas.layer2);
384 canvasHolder.appendChild(this.canvas.layer3);
385 canvasHolder.appendChild(this.canvas.layer4);
386 this.canvas.layer1.addEventListener("mousemove", this.canvas);
387 this.canvas.layer1.addEventListener("mouseleave", this.canvas);
388 this.canvas.layer1.addEventListener("click", this.canvas);
389
390 var canvasIntervalID = null;
391
392 this.playButton = {
393 parent: this,
394 DOM: document.createElement("button"),
395 handleEvent: function (event) {
396 var id = this.parent.parent.id;
397 var str = this.DOM.textContent;
398 if (str == "Play") {
399 audioEngineContext.play(id);
400 } else if (str == "Stop") {
401 audioEngineContext.stop();
402 }
403 }
404 }
405 this.playButton.DOM.addEventListener("click", this.playButton);
406 this.playButton.DOM.className = "timeline-button timeline-button-disabled";
407 this.playButton.DOM.disabled = true;
408 this.playButton.DOM.textContent = "Wait";
409
410 buttonHolder.appendChild(this.playButton.DOM);
411
412 this.resize = function () {
413 var w = window.innerWidth;
414 w = Math.min(w, 800);
415 w = Math.max(w, 200);
416 root.style.width = w + "px";
417 var c_w = w - 100;
418 this.canvas.resize(c_w);
419 }
420
421 this.enable = function () {
422 // This is used to tell the interface object that playback of this node is ready
423 this.canvas.layer1.addEventListener("click", this.canvas);
424 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1";
425 this.playButton.DOM.className = "timeline-button timeline-button-play";
426 this.playButton.DOM.textContent = "Play";
427 this.playButton.DOM.disabled = false;
428
429 this.canvas.drawWaveform();
430 };
431 this.updateLoading = function (progress) {
432 // progress is a value from 0 to 100 indicating the current download state of media files
433 progress = String(progress);
434 progress = progress.substr(0, 5);
435 this.playButton.DOM.textContent = "Loading: " + progress + '%';
436 };
437 this.startPlayback = function () {
438 // Called when playback has begun
439 canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100);
440 this.playButton.DOM.textContent = "Stop";
441 };
442 this.stopPlayback = function () {
443 // Called when playback has stopped. This gets called even if playback never started!
444 window.clearInterval(canvasIntervalID);
445 this.canvas.clearCanvas(this.canvas.layer2);
446 this.playButton.DOM.textContent = "Play";
447 };
448 this.getValue = function () {
449 // Return the current value of the object. If there is no value, return 0
450 return 0;
451 };
452 this.getPresentedId = function () {
453 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
454 return labelSpan.textContent;
455 };
456 this.canMove = function () {
457 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale.
458 // These are checked primarily if the interface check option 'fragmentMoved' is enabled.
459 return false;
460 };
461 this.exportXMLDOM = function (audioObject) {
462 // Called by the audioObject holding this element to export the interface <value> node.
463 // If there is no value node (such as outside reference), return null
464 // If there are multiple value nodes (such as multiple scale / 2D scales), return an array of nodes with each value node having an 'interfaceName' attribute
465 // Use storage.document.createElement('value'); to generate the XML node.
466 return null;
467 };
468 this.error = function () {
469 // If there is an error with the audioObject, this will be called to indicate a failure
470 }
471 };
472
473 function resizeWindow(event) {
474 // Called on every window resize event, use this to scale your page properly
475 for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
476 audioEngineContext.audioObjects[i].interfaceDOM.resize();
477 }
478 }
479
480 function buttonSubmitClick() {
481 if (audioEngineContext.timer.testStarted == false) {
482 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!');
483 return;
484 }
485 var checks = [];
486 checks = checks.concat(testState.currentStateMap.interfaces[0].options);
487 checks = checks.concat(specification.interfaces.options);
488 var canContinue = true;
489 for (var i = 0; i < checks.length; i++) {
490 var checkState = true;
491 if (checks[i].type == 'check') {
492 switch (checks[i].name) {
493 case 'fragmentPlayed':
494 //Check if all fragments have been played
495 checkState = interfaceContext.checkAllPlayed();
496 break;
497 case 'fragmentFullPlayback':
498 //Check if all fragments have played to their full length
499 checkState = interfaceContext.checkFragmentsFullyPlayed();
500 break;
501 case 'fragmentComments':
502 checkState = interfaceContext.checkAllCommented();
503 break;
504 default:
505 console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
506 break;
507 }
508 if (checkState == false) {
509 canContinue == false;
510 }
511 }
512 if (!canContinue) {
513 return;
514 }
515 }
516
517 if (canContinue) {
518 if (audioEngineContext.status == 1) {
519 var playback = document.getElementById('playback-button');
520 playback.click();
521 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
522 }
523 testState.advanceState();
524 }
525 }
526
527 function pageXMLSave(store, pageSpecification) {
528 // MANDATORY
529 // Saves a specific test page
530 // You can use this space to add any extra nodes to your XML <audioHolder> saves
531 // Get the current <page> information in store (remember to appendChild your data to it)
532 // pageSpecification is the current page node configuration
533 // To create new XML nodes, use storage.document.createElement();
534
535 for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
536 var id = audioEngineContext.audioObjects[i].specification.id;
537 var commentsList = audioEngineContext.audioObjects[i].interfaceDOM.comments.list;
538 var root = audioEngineContext.audioObjects[i].storeDOM;
539 for (var j = 0; j < commentsList.length; j++) {
540 commentsList[j].buildXML(root);
541 }
542 }
543 }