annotate interfaces/horizontal-sliders.js @ 496:cb348f6208b2 Dev_main

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