comparison interfaces/horizontal-sliders.js @ 1116:c44fbf72f7f2

All interfaces support comment boxes. Comment box identification matches presented tag (for instance, AB will be Comment on fragment A, rather than 1). Tighter buffer loading protocol, audioObjects register with the buffer rather than checking for buffer existence (which can be buggy depending on the buffer state). Buffers now have a state to ensure exact location in loading chain (downloading, decoding, LUFS, ready).
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Fri, 29 Jan 2016 11:11:57 +0000
parents
children c0022a09c4f6
comparison
equal deleted inserted replaced
-1:000000000000 1116:c44fbf72f7f2
1 // Once this is loaded and parsed, begin execution
2 loadInterface();
3
4 function loadInterface() {
5 // Use this to do any one-time page / element construction. For instance, placing any stationary text objects,
6 // holding div's, or setting up any nodes which are present for the entire test sequence
7
8 // The injection point into the HTML page
9 interfaceContext.insertPoint = document.getElementById("topLevelBody");
10 var testContent = document.createElement('div');
11 testContent.id = 'testContent';
12
13 // Create the top div for the Title element
14 var titleAttr = specification.title;
15 var title = document.createElement('div');
16 title.className = "title";
17 title.align = "center";
18 var titleSpan = document.createElement('span');
19
20 // Set title to that defined in XML, else set to default
21 if (titleAttr != undefined) {
22 titleSpan.textContent = titleAttr;
23 } else {
24 titleSpan.textContent = 'Listening test';
25 }
26 // Insert the titleSpan element into the title div element.
27 title.appendChild(titleSpan);
28
29 var pagetitle = document.createElement('div');
30 pagetitle.className = "pageTitle";
31 pagetitle.align = "center";
32 var titleSpan = document.createElement('span');
33 titleSpan.id = "pageTitle";
34 pagetitle.appendChild(titleSpan);
35
36 // Create Interface buttons!
37 var interfaceButtons = document.createElement('div');
38 interfaceButtons.id = 'interface-buttons';
39 interfaceButtons.style.height = '25px';
40
41 // Create playback start/stop points
42 var playback = document.createElement("button");
43 playback.innerHTML = 'Stop';
44 playback.id = 'playback-button';
45 playback.style.float = 'left';
46 // onclick function. Check if it is playing or not, call the correct function in the
47 // audioEngine, change the button text to reflect the next state.
48 playback.onclick = function() {
49 if (audioEngineContext.status == 1) {
50 audioEngineContext.stop();
51 this.innerHTML = 'Stop';
52 var time = audioEngineContext.timer.getTestTime();
53 console.log('Stopped at ' + time); // DEBUG/SAFETY
54 }
55 };
56 // Create Submit (save) button
57 var submit = document.createElement("button");
58 submit.innerHTML = 'Submit';
59 submit.onclick = buttonSubmitClick;
60 submit.id = 'submit-button';
61 submit.style.float = 'left';
62 // Append the interface buttons into the interfaceButtons object.
63 interfaceButtons.appendChild(playback);
64 interfaceButtons.appendChild(submit);
65
66 // Create a slider box
67 var sliderBox = document.createElement('div');
68 sliderBox.style.width = "100%";
69 sliderBox.style.height = window.innerHeight - 200+12 + 'px';
70 sliderBox.style.marginBottom = '10px';
71 sliderBox.id = 'slider';
72 var scaleHolder = document.createElement('div');
73 scaleHolder.id = "scale-holder";
74 scaleHolder.style.marginLeft = "107px";
75 sliderBox.appendChild(scaleHolder);
76 var scaleText = document.createElement('div');
77 scaleText.id = "scale-text-holder";
78 scaleText.style.height = "25px";
79 scaleText.style.width = "100%";
80 scaleHolder.appendChild(scaleText);
81 var scaleCanvas = document.createElement('canvas');
82 scaleCanvas.id = "scale-canvas";
83 scaleCanvas.style.marginLeft = "100px";
84 scaleHolder.appendChild(scaleCanvas);
85 var sliderObjectHolder = document.createElement('div');
86 sliderObjectHolder.id = 'slider-holder';
87 sliderObjectHolder.align = "center";
88 sliderBox.appendChild(sliderObjectHolder);
89
90 // Global parent for the comment boxes on the page
91 var feedbackHolder = document.createElement('div');
92 feedbackHolder.id = 'feedbackHolder';
93
94 testContent.style.zIndex = 1;
95 interfaceContext.insertPoint.innerHTML = null; // Clear the current schema
96
97 // Inject into HTML
98 testContent.appendChild(title); // Insert the title
99 testContent.appendChild(pagetitle);
100 testContent.appendChild(interfaceButtons);
101 testContent.appendChild(sliderBox);
102 testContent.appendChild(feedbackHolder);
103 interfaceContext.insertPoint.appendChild(testContent);
104
105 // Load the full interface
106 testState.initialise();
107 testState.advanceState();
108 };
109
110 function loadTest(page)
111 {
112 // Called each time a new test page is to be build. The page specification node is the only item passed in
113 var id = page.id;
114
115 var feedbackHolder = document.getElementById('feedbackHolder');
116 feedbackHolder.innerHTML = null;
117
118 var interfaceObj = page.interfaces;
119 if (interfaceObj.length > 1)
120 {
121 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
122 }
123 interfaceObj = interfaceObj[0];
124 if(interfaceObj.title != null)
125 {
126 document.getElementById("pageTitle").textContent = interfaceObj.title;
127 }
128
129 var interfaceOptions = specification.interfaces.options.concat(interfaceObj.options);
130 for (var option of interfaceOptions)
131 {
132 if (option.type == "show")
133 {
134 switch(option.name) {
135 case "playhead":
136 var playbackHolder = document.getElementById('playback-holder');
137 if (playbackHolder == null)
138 {
139 playbackHolder = document.createElement('div');
140 playbackHolder.style.width = "100%";
141 playbackHolder.align = 'center';
142 playbackHolder.appendChild(interfaceContext.playhead.object);
143 feedbackHolder.appendChild(playbackHolder);
144 }
145 break;
146 case "page-count":
147 var pagecountHolder = document.getElementById('page-count');
148 if (pagecountHolder == null)
149 {
150 pagecountHolder = document.createElement('div');
151 pagecountHolder.id = 'page-count';
152 }
153 pagecountHolder.innerHTML = '<span>Page '+(page.presentedId+1)+' of '+specification.pages.length+'</span>';
154 var inject = document.getElementById('interface-buttons');
155 inject.appendChild(pagecountHolder);
156 break;
157 case "volume":
158 if (document.getElementById('master-volume-holder') == null)
159 {
160 feedbackHolder.appendChild(interfaceContext.volume.object);
161 }
162 break;
163 }
164 }
165 }
166
167 // Delete outside reference
168 var outsideReferenceHolder = document.getElementById('outside-reference');
169 if (outsideReferenceHolder != null) {
170 document.getElementById('interface-buttons').removeChild(outsideReferenceHolder);
171 }
172
173 var sliderBox = document.getElementById('slider-holder');
174 sliderBox.innerHTML = null;
175
176 var commentBoxPrefix = "Comment on track";
177 if (interfaceObj.commentBoxPrefix != undefined) {
178 commentBoxPrefix = interfaceObj.commentBoxPrefix;
179 }
180 var loopPlayback = page.loop;
181
182 $(page.commentQuestions).each(function(index,element) {
183 var node = interfaceContext.createCommentQuestion(element);
184 feedbackHolder.appendChild(node.holder);
185 });
186
187 // Find all the audioElements from the audioHolder
188 var label = 0;
189 $(page.audioElements).each(function(index,element){
190 // Find URL of track
191 // In this jQuery loop, variable 'this' holds the current audioElement.
192
193 var audioObject = audioEngineContext.newTrack(element);
194 if (element.type == 'outside-reference')
195 {
196 // Construct outside reference;
197 var orNode = new outsideReferenceDOM(audioObject,index,document.getElementById('interface-buttons'));
198 audioObject.bindInterface(orNode);
199 } else {
200 // Create a slider per track
201 var sliderObj = new sliderObject(audioObject,label);
202
203 if (typeof page.initialPosition === "number")
204 {
205 // Set the values
206 sliderObj.slider.value = page.initalPosition;
207 } else {
208 // Distribute it randomnly
209 sliderObj.slider.value = Math.random();
210 }
211 sliderBox.appendChild(sliderObj.holder);
212 audioObject.bindInterface(sliderObj);
213 interfaceContext.createCommentBox(audioObject);
214 label += 1;
215 }
216
217 });
218 if (page.showElementComments)
219 {
220 interfaceContext.showCommentBoxes(feedbackHolder,true);
221 }
222 // Auto-align
223 resizeWindow(null);
224 }
225
226 function sliderObject(audioObject,label)
227 {
228 // An example node, you can make this however you want for each audioElement.
229 // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following
230 // You attach them by calling audioObject.bindInterface( )
231 this.parent = audioObject;
232
233 this.holder = document.createElement('div');
234 this.title = document.createElement('div');
235 this.slider = document.createElement('input');
236 this.play = document.createElement('button');
237
238 this.holder.className = 'track-slider';
239 this.holder.style.width = window.innerWidth-200 + 'px';
240 this.holder.appendChild(this.title);
241 this.holder.appendChild(this.slider);
242 this.holder.appendChild(this.play);
243 this.holder.setAttribute('trackIndex',audioObject.id);
244 this.title.textContent = label;
245 this.title.className = 'track-slider-title';
246
247 this.slider.type = "range";
248 this.slider.className = "track-slider-range track-slider-not-moved";
249 this.slider.min = "0";
250 this.slider.max = "1";
251 this.slider.step = "0.01";
252 this.slider.style.width = window.innerWidth-420 + 'px';
253 this.slider.onchange = function()
254 {
255 var time = audioEngineContext.timer.getTestTime();
256 var id = Number(this.parentNode.getAttribute('trackIndex'));
257 audioEngineContext.audioObjects[id].metric.moved(time,this.value);
258 console.log('slider '+id+' moved to '+this.value+' ('+time+')');
259 $(this).removeClass('track-slider-not-moved');
260 };
261
262 this.play.className = 'track-slider-button';
263 this.play.textContent = "Loading...";
264 this.play.value = audioObject.id;
265 this.play.disabled = true;
266 this.play.onclick = function(event)
267 {
268 var id = Number(event.currentTarget.value);
269 //audioEngineContext.metric.sliderPlayed(id);
270 audioEngineContext.play(id);
271 };
272 this.resize = function(event)
273 {
274 this.holder.style.width = window.innerWidth-200 + 'px';
275 this.slider.style.width = window.innerWidth-420 + 'px';
276 };
277 this.enable = function()
278 {
279 // This is used to tell the interface object that playback of this node is ready
280 this.play.disabled = false;
281 this.play.textContent = "Play";
282 $(this.slider).removeClass('track-slider-disabled');
283 };
284 this.updateLoading = function(progress)
285 {
286 // progress is a value from 0 to 100 indicating the current download state of media files
287 };
288 this.startPlayback = function()
289 {
290 // Called when playback has begun
291 $(".track-slider").removeClass('track-slider-playing');
292 $(this.holder).addClass('track-slider-playing');
293 var outsideReference = document.getElementById('outside-reference');
294 if (outsideReference != null) {
295 $(outsideReference).removeClass('track-slider-playing');
296 }
297 };
298 this.stopPlayback = function()
299 {
300 // Called when playback has stopped. This gets called even if playback never started!
301 $(this.holder).removeClass('track-slider-playing');
302 };
303 this.getValue = function()
304 {
305 // Return the current value of the object. If there is no value, return 0
306 return this.slider.value;
307 };
308 this.getPresentedId = function()
309 {
310 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
311 return this.title.textContent;
312 };
313 this.canMove = function()
314 {
315 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale.
316 // These are checked primarily if the interface check option 'fragmentMoved' is enabled.
317 return true;
318 };
319 this.exportXMLDOM = function(audioObject) {
320 // Called by the audioObject holding this element to export the interface <value> node.
321 // If there is no value node (such as outside reference), return null
322 // 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
323 // Use storage.document.createElement('value'); to generate the XML node.
324 var node = storage.document.createElement('value');
325 node.textContent = this.slider.value;
326 return node;
327 };
328 };
329
330 function outsideReferenceDOM(audioObject,index,inject)
331 {
332 this.parent = audioObject;
333 this.outsideReferenceHolder = document.createElement('button');
334 this.outsideReferenceHolder.id = 'outside-reference';
335 this.outsideReferenceHolder.className = 'outside-reference';
336 this.outsideReferenceHolder.setAttribute('track-id',index);
337 this.outsideReferenceHolder.textContent = "Play Reference";
338 this.outsideReferenceHolder.disabled = true;
339
340 this.outsideReferenceHolder.onclick = function(event)
341 {
342 audioEngineContext.play(event.currentTarget.getAttribute('track-id'));
343 };
344 inject.appendChild(this.outsideReferenceHolder);
345 this.enable = function()
346 {
347 if (this.parent.state == 1)
348 {
349 this.outsideReferenceHolder.disabled = false;
350 }
351 };
352 this.updateLoading = function(progress)
353 {
354 if (progress != 100)
355 {
356 progress = String(progress);
357 progress = progress.split('.')[0];
358 this.outsideReferenceHolder[0].children[0].textContent = progress+'%';
359 } else {
360 this.outsideReferenceHolder[0].children[0].textContent = "Play Reference";
361 }
362 };
363 this.startPlayback = function()
364 {
365 // Called when playback has begun
366 $('.track-slider').removeClass('track-slider-playing');
367 $('.comment-div').removeClass('comment-box-playing');
368 $(this.outsideReferenceHolder).addClass('track-slider-playing');
369 };
370 this.stopPlayback = function()
371 {
372 // Called when playback has stopped. This gets called even if playback never started!
373 $(this.outsideReferenceHolder).removeClass('track-slider-playing');
374 };
375 this.exportXMLDOM = function(audioObject)
376 {
377 return null;
378 };
379 this.getValue = function()
380 {
381 return 0;
382 };
383 this.getPresentedId = function()
384 {
385 return 'reference';
386 };
387 this.canMove = function()
388 {
389 return false;
390 };
391 }
392
393 function resizeWindow(event)
394 {
395 // Called on every window resize event, use this to scale your page properly
396
397 var numObj = document.getElementsByClassName('track-slider').length;
398 var totalHeight = (numObj * 125)-25;
399 document.getElementById('scale-holder').style.width = window.innerWidth-220 + 'px';
400 var canvas = document.getElementById('scale-canvas');
401 canvas.width = window.innerWidth-420;
402 canvas.height = totalHeight;
403 for (var i in audioEngineContext.audioObjects)
404 {
405 if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference'){
406 audioEngineContext.audioObjects[i].interfaceDOM.resize(event);
407 }
408 }
409 document.getElementById("slider").style.height = totalHeight+50+'px';
410 drawScale();
411 }
412
413 function drawScale()
414 {
415 var interfaceObj = testState.currentStateMap.interfaces[0];
416 var scales = testState.currentStateMap.interfaces[0].scales;
417 scales = scales.sort(function(a,b) {
418 return a.position - b.position;
419 });
420 var canvas = document.getElementById('scale-canvas');
421 var ctx = canvas.getContext("2d");
422 var height = canvas.height;
423 var width = canvas.width;
424 var textHolder = document.getElementById('scale-text-holder');
425 textHolder.innerHTML = null;
426 ctx.fillStyle = "#000000";
427 ctx.setLineDash([1,4]);
428 for (var scale of scales)
429 {
430 var posPercent = scale.position / 100.0;
431 var posPix = Math.round(width * posPercent);
432 if(posPix<=0){posPix=1;}
433 if(posPix>=width){posPix=width-1;}
434 ctx.moveTo(posPix,0);
435 ctx.lineTo(posPix,height);
436 ctx.stroke();
437
438 var text = document.createElement('div');
439 text.align = "center";
440 var textC = document.createElement('span');
441 textC.textContent = scale.text;
442 text.appendChild(textC);
443 text.className = "scale-text";
444 textHolder.appendChild(text);
445 text.style.width = Math.ceil($(text).width())+'px';
446 text.style.left = (posPix+100-($(text).width()/2)) +'px';
447 }
448 }
449
450 function buttonSubmitClick() // TODO: Only when all songs have been played!
451 {
452 var checks = [];
453 checks = checks.concat(testState.currentStateMap.interfaces[0].options);
454 checks = checks.concat(specification.interfaces.options);
455 var canContinue = true;
456
457 // Check that the anchor and reference objects are correctly placed
458 if (interfaceContext.checkHiddenAnchor() == false) {return;}
459 if (interfaceContext.checkHiddenReference() == false) {return;}
460
461 for (var i=0; i<checks.length; i++) {
462 if (checks[i].type == 'check')
463 {
464 switch(checks[i].name) {
465 case 'fragmentPlayed':
466 // Check if all fragments have been played
467 var checkState = interfaceContext.checkAllPlayed();
468 if (checkState == false) {canContinue = false;}
469 break;
470 case 'fragmentFullPlayback':
471 // Check all fragments have been played to their full length
472 var checkState = interfaceContext.checkAllPlayed();
473 if (checkState == false) {canContinue = false;}
474 console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
475 break;
476 case 'fragmentMoved':
477 // Check all fragment sliders have been moved.
478 var checkState = interfaceContext.checkAllMoved();
479 if (checkState == false) {canContinue = false;}
480 break;
481 case 'fragmentComments':
482 // Check all fragment sliders have been moved.
483 var checkState = interfaceContext.checkAllCommented();
484 if (checkState == false) {canContinue = false;}
485 break;
486 //case 'scalerange':
487 // Check the scale is used to its full width outlined by the node
488 //var checkState = interfaceContext.checkScaleRange();
489 //if (checkState == false) {canContinue = false;}
490 // break;
491 default:
492 console.log("WARNING - Check option "+checks[i].check+" is not supported on this interface");
493 break;
494 }
495
496 }
497 if (!canContinue) {break;}
498 }
499
500 if (canContinue) {
501 if (audioEngineContext.status == 1) {
502 var playback = document.getElementById('playback-button');
503 playback.click();
504 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
505 } else
506 {
507 if (audioEngineContext.timer.testStarted == false)
508 {
509 alert('You have not started the test! Please press start to begin the test!');
510 return;
511 }
512 }
513 testState.advanceState();
514 }
515 }
516
517 function pageXMLSave(store, pageSpecification)
518 {
519 // MANDATORY
520 // Saves a specific test page
521 // You can use this space to add any extra nodes to your XML <audioHolder> saves
522 // Get the current <page> information in store (remember to appendChild your data to it)
523 // pageSpecification is the current page node configuration
524 // To create new XML nodes, use storage.document.createElement();
525 }