comparison interfaces/discrete.js @ 1316:279930a008ca

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 <nickjillings@users.noreply.github.com>
date Fri, 29 Jan 2016 11:11:57 +0000
parents
children c0022a09c4f6
comparison
equal deleted inserted replaced
-1:000000000000 1316:279930a008ca
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 = "150px";
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 var interfaceObj = page.interfaces;
118 if (interfaceObj.length > 1)
119 {
120 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
121 }
122 interfaceObj = interfaceObj[0];
123 if(interfaceObj.title != null)
124 {
125 document.getElementById("pageTitle").textContent = interfaceObj.title;
126 }
127
128 var interfaceOptions = specification.interfaces.options.concat(interfaceObj.options);
129 for (var option of interfaceOptions)
130 {
131 if (option.type == "show")
132 {
133 switch(option.name) {
134 case "playhead":
135 var playbackHolder = document.getElementById('playback-holder');
136 if (playbackHolder == null)
137 {
138 playbackHolder = document.createElement('div');
139 playbackHolder.style.width = "100%";
140 playbackHolder.align = 'center';
141 playbackHolder.appendChild(interfaceContext.playhead.object);
142 feedbackHolder.appendChild(playbackHolder);
143 }
144 break;
145 case "page-count":
146 var pagecountHolder = document.getElementById('page-count');
147 if (pagecountHolder == null)
148 {
149 pagecountHolder = document.createElement('div');
150 pagecountHolder.id = 'page-count';
151 }
152 pagecountHolder.innerHTML = '<span>Page '+(page.presentedId+1)+' of '+specification.pages.length+'</span>';
153 var inject = document.getElementById('interface-buttons');
154 inject.appendChild(pagecountHolder);
155 break;
156 case "volume":
157 if (document.getElementById('master-volume-holder') == null)
158 {
159 feedbackHolder.appendChild(interfaceContext.volume.object);
160 }
161 break;
162 }
163 }
164 }
165
166 // Delete outside reference
167 var outsideReferenceHolder = document.getElementById('outside-reference');
168 if (outsideReferenceHolder != null) {
169 document.getElementById('interface-buttons').removeChild(outsideReferenceHolder);
170 }
171
172 var sliderBox = document.getElementById('slider-holder');
173 sliderBox.innerHTML = null;
174
175 var commentBoxPrefix = "Comment on track";
176 if (interfaceObj.commentBoxPrefix != undefined) {
177 commentBoxPrefix = interfaceObj.commentBoxPrefix;
178 }
179 var loopPlayback = page.loop;
180
181 $(page.commentQuestions).each(function(index,element) {
182 var node = interfaceContext.createCommentQuestion(element);
183 feedbackHolder.appendChild(node.holder);
184 });
185
186 // Find all the audioElements from the audioHolder
187 var label = 0;
188 var interfaceScales = testState.currentStateMap.interfaces[0].scales;
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 discreteObject(audioObject,label,interfaceScales);
202 sliderBox.appendChild(sliderObj.holder);
203 audioObject.bindInterface(sliderObj);
204 interfaceContext.createCommentBox(audioObject);
205 label += 1;
206 }
207
208 });
209
210 if (page.showElementComments)
211 {
212 interfaceContext.showCommentBoxes(feedbackHolder,true);
213 }
214
215 // Auto-align
216 resizeWindow(null);
217 }
218
219 function discreteObject(audioObject,label,interfaceScales)
220 {
221 // An example node, you can make this however you want for each audioElement.
222 // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following
223 // You attach them by calling audioObject.bindInterface( )
224 if (interfaceScales == null || interfaceScales.length == 0)
225 {
226 console.log("WARNING: The discrete radio's are built depending on the number of scale points specified! Ensure you have some specified. Defaulting to 5 for now!");
227 numOptions = 5;
228 }
229 this.parent = audioObject;
230
231 this.holder = document.createElement('div');
232 this.title = document.createElement('div');
233 this.discreteHolder = document.createElement('div');
234 this.discretes = [];
235 this.play = document.createElement('button');
236
237 this.holder.className = 'track-slider';
238 this.holder.style.width = window.innerWidth-200 + 'px';
239 this.holder.appendChild(this.title);
240 this.holder.appendChild(this.discreteHolder);
241 this.holder.appendChild(this.play);
242 this.holder.setAttribute('trackIndex',audioObject.id);
243 this.title.textContent = label;
244 this.title.className = 'track-slider-title';
245
246 this.discreteHolder.className = "track-slider-range";
247 this.discreteHolder.style.width = window.innerWidth-500 + 'px';
248 for (var i=0; i<interfaceScales.length; i++)
249 {
250 var node = document.createElement('input');
251 node.setAttribute('type','radio');
252 node.className = 'track-radio';
253 node.disabled = true;
254 node.setAttribute('position',interfaceScales[i].position);
255 node.setAttribute('name',audioObject.specification.id);
256 node.setAttribute('id',audioObject.specification.id+'-'+String(i));
257 this.discretes.push(node);
258 this.discreteHolder.appendChild(node);
259 node.onclick = function(event)
260 {
261 if (audioEngineContext.status == 0)
262 {
263 event.currentTarget.checked = false;
264 return;
265 }
266 var time = audioEngineContext.timer.getTestTime();
267 var id = Number(event.currentTarget.parentNode.parentNode.getAttribute('trackIndex'));
268 var value = event.currentTarget.getAttribute('position') / 100.0;
269 audioEngineContext.audioObjects[id].metric.moved(time,value);
270 console.log('slider '+id+' moved to '+value+' ('+time+')');
271 };
272 }
273
274 this.play.className = 'track-slider-button';
275 this.play.textContent = "Loading...";
276 this.play.value = audioObject.id;
277 this.play.disabled = true;
278 this.play.onclick = function(event)
279 {
280 var id = Number(event.currentTarget.value);
281 //audioEngineContext.metric.sliderPlayed(id);
282 audioEngineContext.play(id);
283 };
284 this.resize = function(event)
285 {
286 this.holder.style.width = window.innerWidth-200 + 'px';
287 this.discreteHolder.style.width = window.innerWidth-500 + 'px';
288 //text.style.left = (posPix+150-($(text).width()/2)) +'px';
289 for (var i=0; i<this.discretes.length; i++)
290 {
291 var width = $(this.discreteHolder).width() - 20;
292 var node = this.discretes[i];
293 var nodeW = $(node).width();
294 var position = node.getAttribute('position');
295 var posPix = Math.round(width * (position / 100.0));
296 node.style.left = (posPix+10 - (nodeW/2)) + 'px';
297 }
298 };
299 this.enable = function()
300 {
301 // This is used to tell the interface object that playback of this node is ready
302 this.play.disabled = false;
303 this.play.textContent = "Play";
304 $(this.slider).removeClass('track-slider-disabled');
305 for (var radio of this.discretes)
306 {
307 radio.disabled = false;
308 }
309 };
310 this.updateLoading = function(progress)
311 {
312 // progress is a value from 0 to 100 indicating the current download state of media files
313 if (progress != 100)
314 {
315 progress = String(progress);
316 progress = progress.split('.')[0];
317 this.play.textContent = progress+'%';
318 } else {
319 this.play.textContent = "Play";
320 }
321 };
322
323 this.startPlayback = function()
324 {
325 // Called by audioObject when playback begins
326 $(".track-slider").removeClass('track-slider-playing');
327 $(this.holder).addClass('track-slider-playing');
328 var outsideReference = document.getElementById('outside-reference');
329 this.play.textContent = "Listening";
330 if (outsideReference != null) {
331 $(outsideReference).removeClass('track-slider-playing');
332 }
333 }
334 this.stopPlayback = function()
335 {
336 // Called by audioObject when playback stops
337 $(this.holder).removeClass('track-slider-playing');
338 this.play.textContent = "Play";
339 }
340
341 this.getValue = function()
342 {
343 // Return the current value of the object. If there is no value, return -1
344 var value = -1;
345 for (var i=0; i<this.discretes.length; i++)
346 {
347 if (this.discretes[i].checked == true)
348 {
349 value = this.discretes[i].getAttribute('position') / 100.0;
350 break;
351 }
352 }
353 return value;
354 };
355 this.getPresentedId = function()
356 {
357 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
358 return this.title.textContent;
359 };
360 this.canMove = function()
361 {
362 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale.
363 // These are checked primarily if the interface check option 'fragmentMoved' is enabled.
364 return true;
365 };
366 this.exportXMLDOM = function(audioObject) {
367 // Called by the audioObject holding this element to export the interface <value> node.
368 // If there is no value node (such as outside reference), return null
369 // 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
370 // Use storage.document.createElement('value'); to generate the XML node.
371 var node = storage.document.createElement('value');
372 node.textContent = this.getValue();
373 return node;
374 };
375 };
376
377 function resizeWindow(event)
378 {
379 // Called on every window resize event, use this to scale your page properly
380 var numObj = document.getElementsByClassName('track-slider').length;
381 var totalHeight = (numObj * 66)-30;
382 document.getElementById('scale-holder').style.width = window.innerWidth-220 + 'px';
383 var canvas = document.getElementById('scale-canvas');
384 canvas.width = window.innerWidth-520;
385 canvas.height = totalHeight;
386 for (var i in audioEngineContext.audioObjects)
387 {
388 if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference'){
389 audioEngineContext.audioObjects[i].interfaceDOM.resize(event);
390 }
391 }
392 document.getElementById('slider-holder').style.height = totalHeight + 'px';
393 document.getElementById('slider').style.height = totalHeight + 70 + 'px';
394 drawScale();
395 }
396
397 function drawScale()
398 {
399 var interfaceObj = testState.currentStateMap.interfaces[0];
400 var scales = testState.currentStateMap.interfaces[0].scales;
401 scales = scales.sort(function(a,b) {
402 return a.position - b.position;
403 });
404 var canvas = document.getElementById('scale-canvas');
405 var ctx = canvas.getContext("2d");
406 var height = canvas.height;
407 var width = canvas.width;
408 var textHolder = document.getElementById('scale-text-holder');
409 textHolder.innerHTML = null;
410 ctx.fillStyle = "#000000";
411 ctx.setLineDash([1,4]);
412 for (var scale of scales)
413 {
414 var posPercent = scale.position / 100.0;
415 var posPix = Math.round(width * posPercent);
416 if(posPix<=0){posPix=1;}
417 if(posPix>=width){posPix=width-1;}
418 ctx.moveTo(posPix,0);
419 ctx.lineTo(posPix,height);
420 ctx.stroke();
421
422 var text = document.createElement('div');
423 text.align = "center";
424 var textC = document.createElement('span');
425 textC.textContent = scale.text;
426 text.appendChild(textC);
427 text.className = "scale-text";
428 textHolder.appendChild(text);
429 text.style.width = $(text.children[0]).width()+'px';
430 text.style.left = (posPix+150-($(text).width()/2)) +'px';
431 }
432 }
433
434 function buttonSubmitClick() // TODO: Only when all songs have been played!
435 {
436 var checks = [];
437 checks = checks.concat(testState.currentStateMap.interfaces[0].options);
438 checks = checks.concat(specification.interfaces.options);
439 var canContinue = true;
440
441 // Check that the anchor and reference objects are correctly placed
442 if (interfaceContext.checkHiddenAnchor() == false) {return;}
443 if (interfaceContext.checkHiddenReference() == false) {return;}
444
445 for (var i=0; i<checks.length; i++) {
446 if (checks[i].type == 'check')
447 {
448 switch(checks[i].name) {
449 case 'fragmentPlayed':
450 // Check if all fragments have been played
451 var checkState = interfaceContext.checkAllPlayed();
452 if (checkState == false) {canContinue = false;}
453 break;
454 case 'fragmentFullPlayback':
455 // Check all fragments have been played to their full length
456 var checkState = interfaceContext.checkAllPlayed();
457 if (checkState == false) {canContinue = false;}
458 console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
459 break;
460 case 'fragmentMoved':
461 // Check all fragment sliders have been moved.
462 var checkState = interfaceContext.checkAllMoved();
463 if (checkState == false) {canContinue = false;}
464 break;
465 case 'fragmentComments':
466 // Check all fragment sliders have been moved.
467 var checkState = interfaceContext.checkAllCommented();
468 if (checkState == false) {canContinue = false;}
469 break;
470 //case 'scalerange':
471 // Check the scale is used to its full width outlined by the node
472 //var checkState = interfaceContext.checkScaleRange();
473 //if (checkState == false) {canContinue = false;}
474 // break;
475 default:
476 console.log("WARNING - Check option "+checks[i].check+" is not supported on this interface");
477 break;
478 }
479
480 }
481 if (!canContinue) {break;}
482 }
483
484 if (canContinue) {
485 if (audioEngineContext.status == 1) {
486 var playback = document.getElementById('playback-button');
487 playback.click();
488 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
489 } else
490 {
491 if (audioEngineContext.timer.testStarted == false)
492 {
493 alert('You have not started the test! Please press start to begin the test!');
494 return;
495 }
496 }
497 testState.advanceState();
498 }
499 }
500
501 function pageXMLSave(store, pageSpecification)
502 {
503 // MANDATORY
504 // Saves a specific test page
505 // You can use this space to add any extra nodes to your XML <audioHolder> saves
506 // Get the current <page> information in store (remember to appendChild your data to it)
507 // pageSpecification is the current page node configuration
508 // To create new XML nodes, use storage.document.createElement();
509 }