annotate js/core.js @ 3102:972b55d81c1b

#171 Completed
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Thu, 18 Jan 2018 10:12:12 +0000
parents cd34732a2e39
children 7820ce4a96c0
rev   line source
nicholas@2224 1 /**
nicholas@2224 2 * core.js
nicholas@2224 3 *
nicholas@2224 4 * Main script to run, calls all other core functions and manages loading/store to backend.
nicholas@2224 5 * Also contains all global variables.
nicholas@2224 6 */
nicholas@2224 7
nicholas@2708 8 /*globals window, document, XMLDocument, Element, XMLHttpRequest, DOMParser, console, Blob, $, Promise, navigator */
nicholas@2708 9 /*globals AudioBuffer, AudioBufferSourceNode */
nicholas@2708 10 /*globals Specification, calculateLoudness, WAVE, validateXML, showdown, pageXMLSave, loadTest, resizeWindow */
nicholas@2708 11
nicholas@2224 12 /* create the web audio API context and store in audioContext*/
nicholas@2224 13 var audioContext; // Hold the browser web audio API
nicholas@2224 14 var projectXML; // Hold the parsed setup XML
nicholas@2224 15 var schemaXSD; // Hold the parsed schema XSD
nicholas@2224 16 var specification;
nicholas@2224 17 var interfaceContext;
nicholas@2224 18 var storage;
nicholas@2224 19 var popup; // Hold the interfacePopup object
nicholas@2224 20 var testState;
nicholas@2224 21 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
nicholas@2224 22 var audioEngineContext; // The custome AudioEngine object
nicholas@2329 23 var gReturnURL;
giuliomoro@2337 24 var gSaveFilenamePrefix;
nicholas@2224 25
nicholas@2224 26
nicholas@2224 27 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it
nicholas@2224 28 AudioBufferSourceNode.prototype.owner = undefined;
nicholas@2224 29 // Add a prototype to the bufferSourceNode to hold when the object was given a play command
nicholas@2224 30 AudioBufferSourceNode.prototype.playbackStartTime = undefined;
nicholas@2224 31 // Add a prototype to the bufferNode to hold the desired LINEAR gain
nicholas@2224 32 AudioBuffer.prototype.playbackGain = undefined;
nicholas@2224 33 // Add a prototype to the bufferNode to hold the computed LUFS loudness
nicholas@2224 34 AudioBuffer.prototype.lufs = undefined;
nicholas@2224 35
nicholas@2224 36 // Convert relative URLs into absolutes
nicholas@2224 37 function escapeHTML(s) {
nicholas@2224 38 return s.split('&').join('&amp;').split('<').join('&lt;').split('"').join('&quot;');
nicholas@2224 39 }
nicholas@2498 40
nicholas@2224 41 function qualifyURL(url) {
nicholas@2498 42 var el = document.createElement('div');
nicholas@2498 43 el.innerHTML = '<a href="' + escapeHTML(url) + '">x</a>';
nicholas@2224 44 return el.firstChild.href;
nicholas@2224 45 }
nicholas@2224 46
nicholas@2224 47 // Firefox does not have an XMLDocument.prototype.getElementsByName
nicholas@2224 48 // and there is no searchAll style command, this custom function will
nicholas@2224 49 // search all children recusrively for the name. Used for XSD where all
nicholas@2224 50 // element nodes must have a name and therefore can pull the schema node
nicholas@2498 51 XMLDocument.prototype.getAllElementsByName = function (name) {
nicholas@2224 52 name = String(name);
nicholas@2224 53 var selected = this.documentElement.getAllElementsByName(name);
nicholas@2224 54 return selected;
nicholas@2708 55 };
nicholas@2224 56
nicholas@2498 57 Element.prototype.getAllElementsByName = function (name) {
nicholas@2224 58 name = String(name);
nicholas@2224 59 var selected = [];
nicholas@2224 60 var node = this.firstElementChild;
nicholas@2708 61 while (node !== null) {
nicholas@2498 62 if (node.getAttribute('name') == name) {
nicholas@2224 63 selected.push(node);
nicholas@2224 64 }
nicholas@2498 65 if (node.childElementCount > 0) {
nicholas@2224 66 selected = selected.concat(node.getAllElementsByName(name));
nicholas@2224 67 }
nicholas@2224 68 node = node.nextElementSibling;
nicholas@2224 69 }
nicholas@2224 70 return selected;
nicholas@2708 71 };
nicholas@2224 72
nicholas@2498 73 XMLDocument.prototype.getAllElementsByTagName = function (name) {
nicholas@2224 74 name = String(name);
nicholas@2224 75 var selected = this.documentElement.getAllElementsByTagName(name);
nicholas@2224 76 return selected;
nicholas@2708 77 };
nicholas@2224 78
nicholas@2498 79 Element.prototype.getAllElementsByTagName = function (name) {
nicholas@2224 80 name = String(name);
nicholas@2224 81 var selected = [];
nicholas@2224 82 var node = this.firstElementChild;
nicholas@2708 83 while (node !== null) {
nicholas@2498 84 if (node.nodeName == name) {
nicholas@2224 85 selected.push(node);
nicholas@2224 86 }
nicholas@2498 87 if (node.childElementCount > 0) {
nicholas@2224 88 selected = selected.concat(node.getAllElementsByTagName(name));
nicholas@2224 89 }
nicholas@2224 90 node = node.nextElementSibling;
nicholas@2224 91 }
nicholas@2224 92 return selected;
nicholas@2708 93 };
nicholas@2224 94
nicholas@2224 95 // Firefox does not have an XMLDocument.prototype.getElementsByName
nicholas@2224 96 if (typeof XMLDocument.prototype.getElementsByName != "function") {
nicholas@2498 97 XMLDocument.prototype.getElementsByName = function (name) {
nicholas@2224 98 name = String(name);
nicholas@2224 99 var node = this.documentElement.firstElementChild;
nicholas@2224 100 var selected = [];
nicholas@2708 101 while (node !== null) {
nicholas@2498 102 if (node.getAttribute('name') == name) {
nicholas@2224 103 selected.push(node);
nicholas@2224 104 }
nicholas@2224 105 node = node.nextElementSibling;
nicholas@2224 106 }
nicholas@2224 107 return selected;
nicholas@2708 108 };
nicholas@2224 109 }
nicholas@2224 110
nicholas@2498 111 var check_dependancies = function () {
nicholas@2401 112 // This will check for the data dependancies
nicholas@2498 113 if (typeof (jQuery) != "function") {
nicholas@2498 114 return false;
nicholas@2498 115 }
nicholas@2498 116 if (typeof (Specification) != "function") {
nicholas@2498 117 return false;
nicholas@2498 118 }
nicholas@2498 119 if (typeof (calculateLoudness) != "function") {
nicholas@2498 120 return false;
nicholas@2498 121 }
nicholas@2498 122 if (typeof (WAVE) != "function") {
nicholas@2498 123 return false;
nicholas@2498 124 }
nicholas@2498 125 if (typeof (validateXML) != "function") {
nicholas@2498 126 return false;
nicholas@2498 127 }
nicholas@2401 128 return true;
nicholas@2708 129 };
nicholas@2401 130
nicholas@2498 131 var onload = function () {
nicholas@2498 132 // Function called once the browser has loaded all files.
nicholas@2498 133 // This should perform any initial commands such as structure / loading documents
nicholas@2498 134
nicholas@2498 135 // Create a web audio API context
nicholas@2498 136 // Fixed for cross-browser support
nicholas@2498 137 var AudioContext = window.AudioContext || window.webkitAudioContext;
nicholas@2708 138 audioContext = new AudioContext();
nicholas@2498 139
nicholas@2498 140 // Create test state
nicholas@2498 141 testState = new stateMachine();
nicholas@2498 142
nicholas@2498 143 // Create the popup interface object
nicholas@2498 144 popup = new interfacePopup();
nicholas@2498 145
nicholas@2224 146 // Create the specification object
nicholas@2498 147 specification = new Specification();
nicholas@2498 148
nicholas@3101 149 // Create the storage object
nicholas@3101 150 storage = new Storage();
nicholas@3101 151
nicholas@2498 152 // Create the interface object
nicholas@2498 153 interfaceContext = new Interface(specification);
nicholas@2498 154
nicholas@2498 155 // Define window callbacks for interface
nicholas@2498 156 window.onresize = function (event) {
nicholas@2498 157 interfaceContext.resizeWindow(event);
nicholas@2498 158 };
nicholas@2498 159
nicholas@2708 160 if (window.location.search.length !== 0) {
nicholas@2319 161 var search = window.location.search.split('?')[1];
nicholas@2319 162 // Now split the requests into pairs
nicholas@2319 163 var searchQueries = search.split('&');
nicholas@2708 164 var url;
nicholas@2498 165 for (var i in searchQueries) {
giuliomoro@2331 166 // Split each key-value pair
nicholas@2319 167 searchQueries[i] = searchQueries[i].split('=');
giuliomoro@2331 168 var key = searchQueries[i][0];
giuliomoro@2331 169 var value = decodeURIComponent(searchQueries[i][1]);
nicholas@2498 170 switch (key) {
nicholas@2498 171 case "url":
nicholas@2498 172 url = value;
nicholas@2708 173 specification.url = url;
nicholas@2498 174 break;
nicholas@2498 175 case "returnURL":
nicholas@2498 176 gReturnURL = value;
nicholas@2498 177 break;
nicholas@2498 178 case "saveFilenamePrefix":
nicholas@2722 179 storage.filenamePrefix = value;
nicholas@2498 180 break;
nicholas@2319 181 }
nicholas@2319 182 }
nicholas@2319 183 loadProjectSpec(url);
nicholas@2498 184 window.onbeforeunload = function () {
nicholas@2319 185 return "Please only leave this page once you have completed the tests. Are you sure you have completed all testing?";
nicholas@2319 186 };
nicholas@2319 187 }
nicholas@2360 188 interfaceContext.lightbox.resize();
nicholas@2224 189 };
nicholas@2224 190
nicholas@2224 191 function loadProjectSpec(url) {
nicholas@2498 192 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
nicholas@2498 193 // If url is null, request client to upload project XML document
nicholas@2498 194 var xmlhttp = new XMLHttpRequest();
nicholas@2498 195 xmlhttp.open("GET", 'xml/test-schema.xsd', true);
nicholas@2498 196 xmlhttp.onload = function () {
nicholas@2708 197 specification.processSchema(xmlhttp.response);
nicholas@2498 198 var r = new XMLHttpRequest();
nicholas@2498 199 r.open('GET', url, true);
nicholas@2498 200 r.onload = function () {
nicholas@2498 201 loadProjectSpecCallback(r.response);
nicholas@2498 202 };
nicholas@2498 203 r.onerror = function () {
nicholas@2224 204 document.getElementsByTagName('body')[0].innerHTML = null;
nicholas@2224 205 var msg = document.createElement("h3");
nicholas@2224 206 msg.textContent = "FATAL ERROR";
nicholas@2224 207 var span = document.createElement("p");
nicholas@2224 208 span.textContent = "There was an error when loading your XML file. Please check your path in the URL. After the path to this page, there should be '?url=path/to/your/file.xml'. Check the spelling of your filename as well. If you are still having issues, check the log of the python server or your webserver distribution for 404 codes for your file.";
nicholas@2224 209 document.getElementsByTagName('body')[0].appendChild(msg);
nicholas@2224 210 document.getElementsByTagName('body')[0].appendChild(span);
nicholas@2708 211 };
nicholas@2498 212 r.send();
nicholas@2498 213 };
nicholas@2498 214 xmlhttp.send();
nicholas@2708 215 }
nicholas@2224 216
nicholas@2224 217 function loadProjectSpecCallback(response) {
nicholas@2498 218 // Function called after asynchronous download of XML project specification
nicholas@2498 219 //var decode = $.parseXML(response);
nicholas@2498 220 //projectXML = $(decode);
nicholas@2498 221
nicholas@2224 222 // Check if XML is new or a resumption
nicholas@2224 223 var parse = new DOMParser();
nicholas@2498 224 var responseDocument = parse.parseFromString(response, 'text/xml');
nicholas@2224 225 var errorNode = responseDocument.getElementsByTagName('parsererror');
nicholas@2708 226 var msg, span;
nicholas@2498 227 if (errorNode.length >= 1) {
nicholas@2708 228 msg = document.createElement("h3");
nicholas@2498 229 msg.textContent = "FATAL ERROR";
nicholas@2708 230 span = document.createElement("span");
nicholas@2498 231 span.textContent = "The XML parser returned the following errors when decoding your XML file";
nicholas@2498 232 document.getElementsByTagName('body')[0].innerHTML = null;
nicholas@2498 233 document.getElementsByTagName('body')[0].appendChild(msg);
nicholas@2498 234 document.getElementsByTagName('body')[0].appendChild(span);
nicholas@2498 235 document.getElementsByTagName('body')[0].appendChild(errorNode[0]);
nicholas@2498 236 return;
nicholas@2498 237 }
nicholas@2708 238 if (responseDocument === undefined || responseDocument.firstChild === undefined) {
nicholas@2708 239 msg = document.createElement("h3");
nicholas@2498 240 msg.textContent = "FATAL ERROR";
nicholas@2708 241 span = document.createElement("span");
nicholas@2498 242 span.textContent = "The project XML was not decoded properly, try refreshing your browser and clearing caches. If the problem persists, contact the test creator.";
nicholas@2498 243 document.getElementsByTagName('body')[0].innerHTML = null;
nicholas@2498 244 document.getElementsByTagName('body')[0].appendChild(msg);
nicholas@2498 245 document.getElementsByTagName('body')[0].appendChild(span);
nicholas@2498 246 return;
nicholas@2224 247 }
nicholas@2247 248 if (responseDocument.firstChild.nodeName == "waet") {
nicholas@2224 249 // document is a specification
nicholas@2498 250
nicholas@2224 251 // Perform XML schema validation
nicholas@2224 252 var Module = {
nicholas@2224 253 xml: response,
nicholas@2708 254 schema: specification.getSchemaString(),
nicholas@2498 255 arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml']
nicholas@2224 256 };
nicholas@2498 257 projectXML = responseDocument;
nicholas@2224 258 var xmllint = validateXML(Module);
nicholas@2224 259 console.log(xmllint);
nicholas@2498 260 if (xmllint != 'document.xml validates\n') {
nicholas@2224 261 document.getElementsByTagName('body')[0].innerHTML = null;
nicholas@2708 262 msg = document.createElement("h3");
nicholas@2224 263 msg.textContent = "FATAL ERROR";
nicholas@2708 264 span = document.createElement("h4");
nicholas@2224 265 span.textContent = "The XML validator returned the following errors when decoding your XML file";
nicholas@2224 266 document.getElementsByTagName('body')[0].appendChild(msg);
nicholas@2224 267 document.getElementsByTagName('body')[0].appendChild(span);
nicholas@2224 268 xmllint = xmllint.split('\n');
nicholas@2498 269 for (var i in xmllint) {
nicholas@2224 270 document.getElementsByTagName('body')[0].appendChild(document.createElement('br'));
nicholas@2708 271 span = document.createElement("span");
nicholas@2224 272 span.textContent = xmllint[i];
nicholas@2224 273 document.getElementsByTagName('body')[0].appendChild(span);
nicholas@2224 274 }
nicholas@2224 275 return;
nicholas@2224 276 }
nicholas@2224 277 // Build the specification
nicholas@2498 278 specification.decode(projectXML);
nicholas@2224 279 // Generate the session-key
nicholas@2224 280 storage.initialise();
nicholas@2498 281
nicholas@2247 282 } else if (responseDocument.firstChild.nodeName == "waetresult") {
nicholas@2224 283 // document is a result
nicholas@2498 284 projectXML = document.implementation.createDocument(null, "waet");
nicholas@2294 285 projectXML.firstChild.appendChild(responseDocument.getElementsByTagName('waet')[0].getElementsByTagName("setup")[0].cloneNode(true));
nicholas@2708 286 var child = responseDocument.firstChild.firstChild,
nicholas@2708 287 copy;
nicholas@2708 288 while (child !== null) {
nicholas@2224 289 if (child.nodeName == "survey") {
nicholas@2224 290 // One of the global survey elements
nicholas@2224 291 if (child.getAttribute("state") == "complete") {
nicholas@2224 292 // We need to remove this survey from <setup>
nicholas@2224 293 var location = child.getAttribute("location");
nicholas@2224 294 var globalSurveys = projectXML.getElementsByTagName("setup")[0].getElementsByTagName("survey")[0];
nicholas@2708 295 while (globalSurveys !== null) {
nicholas@2224 296 if (location == "pre" || location == "before") {
nicholas@2224 297 if (globalSurveys.getAttribute("location") == "pre" || globalSurveys.getAttribute("location") == "before") {
nicholas@2224 298 projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
nicholas@2224 299 break;
nicholas@2224 300 }
nicholas@2224 301 } else {
nicholas@2224 302 if (globalSurveys.getAttribute("location") == "post" || globalSurveys.getAttribute("location") == "after") {
nicholas@2224 303 projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
nicholas@2224 304 break;
nicholas@2224 305 }
nicholas@2224 306 }
nicholas@2224 307 globalSurveys = globalSurveys.nextElementSibling;
nicholas@2224 308 }
nicholas@2224 309 } else {
nicholas@2224 310 // We need to complete this, so it must be regenerated by store
nicholas@2708 311 copy = child;
nicholas@2224 312 child = child.previousElementSibling;
nicholas@2294 313 responseDocument.firstChild.removeChild(copy);
nicholas@2224 314 }
nicholas@2224 315 } else if (child.nodeName == "page") {
nicholas@2224 316 if (child.getAttribute("state") == "empty") {
nicholas@2224 317 // We need to complete this page
nicholas@2294 318 projectXML.firstChild.appendChild(responseDocument.getElementById(child.getAttribute("ref")).cloneNode(true));
nicholas@2708 319 copy = child;
nicholas@2224 320 child = child.previousElementSibling;
nicholas@2294 321 responseDocument.firstChild.removeChild(copy);
nicholas@2224 322 }
nicholas@2224 323 }
nicholas@2224 324 child = child.nextElementSibling;
nicholas@2224 325 }
nicholas@2224 326 // Build the specification
nicholas@2498 327 specification.decode(projectXML);
nicholas@2224 328 // Use the original
nicholas@2224 329 storage.initialise(responseDocument);
nicholas@2224 330 }
nicholas@2498 331 /// CHECK FOR SAMPLE RATE COMPATIBILITY
nicholas@2708 332 if (isFinite(specification.sampleRate)) {
nicholas@2498 333 if (Number(specification.sampleRate) != audioContext.sampleRate) {
nicholas@2498 334 var errStr = 'Sample rates do not match! Requested ' + Number(specification.sampleRate) + ', got ' + audioContext.sampleRate + '. Please set the sample rate to match before completing this test.';
nicholas@2498 335 interfaceContext.lightbox.post("Error", errStr);
nicholas@2498 336 return;
nicholas@2498 337 }
nicholas@2498 338 }
nicholas@2498 339
nicholas@2624 340 var getInterfaces = new XMLHttpRequest();
nicholas@2624 341 getInterfaces.open("GET", "interfaces/interfaces.json");
nicholas@2624 342 getInterfaces.onerror = function (e) {
nicholas@2624 343 throw (e);
nicholas@2708 344 };
nicholas@2624 345 getInterfaces.onload = function () {
nicholas@2624 346 if (getInterfaces.status !== 200) {
nicholas@2624 347 throw (new Error(getInterfaces.status));
nicholas@2624 348 }
nicholas@2624 349 // Get the current interface
nicholas@2624 350 var name = specification.interface,
nicholas@2624 351 head = document.getElementsByTagName("head")[0],
nicholas@2624 352 data = JSON.parse(getInterfaces.responseText),
nicholas@2624 353 interfaceObject = data.interfaces.find(function (e) {
nicholas@2624 354 return e.name == name;
nicholas@2624 355 });
nicholas@2624 356 if (!interfaceObject) {
nicholas@2624 357 throw ("Cannot load desired interface");
nicholas@2624 358 }
nicholas@2624 359 interfaceObject.scripts.forEach(function (v) {
nicholas@2624 360 var script = document.createElement("script");
nicholas@2624 361 script.setAttribute("type", "text/javascript");
nicholas@2624 362 script.setAttribute("src", v);
nicholas@2624 363 head.appendChild(script);
nicholas@2624 364 });
nicholas@2624 365 interfaceObject.css.forEach(function (v) {
nicholas@2624 366 var css = document.createElement("link");
nicholas@2624 367 css.setAttribute("rel", "stylesheet");
nicholas@2624 368 css.setAttribute("type", "text/css");
nicholas@2624 369 css.setAttribute("href", v);
nicholas@2624 370 head.appendChild(css);
nicholas@2624 371 });
nicholas@2708 372 };
nicholas@2624 373 getInterfaces.send();
nicholas@2498 374
nicholas@2708 375 if (gReturnURL !== undefined) {
nicholas@2498 376 console.log("returnURL Overide from " + specification.returnURL + " to " + gReturnURL);
nicholas@2329 377 specification.returnURL = gReturnURL;
nicholas@2329 378 }
nicholas@2708 379 if (gSaveFilenamePrefix !== undefined) {
giuliomoro@2337 380 specification.saveFilenamePrefix = gSaveFilenamePrefix;
giuliomoro@2337 381 }
nicholas@2498 382
nicholas@2498 383 // Create the audio engine object
nicholas@2498 384 audioEngineContext = new AudioEngine(specification);
nicholas@2224 385 }
nicholas@2224 386
nicholas@2224 387 function createProjectSave(destURL) {
nicholas@2224 388 // Clear the window.onbeforeunload
nicholas@2224 389 window.onbeforeunload = null;
nicholas@2498 390 // Save the data from interface into XML and send to destURL
nicholas@2498 391 // If destURL is null then download XML in client
nicholas@2498 392 // Now time to render file locally
nicholas@2733 393 var xmlDoc = storage.finish();
nicholas@2498 394 var parent = document.createElement("div");
nicholas@2498 395 parent.appendChild(xmlDoc);
nicholas@2498 396 var file = [parent.innerHTML];
nicholas@2498 397 if (destURL == "local") {
nicholas@2498 398 var bb = new Blob(file, {
nicholas@2498 399 type: 'application/xml'
nicholas@2498 400 });
nicholas@2498 401 var dnlk = window.URL.createObjectURL(bb);
nicholas@2498 402 var a = document.createElement("a");
nicholas@2498 403 a.hidden = '';
nicholas@2498 404 a.href = dnlk;
nicholas@2498 405 a.download = "save.xml";
nicholas@2498 406 a.textContent = "Save File";
nicholas@2498 407
nicholas@2498 408 popup.showPopup();
nicholas@2498 409 popup.popupContent.innerHTML = "<span>Please save the file below to give to your test supervisor</span><br>";
nicholas@2498 410 popup.popupContent.appendChild(a);
nicholas@2498 411 } else {
nicholas@2498 412 var projectReturn = "";
nicholas@2498 413 if (typeof specification.projectReturn == "string") {
nicholas@2498 414 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2498 415 projectReturn = specification.projectReturn;
nicholas@2498 416 }
nicholas@2498 417 }
nicholas@2723 418 storage.SessionKey.finish().then(function (resolved) {
nicholas@2723 419 if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) {
nicholas@2723 420 window.location = specification.returnURL;
nicholas@2723 421 } else {
nicholas@2723 422 popup.popupContent.textContent = specification.exitText;
nicholas@2723 423 }
nicholas@2723 424 }, function (message) {
nicholas@2723 425 console.log("Save: Error! " + message.textContent);
nicholas@2498 426 createProjectSave("local");
nicholas@2723 427 });
nicholas@2498 428 popup.showPopup();
nicholas@2498 429 popup.popupContent.innerHTML = null;
nicholas@2498 430 popup.popupContent.textContent = "Submitting. Please Wait";
nicholas@2498 431 if (typeof (popup.hideNextButton) === "function") {
nicholas@2498 432 popup.hideNextButton();
nicholas@2498 433 }
nicholas@2498 434 if (typeof (popup.hidePreviousButton) === "function") {
nicholas@2498 435 popup.hidePreviousButton();
nicholas@2498 436 }
nicholas@2498 437 }
nicholas@2224 438 }
nicholas@2224 439
nicholas@2498 440 function errorSessionDump(msg) {
nicholas@2498 441 // Create the partial interface XML save
nicholas@2498 442 // Include error node with message on why the dump occured
nicholas@2498 443 popup.showPopup();
nicholas@2498 444 popup.popupContent.innerHTML = null;
nicholas@2498 445 var err = document.createElement('error');
nicholas@2498 446 var parent = document.createElement("div");
nicholas@2498 447 if (typeof msg === "object") {
nicholas@2498 448 err.appendChild(msg);
nicholas@2498 449 popup.popupContent.appendChild(msg);
nicholas@2498 450
nicholas@2498 451 } else {
nicholas@2498 452 err.textContent = msg;
nicholas@2498 453 popup.popupContent.innerHTML = "ERROR : " + msg;
nicholas@2498 454 }
nicholas@2498 455 var xmlDoc = interfaceXMLSave();
nicholas@2498 456 xmlDoc.appendChild(err);
nicholas@2498 457 parent.appendChild(xmlDoc);
nicholas@2498 458 var file = [parent.innerHTML];
nicholas@2498 459 var bb = new Blob(file, {
nicholas@2498 460 type: 'application/xml'
nicholas@2498 461 });
nicholas@2498 462 var dnlk = window.URL.createObjectURL(bb);
nicholas@2498 463 var a = document.createElement("a");
nicholas@2498 464 a.hidden = '';
nicholas@2498 465 a.href = dnlk;
nicholas@2498 466 a.download = "save.xml";
nicholas@2498 467 a.textContent = "Save File";
nicholas@2498 468
nicholas@2498 469
nicholas@2498 470
nicholas@2498 471 popup.popupContent.appendChild(a);
nicholas@2224 472 }
nicholas@2224 473
nicholas@2224 474 // Only other global function which must be defined in the interface class. Determines how to create the XML document.
nicholas@2498 475 function interfaceXMLSave() {
nicholas@2498 476 // Create the XML string to be exported with results
nicholas@2498 477 return storage.finish();
nicholas@2224 478 }
nicholas@2224 479
nicholas@2498 480 function linearToDecibel(gain) {
nicholas@2498 481 return 20.0 * Math.log10(gain);
nicholas@2224 482 }
nicholas@2224 483
nicholas@2498 484 function decibelToLinear(gain) {
nicholas@2498 485 return Math.pow(10, gain / 20.0);
nicholas@2224 486 }
nicholas@2224 487
nicholas@2498 488 function secondsToSamples(time, fs) {
nicholas@2498 489 return Math.round(time * fs);
nicholas@2224 490 }
nicholas@2224 491
nicholas@2498 492 function samplesToSeconds(samples, fs) {
nicholas@2224 493 return samples / fs;
nicholas@2224 494 }
nicholas@2224 495
nicholas@2224 496 function randomString(length) {
nicholas@2708 497 var str = "";
nicholas@2498 498 for (var i = 0; i < length; i += 2) {
nicholas@2498 499 var num = Math.floor(Math.random() * 1295);
nicholas@2376 500 str += num.toString(36);
nicholas@2376 501 }
nicholas@2376 502 return str;
nicholas@2376 503 //return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
nicholas@2224 504 }
nicholas@2224 505
nicholas@2498 506 function randomiseOrder(input) {
nicholas@2498 507 // This takes an array of information and randomises the order
nicholas@2498 508 var N = input.length;
nicholas@2498 509
nicholas@2498 510 var inputSequence = []; // For safety purposes: keep track of randomisation
nicholas@2498 511 for (var counter = 0; counter < N; ++counter)
nicholas@2708 512 inputSequence.push(counter); // Fill array
nicholas@2498 513 var inputSequenceClone = inputSequence.slice(0);
nicholas@2498 514
nicholas@2498 515 var holdArr = [];
nicholas@2498 516 var outputSequence = [];
nicholas@2498 517 for (var n = 0; n < N; n++) {
nicholas@2498 518 // First pick a random number
nicholas@2498 519 var r = Math.random();
nicholas@2498 520 // Multiply and floor by the number of elements left
nicholas@2498 521 r = Math.floor(r * input.length);
nicholas@2498 522 // Pick out that element and delete from the array
nicholas@2498 523 holdArr.push(input.splice(r, 1)[0]);
nicholas@2498 524 // Do the same with sequence
nicholas@2498 525 outputSequence.push(inputSequence.splice(r, 1)[0]);
nicholas@2498 526 }
nicholas@2498 527 console.log(inputSequenceClone.toString()); // print original array to console
nicholas@2498 528 console.log(outputSequence.toString()); // print randomised array to console
nicholas@2498 529 return holdArr;
nicholas@2224 530 }
nicholas@2224 531
nicholas@2498 532 function randomSubArray(array, num) {
nicholas@2224 533 if (num > array.length) {
nicholas@2224 534 num = array.length;
nicholas@2224 535 }
nicholas@2224 536 var ret = [];
nicholas@2224 537 while (num > 0) {
nicholas@2224 538 var index = Math.floor(Math.random() * array.length);
nicholas@2498 539 ret.push(array.splice(index, 1)[0]);
nicholas@2224 540 num--;
nicholas@2224 541 }
nicholas@2224 542 return ret;
nicholas@2224 543 }
nicholas@2224 544
nicholas@2224 545 function interfacePopup() {
nicholas@2498 546 // Creates an object to manage the popup
nicholas@2498 547 this.popup = null;
nicholas@2498 548 this.popupContent = null;
nicholas@2498 549 this.popupTitle = null;
nicholas@2498 550 this.popupResponse = null;
nicholas@2498 551 this.buttonProceed = null;
nicholas@2498 552 this.buttonPrevious = null;
nicholas@2498 553 this.popupOptions = null;
nicholas@2498 554 this.currentIndex = null;
nicholas@2498 555 this.node = null;
nicholas@2498 556 this.store = null;
nicholas@2775 557 var lastNodeStart;
nicholas@2498 558 $(window).keypress(function (e) {
n@2915 559 if (e.keyCode == 13 && popup.popup.style.visibility == 'visible' && interfaceContext.lightbox.isVisible() === false) {
nicholas@2498 560 console.log(e);
nicholas@2498 561 popup.buttonProceed.onclick();
nicholas@2498 562 e.preventDefault();
nicholas@2498 563 }
nicholas@2498 564 });
nicholas@2708 565 // Generators & Processors //
nicholas@2708 566
nicholas@2708 567 function processConditional(node, value) {
nicholas@2708 568 function jumpToId(jumpID) {
nicholas@2708 569 var index = this.popupOptions.findIndex(function (item, index, element) {
nicholas@2708 570 if (item.specification.id == jumpID) {
nicholas@2708 571 return true;
nicholas@2708 572 } else {
nicholas@2708 573 return false;
nicholas@2708 574 }
nicholas@2708 575 }, this);
nicholas@2708 576 this.currentIndex = index - 1;
nicholas@2708 577 }
nicholas@2708 578 var conditionFunction;
nicholas@2708 579 if (node.specification.type === "question") {
nicholas@2708 580 conditionFunction = processQuestionConditional;
nicholas@2708 581 } else if (node.specification.type === "checkbox") {
nicholas@2708 582 conditionFunction = processCheckboxConditional;
nicholas@2708 583 } else if (node.specification.type === "radio") {
nicholas@2708 584 conditionFunction = processRadioConditional;
nicholas@2708 585 } else if (node.specification.type === "number") {
nicholas@2708 586 conditionFunction = processNumberConditional;
nicholas@2708 587 } else if (node.specification.type === "slider") {
nicholas@2708 588 conditionFunction = processSliderConditional;
nicholas@2708 589 } else {
nicholas@2708 590 return;
nicholas@2708 591 }
nicholas@2708 592 for (var i = 0; i < node.specification.conditions.length; i++) {
nicholas@2708 593 var condition = node.specification.conditions[i];
nicholas@2708 594 var pass = conditionFunction(condition, value);
nicholas@2708 595 var jumpID;
nicholas@2708 596 if (pass) {
nicholas@2708 597 jumpID = condition.jumpToOnPass;
nicholas@2708 598 } else {
nicholas@2708 599 jumpID = condition.jumpToOnFail;
nicholas@2708 600 }
nicholas@2735 601 if (jumpID !== null) {
nicholas@2708 602 jumpToId.call(this, jumpID);
nicholas@2708 603 break;
nicholas@2708 604 }
nicholas@2708 605 }
nicholas@2708 606 }
nicholas@2708 607
nicholas@2708 608 function postQuestion(node) {
nicholas@2708 609 var textArea = document.createElement('textarea');
nicholas@2708 610 switch (node.specification.boxsize) {
nicholas@2708 611 case 'small':
nicholas@2708 612 textArea.cols = "20";
nicholas@2708 613 textArea.rows = "1";
nicholas@2708 614 break;
nicholas@2708 615 case 'normal':
nicholas@2708 616 textArea.cols = "30";
nicholas@2708 617 textArea.rows = "2";
nicholas@2708 618 break;
nicholas@2708 619 case 'large':
nicholas@2708 620 textArea.cols = "40";
nicholas@2708 621 textArea.rows = "5";
nicholas@2708 622 break;
nicholas@2708 623 case 'huge':
nicholas@2708 624 textArea.cols = "50";
nicholas@2708 625 textArea.rows = "10";
nicholas@2708 626 break;
nicholas@2708 627 }
nicholas@2708 628 if (node.response === undefined) {
nicholas@2708 629 node.response = "";
nicholas@2708 630 } else {
nicholas@2708 631 textArea.value = node.response;
nicholas@2708 632 }
nicholas@2708 633 this.popupResponse.appendChild(textArea);
nicholas@2708 634 textArea.focus();
nicholas@2708 635 this.popupResponse.style.textAlign = "center";
nicholas@2708 636 this.popupResponse.style.left = "0%";
nicholas@2708 637 }
nicholas@2708 638
nicholas@2708 639 function processQuestionConditional(condition, value) {
nicholas@2708 640 switch (condition.check) {
nicholas@2708 641 case "equals":
nicholas@2708 642 // Deliberately loose check
nicholas@2708 643 if (value == condition.value) {
nicholas@2708 644 return true;
nicholas@2708 645 }
nicholas@2708 646 break;
nicholas@2708 647 case "greaterThan":
nicholas@2708 648 case "lessThan":
nicholas@2708 649 console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING");
nicholas@2708 650 break;
nicholas@2708 651 case "contains":
nicholas@2708 652 if (value.includes(condition.value)) {
nicholas@2708 653 return true;
nicholas@2708 654 }
nicholas@2708 655 break;
nicholas@2708 656 }
nicholas@2708 657 return false;
nicholas@2708 658 }
nicholas@2708 659
nicholas@2708 660 function processQuestion(node) {
nicholas@2708 661 var textArea = this.popupResponse.getElementsByTagName("textarea")[0];
nicholas@2708 662 if (node.specification.mandatory === true && textArea.value.length === 0) {
nicholas@2708 663 interfaceContext.lightbox.post("Error", "This question is mandatory");
nicholas@2708 664 return false;
nicholas@2708 665 }
nicholas@2708 666 // Save the text content
nicholas@2708 667 console.log("Question: " + node.specification.statement);
nicholas@2708 668 console.log("Question Response: " + textArea.value);
nicholas@2708 669 node.response = textArea.value;
nicholas@2708 670 processConditional.call(this, node, textArea.value);
nicholas@2708 671 return true;
nicholas@2708 672 }
nicholas@2708 673
nicholas@2708 674 function postCheckbox(node) {
n@2926 675 if (node.response === null) {
n@2926 676 node.response = [];
nicholas@2708 677 }
nicholas@2708 678 var table = document.createElement("table");
nicholas@2708 679 table.className = "popup-option-list";
nicholas@2708 680 table.border = "0";
n@2924 681 var nodelist = [];
nicholas@2708 682 node.specification.options.forEach(function (option, index) {
nicholas@2708 683 var tr = document.createElement("tr");
n@2924 684 nodelist.push(tr);
nicholas@2708 685 var td = document.createElement("td");
nicholas@2708 686 tr.appendChild(td);
nicholas@2708 687 var input = document.createElement('input');
nicholas@2708 688 input.id = option.name;
nicholas@2708 689 input.type = 'checkbox';
nicholas@2708 690 td.appendChild(input);
nicholas@2708 691
nicholas@2708 692 td = document.createElement("td");
nicholas@2708 693 tr.appendChild(td);
nicholas@2708 694 var span = document.createElement('span');
nicholas@2708 695 span.textContent = option.text;
nicholas@2708 696 td.appendChild(span);
nicholas@2708 697 tr = document.createElement('div');
nicholas@2708 698 tr.setAttribute('name', 'option');
nicholas@2708 699 tr.className = "popup-option-checbox";
n@2979 700 var resp;
n@2926 701 if (node.response.length > 0) {
n@2926 702 resp = node.response.find(function (a) {
n@2926 703 return a.name == option.name;
n@2926 704 });
n@2926 705 }
n@2926 706 if (resp !== undefined) {
n@2926 707 if (resp.checked === true) {
nicholas@2708 708 input.checked = "true";
nicholas@2708 709 }
n@2926 710 } else {
n@2926 711 node.response.push({
n@2926 712 "name": option.name,
n@2926 713 "text": option.text,
n@2926 714 "checked": false
n@2926 715 });
nicholas@2708 716 }
nicholas@2708 717 index++;
nicholas@2708 718 });
n@2924 719 if (node.specification.randomise) {
n@2924 720 nodelist = randomiseOrder(nodelist);
n@2924 721 }
n@2924 722 nodelist.forEach(function (e) {
n@2924 723 table.appendChild(e);
n@2924 724 });
nicholas@2708 725 this.popupResponse.appendChild(table);
nicholas@2708 726 }
nicholas@2708 727
nicholas@2708 728 function processCheckbox(node) {
nicholas@2708 729 console.log("Checkbox: " + node.specification.statement);
nicholas@2708 730 var inputs = this.popupResponse.getElementsByTagName('input');
nicholas@2708 731 var numChecked = 0,
nicholas@2708 732 i;
nicholas@2708 733 for (i = 0; i < node.specification.options.length; i++) {
nicholas@2708 734 if (inputs[i].checked) {
nicholas@2708 735 numChecked++;
nicholas@2708 736 }
nicholas@2708 737 }
nicholas@2708 738 if (node.specification.min !== undefined) {
nicholas@2708 739 if (node.specification.max === undefined) {
nicholas@2708 740 if (numChecked < node.specification.min) {
nicholas@2708 741 var msg = "You must select at least " + node.specification.min + " option";
nicholas@2708 742 if (node.specification.min > 1) {
nicholas@2708 743 msg += "s";
nicholas@2708 744 }
nicholas@2708 745 interfaceContext.lightbox.post("Error", msg);
nicholas@2708 746 return;
nicholas@2708 747 }
nicholas@2708 748 } else {
nicholas@2708 749 if (numChecked < node.specification.min || numChecked > node.specification.max) {
nicholas@2708 750 if (node.specification.min == node.specification.max) {
nicholas@2708 751 interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min);
nicholas@2708 752 } else {
nicholas@2708 753 interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max);
nicholas@2708 754 }
nicholas@2708 755 return false;
nicholas@2708 756 }
nicholas@2708 757 }
nicholas@2708 758 }
nicholas@2708 759 for (i = 0; i < node.specification.options.length; i++) {
n@2926 760 node.response.forEach(function (a) {
n@2926 761 var input = this.popupResponse.querySelector("#" + a.name);
n@2926 762 a.checked = input.checked;
nicholas@2708 763 });
nicholas@2708 764 console.log(node.specification.options[i].name + ": " + inputs[i].checked);
nicholas@2708 765 }
nicholas@2708 766 processConditional.call(this, node, node.response);
nicholas@2708 767 return true;
nicholas@2708 768 }
nicholas@2708 769
nicholas@2708 770 function processCheckboxConditional(condition, response) {
nicholas@2708 771 switch (condition.check) {
nicholas@2708 772 case "contains":
nicholas@2708 773 for (var i = 0; i < response.length; i++) {
nicholas@2708 774 var value = response[i];
nicholas@2708 775 if (value.name === condition.value && value.checked) {
nicholas@2708 776 return true;
nicholas@2708 777 }
nicholas@2708 778 }
nicholas@2708 779 break;
nicholas@2708 780 case "equals":
nicholas@2708 781 case "greaterThan":
nicholas@2708 782 case "lessThan":
nicholas@2708 783 console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING");
nicholas@2708 784 break;
nicholas@2708 785 default:
nicholas@2708 786 console.log("Unknown condition. IGNORING");
nicholas@2708 787 break;
nicholas@2708 788 }
nicholas@2708 789 return false;
nicholas@2708 790 }
nicholas@2708 791
nicholas@2708 792 function postRadio(node) {
nicholas@2708 793 if (node.response === null) {
nicholas@2708 794 node.response = {
nicholas@2708 795 name: "",
nicholas@2708 796 text: ""
nicholas@2708 797 };
nicholas@2708 798 }
nicholas@2708 799 var table = document.createElement("table");
nicholas@2708 800 table.className = "popup-option-list";
nicholas@2708 801 table.border = "0";
n@2926 802 var nodelist = [];
nicholas@2708 803 node.specification.options.forEach(function (option, index) {
nicholas@2708 804 var tr = document.createElement("tr");
n@2926 805 nodelist.push(tr);
nicholas@2708 806 var td = document.createElement("td");
nicholas@2708 807 tr.appendChild(td);
nicholas@2708 808 var input = document.createElement('input');
nicholas@2708 809 input.id = option.name;
nicholas@2708 810 input.type = 'radio';
nicholas@2708 811 input.name = node.specification.id;
nicholas@2708 812 td.appendChild(input);
nicholas@2708 813
nicholas@2708 814 td = document.createElement("td");
nicholas@2708 815 tr.appendChild(td);
nicholas@2708 816 var span = document.createElement('span');
nicholas@2708 817 span.textContent = option.text;
nicholas@2708 818 td.appendChild(span);
nicholas@2708 819 tr = document.createElement('div');
nicholas@2708 820 tr.setAttribute('name', 'option');
n@2926 821 tr.className = "popup-option-checkbox";
n@2926 822 if (node.response.name === option.name) {
n@2926 823 input.checked = true;
n@2926 824 }
n@2926 825 });
n@2926 826 if (node.specification.randomise) {
n@2926 827 nodelist = randomiseOrder(nodelist);
n@2926 828 }
n@2926 829 nodelist.forEach(function (e) {
n@2926 830 table.appendChild(e);
nicholas@2708 831 });
nicholas@2708 832 this.popupResponse.appendChild(table);
nicholas@2708 833 }
nicholas@2708 834
nicholas@2708 835 function processRadio(node) {
nicholas@2708 836 var optHold = this.popupResponse;
nicholas@2708 837 console.log("Radio: " + node.specification.statement);
nicholas@2708 838 node.response = null;
nicholas@2708 839 var i = 0;
nicholas@2708 840 var inputs = optHold.getElementsByTagName('input');
nicholas@2939 841 var checked;
nicholas@2939 842 while (checked === undefined) {
nicholas@2708 843 if (i == inputs.length) {
nicholas@2708 844 if (node.specification.mandatory === true) {
nicholas@2708 845 interfaceContext.lightbox.post("Error", "Please select one option");
nicholas@2708 846 return false;
nicholas@2708 847 }
nicholas@2708 848 break;
nicholas@2708 849 }
nicholas@2708 850 if (inputs[i].checked === true) {
nicholas@2939 851 checked = inputs[i];
nicholas@2708 852 }
nicholas@2708 853 i++;
nicholas@2708 854 }
nicholas@2939 855 var option = node.specification.options.find(function (a) {
nicholas@2939 856 return checked.id == a.name;
nicholas@2939 857 });
nicholas@2939 858 if (option === undefined) {
nicholas@2939 859 interfaceContext.lightbox.post("Error", "A configuration error has occured, the test cannot be continued");
nicholas@2939 860 throw ("ERROR - Cannot find option");
nicholas@2939 861 }
nicholas@2939 862 node.response = option;
nicholas@2735 863 processConditional.call(this, node, node.response.name);
nicholas@2708 864 return true;
nicholas@2708 865 }
nicholas@2708 866
nicholas@2708 867 function processRadioConditional(condition, response) {
nicholas@2708 868 switch (condition.check) {
nicholas@2708 869 case "equals":
nicholas@2708 870 if (response === condition.value) {
nicholas@2708 871 return true;
nicholas@2708 872 }
nicholas@2708 873 break;
nicholas@2708 874 case "contains":
nicholas@2708 875 case "greaterThan":
nicholas@2708 876 case "lessThan":
nicholas@2708 877 console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING");
nicholas@2708 878 break;
nicholas@2708 879 default:
nicholas@2708 880 console.log("Unknown condition. IGNORING");
nicholas@2708 881 break;
nicholas@2708 882 }
nicholas@2708 883 return false;
nicholas@2708 884 }
nicholas@2708 885
nicholas@2708 886 function postNumber(node) {
nicholas@2708 887 var input = document.createElement('input');
nicholas@2708 888 input.type = 'textarea';
nicholas@2708 889 if (node.specification.min !== null) {
nicholas@2708 890 input.min = node.specification.min;
nicholas@2708 891 }
nicholas@2708 892 if (node.specification.max !== null) {
nicholas@2708 893 input.max = node.specification.max;
nicholas@2708 894 }
nicholas@2708 895 if (node.specification.step !== null) {
nicholas@2708 896 input.step = node.specification.step;
nicholas@2708 897 }
nicholas@2708 898 if (node.response !== undefined) {
nicholas@2708 899 input.value = node.response;
nicholas@2708 900 }
nicholas@2708 901 this.popupResponse.appendChild(input);
nicholas@2708 902 this.popupResponse.style.textAlign = "center";
nicholas@2708 903 this.popupResponse.style.left = "0%";
nicholas@2708 904 }
nicholas@2708 905
nicholas@2708 906 function processNumber(node) {
nicholas@2708 907 var input = this.popupContent.getElementsByTagName('input')[0];
nicholas@2730 908 if (node.specification.mandatory === true && input.value.length === 0) {
nicholas@2708 909 interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number');
nicholas@2708 910 return false;
nicholas@2708 911 }
nicholas@2708 912 var enteredNumber = Number(input.value);
nicholas@2708 913 if (isNaN(enteredNumber)) {
nicholas@2708 914 interfaceContext.lightbox.post("Error", 'Please enter a valid number');
nicholas@2708 915 return false;
nicholas@2708 916 }
nicholas@2730 917 if (enteredNumber < node.specification.min && node.specification.min !== null) {
nicholas@2730 918 interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.specification.min);
nicholas@2708 919 return false;
nicholas@2708 920 }
nicholas@2730 921 if (enteredNumber > node.specification.max && node.specification.max !== null) {
nicholas@2730 922 interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.specification.max);
nicholas@2708 923 return false;
nicholas@2708 924 }
nicholas@2708 925 node.response = input.value;
nicholas@2708 926 processConditional.call(this, node, node.response);
nicholas@2708 927 return true;
nicholas@2708 928 }
nicholas@2708 929
nicholas@2708 930 function processNumberConditional(condtion, value) {
nicholas@2708 931 var condition = condition;
nicholas@2708 932 switch (condition.check) {
nicholas@2708 933 case "greaterThan":
nicholas@2708 934 if (value > Number(condition.value)) {
nicholas@2708 935 return true;
nicholas@2708 936 }
nicholas@2708 937 break;
nicholas@2708 938 case "lessThan":
nicholas@2708 939 if (value < Number(condition.value)) {
nicholas@2708 940 return true;
nicholas@2708 941 }
nicholas@2708 942 break;
nicholas@2708 943 case "equals":
nicholas@2708 944 if (value == condition.value) {
nicholas@2708 945 return true;
nicholas@2708 946 }
nicholas@2708 947 break;
nicholas@2708 948 case "contains":
nicholas@2708 949 console.log("Survey Element of type 'number' cannot interpret \"contains\" conditions. IGNORING");
nicholas@2708 950 break;
nicholas@2708 951 default:
nicholas@2708 952 console.log("Unknown condition. IGNORING");
nicholas@2708 953 break;
nicholas@2708 954 }
nicholas@2708 955 return false;
nicholas@2708 956 }
nicholas@2708 957
nicholas@2708 958 function postVideo(node) {
nicholas@2708 959 var video = document.createElement("video");
nicholas@2708 960 video.src = node.specification.url;
nicholas@2708 961 this.popupResponse.appendChild(video);
nicholas@2708 962 }
nicholas@2708 963
nicholas@2708 964 function postYoutube(node) {
nicholas@2708 965 var iframe = document.createElement("iframe");
nicholas@2708 966 iframe.className = "youtube";
nicholas@2708 967 iframe.src = node.specification.url;
nicholas@2708 968 this.popupResponse.appendChild(iframe);
nicholas@2708 969 }
nicholas@2708 970
nicholas@2708 971 function postSlider(node) {
nicholas@2708 972 var hold = document.createElement('div');
nicholas@2708 973 var input = document.createElement('input');
nicholas@2708 974 input.type = 'range';
nicholas@2708 975 input.style.width = "90%";
nicholas@2708 976 if (node.specification.min !== null) {
nicholas@2708 977 input.min = node.specification.min;
nicholas@2708 978 }
nicholas@2708 979 if (node.specification.max !== null) {
nicholas@2708 980 input.max = node.specification.max;
nicholas@2708 981 }
nicholas@2708 982 if (node.response !== undefined) {
nicholas@2708 983 input.value = node.response;
nicholas@2708 984 }
nicholas@2708 985 hold.className = "survey-slider-text-holder";
nicholas@2708 986 var minText = document.createElement('span');
nicholas@2708 987 var maxText = document.createElement('span');
nicholas@2708 988 minText.textContent = node.specification.leftText;
nicholas@2708 989 maxText.textContent = node.specification.rightText;
nicholas@2708 990 hold.appendChild(minText);
nicholas@2708 991 hold.appendChild(maxText);
nicholas@2708 992 this.popupResponse.appendChild(input);
nicholas@2708 993 this.popupResponse.appendChild(hold);
nicholas@2708 994 this.popupResponse.style.textAlign = "center";
nicholas@2708 995 }
nicholas@2708 996
nicholas@2708 997 function processSlider(node) {
nicholas@2708 998 var input = this.popupContent.getElementsByTagName('input')[0];
nicholas@2708 999 node.response = input.value;
nicholas@2708 1000 processConditional.call(this, node, node.response);
nicholas@2708 1001 return true;
nicholas@2708 1002 }
nicholas@2708 1003
nicholas@2708 1004 function processSliderConditional(condition, value) {
nicholas@2708 1005 switch (condition.check) {
nicholas@2708 1006 case "contains":
nicholas@2708 1007 console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING");
nicholas@2708 1008 break;
nicholas@2708 1009 case "greaterThan":
nicholas@2708 1010 if (value > Number(condition.value)) {
nicholas@2708 1011 return true;
nicholas@2708 1012 }
nicholas@2708 1013 break;
nicholas@2708 1014 case "lessThan":
nicholas@2708 1015 if (value < Number(condition.value)) {
nicholas@2708 1016 return true;
nicholas@2708 1017 }
nicholas@2708 1018 break;
nicholas@2708 1019 case "equals":
nicholas@2708 1020 if (value == condition.value) {
nicholas@2708 1021 return true;
nicholas@2708 1022 }
nicholas@2708 1023 break;
nicholas@2708 1024 default:
nicholas@2708 1025 console.log("Unknown condition. IGNORING");
nicholas@2708 1026 break;
nicholas@2708 1027 }
nicholas@2708 1028 return false;
nicholas@2708 1029 }
nicholas@2498 1030
nicholas@2498 1031 this.createPopup = function () {
nicholas@2498 1032 // Create popup window interface
nicholas@2498 1033 var insertPoint = document.getElementById("topLevelBody");
nicholas@2498 1034
nicholas@2498 1035 this.popup = document.getElementById('popupHolder');
nicholas@2498 1036 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2498 1037 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
nicholas@2498 1038
nicholas@2498 1039 this.popupContent = document.getElementById('popupContent');
nicholas@2498 1040
nicholas@2645 1041 this.popupTitle = document.getElementById('popupTitleHolder');
nicholas@2498 1042
nicholas@2498 1043 this.popupResponse = document.getElementById('popupResponse');
nicholas@2498 1044
nicholas@2498 1045 this.buttonProceed = document.getElementById('popup-proceed');
nicholas@2498 1046 this.buttonProceed.onclick = function () {
nicholas@2498 1047 popup.proceedClicked();
nicholas@2498 1048 };
nicholas@2498 1049
nicholas@2498 1050 this.buttonPrevious = document.getElementById('popup-previous');
nicholas@2498 1051 this.buttonPrevious.onclick = function () {
nicholas@2498 1052 popup.previousClick();
nicholas@2498 1053 };
nicholas@2498 1054
nicholas@2224 1055 this.hidePopup();
nicholas@2498 1056 this.popup.style.visibility = 'hidden';
nicholas@2498 1057 };
nicholas@2498 1058
nicholas@2498 1059 this.showPopup = function () {
nicholas@2708 1060 if (this.popup === null) {
nicholas@2498 1061 this.createPopup();
nicholas@2498 1062 }
nicholas@2498 1063 this.popup.style.visibility = 'visible';
nicholas@2498 1064 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2498 1065 blank.style.visibility = 'visible';
nicholas@2498 1066 this.popupResponse.style.left = "0%";
nicholas@2498 1067 };
nicholas@2498 1068
nicholas@2498 1069 this.hidePopup = function () {
nicholas@2224 1070 if (this.popup) {
nicholas@2224 1071 this.popup.style.visibility = 'hidden';
nicholas@2224 1072 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2224 1073 blank.style.visibility = 'hidden';
nicholas@2224 1074 this.buttonPrevious.style.visibility = 'inherit';
nicholas@2224 1075 }
nicholas@2498 1076 };
nicholas@2498 1077
nicholas@2498 1078 this.postNode = function () {
nicholas@2498 1079 // This will take the node from the popupOptions and display it
nicholas@2645 1080 var node = this.popupOptions[this.currentIndex],
nicholas@2646 1081 converter = new showdown.Converter(),
nicholas@2646 1082 p = new DOMParser();
nicholas@2774 1083 lastNodeStart = new Date();
nicholas@2498 1084 this.popupResponse.innerHTML = "";
nicholas@2648 1085 this.popupTitle.innerHTML = "";
nicholas@2943 1086 var strings = node.specification.statement.split("\n");
nicholas@2949 1087 strings.forEach(function (e, i, a) {
nicholas@2943 1088 a[i] = e.trim();
nicholas@2943 1089 });
nicholas@2943 1090 node.specification.statement = strings.join("\n");
nicholas@2943 1091 var statementElements = p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").querySelector("body").children;
nicholas@2949 1092 while (statementElements.length > 0) {
nicholas@2943 1093 this.popupTitle.appendChild(statementElements[0]);
nicholas@2943 1094 }
nicholas@2498 1095 if (node.specification.type == 'question') {
nicholas@2708 1096 postQuestion.call(this, node);
nicholas@2498 1097 } else if (node.specification.type == 'checkbox') {
nicholas@2708 1098 postCheckbox.call(this, node);
nicholas@2498 1099 } else if (node.specification.type == 'radio') {
nicholas@2708 1100 postRadio.call(this, node);
nicholas@2498 1101 } else if (node.specification.type == 'number') {
nicholas@2708 1102 postNumber.call(this, node);
nicholas@2498 1103 } else if (node.specification.type == "video") {
nicholas@2708 1104 postVideo.call(this, node);
nicholas@2491 1105 } else if (node.specification.type == "youtube") {
nicholas@2708 1106 postYoutube.call(this, node);
n@2583 1107 } else if (node.specification.type == "slider") {
nicholas@2708 1108 postSlider.call(this, node);
nicholas@2491 1109 }
nicholas@2498 1110 if (this.currentIndex + 1 == this.popupOptions.length) {
nicholas@2498 1111 if (this.node.location == "pre") {
nicholas@2498 1112 this.buttonProceed.textContent = 'Start';
nicholas@2498 1113 } else {
nicholas@2498 1114 this.buttonProceed.textContent = 'Submit';
nicholas@2498 1115 }
nicholas@2498 1116 } else {
nicholas@2498 1117 this.buttonProceed.textContent = 'Next';
nicholas@2498 1118 }
nicholas@2498 1119 if (this.currentIndex > 0)
nicholas@2498 1120 this.buttonPrevious.style.visibility = 'visible';
nicholas@2498 1121 else
nicholas@2498 1122 this.buttonPrevious.style.visibility = 'hidden';
nicholas@2498 1123 };
nicholas@2498 1124
nicholas@2498 1125 this.initState = function (node, store) {
nicholas@2498 1126 //Call this with your preTest and postTest nodes when needed to
nicholas@2498 1127 // initialise the popup procedure.
nicholas@2498 1128 if (node.options.length > 0) {
nicholas@2498 1129 this.popupOptions = [];
nicholas@2498 1130 this.node = node;
nicholas@2498 1131 this.store = store;
nicholas@2708 1132 node.options.forEach(function (opt) {
nicholas@2498 1133 this.popupOptions.push({
nicholas@2498 1134 specification: opt,
nicholas@2498 1135 response: null
nicholas@2498 1136 });
nicholas@2708 1137 }, this);
nicholas@2498 1138 this.currentIndex = 0;
nicholas@2498 1139 this.showPopup();
nicholas@2498 1140 this.postNode();
nicholas@2498 1141 } else {
nicholas@2498 1142 advanceState();
nicholas@2498 1143 }
nicholas@2498 1144 };
nicholas@2498 1145
nicholas@2498 1146 this.proceedClicked = function () {
nicholas@2498 1147 // Each time the popup button is clicked!
nicholas@3101 1148 if (testState.stateIndex === -1 && specification.calibration) {
nicholas@2224 1149 advanceState();
nicholas@2224 1150 return;
nicholas@2224 1151 }
nicholas@2708 1152 var node = this.popupOptions[this.currentIndex],
nicholas@2774 1153 pass = true,
nicholas@2778 1154 timeDelta = (new Date() - lastNodeStart) / 1000.0;
nicholas@2774 1155 if (timeDelta < node.specification.minWait) {
nicholas@2778 1156 interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds");
nicholas@2774 1157 return;
nicholas@2774 1158 }
nicholas@2775 1159 node.elapsedTime = timeDelta;
nicholas@2498 1160 if (node.specification.type == 'question') {
nicholas@2498 1161 // Must extract the question data
nicholas@2708 1162 pass = processQuestion.call(this, node);
nicholas@2498 1163 } else if (node.specification.type == 'checkbox') {
nicholas@2498 1164 // Must extract checkbox data
nicholas@2708 1165 pass = processCheckbox.call(this, node);
nicholas@2708 1166 } else if (node.specification.type == "radio") {
nicholas@2464 1167 // Perform the conditional
nicholas@2708 1168 pass = processRadio.call(this, node);
nicholas@2708 1169 } else if (node.specification.type == "number") {
nicholas@2464 1170 // Perform the conditional
nicholas@2708 1171 pass = processNumber.call(this, node);
n@2583 1172 } else if (node.specification.type == 'slider') {
nicholas@2708 1173 pass = processSlider.call(this, node);
nicholas@2708 1174 }
nicholas@2708 1175 if (pass === false) {
nicholas@2708 1176 return;
nicholas@2498 1177 }
nicholas@2498 1178 this.currentIndex++;
nicholas@2498 1179 if (this.currentIndex < this.popupOptions.length) {
nicholas@2498 1180 this.postNode();
nicholas@2498 1181 } else {
nicholas@2498 1182 // Reached the end of the popupOptions
nicholas@2645 1183 this.popupTitle.innerHTML = "";
nicholas@2498 1184 this.popupResponse.innerHTML = "";
nicholas@2498 1185 this.hidePopup();
nicholas@2708 1186 this.popupOptions.forEach(function (node) {
nicholas@2498 1187 this.store.postResult(node);
nicholas@2708 1188 }, this);
nicholas@2224 1189 this.store.complete();
nicholas@2498 1190 advanceState();
nicholas@2498 1191 }
nicholas@2498 1192 };
nicholas@2498 1193
nicholas@2498 1194 this.previousClick = function () {
nicholas@2498 1195 // Triggered when the 'Back' button is clicked in the survey
nicholas@2498 1196 if (this.currentIndex > 0) {
nicholas@2498 1197 this.currentIndex--;
nicholas@2498 1198 this.postNode();
nicholas@2498 1199 }
nicholas@2498 1200 };
nicholas@2498 1201
nicholas@2498 1202 this.resize = function (event) {
nicholas@2498 1203 // Called on window resize;
nicholas@2708 1204 if (this.popup !== null) {
nicholas@2498 1205 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2498 1206 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
nicholas@2498 1207 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2498 1208 blank.style.width = window.innerWidth;
nicholas@2498 1209 blank.style.height = window.innerHeight;
nicholas@2498 1210 }
nicholas@2498 1211 };
nicholas@2498 1212 this.hideNextButton = function () {
nicholas@2224 1213 this.buttonProceed.style.visibility = "hidden";
nicholas@2708 1214 };
nicholas@2498 1215 this.hidePreviousButton = function () {
nicholas@2224 1216 this.buttonPrevious.style.visibility = "hidden";
nicholas@2708 1217 };
nicholas@2498 1218 this.showNextButton = function () {
nicholas@2224 1219 this.buttonProceed.style.visibility = "visible";
nicholas@2708 1220 };
nicholas@2498 1221 this.showPreviousButton = function () {
nicholas@2224 1222 this.buttonPrevious.style.visibility = "visible";
nicholas@2708 1223 };
nicholas@2224 1224 }
nicholas@2224 1225
nicholas@2498 1226 function advanceState() {
nicholas@2498 1227 // Just for complete clarity
nicholas@2498 1228 testState.advanceState();
nicholas@2224 1229 }
nicholas@2224 1230
nicholas@2498 1231 function stateMachine() {
nicholas@2498 1232 // Object prototype for tracking and managing the test state
nicholas@2722 1233
n@2716 1234 function pickSubPool(pool, numElements) {
n@2716 1235 // Assumes each element of pool has function "alwaysInclude"
n@2716 1236
n@2716 1237 // First extract those excluded from picking process
n@2716 1238 var picked = [];
nicholas@2833 1239 pool.forEach(function (e, i) {
n@2716 1240 if (e.alwaysInclude) {
nicholas@2833 1241 picked.push(pool.splice(i, 1)[0]);
n@2716 1242 }
n@2716 1243 });
n@2716 1244
n@2716 1245 return picked.concat(randomSubArray(pool, numElements - picked.length));
n@2716 1246 }
nicholas@2722 1247
nicholas@2498 1248 this.stateMap = [];
nicholas@2498 1249 this.preTestSurvey = null;
nicholas@2498 1250 this.postTestSurvey = null;
nicholas@2498 1251 this.stateIndex = null;
nicholas@2498 1252 this.currentStateMap = null;
nicholas@2498 1253 this.currentStatePosition = null;
nicholas@2224 1254 this.currentStore = null;
nicholas@2498 1255 this.initialise = function () {
nicholas@2498 1256
n@2909 1257 function randomiseElements(page) {
n@2909 1258 // Get the elements which are fixed / labelled
n@2909 1259 var fixed = [],
n@2909 1260 or = [],
n@2909 1261 remainder = [];
n@2909 1262 page.audioElements.forEach(function (a) {
n@2909 1263 if (a.label.length > 0 || a.postion !== undefined) {
n@2909 1264 fixed.push(a);
n@2909 1265 } else if (a.type === "outside-reference") {
n@2909 1266 or.push(a);
n@2909 1267 } else {
n@2909 1268 remainder.push(a);
n@2909 1269 }
n@2979 1270 });
n@2909 1271 if (page.poolSize > 0 || page.randomiseOrder) {
n@2909 1272 page.randomiseOrder = true;
n@2909 1273 if (page.poolSize === 0) {
n@2909 1274 page.poolSize = page.audioElements.length;
n@2909 1275 }
n@2909 1276 page.poolSize -= fixed.length;
n@2909 1277 remainder = pickSubPool(remainder, page.poolSize);
n@2909 1278 }
n@2909 1279 // Randomise the remainders
n@2909 1280 if (page.randomiseOrder) {
n@2909 1281 remainder = randomiseOrder(remainder);
n@2909 1282 }
n@2909 1283 fixed = fixed.concat(remainder);
n@2909 1284 page.audioElements = fixed.concat(or);
n@2909 1285 page.audioElements.forEach(function (a, i) {
n@2909 1286 a.position = i;
n@2909 1287 });
n@2909 1288 }
n@2909 1289
nicholas@2498 1290 // Get the data from Specification
nicholas@2498 1291 var pagePool = [];
nicholas@2722 1292 specification.pages.forEach(function (page) {
n@2716 1293 if (page.position !== null || page.alwaysInclude) {
n@2716 1294 page.alwaysInclude = true;
n@2716 1295 }
n@2716 1296 pagePool.push(page);
n@2717 1297 });
n@2716 1298 if (specification.numPages > 0) {
n@2716 1299 specification.randomiseOrder = true;
n@2716 1300 pagePool = pickSubPool(pagePool, specification.numPages);
n@2716 1301 }
n@2716 1302
n@2716 1303 // Now get the order of pages
n@2716 1304 var fixed = [];
nicholas@2722 1305 pagePool.forEach(function (page) {
nicholas@2748 1306 if (page.position !== undefined) {
n@2716 1307 fixed.push(page);
n@2716 1308 var i = pagePool.indexOf(page);
n@2716 1309 pagePool.splice(i, 1);
nicholas@2224 1310 }
n@2717 1311 });
nicholas@2498 1312
n@2716 1313 if (specification.randomiseOrder) {
n@2716 1314 pagePool = randomiseOrder(pagePool);
nicholas@2224 1315 }
nicholas@2498 1316
n@2716 1317 // Place in the correct order
nicholas@2722 1318 fixed.forEach(function (page) {
n@2716 1319 pagePool.splice(page.position, 0, page);
n@2717 1320 });
n@2716 1321
n@2716 1322 // Now process the pages
n@2716 1323 pagePool.forEach(function (page, i) {
n@2716 1324 page.presentedId = i;
n@2716 1325 this.stateMap.push(page);
n@2716 1326 var elements = page.audioElements;
n@2909 1327 randomiseElements(page);
n@2716 1328 storage.createTestPageStore(page);
n@2716 1329 audioEngineContext.loadPageData(page);
n@2716 1330 }, this);
nicholas@2674 1331
nicholas@2708 1332 if (specification.preTest !== null) {
nicholas@2498 1333 this.preTestSurvey = specification.preTest;
nicholas@2498 1334 }
nicholas@2708 1335 if (specification.postTest !== null) {
nicholas@2498 1336 this.postTestSurvey = specification.postTest;
nicholas@2498 1337 }
nicholas@2498 1338
nicholas@2498 1339 if (this.stateMap.length > 0) {
nicholas@2708 1340 if (this.stateIndex !== null) {
nicholas@2498 1341 console.log('NOTE - State already initialise');
nicholas@2498 1342 }
nicholas@2498 1343 this.stateIndex = -2;
nicholas@2224 1344 console.log('Starting test...');
nicholas@2498 1345 } else {
nicholas@2498 1346 console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
nicholas@2498 1347 }
nicholas@2498 1348 };
nicholas@2498 1349 this.advanceState = function () {
nicholas@2708 1350 if (this.stateIndex === null) {
nicholas@2498 1351 this.initialise();
nicholas@2498 1352 }
nicholas@2357 1353 if (this.stateIndex > -2) {
nicholas@2357 1354 storage.update();
nicholas@2357 1355 }
nicholas@2498 1356 if (this.stateIndex == -2) {
nicholas@2224 1357 this.stateIndex++;
nicholas@2708 1358 if (this.preTestSurvey !== undefined) {
nicholas@2498 1359 popup.initState(this.preTestSurvey, storage.globalPreTest);
nicholas@2498 1360 } else {
nicholas@2498 1361 this.advanceState();
nicholas@2498 1362 }
nicholas@2498 1363 } else if (this.stateIndex == -1) {
nicholas@3101 1364 if (interfaceContext.calibrationTests.checkFrequencies) {
nicholas@2224 1365 popup.showPopup();
nicholas@3101 1366 popup.popupTitle.textContent = "Set the levels so all tones are of equal amplitude. Move your mouse over the sliders to hear the tones. The red slider is the reference tone";
nicholas@3101 1367 interfaceContext.calibrationTests.performFrequencyCheck(popup.popupResponse);
nicholas@3101 1368 popup.hidePreviousButton();
nicholas@3101 1369 } else if (interfaceContext.calibrationTests.checkChannels) {
nicholas@3101 1370 popup.showPopup();
nicholas@3101 1371 popup.popupTitle.textContent = "Click play to start the audio, the click the button corresponding to where the sound appears to be coming from.";
nicholas@3101 1372 interfaceContext.calibrationTests.performChannelCheck(popup.popupResponse);
nicholas@2224 1373 popup.hidePreviousButton();
nicholas@2224 1374 } else {
nicholas@3101 1375 this.stateIndex++;
nicholas@2224 1376 this.advanceState();
nicholas@2224 1377 }
nicholas@2498 1378 } else if (this.stateIndex == this.stateMap.length) {
nicholas@2498 1379 // All test pages complete, post test
nicholas@2498 1380 console.log('Ending test ...');
nicholas@2498 1381 this.stateIndex++;
nicholas@2708 1382 if (this.postTestSurvey === undefined) {
nicholas@2498 1383 this.advanceState();
nicholas@2498 1384 } else {
nicholas@2498 1385 popup.initState(this.postTestSurvey, storage.globalPostTest);
nicholas@2498 1386 }
nicholas@2498 1387 } else if (this.stateIndex > this.stateMap.length) {
nicholas@2498 1388 createProjectSave(specification.projectReturn);
nicholas@2498 1389 } else {
nicholas@2224 1390 popup.hidePopup();
nicholas@2708 1391 if (this.currentStateMap === null) {
nicholas@2498 1392 this.currentStateMap = this.stateMap[this.stateIndex];
nicholas@2498 1393
nicholas@2224 1394 this.currentStore = storage.testPages[this.stateIndex];
nicholas@2708 1395 if (this.currentStateMap.preTest !== undefined) {
nicholas@2498 1396 this.currentStatePosition = 'pre';
nicholas@2498 1397 popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest);
nicholas@2498 1398 } else {
nicholas@2498 1399 this.currentStatePosition = 'test';
nicholas@2498 1400 }
nicholas@2498 1401 interfaceContext.newPage(this.currentStateMap, storage.testPages[this.stateIndex]);
nicholas@2498 1402 return;
nicholas@2498 1403 }
nicholas@2498 1404 switch (this.currentStatePosition) {
nicholas@2498 1405 case 'pre':
nicholas@2498 1406 this.currentStatePosition = 'test';
nicholas@2498 1407 break;
nicholas@2498 1408 case 'test':
nicholas@2498 1409 this.currentStatePosition = 'post';
nicholas@2498 1410 // Save the data
nicholas@2498 1411 this.testPageCompleted();
nicholas@2708 1412 if (this.currentStateMap.postTest === undefined) {
nicholas@2498 1413 this.advanceState();
nicholas@2498 1414 return;
nicholas@2498 1415 } else {
nicholas@2498 1416 popup.initState(this.currentStateMap.postTest, storage.testPages[this.stateIndex].postTest);
nicholas@2498 1417 }
nicholas@2498 1418 break;
nicholas@2498 1419 case 'post':
nicholas@2498 1420 this.stateIndex++;
nicholas@2498 1421 this.currentStateMap = null;
nicholas@2498 1422 this.advanceState();
nicholas@2498 1423 break;
nicholas@2708 1424 }
nicholas@2498 1425 }
nicholas@2498 1426 };
nicholas@2498 1427
nicholas@2498 1428 this.testPageCompleted = function () {
nicholas@2498 1429 // Function called each time a test page has been completed
nicholas@2498 1430 var storePoint = storage.testPages[this.stateIndex];
nicholas@2498 1431 // First get the test metric
nicholas@2498 1432
nicholas@2498 1433 var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0];
nicholas@2498 1434 if (audioEngineContext.metric.enableTestTimer) {
nicholas@2498 1435 var testTime = storePoint.parent.document.createElement('metricresult');
nicholas@2498 1436 testTime.id = 'testTime';
nicholas@2498 1437 testTime.textContent = audioEngineContext.timer.testDuration;
nicholas@2498 1438 metric.appendChild(testTime);
nicholas@2498 1439 }
nicholas@2498 1440
nicholas@2498 1441 var audioObjects = audioEngineContext.audioObjects;
nicholas@2708 1442 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2498 1443 ao.exportXMLDOM();
nicholas@2708 1444 });
nicholas@2708 1445 interfaceContext.commentQuestions.forEach(function (element) {
nicholas@2498 1446 element.exportXMLDOM(storePoint);
nicholas@2708 1447 });
nicholas@2498 1448 pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
nicholas@2224 1449 storePoint.complete();
nicholas@2498 1450 };
nicholas@2498 1451
nicholas@2498 1452 this.getCurrentTestPage = function () {
nicholas@2498 1453 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
nicholas@2310 1454 return this.currentStateMap;
nicholas@2310 1455 } else {
nicholas@2310 1456 return null;
nicholas@2310 1457 }
nicholas@2708 1458 };
nicholas@2498 1459 this.getCurrentTestPageStore = function () {
nicholas@2498 1460 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
nicholas@2312 1461 return this.currentStore;
nicholas@2312 1462 } else {
nicholas@2312 1463 return null;
nicholas@2312 1464 }
nicholas@2708 1465 };
nicholas@2224 1466 }
nicholas@2224 1467
nicholas@2224 1468 function AudioEngine(specification) {
nicholas@2498 1469
nicholas@2498 1470 // Create two output paths, the main outputGain and fooGain.
nicholas@2498 1471 // Output gain is default to 1 and any items for playback route here
nicholas@2498 1472 // Foo gain is used for analysis to ensure paths get processed, but are not heard
nicholas@2498 1473 // because web audio will optimise and any route which does not go to the destination gets ignored.
nicholas@2498 1474 this.outputGain = audioContext.createGain();
nicholas@2498 1475 this.fooGain = audioContext.createGain();
nicholas@2508 1476 this.fooGain.gain.value = 0;
nicholas@2498 1477
nicholas@2498 1478 // Use this to detect playback state: 0 - stopped, 1 - playing
nicholas@2498 1479 this.status = 0;
nicholas@2498 1480
nicholas@2498 1481 // Connect both gains to output
nicholas@2498 1482 this.outputGain.connect(audioContext.destination);
nicholas@2498 1483 this.fooGain.connect(audioContext.destination);
nicholas@2498 1484
nicholas@2498 1485 // Create the timer Object
nicholas@2498 1486 this.timer = new timer();
nicholas@2498 1487 // Create session metrics
nicholas@2498 1488 this.metric = new sessionMetrics(this, specification);
nicholas@2498 1489
nicholas@2498 1490 this.loopPlayback = false;
nicholas@2351 1491 this.synchPlayback = false;
nicholas@2351 1492 this.pageSpecification = null;
nicholas@2498 1493
nicholas@2498 1494 this.pageStore = null;
nicholas@2498 1495
nicholas@2508 1496 // Chrome 53+ Error solution
nicholas@2508 1497 // Empty buffer for keep-alive
nicholas@2508 1498 var nullBuffer = audioContext.createBuffer(1, audioContext.sampleRate, audioContext.sampleRate);
nicholas@2508 1499 this.nullBufferSource = audioContext.createBufferSource();
nicholas@2508 1500 this.nullBufferSource.buffer = nullBuffer;
nicholas@2508 1501 this.nullBufferSource.loop = true;
nicholas@2508 1502 this.nullBufferSource.start(0);
nicholas@2508 1503
nicholas@2498 1504 // Create store for new audioObjects
nicholas@2498 1505 this.audioObjects = [];
nicholas@2498 1506
nicholas@2498 1507 this.buffers = [];
nicholas@2498 1508 this.bufferObj = function () {
nicholas@2617 1509 var urls = [];
nicholas@2498 1510 this.buffer = null;
nicholas@2498 1511 this.users = [];
nicholas@2224 1512 this.progress = 0;
nicholas@2224 1513 this.status = 0;
nicholas@2498 1514 this.ready = function () {
nicholas@2498 1515 if (this.status >= 2) {
nicholas@2224 1516 this.status = 3;
nicholas@2224 1517 }
nicholas@2498 1518 for (var i = 0; i < this.users.length; i++) {
nicholas@2498 1519 this.users[i].state = 1;
nicholas@2708 1520 if (this.users[i].interfaceDOM !== null) {
nicholas@2498 1521 this.users[i].bufferLoaded(this);
nicholas@2498 1522 }
nicholas@2498 1523 }
nicholas@2498 1524 };
nicholas@2617 1525 this.setUrls = function (obj) {
nicholas@2617 1526 // Obj must be an array of pairs:
nicholas@2617 1527 // [{sampleRate, url}]
nicholas@2617 1528 var localFs = audioContext.sampleRate,
nicholas@2617 1529 list = [],
nicholas@2617 1530 i;
nicholas@2617 1531 for (i = 0; i < obj.length; i++) {
nicholas@2617 1532 if (obj[i].sampleRate == localFs) {
nicholas@2617 1533 list.push(obj.splice(i, 1)[0]);
nicholas@2617 1534 }
nicholas@2617 1535 }
nicholas@2617 1536 list = list.concat(obj);
nicholas@2617 1537 urls = list;
nicholas@2617 1538 };
nicholas@2617 1539 this.hasUrl = function (checkUrl) {
nicholas@2617 1540 var l = urls.length,
nicholas@2617 1541 i;
nicholas@2617 1542 for (i = 0; i < l; i++) {
nicholas@2617 1543 if (urls[i].url == checkUrl) {
nicholas@2617 1544 return true;
nicholas@2617 1545 }
nicholas@2617 1546 }
nicholas@2617 1547 return false;
nicholas@2708 1548 };
nicholas@2617 1549 this.getMedia = function () {
nicholas@2615 1550 var self = this;
nicholas@2616 1551 var currentUrlIndex = 0;
nicholas@2498 1552
nicholas@2615 1553 function get(fqurl) {
nicholas@2615 1554 return new Promise(function (resolve, reject) {
nicholas@2615 1555 var req = new XMLHttpRequest();
nicholas@2615 1556 req.open('GET', fqurl, true);
nicholas@2615 1557 req.responseType = 'arraybuffer';
nicholas@2615 1558 req.onload = function () {
nicholas@2615 1559 if (req.status == 200) {
nicholas@2615 1560 resolve(req.response);
nicholas@2615 1561 }
nicholas@2615 1562 };
nicholas@2615 1563 req.onerror = function () {
nicholas@2615 1564 reject(new Error(req.statusText));
nicholas@2615 1565 };
nicholas@2615 1566
nicholas@2615 1567 req.addEventListener("progress", progressCallback.bind(self));
nicholas@2615 1568 req.send();
nicholas@2615 1569 });
nicholas@2615 1570 }
nicholas@2615 1571
nicholas@2615 1572 function getNextURL() {
nicholas@2615 1573 currentUrlIndex++;
nicholas@2615 1574 var self = this;
nicholas@2617 1575 if (currentUrlIndex >= urls.length) {
nicholas@2615 1576 processError();
nicholas@2615 1577 } else {
nicholas@2617 1578 return get(urls[currentUrlIndex].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
nicholas@2615 1579 }
nicholas@2615 1580 }
nicholas@2498 1581
nicholas@2498 1582 // Create callback to decode the data asynchronously
nicholas@2615 1583 function processAudio(response) {
nicholas@2615 1584 var self = this;
nicholas@2615 1585 return audioContext.decodeAudioData(response, function (decodedData) {
nicholas@2615 1586 self.buffer = decodedData;
nicholas@2615 1587 self.status = 2;
nicholas@2615 1588 calculateLoudness(self, "I");
nicholas@2615 1589 return true;
nicholas@2498 1590 }, function (e) {
nicholas@2403 1591 var waveObj = new WAVE();
nicholas@2708 1592 if (waveObj.open(response) === 0) {
nicholas@2615 1593 self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate);
nicholas@2498 1594 for (var c = 0; c < waveObj.num_channels; c++) {
nicholas@2615 1595 var buffer_ptr = self.buffer.getChannelData(c);
nicholas@2498 1596 for (var n = 0; n < waveObj.num_samples; n++) {
nicholas@2403 1597 buffer_ptr[n] = waveObj.decoded_data[c][n];
nicholas@2224 1598 }
nicholas@2224 1599 }
nicholas@2403 1600 }
nicholas@2708 1601 if (self.buffer !== undefined) {
nicholas@2615 1602 self.status = 2;
nicholas@2615 1603 calculateLoudness(self, "I");
nicholas@2615 1604 return true;
nicholas@2403 1605 }
nicholas@2708 1606 waveObj = undefined;
nicholas@2615 1607 return false;
nicholas@2403 1608 });
nicholas@2615 1609 }
nicholas@2498 1610
nicholas@2224 1611 // Create callback for any error in loading
nicholas@2615 1612 function processError() {
nicholas@2615 1613 this.status = -1;
nicholas@2615 1614 for (var i = 0; i < this.users.length; i++) {
nicholas@2615 1615 this.users[i].state = -1;
nicholas@2708 1616 if (this.users[i].interfaceDOM !== null) {
nicholas@2615 1617 this.users[i].bufferLoaded(this);
nicholas@2224 1618 }
nicholas@2224 1619 }
nicholas@2617 1620 interfaceContext.lightbox.post("Error", "Could not load resource " + urls[currentUrlIndex].url);
nicholas@2224 1621 }
nicholas@2498 1622
nicholas@2615 1623 function progressCallback(event) {
nicholas@2498 1624 if (event.lengthComputable) {
nicholas@2615 1625 this.progress = event.loaded / event.total;
nicholas@2615 1626 for (var i = 0; i < this.users.length; i++) {
nicholas@2708 1627 if (this.users[i].interfaceDOM !== null) {
nicholas@2615 1628 if (typeof this.users[i].interfaceDOM.updateLoading === "function") {
nicholas@2615 1629 this.users[i].interfaceDOM.updateLoading(this.progress * 100);
nicholas@2498 1630 }
nicholas@2498 1631 }
nicholas@2498 1632 }
nicholas@2498 1633 }
nicholas@2708 1634 }
nicholas@2615 1635
nicholas@2615 1636 this.progress = 0;
nicholas@2224 1637 this.status = 1;
nicholas@2617 1638 currentUrlIndex = 0;
nicholas@2617 1639 get(urls[0].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
nicholas@2498 1640 };
nicholas@2498 1641
nicholas@2498 1642 this.registerAudioObject = function (audioObject) {
nicholas@2224 1643 // Called by an audioObject to register to the buffer for use
nicholas@2224 1644 // First check if already in the register pool
nicholas@2708 1645 this.users.forEach(function (object) {
nicholas@2708 1646 if (audioObject.id == object.id) {
nicholas@2498 1647 return 0;
nicholas@2498 1648 }
nicholas@2708 1649 });
nicholas@2224 1650 this.users.push(audioObject);
nicholas@2498 1651 if (this.status == 3 || this.status == -1) {
nicholas@2224 1652 // The buffer is already ready, trigger bufferLoaded
nicholas@2224 1653 audioObject.bufferLoaded(this);
nicholas@2224 1654 }
nicholas@2224 1655 };
nicholas@2498 1656
nicholas@2498 1657 this.copyBuffer = function (preSilenceTime, postSilenceTime) {
nicholas@2224 1658 // Copies the entire bufferObj.
nicholas@2708 1659 if (preSilenceTime === undefined) {
nicholas@2498 1660 preSilenceTime = 0;
nicholas@2498 1661 }
nicholas@2708 1662 if (postSilenceTime === undefined) {
nicholas@2498 1663 postSilenceTime = 0;
nicholas@2498 1664 }
nicholas@2498 1665 var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate);
nicholas@2498 1666 var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate);
nicholas@2498 1667 var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples;
nicholas@2460 1668 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
nicholas@2708 1669 var c;
nicholas@2224 1670 // Now we can use some efficient background copy schemes if we are just padding the end
nicholas@2708 1671 if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") {
nicholas@2708 1672 for (c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2498 1673 copybuffer.copyToChannel(this.buffer.getChannelData(c), c);
nicholas@2224 1674 }
nicholas@2224 1675 } else {
nicholas@2708 1676 for (c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2224 1677 var src = this.buffer.getChannelData(c);
nicholas@2460 1678 var dst = copybuffer.getChannelData(c);
nicholas@2498 1679 for (var n = 0; n < src.length; n++)
nicholas@2498 1680 dst[n + preSilenceSamples] = src[n];
nicholas@2224 1681 }
nicholas@2224 1682 }
nicholas@2224 1683 // Copy in the rest of the buffer information
nicholas@2460 1684 copybuffer.lufs = this.buffer.lufs;
nicholas@2460 1685 copybuffer.playbackGain = this.buffer.playbackGain;
nicholas@2460 1686 return copybuffer;
nicholas@2708 1687 };
nicholas@2498 1688
nicholas@2498 1689 this.cropBuffer = function (startTime, stopTime) {
nicholas@2460 1690 // Copy and return the cropped buffer
nicholas@2498 1691 var start_sample = Math.floor(startTime * this.buffer.sampleRate);
nicholas@2498 1692 var stop_sample = Math.floor(stopTime * this.buffer.sampleRate);
nicholas@2460 1693 var newLength = stop_sample - start_sample;
nicholas@2460 1694 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
nicholas@2460 1695 // Now we can use some efficient background copy schemes if we are just padding the end
nicholas@2498 1696 for (var c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2460 1697 var buffer = this.buffer.getChannelData(c);
nicholas@2498 1698 var sub_frame = buffer.subarray(start_sample, stop_sample);
nicholas@2460 1699 if (typeof copybuffer.copyToChannel == "function") {
nicholas@2498 1700 copybuffer.copyToChannel(sub_frame, c);
nicholas@2460 1701 } else {
nicholas@2460 1702 var dst = copybuffer.getChannelData(c);
nicholas@2498 1703 for (var n = 0; n < newLength; n++)
nicholas@2505 1704 dst[n] = buffer[n + start_sample];
nicholas@2460 1705 }
nicholas@2460 1706 }
nicholas@2460 1707 return copybuffer;
nicholas@2708 1708 };
nicholas@2498 1709 };
nicholas@2498 1710
nicholas@2498 1711 this.loadPageData = function (page) {
nicholas@2224 1712 // Load the URL from pages
nicholas@2708 1713 function loadAudioElementData(element) {
nicholas@2224 1714 var URL = page.hostURL + element.url;
nicholas@2708 1715 var buffer = this.buffers.find(function (buffObj) {
nicholas@2708 1716 return buffObj.hasUrl(URL);
nicholas@2708 1717 });
nicholas@2708 1718 if (buffer === undefined) {
nicholas@2224 1719 buffer = new this.bufferObj();
nicholas@2617 1720 var urls = [{
nicholas@2617 1721 url: URL,
nicholas@2617 1722 sampleRate: element.sampleRate
nicholas@2617 1723 }];
nicholas@2615 1724 element.alternatives.forEach(function (e) {
nicholas@2617 1725 urls.push({
nicholas@2617 1726 url: e.url,
nicholas@2617 1727 sampleRate: e.sampleRate
nicholas@2617 1728 });
nicholas@2615 1729 });
nicholas@2617 1730 buffer.setUrls(urls);
nicholas@2617 1731 buffer.getMedia();
nicholas@2224 1732 this.buffers.push(buffer);
nicholas@2224 1733 }
nicholas@2224 1734 }
nicholas@2708 1735 page.audioElements.forEach(loadAudioElementData, this);
nicholas@2224 1736 };
nicholas@2498 1737
nicholas@2708 1738 function playNormal(id) {
nicholas@2708 1739 var playTime = audioContext.currentTime + 0.1;
nicholas@2708 1740 var stopTime = playTime + specification.crossFade;
nicholas@2708 1741 this.audioObjects.forEach(function (ao) {
nicholas@2708 1742 if (ao.id === id) {
nicholas@2942 1743 ao.setupPlayback();
nicholas@2942 1744 ao.bufferStart(playTime);
nicholas@2942 1745 ao.listenStart(playTime);
nicholas@2708 1746 } else {
nicholas@2942 1747 ao.listenStop(playTime);
nicholas@2942 1748 ao.bufferStop(stopTime);
nicholas@2708 1749 }
nicholas@2708 1750 });
nicholas@2708 1751 }
nicholas@2708 1752
nicholas@2942 1753 function playSync(id) {
nicholas@2708 1754 var playTime = audioContext.currentTime + 0.1;
nicholas@2708 1755 var stopTime = playTime + specification.crossFade;
nicholas@2708 1756 this.audioObjects.forEach(function (ao) {
nicholas@2942 1757 ao.setupPlayback();
nicholas@2942 1758 ao.bufferStart(playTime);
nicholas@2708 1759 if (ao.id === id) {
nicholas@2942 1760 ao.listenStart(playTime);
nicholas@2708 1761 } else {
nicholas@2942 1762 ao.listenStop(playTime);
nicholas@2708 1763 }
nicholas@2708 1764 });
nicholas@2708 1765 }
nicholas@2708 1766
nicholas@2498 1767 this.play = function (id) {
nicholas@2498 1768 // Start the timer and set the audioEngine state to playing (1)
nicholas@2708 1769 if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) {
nicholas@2708 1770 throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
nicholas@2498 1771 }
nicholas@2823 1772 var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays;
nicholas@2823 1773 if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) {
nicholas@2823 1774 interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times");
nicholas@2823 1775 return;
nicholas@2823 1776 }
nicholas@2708 1777 if (this.status === 1) {
nicholas@2498 1778 this.timer.startTest();
nicholas@2708 1779 interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
nicholas@2942 1780 if (this.synchPlayback) {
nicholas@2351 1781 // Traditional looped playback
nicholas@2942 1782 playSync.call(this, id);
nicholas@2708 1783 } else {
nicholas@2708 1784 if (this.bufferReady(id) === false) {
nicholas@2708 1785 console.log("Cannot play. Buffer not ready");
nicholas@2708 1786 return;
nicholas@2498 1787 }
nicholas@2708 1788 playNormal.call(this, id);
nicholas@2498 1789 }
nicholas@2498 1790 interfaceContext.playhead.start();
nicholas@2498 1791 }
nicholas@2498 1792 };
nicholas@2224 1793
nicholas@2498 1794 this.stop = function () {
nicholas@2498 1795 // Send stop and reset command to all playback buffers
nicholas@2498 1796 if (this.status == 1) {
nicholas@2498 1797 var setTime = audioContext.currentTime + 0.1;
nicholas@2708 1798 this.audioObjects.forEach(function (a) {
nicholas@2942 1799 a.listenStop(setTime);
nicholas@2942 1800 a.bufferStop(setTime);
nicholas@2708 1801 });
nicholas@2498 1802 interfaceContext.playhead.stop();
nicholas@2498 1803 }
nicholas@2498 1804 };
nicholas@2498 1805
nicholas@2498 1806 this.newTrack = function (element) {
nicholas@2498 1807 // Pull data from given URL into new audio buffer
nicholas@2498 1808 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
nicholas@2498 1809
nicholas@2498 1810 // Create the audioObject with ID of the new track length;
nicholas@2708 1811 var audioObjectId = this.audioObjects.length;
nicholas@2498 1812 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
nicholas@2498 1813
nicholas@2498 1814 // Check if audioObject buffer is currently stored by full URL
nicholas@2498 1815 var URL = testState.currentStateMap.hostURL + element.url;
nicholas@2708 1816 var buffer = this.buffers.find(function (buffObj) {
nicholas@2708 1817 return buffObj.hasUrl(URL);
nicholas@2708 1818 });
nicholas@2708 1819 if (buffer === undefined) {
nicholas@2498 1820 console.log("[WARN]: Buffer was not loaded in pre-test! " + URL);
nicholas@2498 1821 buffer = new this.bufferObj();
nicholas@2224 1822 this.buffers.push(buffer);
nicholas@2498 1823 buffer.getMedia(URL);
nicholas@2498 1824 }
nicholas@2498 1825 this.audioObjects[audioObjectId].specification = element;
nicholas@2498 1826 this.audioObjects[audioObjectId].url = URL;
nicholas@2498 1827 // Obtain store node
nicholas@2498 1828 var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
nicholas@2498 1829 for (var i = 0; i < aeNodes.length; i++) {
nicholas@2498 1830 if (aeNodes[i].getAttribute("ref") == element.id) {
nicholas@2498 1831 this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
nicholas@2498 1832 break;
nicholas@2498 1833 }
nicholas@2498 1834 }
nicholas@2224 1835 buffer.registerAudioObject(this.audioObjects[audioObjectId]);
nicholas@2498 1836 return this.audioObjects[audioObjectId];
nicholas@2498 1837 };
nicholas@2498 1838
nicholas@2498 1839 this.newTestPage = function (audioHolderObject, store) {
nicholas@2498 1840 this.pageStore = store;
nicholas@2351 1841 this.pageSpecification = audioHolderObject;
nicholas@2498 1842 this.status = 0;
nicholas@2498 1843 this.audioObjectsReady = false;
nicholas@2498 1844 this.metric.reset();
nicholas@2708 1845 this.buffers.forEach(function (buffer) {
nicholas@2708 1846 buffer.users = [];
nicholas@2708 1847 });
nicholas@2498 1848 this.audioObjects = [];
nicholas@2224 1849 this.timer = new timer();
nicholas@2224 1850 this.loopPlayback = audioHolderObject.loop;
nicholas@2351 1851 this.synchPlayback = audioHolderObject.synchronous;
nicholas@2955 1852 interfaceContext.keyboardInterface.resetKeyBindings();
nicholas@2498 1853 };
nicholas@2498 1854
nicholas@2498 1855 this.checkAllPlayed = function () {
nicholas@2708 1856 var arr = [];
nicholas@2498 1857 for (var id = 0; id < this.audioObjects.length; id++) {
nicholas@2708 1858 if (this.audioObjects[id].metric.wasListenedTo === false) {
nicholas@2498 1859 arr.push(this.audioObjects[id].id);
nicholas@2498 1860 }
nicholas@2498 1861 }
nicholas@2498 1862 return arr;
nicholas@2498 1863 };
nicholas@2498 1864
nicholas@2498 1865 this.checkAllReady = function () {
nicholas@2498 1866 var ready = true;
nicholas@2498 1867 for (var i = 0; i < this.audioObjects.length; i++) {
nicholas@2708 1868 if (this.audioObjects[i].state === 0) {
nicholas@2498 1869 // Track not ready
nicholas@2498 1870 console.log('WAIT -- audioObject ' + i + ' not ready yet!');
nicholas@2498 1871 ready = false;
nicholas@2708 1872 }
nicholas@2498 1873 }
nicholas@2498 1874 return ready;
nicholas@2498 1875 };
nicholas@2498 1876
nicholas@2498 1877 this.setSynchronousLoop = function () {
nicholas@2570 1878 // Pads the signals so they are all exactly the same duration
nicholas@2570 1879 // Get the duration of the longest signal.
nicholas@2570 1880 var duration = 0;
nicholas@2498 1881 var maxId;
nicholas@2498 1882 for (var i = 0; i < this.audioObjects.length; i++) {
nicholas@2570 1883 if (duration < this.audioObjects[i].buffer.buffer.duration) {
nicholas@2570 1884 duration = this.audioObjects[i].buffer.buffer.duration;
nicholas@2498 1885 maxId = i;
nicholas@2498 1886 }
nicholas@2498 1887 }
nicholas@2498 1888 // Extract the audio and zero-pad
nicholas@2708 1889 this.audioObjects.forEach(function (ao) {
nicholas@2570 1890 if (ao.buffer.buffer.duration !== duration) {
nicholas@2570 1891 ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration);
nicholas@2500 1892 }
nicholas@2708 1893 });
nicholas@2498 1894 };
nicholas@2498 1895
nicholas@2498 1896 this.bufferReady = function (id) {
nicholas@2498 1897 if (this.checkAllReady()) {
nicholas@2498 1898 if (this.synchPlayback) {
nicholas@2498 1899 this.setSynchronousLoop();
nicholas@2498 1900 }
nicholas@2460 1901 this.status = 1;
nicholas@2460 1902 return true;
nicholas@2460 1903 }
nicholas@2460 1904 return false;
nicholas@2224 1905 };
nicholas@2498 1906
nicholas@2224 1907 }
nicholas@2224 1908
nicholas@2224 1909 function audioObject(id) {
nicholas@2498 1910 // The main buffer object with common control nodes to the AudioEngine
nicholas@2498 1911
nicholas@2823 1912 var playCounter = 0;
nicholas@2823 1913
nicholas@2708 1914 this.specification = undefined;
nicholas@2498 1915 this.id = id;
nicholas@2498 1916 this.state = 0; // 0 - no data, 1 - ready
nicholas@2498 1917 this.url = null; // Hold the URL given for the output back to the results.
nicholas@2498 1918 this.metric = new metricTracker(this);
nicholas@2498 1919 this.storeDOM = null;
nicholas@2949 1920 this.playing = false;
nicholas@2498 1921
nicholas@2498 1922 // Bindings for GUI
nicholas@2498 1923 this.interfaceDOM = null;
nicholas@2498 1924 this.commentDOM = null;
nicholas@2498 1925
nicholas@2498 1926 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
nicholas@2498 1927 this.bufferNode = undefined;
nicholas@2498 1928 this.outputGain = audioContext.createGain();
nicholas@2942 1929 this.outputGain.gain.value = 0.0;
nicholas@2498 1930
nicholas@2498 1931 this.onplayGain = 1.0;
nicholas@2498 1932
nicholas@2498 1933 // Connect buffer to the audio graph
nicholas@2498 1934 this.outputGain.connect(audioEngineContext.outputGain);
nicholas@2508 1935 audioEngineContext.nullBufferSource.connect(this.outputGain);
nicholas@2498 1936
nicholas@2498 1937 // the audiobuffer is not designed for multi-start playback
nicholas@2498 1938 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
nicholas@2708 1939 this.buffer = undefined;
nicholas@2498 1940
nicholas@2498 1941 this.bufferLoaded = function (callee) {
nicholas@2498 1942 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
nicholas@2498 1943 // audioObject and trigger the interfaceDOM.enable() function for user feedback
nicholas@2224 1944 if (callee.status == -1) {
nicholas@2224 1945 // ERROR
nicholas@2224 1946 this.state = -1;
nicholas@2708 1947 if (this.interfaceDOM !== null) {
nicholas@2498 1948 this.interfaceDOM.error();
nicholas@2498 1949 }
nicholas@2224 1950 this.buffer = callee;
nicholas@2224 1951 return;
nicholas@2224 1952 }
nicholas@2224 1953 this.buffer = callee;
nicholas@2224 1954 var preSilenceTime = this.specification.preSilence || this.specification.parent.preSilence || specification.preSilence || 0.0;
nicholas@2224 1955 var postSilenceTime = this.specification.postSilence || this.specification.parent.postSilence || specification.postSilence || 0.0;
nicholas@2460 1956 var startTime = this.specification.startTime;
nicholas@2460 1957 var stopTime = this.specification.stopTime;
nicholas@2460 1958 var copybuffer = new callee.constructor();
nicholas@2500 1959
nicholas@2500 1960 copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration);
nicholas@2708 1961 if (preSilenceTime !== 0 || postSilenceTime !== 0) {
nicholas@2500 1962 copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime);
nicholas@2460 1963 }
nicholas@2500 1964
nicholas@2660 1965 copybuffer.buffer.lufs = callee.buffer.lufs;
nicholas@2500 1966 this.buffer = copybuffer;
nicholas@2498 1967
nicholas@2661 1968 var targetLUFS = this.specification.loudness || this.specification.parent.loudness || specification.loudness;
nicholas@2498 1969 if (typeof targetLUFS === "number" && isFinite(targetLUFS)) {
nicholas@2498 1970 this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
nicholas@2498 1971 } else {
nicholas@2498 1972 this.buffer.buffer.playbackGain = 1.0;
nicholas@2498 1973 }
nicholas@2708 1974 if (this.interfaceDOM !== null) {
nicholas@2498 1975 this.interfaceDOM.enable();
nicholas@2498 1976 }
nicholas@2498 1977 this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0);
nicholas@2498 1978 this.storeDOM.setAttribute('playGain', linearToDecibel(this.onplayGain));
nicholas@2460 1979 this.state = 1;
nicholas@2460 1980 audioEngineContext.bufferReady(id);
nicholas@2498 1981 };
nicholas@2498 1982
nicholas@2498 1983 this.bindInterface = function (interfaceObject) {
nicholas@2498 1984 this.interfaceDOM = interfaceObject;
nicholas@2498 1985 this.metric.initialise(interfaceObject.getValue());
nicholas@2498 1986 if (this.state == 1) {
nicholas@2498 1987 this.interfaceDOM.enable();
nicholas@2498 1988 } else if (this.state == -1) {
nicholas@2224 1989 // ERROR
nicholas@2224 1990 this.interfaceDOM.error();
nicholas@2224 1991 return;
nicholas@2224 1992 }
nicholas@2955 1993 var presentedId = interfaceObject.getPresentedId();
nicholas@2955 1994 this.storeDOM.setAttribute('presentedId', presentedId);
nicholas@2955 1995
nicholas@2955 1996 // Key-bindings
nicholas@2955 1997 if (presentedId.length == 1) {
nicholas@2955 1998 interfaceContext.keyboardInterface.registerKeyBinding(presentedId, this);
nicholas@2955 1999 }
nicholas@2498 2000 };
nicholas@2498 2001
nicholas@2942 2002 this.listenStart = function (setTime) {
nicholas@2949 2003 if (this.playing === false) {
nicholas@2942 2004 playCounter++;
nicholas@2942 2005 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, setTime);
nicholas@2942 2006 this.metric.startListening(audioEngineContext.timer.getTestTime());
nicholas@2942 2007 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
nicholas@2942 2008 this.interfaceDOM.startPlayback();
nicholas@2949 2009 this.playing = true;
nicholas@2942 2010 }
nicholas@2498 2011 };
nicholas@2498 2012
nicholas@2942 2013 this.listenStop = function (setTime) {
nicholas@2949 2014 if (this.playing === true) {
nicholas@2498 2015 this.outputGain.gain.linearRampToValueAtTime(0.0, setTime);
nicholas@2942 2016 this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
nicholas@2498 2017 }
nicholas@2224 2018 this.interfaceDOM.stopPlayback();
nicholas@2949 2019 this.playing = false;
nicholas@2498 2020 };
nicholas@2498 2021
nicholas@2942 2022 this.setupPlayback = function () {
nicholas@2708 2023 if (this.bufferNode === undefined && this.buffer.buffer !== undefined) {
nicholas@2498 2024 this.bufferNode = audioContext.createBufferSource();
nicholas@2498 2025 this.bufferNode.owner = this;
nicholas@2498 2026 this.bufferNode.connect(this.outputGain);
nicholas@2498 2027 this.bufferNode.buffer = this.buffer.buffer;
nicholas@2942 2028 if (audioEngineContext.loopPlayback) {
nicholas@2942 2029 this.bufferNode.loopStart = this.specification.startTime || 0;
nicholas@2942 2030 this.bufferNode.loopEnd = this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration;
nicholas@2942 2031 this.bufferNode.loop = true;
nicholas@2942 2032 }
nicholas@2498 2033 this.bufferNode.onended = function (event) {
nicholas@2498 2034 // Safari does not like using 'this' to reference the calling object!
nicholas@2498 2035 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
nicholas@2708 2036 if (event.currentTarget !== null) {
nicholas@2942 2037 event.currentTarget.owner.bufferStop(audioContext.currentTime + 0.1);
nicholas@2950 2038 event.currentTarget.owner.listenStop(audioContext.currentTime + 0.1);
nicholas@2224 2039 }
nicholas@2498 2040 };
nicholas@2942 2041 this.bufferNode.state = 0;
nicholas@2942 2042 }
nicholas@2942 2043 };
nicholas@2942 2044
nicholas@2942 2045 this.bufferStart = function (startTime) {
nicholas@2942 2046 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
n@2979 2047 if (this.bufferNode && this.bufferNode.state === 0) {
nicholas@2942 2048 this.bufferNode.state = 1;
n@2979 2049 if (this.bufferNode.loop === true) {
nicholas@2499 2050 this.bufferNode.start(startTime);
nicholas@2499 2051 } else {
nicholas@2499 2052 this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration);
nicholas@2499 2053 }
nicholas@2498 2054 }
nicholas@2498 2055 };
nicholas@2498 2056
nicholas@2942 2057 this.bufferStop = function (stopTime) {
nicholas@2224 2058 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
nicholas@2942 2059 if (this.bufferNode !== undefined && this.bufferNode.state > 0) {
nicholas@2498 2060 this.bufferNode.stop(stopTime);
nicholas@2498 2061 this.bufferNode = undefined;
nicholas@2498 2062 }
nicholas@2529 2063 this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime);
nicholas@2224 2064 this.interfaceDOM.stopPlayback();
nicholas@2498 2065 };
nicholas@2498 2066
nicholas@2498 2067 this.getCurrentPosition = function () {
nicholas@2498 2068 var time = audioEngineContext.timer.getTestTime();
nicholas@2708 2069 if (this.bufferNode !== undefined) {
nicholas@2498 2070 var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
nicholas@2498 2071 if (isNaN(position)) {
nicholas@2498 2072 return 0;
nicholas@2498 2073 }
nicholas@2224 2074 return position;
nicholas@2498 2075 } else {
nicholas@2498 2076 return 0;
nicholas@2498 2077 }
nicholas@2498 2078 };
nicholas@2498 2079
nicholas@2498 2080 this.exportXMLDOM = function () {
nicholas@2498 2081 var file = storage.document.createElement('file');
nicholas@2498 2082 file.setAttribute('sampleRate', this.buffer.buffer.sampleRate);
nicholas@2498 2083 file.setAttribute('channels', this.buffer.buffer.numberOfChannels);
nicholas@2498 2084 file.setAttribute('sampleCount', this.buffer.buffer.length);
nicholas@2498 2085 file.setAttribute('duration', this.buffer.buffer.duration);
nicholas@2498 2086 this.storeDOM.appendChild(file);
nicholas@2498 2087 if (this.specification.type != 'outside-reference') {
nicholas@2498 2088 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
nicholas@2708 2089 if (interfaceXML !== null) {
nicholas@2708 2090 if (interfaceXML.length === undefined) {
nicholas@2498 2091 this.storeDOM.appendChild(interfaceXML);
nicholas@2498 2092 } else {
nicholas@2498 2093 for (var i = 0; i < interfaceXML.length; i++) {
nicholas@2498 2094 this.storeDOM.appendChild(interfaceXML[i]);
nicholas@2498 2095 }
nicholas@2498 2096 }
nicholas@2498 2097 }
nicholas@2708 2098 if (this.commentDOM !== null) {
nicholas@2498 2099 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
nicholas@2498 2100 }
nicholas@2498 2101 }
nicholas@2708 2102 this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]);
nicholas@2498 2103 };
nicholas@2823 2104
nicholas@2823 2105 Object.defineProperties(this, {
nicholas@2823 2106 "numberOfPlays": {
nicholas@2823 2107 'get': function () {
nicholas@2823 2108 return playCounter;
nicholas@2823 2109 },
nicholas@2823 2110 'set': function () {
nicholas@2823 2111 return playCounter;
nicholas@2823 2112 }
nicholas@2823 2113 }
nicholas@2823 2114 });
nicholas@2224 2115 }
nicholas@2224 2116
nicholas@2498 2117 function timer() {
nicholas@2498 2118 /* Timer object used in audioEngine to keep track of session timings
nicholas@2498 2119 * Uses the timer of the web audio API, so sample resolution
nicholas@2498 2120 */
nicholas@2498 2121 this.testStarted = false;
nicholas@2498 2122 this.testStartTime = 0;
nicholas@2498 2123 this.testDuration = 0;
nicholas@2498 2124 this.minimumTestTime = 0; // No minimum test time
nicholas@2498 2125 this.startTest = function () {
nicholas@2708 2126 if (this.testStarted === false) {
nicholas@2498 2127 this.testStartTime = audioContext.currentTime;
nicholas@2498 2128 this.testStarted = true;
nicholas@2498 2129 this.updateTestTime();
nicholas@2498 2130 audioEngineContext.metric.initialiseTest();
nicholas@2498 2131 }
nicholas@2498 2132 };
nicholas@2498 2133 this.stopTest = function () {
nicholas@2498 2134 if (this.testStarted) {
nicholas@2498 2135 this.testDuration = this.getTestTime();
nicholas@2498 2136 this.testStarted = false;
nicholas@2498 2137 } else {
nicholas@2498 2138 console.log('ERR: Test tried to end before beginning');
nicholas@2498 2139 }
nicholas@2498 2140 };
nicholas@2498 2141 this.updateTestTime = function () {
nicholas@2498 2142 if (this.testStarted) {
nicholas@2498 2143 this.testDuration = audioContext.currentTime - this.testStartTime;
nicholas@2498 2144 }
nicholas@2498 2145 };
nicholas@2498 2146 this.getTestTime = function () {
nicholas@2498 2147 this.updateTestTime();
nicholas@2498 2148 return this.testDuration;
nicholas@2498 2149 };
nicholas@2224 2150 }
nicholas@2224 2151
nicholas@2498 2152 function sessionMetrics(engine, specification) {
nicholas@2498 2153 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
nicholas@2498 2154 */
nicholas@2498 2155 this.engine = engine;
nicholas@2498 2156 this.lastClicked = -1;
nicholas@2498 2157 this.data = -1;
nicholas@2498 2158 this.reset = function () {
nicholas@2498 2159 this.lastClicked = -1;
nicholas@2498 2160 this.data = -1;
nicholas@2498 2161 };
nicholas@2498 2162
nicholas@2498 2163 this.enableElementInitialPosition = false;
nicholas@2498 2164 this.enableElementListenTracker = false;
nicholas@2498 2165 this.enableElementTimer = false;
nicholas@2498 2166 this.enableElementTracker = false;
nicholas@2498 2167 this.enableFlagListenedTo = false;
nicholas@2498 2168 this.enableFlagMoved = false;
nicholas@2498 2169 this.enableTestTimer = false;
nicholas@2498 2170 // Obtain the metrics enabled
nicholas@2498 2171 for (var i = 0; i < specification.metrics.enabled.length; i++) {
nicholas@2498 2172 var node = specification.metrics.enabled[i];
nicholas@2498 2173 switch (node) {
nicholas@2498 2174 case 'testTimer':
nicholas@2498 2175 this.enableTestTimer = true;
nicholas@2498 2176 break;
nicholas@2498 2177 case 'elementTimer':
nicholas@2498 2178 this.enableElementTimer = true;
nicholas@2498 2179 break;
nicholas@2498 2180 case 'elementTracker':
nicholas@2498 2181 this.enableElementTracker = true;
nicholas@2498 2182 break;
nicholas@2498 2183 case 'elementListenTracker':
nicholas@2498 2184 this.enableElementListenTracker = true;
nicholas@2498 2185 break;
nicholas@2498 2186 case 'elementInitialPosition':
nicholas@2498 2187 this.enableElementInitialPosition = true;
nicholas@2498 2188 break;
nicholas@2498 2189 case 'elementFlagListenedTo':
nicholas@2498 2190 this.enableFlagListenedTo = true;
nicholas@2498 2191 break;
nicholas@2498 2192 case 'elementFlagMoved':
nicholas@2498 2193 this.enableFlagMoved = true;
nicholas@2498 2194 break;
nicholas@2498 2195 case 'elementFlagComments':
nicholas@2498 2196 this.enableFlagComments = true;
nicholas@2498 2197 break;
nicholas@2498 2198 }
nicholas@2498 2199 }
nicholas@2498 2200 this.initialiseTest = function () {};
nicholas@2224 2201 }
nicholas@2224 2202
nicholas@2498 2203 function metricTracker(caller) {
nicholas@2498 2204 /* Custom object to track and collect metric data
nicholas@2498 2205 * Used only inside the audioObjects object.
nicholas@2498 2206 */
nicholas@2498 2207
nicholas@2498 2208 this.listenedTimer = 0;
nicholas@2498 2209 this.listenStart = 0;
nicholas@2498 2210 this.listenHold = false;
nicholas@2498 2211 this.initialPosition = -1;
nicholas@2498 2212 this.movementTracker = [];
nicholas@2498 2213 this.listenTracker = [];
nicholas@2498 2214 this.wasListenedTo = false;
nicholas@2498 2215 this.wasMoved = false;
nicholas@2498 2216 this.hasComments = false;
nicholas@2498 2217 this.parent = caller;
nicholas@2498 2218
nicholas@2498 2219 this.initialise = function (position) {
nicholas@2498 2220 if (this.initialPosition == -1) {
nicholas@2498 2221 this.initialPosition = position;
nicholas@2498 2222 this.moved(0, position);
nicholas@2498 2223 }
nicholas@2498 2224 };
nicholas@2498 2225
nicholas@2498 2226 this.moved = function (time, position) {
nicholas@3064 2227 var last;
nicholas@2498 2228 if (time > 0) {
nicholas@2498 2229 this.wasMoved = true;
nicholas@2498 2230 }
nicholas@3064 2231 // Get the last entry
nicholas@3064 2232 if (this.movementTracker.length > 0) {
nicholas@3064 2233 last = this.movementTracker[this.movementTracker.length - 1];
nicholas@3064 2234 } else {
nicholas@3064 2235 last = -1;
nicholas@3064 2236 }
nicholas@3064 2237 if (position != last[1]) {
nicholas@3064 2238 this.movementTracker[this.movementTracker.length] = [time, position];
nicholas@3064 2239 }
nicholas@2498 2240 };
nicholas@2498 2241
nicholas@2498 2242 this.startListening = function (time) {
nicholas@2708 2243 if (this.listenHold === false) {
nicholas@2498 2244 this.wasListenedTo = true;
nicholas@2498 2245 this.listenStart = time;
nicholas@2498 2246 this.listenHold = true;
nicholas@2498 2247
nicholas@2498 2248 var evnt = document.createElement('event');
nicholas@2498 2249 var testTime = document.createElement('testTime');
nicholas@2498 2250 testTime.setAttribute('start', time);
nicholas@2498 2251 var bufferTime = document.createElement('bufferTime');
nicholas@2498 2252 bufferTime.setAttribute('start', this.parent.getCurrentPosition());
nicholas@2498 2253 evnt.appendChild(testTime);
nicholas@2498 2254 evnt.appendChild(bufferTime);
nicholas@2498 2255 this.listenTracker.push(evnt);
nicholas@2498 2256
nicholas@2498 2257 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2258 }
nicholas@2498 2259 };
nicholas@2498 2260
nicholas@2498 2261 this.stopListening = function (time, bufferStopTime) {
nicholas@2708 2262 if (this.listenHold === true) {
nicholas@2498 2263 var diff = time - this.listenStart;
nicholas@2498 2264 this.listenedTimer += (diff);
nicholas@2498 2265 this.listenStart = 0;
nicholas@2498 2266 this.listenHold = false;
nicholas@2498 2267
nicholas@2498 2268 var evnt = this.listenTracker[this.listenTracker.length - 1];
nicholas@2498 2269 var testTime = evnt.getElementsByTagName('testTime')[0];
nicholas@2498 2270 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
nicholas@2498 2271 testTime.setAttribute('stop', time);
nicholas@2708 2272 if (bufferStopTime === undefined) {
nicholas@2498 2273 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
nicholas@2498 2274 } else {
nicholas@2498 2275 bufferTime.setAttribute('stop', bufferStopTime);
nicholas@2498 2276 }
nicholas@2498 2277 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2278 }
nicholas@2498 2279 };
nicholas@2498 2280
nicholas@2708 2281 function exportElementTimer(parentElement) {
nicholas@2708 2282 var mElementTimer = storage.document.createElement('metricresult');
nicholas@2708 2283 mElementTimer.setAttribute('name', 'enableElementTimer');
nicholas@2708 2284 mElementTimer.textContent = this.listenedTimer;
nicholas@2708 2285 parentElement.appendChild(mElementTimer);
nicholas@2708 2286 return mElementTimer;
nicholas@2708 2287 }
nicholas@2708 2288
nicholas@2708 2289 function exportElementTrack(parentElement) {
nicholas@2708 2290 var elementTrackerFull = storage.document.createElement('metricresult');
nicholas@2708 2291 elementTrackerFull.setAttribute('name', 'elementTrackerFull');
nicholas@2708 2292 for (var k = 0; k < this.movementTracker.length; k++) {
nicholas@2708 2293 var timePos = storage.document.createElement('movement');
nicholas@2708 2294 timePos.setAttribute("time", this.movementTracker[k][0]);
nicholas@2708 2295 timePos.setAttribute("value", this.movementTracker[k][1]);
nicholas@2708 2296 elementTrackerFull.appendChild(timePos);
nicholas@2708 2297 }
nicholas@2708 2298 parentElement.appendChild(elementTrackerFull);
nicholas@2708 2299 return elementTrackerFull;
nicholas@2708 2300 }
nicholas@2708 2301
nicholas@2708 2302 function exportElementListenTracker(parentElement) {
nicholas@2708 2303 var elementListenTracker = storage.document.createElement('metricresult');
nicholas@2708 2304 elementListenTracker.setAttribute('name', 'elementListenTracker');
nicholas@2708 2305 for (var k = 0; k < this.listenTracker.length; k++) {
nicholas@2708 2306 elementListenTracker.appendChild(this.listenTracker[k]);
nicholas@2708 2307 }
nicholas@2708 2308 parentElement.appendChild(elementListenTracker);
nicholas@2708 2309 return elementListenTracker;
nicholas@2708 2310 }
nicholas@2708 2311
nicholas@2708 2312 function exportElementInitialPosition(parentElement) {
nicholas@2708 2313 var elementInitial = storage.document.createElement('metricresult');
nicholas@2708 2314 elementInitial.setAttribute('name', 'elementInitialPosition');
nicholas@2708 2315 elementInitial.textContent = this.initialPosition;
nicholas@2708 2316 parentElement.appendChild(elementInitial);
nicholas@2708 2317 return elementInitial;
nicholas@2708 2318 }
nicholas@2708 2319
nicholas@2708 2320 function exportFlagListenedTo(parentElement) {
nicholas@2708 2321 var flagListenedTo = storage.document.createElement('metricresult');
nicholas@2708 2322 flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
nicholas@2708 2323 flagListenedTo.textContent = this.wasListenedTo;
nicholas@2708 2324 parentElement.appendChild(flagListenedTo);
nicholas@2708 2325 return flagListenedTo;
nicholas@2708 2326 }
nicholas@2708 2327
nicholas@2708 2328 function exportFlagMoved(parentElement) {
nicholas@2708 2329 var flagMoved = storage.document.createElement('metricresult');
nicholas@2708 2330 flagMoved.setAttribute('name', 'elementFlagMoved');
nicholas@2708 2331 flagMoved.textContent = this.wasMoved;
nicholas@2708 2332 parentElement.appendChild(flagMoved);
nicholas@2708 2333 return flagMoved;
nicholas@2708 2334 }
nicholas@2708 2335
nicholas@2708 2336 function exportFlagComments(parentElement) {
nicholas@2708 2337 var flagComments = storage.document.createElement('metricresult');
nicholas@2708 2338 flagComments.setAttribute('name', 'elementFlagComments');
nicholas@2708 2339 if (this.parent.commentDOM === null) {
nicholas@2708 2340 flagComments.textContent = 'false';
nicholas@2708 2341 } else if (this.parent.commentDOM.textContent.length === 0) {
nicholas@2708 2342 flagComments.textContent = 'false';
nicholas@2708 2343 } else {
nicholas@2708 2344 flagComments.textContet = 'true';
nicholas@2708 2345 }
nicholas@2708 2346 parentElement.appendChild(flagComments);
nicholas@2708 2347 return flagComments;
nicholas@2708 2348 }
nicholas@2708 2349
nicholas@2708 2350 this.exportXMLDOM = function (parentElement) {
nicholas@2708 2351 var elems = [];
nicholas@2498 2352 if (audioEngineContext.metric.enableElementTimer) {
nicholas@2708 2353 elems.push(exportElementTimer.call(this, parentElement));
nicholas@2498 2354 }
nicholas@2498 2355 if (audioEngineContext.metric.enableElementTracker) {
nicholas@2708 2356 elems.push(exportElementTrack.call(this, parentElement));
nicholas@2498 2357 }
nicholas@2498 2358 if (audioEngineContext.metric.enableElementListenTracker) {
nicholas@2708 2359 elems.push(exportElementListenTracker.call(this, parentElement));
nicholas@2498 2360 }
nicholas@2498 2361 if (audioEngineContext.metric.enableElementInitialPosition) {
nicholas@2708 2362 elems.push(exportElementInitialPosition.call(this, parentElement));
nicholas@2498 2363 }
nicholas@2498 2364 if (audioEngineContext.metric.enableFlagListenedTo) {
nicholas@2708 2365 elems.push(exportFlagListenedTo.call(this, parentElement));
nicholas@2498 2366 }
nicholas@2498 2367 if (audioEngineContext.metric.enableFlagMoved) {
nicholas@2708 2368 elems.push(exportFlagMoved.call(this, parentElement));
nicholas@2498 2369 }
nicholas@2498 2370 if (audioEngineContext.metric.enableFlagComments) {
nicholas@2708 2371 elems.push(exportFlagComments.call(this, parentElement));
nicholas@2498 2372 }
nicholas@2708 2373 return elems;
nicholas@2498 2374 };
nicholas@2224 2375 }
nicholas@2498 2376
nicholas@2224 2377 function Interface(specificationObject) {
nicholas@2498 2378 // This handles the bindings between the interface and the audioEngineContext;
nicholas@2498 2379 this.specification = specificationObject;
nicholas@2498 2380 this.insertPoint = document.getElementById("topLevelBody");
nicholas@2498 2381
nicholas@2498 2382 this.newPage = function (audioHolderObject, store) {
nicholas@2498 2383 audioEngineContext.newTestPage(audioHolderObject, store);
nicholas@2498 2384 interfaceContext.commentBoxes.deleteCommentBoxes();
nicholas@2498 2385 interfaceContext.deleteCommentQuestions();
nicholas@2498 2386 loadTest(audioHolderObject, store);
nicholas@2498 2387 };
nicholas@2498 2388
nicholas@2955 2389 this.keyboardInterface = (function () {
nicholas@2955 2390 var keyboardInterfaceController = {
nicholas@2955 2391 keys: [],
nicholas@2955 2392 registerKeyBinding: function (key, audioObject) {
nicholas@2955 2393 if (typeof key != "string" || key.length != 1) {
nicholas@2955 2394 throw ("Key must be a singular character");
nicholas@2955 2395 }
nicholas@2955 2396 var included = this.keys.findIndex(function (k) {
n@2979 2397 return k.key == key;
nicholas@2955 2398 }) >= 0;
nicholas@2955 2399 if (included) {
nicholas@2955 2400 throw ("Key " + key + " already bounded!");
nicholas@2955 2401 }
nicholas@2955 2402 this.keys.push({
nicholas@2955 2403 key: key,
nicholas@2955 2404 audioObject: audioObject
nicholas@2955 2405 });
nicholas@2955 2406 return true;
nicholas@2955 2407 },
nicholas@2955 2408 deregisterKeyBinding: function (key) {
nicholas@2955 2409 var index = this.keys.findIndex(function (k) {
nicholas@2955 2410 return k.key == key;
nicholas@2955 2411 });
nicholas@2955 2412 if (index == -1) {
nicholas@2955 2413 throw ("Key " + key + " not bounded!");
nicholas@2955 2414 }
nicholas@2955 2415 this.keys.splice(index, 1);
nicholas@2955 2416 return true;
nicholas@2955 2417 },
nicholas@2955 2418 resetKeyBindings: function () {
nicholas@2955 2419 this.keys = [];
nicholas@2955 2420 },
nicholas@2955 2421 handleEvent: function (e) {
nicholas@2955 2422 function isPlaying() {
nicholas@2955 2423 return audioEngineContext.audioObjects.some(function (a) {
nicholas@2955 2424 return a.playing;
nicholas@2955 2425 });
nicholas@2955 2426 }
nicholas@2955 2427
nicholas@2955 2428 function keypress(key) {
nicholas@2955 2429 var index = this.keys.findIndex(function (k) {
n@2979 2430 return k.key == key;
nicholas@2955 2431 });
nicholas@2955 2432 if (index >= 0) {
nicholas@2955 2433 audioEngineContext.play(this.keys[index].audioObject.id);
nicholas@2955 2434 }
nicholas@2955 2435 }
n@2965 2436
n@2965 2437 function trackCommentFocus() {
n@2965 2438 return document.activeElement.className.indexOf("trackComment") >= 0;
n@2965 2439 }
nicholas@2955 2440 if (e.key === " ") {
n@2979 2441 if (isPlaying() && trackCommentFocus() === false) {
n@2965 2442 e.preventDefault();
nicholas@2955 2443 audioEngineContext.stop();
nicholas@2955 2444 }
nicholas@2955 2445 } else {
nicholas@2955 2446 keypress.call(this, e.key);
nicholas@2955 2447 }
n@2965 2448 console.log(e);
nicholas@2955 2449 }
nicholas@2955 2450 };
nicholas@2955 2451 document.addEventListener("keydown", keyboardInterfaceController, false);
nicholas@2955 2452 return keyboardInterfaceController;
nicholas@2955 2453 })();
nicholas@2955 2454
nicholas@2498 2455 // Bounded by interface!!
nicholas@2498 2456 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
nicholas@2498 2457 // For example, APE returns the slider position normalised in a <value> tag.
nicholas@2498 2458 this.interfaceObjects = [];
nicholas@2498 2459 this.interfaceObject = function () {};
nicholas@2498 2460
nicholas@2498 2461 this.resizeWindow = function (event) {
nicholas@2498 2462 popup.resize(event);
nicholas@2352 2463 this.volume.resize();
nicholas@2360 2464 this.lightbox.resize();
n@2718 2465 this.commentBoxes.boxes.forEach(function (elem) {
nicholas@2708 2466 elem.resize();
nicholas@2708 2467 });
nicholas@2708 2468 this.commentQuestions.forEach(function (elem) {
nicholas@2708 2469 elem.resize();
nicholas@2708 2470 });
nicholas@2498 2471 try {
nicholas@2498 2472 resizeWindow(event);
nicholas@2498 2473 } catch (err) {
nicholas@2498 2474 console.log("Warning - Interface does not have Resize option");
nicholas@2498 2475 console.log(err);
nicholas@2498 2476 }
nicholas@2498 2477 };
nicholas@2498 2478
nicholas@2498 2479 this.returnNavigator = function () {
nicholas@2498 2480 var node = storage.document.createElement("navigator");
nicholas@2498 2481 var platform = storage.document.createElement("platform");
nicholas@2498 2482 platform.textContent = navigator.platform;
nicholas@2498 2483 var vendor = storage.document.createElement("vendor");
nicholas@2498 2484 vendor.textContent = navigator.vendor;
nicholas@2498 2485 var userAgent = storage.document.createElement("uagent");
nicholas@2498 2486 userAgent.textContent = navigator.userAgent;
nicholas@2224 2487 var screen = storage.document.createElement("window");
nicholas@2498 2488 screen.setAttribute('innerWidth', window.innerWidth);
nicholas@2498 2489 screen.setAttribute('innerHeight', window.innerHeight);
nicholas@2498 2490 node.appendChild(platform);
nicholas@2498 2491 node.appendChild(vendor);
nicholas@2498 2492 node.appendChild(userAgent);
nicholas@2224 2493 node.appendChild(screen);
nicholas@2498 2494 return node;
nicholas@2498 2495 };
nicholas@2498 2496
nicholas@2498 2497 this.returnDateNode = function () {
nicholas@2224 2498 // Create an XML Node for the Date and Time a test was conducted
nicholas@2224 2499 // Structure is
nicholas@2224 2500 // <datetime>
nicholas@2224 2501 // <date year="##" month="##" day="##">DD/MM/YY</date>
nicholas@2224 2502 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
nicholas@2224 2503 // </datetime>
nicholas@2224 2504 var dateTime = new Date();
nicholas@2224 2505 var hold = storage.document.createElement("datetime");
nicholas@2224 2506 var date = storage.document.createElement("date");
nicholas@2224 2507 var time = storage.document.createElement("time");
nicholas@2498 2508 date.setAttribute('year', dateTime.getFullYear());
nicholas@2498 2509 date.setAttribute('month', dateTime.getMonth() + 1);
nicholas@2498 2510 date.setAttribute('day', dateTime.getDate());
nicholas@2498 2511 time.setAttribute('hour', dateTime.getHours());
nicholas@2498 2512 time.setAttribute('minute', dateTime.getMinutes());
nicholas@2498 2513 time.setAttribute('secs', dateTime.getSeconds());
nicholas@2498 2514
nicholas@2224 2515 hold.appendChild(date);
nicholas@2224 2516 hold.appendChild(time);
nicholas@2224 2517 return hold;
nicholas@2224 2518
nicholas@2708 2519 };
nicholas@2498 2520
nicholas@2360 2521 this.lightbox = {
nicholas@2360 2522 parent: this,
nicholas@2360 2523 root: document.createElement("div"),
nicholas@2360 2524 content: document.createElement("div"),
nicholas@2360 2525 accept: document.createElement("button"),
nicholas@2360 2526 blanker: document.createElement("div"),
nicholas@2498 2527 post: function (type, message) {
nicholas@2498 2528 switch (type) {
nicholas@2360 2529 case "Error":
nicholas@2360 2530 this.content.className = "lightbox-error";
nicholas@2360 2531 break;
nicholas@2360 2532 case "Warning":
nicholas@2360 2533 this.content.className = "lightbox-warning";
nicholas@2360 2534 break;
nicholas@2360 2535 default:
nicholas@2360 2536 this.content.className = "lightbox-message";
nicholas@2360 2537 break;
nicholas@2360 2538 }
nicholas@2360 2539 var msg = document.createElement("p");
nicholas@2360 2540 msg.textContent = message;
nicholas@2360 2541 this.content.appendChild(msg);
nicholas@2360 2542 this.show();
nicholas@2360 2543 },
nicholas@2498 2544 show: function () {
nicholas@2360 2545 this.root.style.visibility = "visible";
nicholas@2360 2546 this.blanker.style.visibility = "visible";
n@2914 2547 this.accept.focus();
nicholas@2360 2548 },
nicholas@2498 2549 clear: function () {
nicholas@2360 2550 this.root.style.visibility = "";
nicholas@2360 2551 this.blanker.style.visibility = "";
nicholas@2360 2552 this.content.textContent = "";
nicholas@2360 2553 },
nicholas@2498 2554 handleEvent: function (event) {
nicholas@2360 2555 if (event.currentTarget == this.accept) {
nicholas@2360 2556 this.clear();
nicholas@2360 2557 }
nicholas@2360 2558 },
nicholas@2498 2559 resize: function (event) {
nicholas@2498 2560 this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
n@2915 2561 },
n@2915 2562 isVisible: function () {
n@2915 2563 return this.root.style.visibility == "visible";
nicholas@2360 2564 }
nicholas@2708 2565 };
nicholas@2498 2566
nicholas@2360 2567 this.lightbox.root.appendChild(this.lightbox.content);
nicholas@2360 2568 this.lightbox.root.appendChild(this.lightbox.accept);
nicholas@2360 2569 this.lightbox.root.className = "popupHolder";
nicholas@2360 2570 this.lightbox.root.id = "lightbox-root";
nicholas@2360 2571 this.lightbox.accept.className = "popupButton";
nicholas@2360 2572 this.lightbox.accept.style.bottom = "10px";
nicholas@2360 2573 this.lightbox.accept.textContent = "OK";
nicholas@2360 2574 this.lightbox.accept.style.left = "237.5px";
nicholas@2498 2575 this.lightbox.accept.addEventListener("click", this.lightbox);
nicholas@2360 2576 this.lightbox.blanker.className = "testHalt";
nicholas@2360 2577 this.lightbox.blanker.id = "lightbox-blanker";
nicholas@2360 2578 document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
nicholas@2360 2579 document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
nicholas@2498 2580
nicholas@2712 2581 this.commentBoxes = (function () {
nicholas@2712 2582 var commentBoxes = {};
nicholas@2712 2583 commentBoxes.boxes = [];
nicholas@2712 2584 commentBoxes.injectPoint = null;
nicholas@2712 2585 commentBoxes.elementCommentBox = function (audioObject) {
nicholas@2224 2586 var element = audioObject.specification;
nicholas@2224 2587 this.audioObject = audioObject;
nicholas@2224 2588 this.id = audioObject.id;
nicholas@2224 2589 var audioHolderObject = audioObject.specification.parent;
nicholas@2224 2590 // Create document objects to hold the comment boxes
nicholas@2224 2591 this.trackComment = document.createElement('div');
nicholas@2224 2592 this.trackComment.className = 'comment-div';
nicholas@2498 2593 this.trackComment.id = 'comment-div-' + audioObject.id;
nicholas@2224 2594 // Create a string next to each comment asking for a comment
nicholas@2224 2595 this.trackString = document.createElement('span');
nicholas@2498 2596 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId();
nicholas@2224 2597 // Create the HTML5 comment box 'textarea'
nicholas@2224 2598 this.trackCommentBox = document.createElement('textarea');
nicholas@2224 2599 this.trackCommentBox.rows = '4';
nicholas@2224 2600 this.trackCommentBox.cols = '100';
nicholas@2498 2601 this.trackCommentBox.name = 'trackComment' + audioObject.id;
nicholas@2224 2602 this.trackCommentBox.className = 'trackComment';
nicholas@2224 2603 var br = document.createElement('br');
nicholas@2224 2604 // Add to the holder.
nicholas@2224 2605 this.trackComment.appendChild(this.trackString);
nicholas@2224 2606 this.trackComment.appendChild(br);
nicholas@2224 2607 this.trackComment.appendChild(this.trackCommentBox);
nicholas@2224 2608
nicholas@2498 2609 this.exportXMLDOM = function () {
nicholas@2224 2610 var root = document.createElement('comment');
nicholas@2224 2611 var question = document.createElement('question');
nicholas@2224 2612 question.textContent = this.trackString.textContent;
nicholas@2224 2613 var response = document.createElement('response');
nicholas@2224 2614 response.textContent = this.trackCommentBox.value;
nicholas@2498 2615 console.log("Comment frag-" + this.id + ": " + response.textContent);
nicholas@2224 2616 root.appendChild(question);
nicholas@2224 2617 root.appendChild(response);
nicholas@2224 2618 return root;
nicholas@2224 2619 };
nicholas@2498 2620 this.resize = function () {
nicholas@2498 2621 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2622 if (boxwidth >= 600) {
nicholas@2224 2623 boxwidth = 600;
nicholas@2498 2624 } else if (boxwidth < 400) {
nicholas@2224 2625 boxwidth = 400;
nicholas@2224 2626 }
nicholas@2498 2627 this.trackComment.style.width = boxwidth + "px";
nicholas@2498 2628 this.trackCommentBox.style.width = boxwidth - 6 + "px";
nicholas@2224 2629 };
nicholas@2224 2630 this.resize();
nicholas@2725 2631 this.highlight = function (state) {
nicholas@2725 2632 if (state === true) {
nicholas@2725 2633 $(this.trackComment).addClass("comment-box-playing");
nicholas@2725 2634 } else {
nicholas@2725 2635 $(this.trackComment).removeClass("comment-box-playing");
nicholas@2725 2636 }
nicholas@2725 2637 };
nicholas@2224 2638 };
nicholas@2712 2639 commentBoxes.createCommentBox = function (audioObject) {
nicholas@2224 2640 var node = new this.elementCommentBox(audioObject);
nicholas@2224 2641 this.boxes.push(node);
nicholas@2224 2642 audioObject.commentDOM = node;
nicholas@2224 2643 return node;
nicholas@2224 2644 };
nicholas@2712 2645 commentBoxes.sortCommentBoxes = function () {
nicholas@2498 2646 this.boxes.sort(function (a, b) {
nicholas@2498 2647 return a.id - b.id;
nicholas@2498 2648 });
nicholas@2224 2649 };
nicholas@2224 2650
nicholas@2712 2651 commentBoxes.showCommentBoxes = function (inject, sort) {
nicholas@2224 2652 this.injectPoint = inject;
nicholas@2498 2653 if (sort) {
nicholas@2498 2654 this.sortCommentBoxes();
nicholas@2498 2655 }
nicholas@2708 2656 this.boxes.forEach(function (box) {
nicholas@2224 2657 inject.appendChild(box.trackComment);
nicholas@2708 2658 });
nicholas@2224 2659 };
nicholas@2224 2660
nicholas@2712 2661 commentBoxes.deleteCommentBoxes = function () {
nicholas@2708 2662 if (this.injectPoint !== null) {
nicholas@2708 2663 this.boxes.forEach(function (box) {
nicholas@2224 2664 this.injectPoint.removeChild(box.trackComment);
nicholas@2708 2665 }, this);
nicholas@2224 2666 this.injectPoint = null;
nicholas@2224 2667 }
nicholas@2224 2668 this.boxes = [];
nicholas@2224 2669 };
nicholas@2725 2670 commentBoxes.highlightById = function (id) {
nicholas@2725 2671 if (id === undefined || typeof id !== "number" || id >= this.boxes.length) {
nicholas@2725 2672 console.log("Error - Invalid id");
nicholas@2725 2673 id = -1;
nicholas@2725 2674 }
nicholas@2725 2675 this.boxes.forEach(function (a) {
nicholas@2725 2676 if (a.id === id) {
nicholas@2725 2677 a.highlight(true);
nicholas@2725 2678 } else {
nicholas@2725 2679 a.highlight(false);
nicholas@2725 2680 }
nicholas@2725 2681 });
nicholas@2725 2682 };
nicholas@2712 2683 return commentBoxes;
nicholas@2712 2684 })();
nicholas@2498 2685
nicholas@2498 2686 this.commentQuestions = [];
nicholas@2498 2687
nicholas@2498 2688 this.commentBox = function (commentQuestion) {
nicholas@2498 2689 this.specification = commentQuestion;
nicholas@2498 2690 // Create document objects to hold the comment boxes
nicholas@2498 2691 this.holder = document.createElement('div');
nicholas@2498 2692 this.holder.className = 'comment-div';
nicholas@2498 2693 // Create a string next to each comment asking for a comment
nicholas@2498 2694 this.string = document.createElement('span');
nicholas@2498 2695 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2696 // Create the HTML5 comment box 'textarea'
nicholas@2498 2697 this.textArea = document.createElement('textarea');
nicholas@2498 2698 this.textArea.rows = '4';
nicholas@2498 2699 this.textArea.cols = '100';
nicholas@2498 2700 this.textArea.className = 'trackComment';
nicholas@2498 2701 var br = document.createElement('br');
nicholas@2498 2702 // Add to the holder.
nicholas@2498 2703 this.holder.appendChild(this.string);
nicholas@2498 2704 this.holder.appendChild(br);
nicholas@2498 2705 this.holder.appendChild(this.textArea);
nicholas@2498 2706
nicholas@2498 2707 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2708 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2709 root.id = this.specification.id;
nicholas@2498 2710 root.setAttribute('type', this.specification.type);
nicholas@2498 2711 console.log("Question: " + this.string.textContent);
nicholas@2498 2712 console.log("Response: " + root.textContent);
nicholas@2224 2713 var question = storePoint.parent.document.createElement('question');
nicholas@2224 2714 question.textContent = this.string.textContent;
nicholas@2224 2715 var response = storePoint.parent.document.createElement('response');
nicholas@2224 2716 response.textContent = this.textArea.value;
nicholas@2224 2717 root.appendChild(question);
nicholas@2224 2718 root.appendChild(response);
nicholas@2224 2719 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2720 return root;
nicholas@2498 2721 };
nicholas@2498 2722 this.resize = function () {
nicholas@2498 2723 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2724 if (boxwidth >= 600) {
nicholas@2498 2725 boxwidth = 600;
nicholas@2498 2726 } else if (boxwidth < 400) {
nicholas@2498 2727 boxwidth = 400;
nicholas@2498 2728 }
nicholas@2498 2729 this.holder.style.width = boxwidth + "px";
nicholas@2498 2730 this.textArea.style.width = boxwidth - 6 + "px";
nicholas@2498 2731 };
nicholas@2498 2732 this.resize();
nicholas@3063 2733 this.check = function () {
n@3095 2734 if (this.specification.mandatory && this.textArea.value.length === 0) {
nicholas@3063 2735 return false;
nicholas@3063 2736 }
nicholas@3063 2737 return true;
n@3095 2738 };
nicholas@2498 2739 };
nicholas@2498 2740
nicholas@2498 2741 this.radioBox = function (commentQuestion) {
nicholas@2498 2742 this.specification = commentQuestion;
nicholas@2498 2743 // Create document objects to hold the comment boxes
nicholas@2498 2744 this.holder = document.createElement('div');
nicholas@2498 2745 this.holder.className = 'comment-div';
nicholas@2498 2746 // Create a string next to each comment asking for a comment
nicholas@2498 2747 this.string = document.createElement('span');
nicholas@2498 2748 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2749 // Add to the holder.
nicholas@2498 2750 this.holder.appendChild(this.string);
nicholas@2498 2751 this.options = [];
nicholas@2498 2752 this.inputs = document.createElement('div');
nicholas@2711 2753 this.inputs.className = "comment-checkbox-inputs-holder";
nicholas@2498 2754
nicholas@2498 2755 var optCount = commentQuestion.options.length;
nicholas@2711 2756 for (var i = 0; i < optCount; i++) {
nicholas@2498 2757 var div = document.createElement('div');
nicholas@2711 2758 div.className = "comment-checkbox-inputs-flex";
nicholas@2722 2759
nicholas@2711 2760 var span = document.createElement('span');
nicholas@2711 2761 span.textContent = commentQuestion.options[i].text;
nicholas@2711 2762 span.className = 'comment-radio-span';
nicholas@2711 2763 div.appendChild(span);
nicholas@2722 2764
nicholas@2498 2765 var input = document.createElement('input');
nicholas@2498 2766 input.type = 'radio';
nicholas@2498 2767 input.name = commentQuestion.id;
nicholas@2711 2768 input.setAttribute('setvalue', commentQuestion.options[i].name);
nicholas@2498 2769 input.className = 'comment-radio';
nicholas@2498 2770 div.appendChild(input);
nicholas@2722 2771
nicholas@2498 2772 this.inputs.appendChild(div);
nicholas@2498 2773 this.options.push(input);
nicholas@2498 2774 }
nicholas@2498 2775 this.holder.appendChild(this.inputs);
nicholas@2498 2776
nicholas@2498 2777 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2778 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2779 root.id = this.specification.id;
nicholas@2498 2780 root.setAttribute('type', this.specification.type);
nicholas@2498 2781 var question = document.createElement('question');
nicholas@2498 2782 question.textContent = this.string.textContent;
nicholas@2498 2783 var response = document.createElement('response');
nicholas@2498 2784 var i = 0;
nicholas@2708 2785 while (this.options[i].checked === false) {
nicholas@2498 2786 i++;
nicholas@2498 2787 if (i >= this.options.length) {
nicholas@2498 2788 break;
nicholas@2498 2789 }
nicholas@2498 2790 }
nicholas@2498 2791 if (i >= this.options.length) {
nicholas@2498 2792 response.textContent = 'null';
nicholas@2498 2793 } else {
nicholas@2498 2794 response.textContent = this.options[i].getAttribute('setvalue');
nicholas@2498 2795 response.setAttribute('number', i);
nicholas@2498 2796 }
nicholas@2498 2797 console.log('Comment: ' + question.textContent);
nicholas@2498 2798 console.log('Response: ' + response.textContent);
nicholas@2498 2799 root.appendChild(question);
nicholas@2498 2800 root.appendChild(response);
nicholas@2224 2801 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2802 return root;
nicholas@2498 2803 };
nicholas@2498 2804 this.resize = function () {
nicholas@2498 2805 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2806 if (boxwidth >= 600) {
nicholas@2498 2807 boxwidth = 600;
nicholas@2498 2808 } else if (boxwidth < 400) {
nicholas@2498 2809 boxwidth = 400;
nicholas@2498 2810 }
nicholas@2498 2811 this.holder.style.width = boxwidth + "px";
nicholas@2498 2812 };
nicholas@3063 2813 this.check = function () {
nicholas@3063 2814 var anyChecked = this.options.some(function (a) {
nicholas@3063 2815 return a.checked;
nicholas@3063 2816 });
n@3095 2817 if (this.specification.mandatory && anyChecked === false) {
nicholas@3063 2818 return false;
nicholas@3063 2819 }
nicholas@3063 2820 return true;
n@3095 2821 };
nicholas@2498 2822 this.resize();
nicholas@2498 2823 };
nicholas@2498 2824
nicholas@2498 2825 this.checkboxBox = function (commentQuestion) {
nicholas@2498 2826 this.specification = commentQuestion;
nicholas@2498 2827 // Create document objects to hold the comment boxes
nicholas@2498 2828 this.holder = document.createElement('div');
nicholas@2498 2829 this.holder.className = 'comment-div';
nicholas@2498 2830 // Create a string next to each comment asking for a comment
nicholas@2498 2831 this.string = document.createElement('span');
nicholas@2498 2832 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2833 // Add to the holder.
nicholas@2498 2834 this.holder.appendChild(this.string);
nicholas@2498 2835 this.options = [];
nicholas@2498 2836 this.inputs = document.createElement('div');
nicholas@2294 2837 this.inputs.className = "comment-checkbox-inputs-holder";
nicholas@2498 2838
nicholas@2498 2839 var optCount = commentQuestion.options.length;
nicholas@2498 2840 for (var i = 0; i < optCount; i++) {
nicholas@2498 2841 var div = document.createElement('div');
nicholas@2711 2842 div.className = "comment-checkbox-inputs-flex";
nicholas@2722 2843
nicholas@2711 2844 var span = document.createElement('span');
nicholas@2711 2845 span.textContent = commentQuestion.options[i].text;
nicholas@2711 2846 span.className = 'comment-radio-span';
nicholas@2711 2847 div.appendChild(span);
nicholas@2722 2848
nicholas@2498 2849 var input = document.createElement('input');
nicholas@2498 2850 input.type = 'checkbox';
nicholas@2498 2851 input.name = commentQuestion.id;
nicholas@2498 2852 input.setAttribute('setvalue', commentQuestion.options[i].name);
nicholas@2498 2853 input.className = 'comment-radio';
nicholas@2498 2854 div.appendChild(input);
nicholas@2722 2855
nicholas@2498 2856 this.inputs.appendChild(div);
nicholas@2498 2857 this.options.push(input);
nicholas@2498 2858 }
nicholas@2498 2859 this.holder.appendChild(this.inputs);
nicholas@2498 2860
nicholas@2498 2861 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2862 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2863 root.id = this.specification.id;
nicholas@2498 2864 root.setAttribute('type', this.specification.type);
nicholas@2498 2865 var question = document.createElement('question');
nicholas@2498 2866 question.textContent = this.string.textContent;
nicholas@2498 2867 root.appendChild(question);
nicholas@2498 2868 console.log('Comment: ' + question.textContent);
nicholas@2498 2869 for (var i = 0; i < this.options.length; i++) {
nicholas@2498 2870 var response = document.createElement('response');
nicholas@2498 2871 response.textContent = this.options[i].checked;
nicholas@2498 2872 response.setAttribute('name', this.options[i].getAttribute('setvalue'));
nicholas@2498 2873 root.appendChild(response);
nicholas@2498 2874 console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent);
nicholas@2498 2875 }
nicholas@2224 2876 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2877 return root;
nicholas@2498 2878 };
nicholas@2498 2879 this.resize = function () {
nicholas@2498 2880 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2881 if (boxwidth >= 600) {
nicholas@2498 2882 boxwidth = 600;
nicholas@2498 2883 } else if (boxwidth < 400) {
nicholas@2498 2884 boxwidth = 400;
nicholas@2498 2885 }
nicholas@2498 2886 this.holder.style.width = boxwidth + "px";
nicholas@2498 2887 };
nicholas@3063 2888 this.check = function () {
nicholas@3063 2889 var anyChecked = this.options.some(function (a) {
nicholas@3063 2890 return a.checked;
nicholas@3063 2891 });
n@3095 2892 if (this.specification.mandatory && anyChecked === false) {
nicholas@3063 2893 return false;
nicholas@3063 2894 }
nicholas@3063 2895 return true;
nicholas@3063 2896 };
nicholas@2498 2897 this.resize();
nicholas@2498 2898 };
nicholas@2498 2899
n@2579 2900 this.sliderBox = function (commentQuestion) {
n@2579 2901 this.specification = commentQuestion;
n@2579 2902 this.holder = document.createElement("div");
n@2579 2903 this.holder.className = 'comment-div';
n@2579 2904 this.string = document.createElement("span");
n@2579 2905 this.string.innerHTML = commentQuestion.statement;
n@2579 2906 this.slider = document.createElement("input");
n@2579 2907 this.slider.type = "range";
n@2579 2908 this.slider.min = commentQuestion.min;
n@2579 2909 this.slider.max = commentQuestion.max;
n@2579 2910 this.slider.step = commentQuestion.step;
n@2579 2911 this.slider.value = commentQuestion.value;
n@2579 2912 var br = document.createElement('br');
n@2579 2913
n@2580 2914 var textHolder = document.createElement("div");
n@2580 2915 textHolder.className = "comment-slider-text-holder";
n@2580 2916
n@2580 2917 this.leftText = document.createElement("span");
n@2580 2918 this.leftText.textContent = commentQuestion.leftText;
n@2580 2919 this.rightText = document.createElement("span");
n@2580 2920 this.rightText.textContent = commentQuestion.rightText;
n@2580 2921 textHolder.appendChild(this.leftText);
n@2580 2922 textHolder.appendChild(this.rightText);
n@2580 2923
n@2579 2924 this.holder.appendChild(this.string);
n@2579 2925 this.holder.appendChild(br);
n@2579 2926 this.holder.appendChild(this.slider);
n@2580 2927 this.holder.appendChild(textHolder);
n@2579 2928
n@2579 2929 this.exportXMLDOM = function (storePoint) {
n@2579 2930 var root = storePoint.parent.document.createElement('comment');
n@2579 2931 root.id = this.specification.id;
n@2579 2932 root.setAttribute('type', this.specification.type);
n@2579 2933 console.log("Question: " + this.string.textContent);
n@2579 2934 console.log("Response: " + this.slider.value);
n@2579 2935 var question = storePoint.parent.document.createElement('question');
n@2579 2936 question.textContent = this.string.textContent;
n@2579 2937 var response = storePoint.parent.document.createElement('response');
n@2579 2938 response.textContent = this.slider.value;
n@2579 2939 root.appendChild(question);
n@2579 2940 root.appendChild(response);
n@2579 2941 storePoint.XMLDOM.appendChild(root);
n@2579 2942 return root;
n@2579 2943 };
n@2579 2944 this.resize = function () {
n@2579 2945 var boxwidth = (window.innerWidth - 100) / 2;
n@2579 2946 if (boxwidth >= 600) {
n@2579 2947 boxwidth = 600;
n@2579 2948 } else if (boxwidth < 400) {
n@2579 2949 boxwidth = 400;
n@2579 2950 }
n@2579 2951 this.holder.style.width = boxwidth + "px";
n@2579 2952 this.slider.style.width = boxwidth - 24 + "px";
n@2579 2953 };
nicholas@3063 2954 this.check = function () {
nicholas@3063 2955 return true;
n@3095 2956 };
n@2579 2957 this.resize();
n@2579 2958 };
n@2579 2959
nicholas@2498 2960 this.createCommentQuestion = function (element) {
nicholas@2498 2961 var node;
nicholas@2498 2962 if (element.type == 'question') {
nicholas@2498 2963 node = new this.commentBox(element);
nicholas@2498 2964 } else if (element.type == 'radio') {
nicholas@2498 2965 node = new this.radioBox(element);
nicholas@2498 2966 } else if (element.type == 'checkbox') {
nicholas@2498 2967 node = new this.checkboxBox(element);
n@2579 2968 } else if (element.type == 'slider') {
n@2579 2969 node = new this.sliderBox(element);
nicholas@2498 2970 }
nicholas@2498 2971 this.commentQuestions.push(node);
nicholas@2498 2972 return node;
nicholas@2498 2973 };
nicholas@2498 2974
nicholas@2498 2975 this.deleteCommentQuestions = function () {
nicholas@2498 2976 this.commentQuestions = [];
nicholas@2498 2977 };
nicholas@2498 2978
nicholas@3063 2979 this.checkCommentQuestions = function () {
nicholas@3063 2980 var errored = this.commentQuestions.reduce(function (a, cq) {
n@3095 2981 if (cq.check() === false) {
nicholas@3063 2982 a.push(cq);
nicholas@3063 2983 }
nicholas@3063 2984 return a;
nicholas@3063 2985 }, []);
n@3095 2986 if (errored.length === 0) {
nicholas@3063 2987 return true;
nicholas@3063 2988 }
nicholas@3063 2989 interfaceContext.lightbox.post("Message", "Not all the mandatory comment boxes below have been filled.");
n@3095 2990 };
nicholas@3063 2991
nicholas@2498 2992 this.outsideReferenceDOM = function (audioObject, index, inject) {
nicholas@2224 2993 this.parent = audioObject;
nicholas@2224 2994 this.outsideReferenceHolder = document.createElement('button');
nicholas@2224 2995 this.outsideReferenceHolder.className = 'outside-reference';
nicholas@2498 2996 this.outsideReferenceHolder.setAttribute('track-id', index);
nicholas@2409 2997 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 2998 this.outsideReferenceHolder.disabled = true;
nicholas@2708 2999 this.handleEvent = function (event) {
nicholas@2708 3000 audioEngineContext.play(this.parent.id);
nicholas@2224 3001 };
nicholas@2708 3002 this.outsideReferenceHolder.addEventListener("click", this);
nicholas@2224 3003 inject.appendChild(this.outsideReferenceHolder);
nicholas@2498 3004 this.enable = function () {
nicholas@2498 3005 if (this.parent.state == 1) {
nicholas@2224 3006 this.outsideReferenceHolder.disabled = false;
nicholas@2224 3007 }
nicholas@2224 3008 };
nicholas@2498 3009 this.updateLoading = function (progress) {
nicholas@2498 3010 if (progress != 100) {
nicholas@2224 3011 progress = String(progress);
nicholas@2224 3012 progress = progress.split('.')[0];
nicholas@2498 3013 this.outsideReferenceHolder.textContent = progress + '%';
nicholas@2224 3014 } else {
nicholas@2409 3015 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 3016 }
nicholas@2224 3017 };
nicholas@2498 3018 this.startPlayback = function () {
nicholas@2224 3019 // Called when playback has begun
nicholas@2224 3020 $('.track-slider').removeClass('track-slider-playing');
nicholas@2224 3021 $('.comment-div').removeClass('comment-box-playing');
nicholas@2224 3022 this.outsideReferenceHolder.style.backgroundColor = "#FDD";
nicholas@2224 3023 };
nicholas@2498 3024 this.stopPlayback = function () {
nicholas@2224 3025 // Called when playback has stopped. This gets called even if playback never started!
nicholas@2224 3026 this.outsideReferenceHolder.style.backgroundColor = "";
nicholas@2224 3027 };
nicholas@2498 3028 this.exportXMLDOM = function (audioObject) {
nicholas@2224 3029 return null;
nicholas@2224 3030 };
nicholas@2498 3031 this.getValue = function () {
nicholas@2224 3032 return 0;
nicholas@2224 3033 };
nicholas@2498 3034 this.getPresentedId = function () {
nicholas@2409 3035 return this.parent.specification.label || "Reference";
nicholas@2224 3036 };
nicholas@2498 3037 this.canMove = function () {
nicholas@2224 3038 return false;
nicholas@2224 3039 };
nicholas@2498 3040 this.error = function () {
nicholas@2498 3041 // audioObject has an error!!
nicholas@2224 3042 this.outsideReferenceHolder.textContent = "Error";
nicholas@2224 3043 this.outsideReferenceHolder.style.backgroundColor = "#F00";
nicholas@2708 3044 };
nicholas@2708 3045 };
nicholas@2498 3046
nicholas@2712 3047 this.playhead = (function () {
nicholas@2722 3048 var playhead = {};
nicholas@2712 3049 playhead.object = document.createElement('div');
nicholas@2712 3050 playhead.object.className = 'playhead';
nicholas@2712 3051 playhead.object.align = 'left';
nicholas@2498 3052 var curTime = document.createElement('div');
nicholas@2498 3053 curTime.style.width = '50px';
nicholas@2712 3054 playhead.curTimeSpan = document.createElement('span');
nicholas@2712 3055 playhead.curTimeSpan.textContent = '00:00';
nicholas@2712 3056 curTime.appendChild(playhead.curTimeSpan);
nicholas@2712 3057 playhead.object.appendChild(curTime);
nicholas@2712 3058 playhead.scrubberTrack = document.createElement('div');
nicholas@2712 3059 playhead.scrubberTrack.className = 'playhead-scrub-track';
nicholas@2498 3060
nicholas@2712 3061 playhead.scrubberHead = document.createElement('div');
nicholas@2712 3062 playhead.scrubberHead.id = 'playhead-scrubber';
nicholas@2712 3063 playhead.scrubberTrack.appendChild(playhead.scrubberHead);
nicholas@2712 3064 playhead.object.appendChild(playhead.scrubberTrack);
nicholas@2498 3065
nicholas@2712 3066 playhead.timePerPixel = 0;
nicholas@2712 3067 playhead.maxTime = 0;
nicholas@2498 3068
nicholas@2712 3069 playhead.playbackObject = undefined;
nicholas@2498 3070
nicholas@2712 3071 playhead.setTimePerPixel = function (audioObject) {
nicholas@2498 3072 //maxTime must be in seconds
nicholas@2498 3073 this.playbackObject = audioObject;
nicholas@2498 3074 this.maxTime = audioObject.buffer.buffer.duration;
nicholas@2498 3075 var width = 490; //500 - 10, 5 each side of the tracker head
nicholas@2498 3076 this.timePerPixel = this.maxTime / 490;
nicholas@2498 3077 if (this.maxTime < 60) {
nicholas@2498 3078 this.curTimeSpan.textContent = '0.00';
nicholas@2498 3079 } else {
nicholas@2498 3080 this.curTimeSpan.textContent = '00:00';
nicholas@2498 3081 }
nicholas@2498 3082 };
nicholas@2498 3083
nicholas@2712 3084 playhead.update = function () {
nicholas@2498 3085 // Update the playhead position, startPlay must be called
nicholas@2498 3086 if (this.timePerPixel > 0) {
nicholas@2498 3087 var time = this.playbackObject.getCurrentPosition();
nicholas@2498 3088 if (time > 0 && time < this.maxTime) {
nicholas@2498 3089 var width = 490;
nicholas@2498 3090 var pix = Math.floor(time / this.timePerPixel);
nicholas@2498 3091 this.scrubberHead.style.left = pix + 'px';
nicholas@2498 3092 if (this.maxTime > 60.0) {
nicholas@2498 3093 var secs = time % 60;
nicholas@2498 3094 var mins = Math.floor((time - secs) / 60);
nicholas@2498 3095 secs = secs.toString();
nicholas@2498 3096 secs = secs.substr(0, 2);
nicholas@2498 3097 mins = mins.toString();
nicholas@2498 3098 this.curTimeSpan.textContent = mins + ':' + secs;
nicholas@2498 3099 } else {
nicholas@2498 3100 time = time.toString();
nicholas@2498 3101 this.curTimeSpan.textContent = time.substr(0, 4);
nicholas@2498 3102 }
nicholas@2498 3103 } else {
nicholas@2498 3104 this.scrubberHead.style.left = '0px';
nicholas@2498 3105 if (this.maxTime < 60) {
nicholas@2498 3106 this.curTimeSpan.textContent = '0.00';
nicholas@2498 3107 } else {
nicholas@2498 3108 this.curTimeSpan.textContent = '00:00';
nicholas@2498 3109 }
nicholas@2498 3110 }
nicholas@2498 3111 }
nicholas@2817 3112 if (this.playbackObject !== undefined && this.interval === undefined) {
nicholas@2817 3113 window.requestAnimationFrame(this.update.bind(this));
nicholas@2817 3114 }
nicholas@2498 3115 };
nicholas@2498 3116
nicholas@2712 3117 playhead.interval = undefined;
nicholas@2498 3118
nicholas@2712 3119 playhead.start = function () {
nicholas@2708 3120 if (this.playbackObject !== undefined && this.interval === undefined) {
nicholas@2817 3121 window.requestAnimationFrame(this.update.bind(this));
nicholas@2498 3122 }
nicholas@2498 3123 };
nicholas@2712 3124 playhead.stop = function () {
nicholas@2817 3125 this.timePerPixel = 0;
nicholas@2498 3126 };
nicholas@2712 3127 return playhead;
nicholas@2712 3128 })();
nicholas@2498 3129
nicholas@2712 3130 this.volume = (function () {
nicholas@2224 3131 // An in-built volume module which can be viewed on page
nicholas@2224 3132 // Includes trackers on page-by-page data
nicholas@2224 3133 // Volume does NOT reset to 0dB on each page load
nicholas@2712 3134 var volume = {};
nicholas@2712 3135 volume.valueLin = 1.0;
nicholas@2712 3136 volume.valueDB = 0.0;
nicholas@2712 3137 volume.root = document.createElement('div');
nicholas@2712 3138 volume.root.id = 'master-volume-root';
nicholas@2712 3139 volume.object = document.createElement('div');
nicholas@2712 3140 volume.object.className = 'master-volume-holder-float';
nicholas@2712 3141 volume.object.appendChild(volume.root);
nicholas@2712 3142 volume.slider = document.createElement('input');
nicholas@2712 3143 volume.slider.id = 'master-volume-control';
nicholas@2712 3144 volume.slider.type = 'range';
nicholas@2712 3145 volume.valueText = document.createElement('span');
nicholas@2712 3146 volume.valueText.id = 'master-volume-feedback';
nicholas@2712 3147 volume.valueText.textContent = '0dB';
nicholas@2498 3148
nicholas@2712 3149 volume.slider.min = -60;
nicholas@2712 3150 volume.slider.max = 12;
nicholas@2712 3151 volume.slider.value = 0;
nicholas@2712 3152 volume.slider.step = 1;
nicholas@2712 3153 volume.handleEvent = function (event) {
nicholas@2951 3154 if (event.type == "mousemove" || event.type == "mouseup") {
nicholas@2669 3155 this.valueDB = Number(this.slider.value);
nicholas@2669 3156 this.valueLin = decibelToLinear(this.valueDB);
nicholas@2669 3157 this.valueText.textContent = this.valueDB + 'dB';
nicholas@2669 3158 audioEngineContext.outputGain.gain.value = this.valueLin;
nicholas@2951 3159 }
nicholas@2951 3160 if (event.type == "mouseup") {
nicholas@2669 3161 this.onmouseup();
nicholas@2669 3162 }
nicholas@2669 3163 this.slider.value = this.valueDB;
nicholas@2669 3164
nicholas@2669 3165 if (event.stopPropagation) {
nicholas@2669 3166 event.stopPropagation();
nicholas@2669 3167 }
nicholas@2711 3168 };
nicholas@2712 3169 volume.onmouseup = function () {
nicholas@2224 3170 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
nicholas@2708 3171 if (storePoint.length === 0) {
nicholas@2224 3172 storePoint = storage.document.createElement('metricresult');
nicholas@2498 3173 storePoint.setAttribute('name', 'volumeTracker');
nicholas@2224 3174 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
nicholas@2498 3175 } else {
nicholas@2224 3176 storePoint = storePoint[0];
nicholas@2224 3177 }
nicholas@2224 3178 var node = storage.document.createElement('movement');
nicholas@2498 3179 node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
nicholas@2669 3180 node.setAttribute('volume', this.valueDB);
nicholas@2498 3181 node.setAttribute('format', 'dBFS');
nicholas@2224 3182 storePoint.appendChild(node);
nicholas@2711 3183 };
nicholas@2712 3184 volume.slider.addEventListener("mousemove", volume);
nicholas@2712 3185 volume.root.addEventListener("mouseup", volume);
nicholas@2498 3186
nicholas@2224 3187 var title = document.createElement('div');
nicholas@2224 3188 title.innerHTML = '<span>Master Volume Control</span>';
nicholas@2224 3189 title.style.fontSize = '0.75em';
nicholas@2224 3190 title.style.width = "100%";
nicholas@2224 3191 title.align = 'center';
nicholas@2712 3192 volume.root.appendChild(title);
nicholas@2498 3193
nicholas@2712 3194 volume.root.appendChild(volume.slider);
nicholas@2712 3195 volume.root.appendChild(volume.valueText);
nicholas@2498 3196
nicholas@2712 3197 volume.resize = function (event) {
nicholas@2352 3198 if (window.innerWidth < 1000) {
nicholas@2708 3199 this.object.className = "master-volume-holder-inline";
nicholas@2352 3200 } else {
nicholas@2352 3201 this.object.className = 'master-volume-holder-float';
nicholas@2352 3202 }
nicholas@2708 3203 };
nicholas@2712 3204 return volume;
nicholas@2712 3205 })();
nicholas@2498 3206
nicholas@2778 3207 this.imageHolder = (function () {
nicholas@2778 3208 var imageController = {};
nicholas@2778 3209 imageController.root = document.createElement("div");
nicholas@2778 3210 imageController.root.id = "imageController";
nicholas@2778 3211 imageController.img = document.createElement("img");
nicholas@2778 3212 imageController.root.appendChild(imageController.img);
nicholas@2778 3213 imageController.setImage = function (src) {
nicholas@2778 3214 imageController.img.src = "";
n@2785 3215 if (typeof src !== "string" || src.length === undefined) {
nicholas@2778 3216 return;
nicholas@2778 3217 }
nicholas@2778 3218 imageController.img.src = src;
n@2785 3219 };
nicholas@2778 3220 return imageController;
nicholas@2778 3221 })();
nicholas@2778 3222
nicholas@3101 3223 this.calibrationTests = (function () {
nicholas@3101 3224 function readonly(t) {
nicholas@3101 3225 throw ("Cannot set read-only variable");
nicholas@3101 3226 }
nicholas@3101 3227
nicholas@3101 3228 function getStorageRoot() {
nicholas@3101 3229 var storageRoot = storage.root.querySelector("calibration");
nicholas@3101 3230 if (storageRoot === undefined) {
nicholas@3101 3231 storageRoot = storage.document.createElement("calibration");
nicholas@3101 3232 storage.root.appendChild(storageRoot);
nicholas@3101 3233 }
nicholas@3101 3234 return storageRoot;
nicholas@3101 3235 }
n@3102 3236 var calibrationObject,
nicholas@3101 3237 _checkedFrequency = false,
nicholas@3101 3238 _checkedChannels = false;
nicholas@3101 3239
nicholas@3101 3240 // Define the checkFrequencies test!
nicholas@3101 3241 var checkFrequencyUnit = function (htmlRoot, storageRoot) {
nicholas@3101 3242
nicholas@3101 3243 function createFrequencyElement(frequency) {
nicholas@3101 3244 return (function (frequency) {
n@3102 3245 var hold = document.createElement("div");
n@3102 3246 hold.className = "calibration-slider";
nicholas@3101 3247 var range = document.createElement("input");
nicholas@3101 3248 range.type = "range";
nicholas@3101 3249 range.min = "-24";
nicholas@3101 3250 range.max = "24";
nicholas@3101 3251 range.step = "0.5";
nicholas@3101 3252 range.setAttribute("orient", "vertical");
nicholas@3101 3253 range.value = (Math.random() - 0.5) * 24;
nicholas@3101 3254 range.setAttribute("frequency", frequency);
n@3102 3255 hold.appendChild(range);
n@3102 3256 htmlRoot.appendChild(hold);
nicholas@3101 3257
nicholas@3101 3258 var gain = audioContext.createGain();
nicholas@3101 3259 gain.connect(outputGain);
nicholas@3101 3260 gain.gain.value = Math.pow(10, Number(range.value) / 20.0);
n@3102 3261 var osc;
nicholas@3101 3262
nicholas@3101 3263 var store = storage.document.createElement("response");
nicholas@3101 3264 store.setAttribute("frequency", frequency);
nicholas@3101 3265 storageHook.appendChild(store);
nicholas@3101 3266 var interface = {};
nicholas@3101 3267 Object.defineProperties(interface, {
nicholas@3101 3268 "handleEvent": {
nicholas@3101 3269 "value": function (e) {
nicholas@3101 3270 if (e.type == "mouseenter") {
nicholas@3101 3271 osc = audioContext.createOscillator();
nicholas@3101 3272 osc.frequency.value = frequency;
nicholas@3101 3273 osc.connect(gain);
nicholas@3101 3274 osc.start();
nicholas@3101 3275 console.log("start " + frequency);
nicholas@3101 3276 } else if (e.type == "mouseleave") {
nicholas@3101 3277 console.log("stop " + frequency);
nicholas@3101 3278 osc.stop();
nicholas@3101 3279 osc = undefined;
nicholas@2224 3280 }
nicholas@3101 3281 store.textContent = e.currentTarget.value;
nicholas@3101 3282 gain.gain.value = Math.pow(10, Number(e.currentTarget.value) / 20.0);
nicholas@3101 3283 }
nicholas@2224 3284 }
nicholas@3101 3285 });
nicholas@3101 3286 range.addEventListener("mousemove", interface);
nicholas@3101 3287 range.addEventListener("mouseenter", interface);
nicholas@3101 3288 range.addEventListener("mouseleave", interface);
nicholas@3101 3289 return interface;
nicholas@3101 3290 })(frequency);
nicholas@3101 3291 }
nicholas@3101 3292 var htmlHook = document.createElement("div");
nicholas@3101 3293 htmlRoot.appendChild(htmlHook);
nicholas@3101 3294 var storageHook = storage.document.createElement("frequency");
nicholas@3101 3295 storageRoot.appendChild(storageHook);
nicholas@3101 3296 var frequencies = [100, 200, 400, 800, 1200, 1600, 2000, 4000, 8000, 12000];
nicholas@3101 3297 var outputGain = audioContext.createGain();
nicholas@3101 3298 outputGain.gain.value = 0.25;
nicholas@3101 3299 outputGain.connect(audioContext.destination);
nicholas@3101 3300 this.sliders = frequencies.map(createFrequencyElement);
n@3102 3301 };
nicholas@3101 3302
nicholas@3101 3303 var checkChannelsUnit = function (htmlRoot, storageRoot) {
nicholas@3101 3304
nicholas@3101 3305 function onclick(ev) {
nicholas@3101 3306 var storageHook = storage.document.querySelector("calibration").querySelector("channels");
nicholas@3101 3307 storageHook.setAttribute("selected", ev.currentTarget.value);
nicholas@3101 3308 storageHook.setAttribute("selectedText", ev.currentTarget.textContent);
nicholas@3101 3309 osc.stop();
nicholas@3101 3310 gainL = undefined;
nicholas@3101 3311 gainR = undefined;
nicholas@3101 3312 cmerge = undefined;
nicholas@3101 3313 popup.proceedClicked();
nicholas@3101 3314 }
nicholas@3101 3315 var osc = audioContext.createOscillator();
nicholas@3101 3316 var gainL = audioContext.createGain();
nicholas@3101 3317 var gainR = audioContext.createGain();
nicholas@3101 3318 gainL.channelCount = 1;
nicholas@3101 3319 gainR.channelCount = 1;
nicholas@3101 3320 var cmerge = audioContext.createChannelMerger(2);
nicholas@3101 3321 osc.connect(gainL, 0, 0);
nicholas@3101 3322 osc.connect(gainR, 0, 0);
nicholas@3101 3323 gainL.connect(cmerge, 0, 0);
nicholas@3101 3324 gainR.connect(cmerge, 0, 1);
nicholas@3101 3325 cmerge.connect(audioContext.destination);
nicholas@3101 3326 var play = document.createElement("button");
nicholas@3101 3327 play.textContent = "Play Audio";
nicholas@3101 3328 play.onclick = function () {
nicholas@3101 3329 osc.start();
nicholas@3101 3330 play.disabled = true;
n@3102 3331 };
n@3102 3332 play.className = "calibration-button";
nicholas@3101 3333 htmlRoot.appendChild(play);
nicholas@3101 3334 var choiceHolder = document.createElement("div");
nicholas@3101 3335 var leftButton = document.createElement("button");
nicholas@3101 3336 leftButton.textContent = "Left";
nicholas@3101 3337 leftButton.value = "-1";
n@3102 3338 leftButton.className = "calibration-button";
nicholas@3101 3339 var centerButton = document.createElement("button");
nicholas@3101 3340 centerButton.textContent = "Middle";
nicholas@3101 3341 centerButton.value = "0";
n@3102 3342 centerButton.className = "calibration-button";
nicholas@3101 3343 var rightButton = document.createElement("button");
nicholas@3101 3344 rightButton.textContent = "Right";
nicholas@3101 3345 rightButton.value = "1";
n@3102 3346 rightButton.className = "calibration-button";
nicholas@3101 3347 choiceHolder.appendChild(leftButton);
nicholas@3101 3348 choiceHolder.appendChild(centerButton);
nicholas@3101 3349 choiceHolder.appendChild(rightButton);
nicholas@3101 3350 htmlRoot.appendChild(choiceHolder);
nicholas@3101 3351 leftButton.addEventListener("click", onclick);
nicholas@3101 3352 centerButton.addEventListener("click", onclick);
nicholas@3101 3353 rightButton.addEventListener("click", onclick);
nicholas@3101 3354
nicholas@3101 3355 var storageHook = storage.document.createElement("channels");
nicholas@3101 3356 storageRoot.appendChild(storageHook);
nicholas@3101 3357
nicholas@3101 3358 var pan;
nicholas@3101 3359 if (Math.random() > 0.5) {
nicholas@3101 3360 pan = 1;
nicholas@3101 3361 gainL.gain.value = 0.0;
nicholas@3101 3362 gainR.gain.value = 0.25;
nicholas@3101 3363 storageHook.setAttribute("presented", pan);
nicholas@3101 3364 storageHook.setAttribute("presentedText", "Right");
nicholas@3101 3365 } else {
nicholas@3101 3366 pan = -1;
nicholas@3101 3367 gainL.gain.value = 0.25;
nicholas@3101 3368 gainR.gain.value = 0.0;
nicholas@3101 3369 storageHook.setAttribute("presented", pan);
nicholas@3101 3370 storageHook.setAttribute("presentedText", "Left");
nicholas@3101 3371 }
n@3102 3372 };
nicholas@3101 3373
nicholas@3101 3374 var interface = {};
nicholas@3101 3375 Object.defineProperties(interface, {
nicholas@3101 3376 "calibrationObject": {
nicholas@3101 3377 "get": function () {
n@3102 3378 return calibrationObject;
nicholas@3101 3379 },
nicholas@3101 3380 "set": readonly
nicholas@3101 3381 },
nicholas@3101 3382 "checkFrequencies": {
nicholas@3101 3383 "get": function () {
nicholas@3101 3384 if (specification.calibration.checkFrequencies && _checkedFrequency === false) {
nicholas@3101 3385 return true;
nicholas@2224 3386 }
nicholas@3101 3387 return false;
nicholas@3101 3388 },
nicholas@3101 3389 "set": readonly
nicholas@3101 3390 },
nicholas@3101 3391 "checkChannels": {
nicholas@3101 3392 "get": function () {
nicholas@3101 3393 if (specification.calibration.checkChannels && _checkedChannels === false) {
nicholas@3101 3394 return true;
nicholas@3101 3395 }
nicholas@3101 3396 return false;
nicholas@3101 3397 },
nicholas@3101 3398 "set": readonly
nicholas@3101 3399 },
nicholas@3101 3400 "performFrequencyCheck": {
nicholas@3101 3401 "value": function (htmlRoot) {
nicholas@3101 3402 htmlRoot.innerHTML = "";
nicholas@3101 3403 calibrationObject = new checkFrequencyUnit(htmlRoot, getStorageRoot());
nicholas@3101 3404 _checkedFrequency = true;
nicholas@2224 3405 }
nicholas@3101 3406 },
nicholas@3101 3407 "performChannelCheck": {
nicholas@3101 3408 "value": function (htmlRoot) {
nicholas@3101 3409 htmlRoot.innerHTML = "";
nicholas@3101 3410 calibrationObject = new checkChannelsUnit(htmlRoot, getStorageRoot());
nicholas@3101 3411 _checkedChannels = true;
nicholas@3101 3412 }
nicholas@2224 3413 }
n@3102 3414 });
nicholas@3101 3415 return interface;
nicholas@3101 3416 })();
nicholas@2498 3417
nicholas@2498 3418
nicholas@2498 3419 // Global Checkers
nicholas@2498 3420 // These functions will help enforce the checkers
n@2789 3421 this.checkHiddenAnchor = function (message) {
nicholas@2708 3422 var anchors = audioEngineContext.audioObjects.filter(function (ao) {
nicholas@2708 3423 return ao.specification.type === "anchor";
nicholas@2708 3424 });
nicholas@2708 3425 var state = anchors.some(function (ao) {
nicholas@2708 3426 return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0);
nicholas@2708 3427 });
nicholas@2708 3428 if (state) {
nicholas@2708 3429 console.log('Anchor node not below marker value');
n@2789 3430 if (message) {
n@2789 3431 interfaceContext.lightbox.post("Message", message);
n@2789 3432 } else {
n@2789 3433 interfaceContext.lightbox.post("Message", 'Please keep listening');
n@2789 3434 }
nicholas@2708 3435 this.storeErrorNode('Anchor node not below marker value');
nicholas@2708 3436 return false;
nicholas@2498 3437 }
nicholas@2498 3438 return true;
nicholas@2498 3439 };
nicholas@2498 3440
n@2789 3441 this.checkHiddenReference = function (message) {
nicholas@2708 3442 var references = audioEngineContext.audioObjects.filter(function (ao) {
nicholas@2708 3443 return ao.specification.type === "reference";
nicholas@2708 3444 });
nicholas@2708 3445 var state = references.some(function (ao) {
nicholas@2708 3446 return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0);
nicholas@2708 3447 });
nicholas@2708 3448 if (state) {
nicholas@2708 3449 console.log('Reference node not below marker value');
n@2789 3450 if (message) {
n@2789 3451 interfaceContext.lightbox.post("Message", message);
n@2789 3452 } else {
n@2789 3453 interfaceContext.lightbox.post("Message", 'Please keep listening');
n@2789 3454 }
nicholas@2708 3455 this.storeErrorNode('Reference node not below marker value');
nicholas@2708 3456 return false;
nicholas@2498 3457 }
nicholas@2498 3458 return true;
nicholas@2498 3459 };
nicholas@2498 3460
n@2789 3461 this.checkFragmentsFullyPlayed = function (message) {
nicholas@2498 3462 // Checks the entire file has been played back
nicholas@2498 3463 // NOTE ! This will return true IF playback is Looped!!!
nicholas@2498 3464 if (audioEngineContext.loopPlayback) {
nicholas@2498 3465 console.log("WARNING - Looped source: Cannot check fragments are fully played");
nicholas@2498 3466 return true;
nicholas@2498 3467 }
nicholas@2498 3468 var check_pass = true;
nicholas@2708 3469 var error_obj = [],
nicholas@2708 3470 i;
nicholas@2708 3471 for (i = 0; i < audioEngineContext.audioObjects.length; i++) {
nicholas@2498 3472 var object = audioEngineContext.audioObjects[i];
nicholas@2498 3473 var time = object.buffer.buffer.duration;
nicholas@2498 3474 var metric = object.metric;
nicholas@2498 3475 var passed = false;
nicholas@2498 3476 for (var j = 0; j < metric.listenTracker.length; j++) {
nicholas@2498 3477 var bt = metric.listenTracker[j].getElementsByTagName('testtime');
nicholas@2498 3478 var start_time = Number(bt[0].getAttribute('start'));
nicholas@2498 3479 var stop_time = Number(bt[0].getAttribute('stop'));
nicholas@2498 3480 var delta = stop_time - start_time;
nicholas@2498 3481 if (delta >= time) {
nicholas@2498 3482 passed = true;
nicholas@2498 3483 break;
nicholas@2498 3484 }
nicholas@2498 3485 }
nicholas@2708 3486 if (passed === false) {
nicholas@2498 3487 check_pass = false;
nicholas@2498 3488 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
nicholas@2498 3489 error_obj.push(object.interfaceDOM.getPresentedId());
nicholas@2498 3490 }
nicholas@2498 3491 }
nicholas@2708 3492 if (check_pass === false) {
nicholas@2498 3493 var str_start = "You have not completely listened to fragments ";
nicholas@2708 3494 for (i = 0; i < error_obj.length; i++) {
nicholas@2498 3495 str_start += error_obj[i];
nicholas@2498 3496 if (i != error_obj.length - 1) {
nicholas@2498 3497 str_start += ', ';
nicholas@2498 3498 }
nicholas@2498 3499 }
nicholas@2498 3500 str_start += ". Please keep listening";
n@2789 3501 console.log(str_start);
n@2789 3502 this.storeErrorNode(str_start);
n@2789 3503 if (message) {
n@2789 3504 str_start = message;
n@2789 3505 }
nicholas@2498 3506 interfaceContext.lightbox.post("Error", str_start);
nicholas@2444 3507 return false;
nicholas@2498 3508 }
nicholas@2444 3509 return true;
nicholas@2498 3510 };
n@2789 3511 this.checkAllMoved = function (message) {
nicholas@2498 3512 var str = "You have not moved ";
nicholas@2498 3513 var failed = [];
nicholas@2708 3514 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2708 3515 if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) {
nicholas@2498 3516 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3517 }
nicholas@2708 3518 }, this);
nicholas@2708 3519 if (failed.length === 0) {
nicholas@2498 3520 return true;
nicholas@2498 3521 } else if (failed.length == 1) {
nicholas@2498 3522 str += 'track ' + failed[0];
nicholas@2498 3523 } else {
nicholas@2498 3524 str += 'tracks ';
nicholas@2498 3525 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3526 str += failed[i] + ', ';
nicholas@2498 3527 }
nicholas@2498 3528 str += 'and ' + failed[i];
nicholas@2498 3529 }
nicholas@2498 3530 str += '.';
nicholas@2498 3531 console.log(str);
nicholas@2224 3532 this.storeErrorNode(str);
n@2789 3533 if (message) {
n@2789 3534 str = message;
n@2789 3535 }
n@2789 3536 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3537 return false;
nicholas@2498 3538 };
n@2789 3539 this.checkAllPlayed = function (message) {
nicholas@2498 3540 var str = "You have not played ";
nicholas@2498 3541 var failed = [];
nicholas@2708 3542 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2708 3543 if (ao.metric.wasListenedTo === false) {
nicholas@2498 3544 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3545 }
nicholas@2708 3546 }, this);
nicholas@2708 3547 if (failed.length === 0) {
nicholas@2498 3548 return true;
nicholas@2498 3549 } else if (failed.length == 1) {
nicholas@2498 3550 str += 'track ' + failed[0];
nicholas@2498 3551 } else {
nicholas@2498 3552 str += 'tracks ';
nicholas@2498 3553 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3554 str += failed[i] + ', ';
nicholas@2498 3555 }
nicholas@2498 3556 str += 'and ' + failed[i];
nicholas@2498 3557 }
nicholas@2498 3558 str += '.';
nicholas@2498 3559 console.log(str);
nicholas@2224 3560 this.storeErrorNode(str);
n@2789 3561 if (message) {
n@2789 3562 str = message;
n@2789 3563 }
n@2789 3564 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3565 return false;
nicholas@2498 3566 };
n@2789 3567 this.checkAllCommented = function (message) {
nicholas@2540 3568 var str = "You have not commented on all the fragments.";
nicholas@2540 3569 var cont = true,
nicholas@2540 3570 boxes = this.commentBoxes.boxes,
nicholas@2540 3571 numBoxes = boxes.length,
nicholas@2540 3572 i;
nicholas@2540 3573 for (i = 0; i < numBoxes; i++) {
nicholas@2540 3574 if (boxes[i].trackCommentBox.value === "") {
nicholas@2540 3575 console.log(str);
nicholas@2540 3576 this.storeErrorNode(str);
n@2789 3577 if (message) {
n@2789 3578 str = message;
n@2789 3579 }
n@2789 3580 interfaceContext.lightbox.post("Error", str);
nicholas@2540 3581 return false;
nicholas@2540 3582 }
nicholas@2540 3583 }
nicholas@2540 3584 return true;
nicholas@2708 3585 };
n@2789 3586 this.checkScaleRange = function (message) {
nicholas@2310 3587 var page = testState.getCurrentTestPage();
nicholas@2708 3588 var interfaceObject = page.interfaces;
nicholas@2310 3589 var state = true;
nicholas@2310 3590 var str = "Please keep listening. ";
nicholas@2708 3591 if (interfaceObject === undefined) {
nicholas@2708 3592 return true;
nicholas@2310 3593 }
nicholas@2708 3594 interfaceObject = interfaceObject[0];
nicholas@2708 3595 var scales = (function () {
nicholas@2708 3596 var scaleRange = interfaceObject.options.find(function (a) {
nicholas@2708 3597 return a.name == "scalerange";
nicholas@2708 3598 });
nicholas@2708 3599 return {
nicholas@2708 3600 min: scaleRange.min,
nicholas@2708 3601 max: scaleRange.max
nicholas@2708 3602 };
nicholas@2708 3603 })();
nicholas@2708 3604 var range = audioEngineContext.audioObjects.reduce(function (a, b) {
nicholas@2742 3605 var v = b.interfaceDOM.getValue() * 100.0;
nicholas@2708 3606 return {
nicholas@2708 3607 min: Math.min(a.min, v),
nicholas@2708 3608 max: Math.max(a.max, v)
nicholas@2712 3609 };
nicholas@2708 3610 }, {
nicholas@2708 3611 min: 100,
nicholas@2708 3612 max: 0
nicholas@2708 3613 });
nicholas@2708 3614 if (range.min > scales.min) {
nicholas@2742 3615 str += "At least one fragment must be below the " + scales.min + " mark.";
nicholas@2708 3616 state = false;
nicholas@2712 3617 } else if (range.max < scales.max) {
nicholas@2742 3618 str += "At least one fragment must be above the " + scales.max + " mark.";
nicholas@2310 3619 state = false;
nicholas@2310 3620 }
nicholas@2708 3621 if (state === false) {
nicholas@2310 3622 console.log(str);
nicholas@2310 3623 this.storeErrorNode(str);
n@2789 3624 if (message) {
n@2789 3625 str = message;
n@2789 3626 }
nicholas@2498 3627 interfaceContext.lightbox.post("Error", str);
nicholas@2310 3628 }
nicholas@2310 3629 return state;
nicholas@2708 3630 };
nicholas@2826 3631 this.checkFragmentMinPlays = function () {
nicholas@2826 3632 var failedObjects = audioEngineContext.audioObjects.filter(function (a) {
nicholas@2826 3633 var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays;
nicholas@2826 3634 if (minPlays === undefined || a.numberOfPlays >= minPlays) {
nicholas@2826 3635 return false;
nicholas@2826 3636 }
nicholas@2826 3637 return true;
nicholas@2826 3638 });
nicholas@2826 3639 if (failedObjects.length === 0) {
nicholas@2827 3640 return true;
nicholas@2826 3641 }
nicholas@2826 3642 var failedString = [];
nicholas@2826 3643 failedObjects.forEach(function (a) {
nicholas@2826 3644 failedString.push(a.interfaceDOM.getPresentedId());
nicholas@2826 3645 });
nicholas@2826 3646 var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening";
nicholas@2826 3647 interfaceContext.lightbox.post("Message", str);
nicholas@2826 3648 this.storeErrorNode(str);
nicholas@2827 3649 return false;
nicholas@2826 3650 };
nicholas@2826 3651
nicholas@2498 3652
nicholas@2849 3653 this.sortFragmentsByScore = function () {
nicholas@2849 3654 var elements = audioEngineContext.audioObjects.filter(function (elem) {
nicholas@2849 3655 return elem.specification.type !== "outside-reference";
nicholas@2849 3656 });
nicholas@2849 3657 var indexes = [];
nicholas@2849 3658 var i = 0;
nicholas@2849 3659 while (indexes.push(i++) < elements.length);
nicholas@2849 3660 return indexes.sort(function (x, y) {
nicholas@2849 3661 var a = elements[x].interfaceDOM.getValue();
nicholas@2849 3662 var b = elements[y].interfaceDOM.getValue();
nicholas@2849 3663 if (a > b) {
nicholas@2849 3664 return 1;
nicholas@2849 3665 } else if (a < b) {
nicholas@2849 3666 return -1;
nicholas@2849 3667 }
nicholas@2849 3668 return 0;
nicholas@2849 3669 }, elements[0].interfaceDOM.getValue());
nicholas@2849 3670 };
nicholas@2849 3671
nicholas@2498 3672 this.storeErrorNode = function (errorMessage) {
nicholas@2224 3673 var time = audioEngineContext.timer.getTestTime();
nicholas@2224 3674 var node = storage.document.createElement('error');
nicholas@2498 3675 node.setAttribute('time', time);
nicholas@2224 3676 node.textContent = errorMessage;
nicholas@2224 3677 testState.currentStore.XMLDOM.appendChild(node);
nicholas@2224 3678 };
nicholas@2595 3679
nicholas@2595 3680 this.getLabel = function (labelType, index, labelStart) {
nicholas@2595 3681 /*
nicholas@2595 3682 Get the correct label based on type, index and offset
nicholas@2595 3683 */
nicholas@2595 3684
nicholas@2595 3685 function calculateLabel(labelType, index, offset) {
nicholas@2595 3686 if (labelType == "none") {
nicholas@2595 3687 return "";
nicholas@2595 3688 }
nicholas@2595 3689 switch (labelType) {
nicholas@2595 3690 case "letter":
nicholas@2596 3691 return String.fromCharCode((index + offset) % 26 + 97);
nicholas@2595 3692 case "capital":
nicholas@2607 3693 return String.fromCharCode((index + offset) % 26 + 65);
nicholas@2625 3694 case "samediff":
nicholas@2708 3695 if (index === 0) {
nicholas@2625 3696 return "Same";
nicholas@2625 3697 } else if (index == 1) {
nicholas@2625 3698 return "Difference";
nicholas@2625 3699 }
nicholas@2708 3700 return "";
nicholas@2595 3701 case "number":
nicholas@2595 3702 return String(index + offset);
nicholas@2595 3703 default:
nicholas@2595 3704 return "";
nicholas@2595 3705 }
nicholas@2595 3706 }
nicholas@2595 3707
nicholas@2708 3708 if (typeof labelStart !== "string" || labelStart.length === 0) {
nicholas@2595 3709 labelStart = String.fromCharCode(0);
nicholas@2595 3710 }
nicholas@2595 3711
nicholas@2595 3712 switch (labelType) {
nicholas@2595 3713 case "letter":
nicholas@2595 3714 labelStart = labelStart.charCodeAt(0);
nicholas@2596 3715 if (labelStart < 97 || labelStart > 122) {
nicholas@2595 3716 labelStart = 97;
nicholas@2595 3717 }
nicholas@2595 3718 labelStart -= 97;
nicholas@2595 3719 break;
nicholas@2595 3720 case "capital":
nicholas@2595 3721 labelStart = labelStart.charCodeAt(0);
nicholas@2596 3722 if (labelStart < 65 || labelStart > 90) {
nicholas@2595 3723 labelStart = 65;
nicholas@2595 3724 }
nicholas@2595 3725 labelStart -= 65;
nicholas@2595 3726 break;
nicholas@2595 3727 case "number":
nicholas@2608 3728 labelStart = Number(labelStart);
nicholas@2608 3729 if (!isFinite(labelStart)) {
nicholas@2595 3730 labelStart = 1;
nicholas@2595 3731 }
nicholas@2595 3732 break;
nicholas@2595 3733 default:
nicholas@2596 3734 labelStart = 0;
nicholas@2595 3735 }
nicholas@2595 3736 if (typeof index == "number") {
nicholas@2595 3737 return calculateLabel(labelType, index, labelStart);
nicholas@2595 3738 } else if (index.length && index.length > 0) {
nicholas@2595 3739 var a = [],
nicholas@2595 3740 l = index.length,
nicholas@2595 3741 i;
nicholas@2595 3742 for (i = 0; i < l; i++) {
nicholas@2595 3743 a[i] = calculateLabel(labelType, index[i], labelStart);
nicholas@2595 3744 }
nicholas@2595 3745 return a;
nicholas@2595 3746 } else {
nicholas@2595 3747 throw ("Invalid arguments");
nicholas@2595 3748 }
nicholas@2708 3749 };
nicholas@2649 3750
nicholas@2649 3751 this.getCombinedInterfaces = function (page) {
nicholas@2649 3752 // Combine the interfaces with the global interface nodes
nicholas@2649 3753 var global = specification.interfaces,
nicholas@2649 3754 local = page.interfaces;
nicholas@2649 3755 local.forEach(function (locInt) {
nicholas@2649 3756 // Iterate through the options nodes
nicholas@2649 3757 var addList = [];
nicholas@2649 3758 global.options.forEach(function (gopt) {
nicholas@2649 3759 var lopt = locInt.options.find(function (lopt) {
nicholas@2649 3760 return (lopt.name == gopt.name) && (lopt.type == gopt.type);
nicholas@2649 3761 });
nicholas@2649 3762 if (!lopt) {
nicholas@2649 3763 // Global option doesn't exist locally
nicholas@2649 3764 addList.push(gopt);
nicholas@2649 3765 }
nicholas@2649 3766 });
nicholas@2649 3767 locInt.options = locInt.options.concat(addList);
nicholas@2649 3768 if (!locInt.scales && global.scales) {
nicholas@2649 3769 // Use the global default scales
nicholas@2649 3770 locInt.scales = global.scales;
nicholas@2649 3771 }
nicholas@2649 3772 });
nicholas@2649 3773 return local;
nicholas@2708 3774 };
nicholas@2224 3775 }
nicholas@2224 3776
nicholas@2498 3777 function Storage() {
nicholas@2498 3778 // Holds results in XML format until ready for collection
nicholas@2498 3779 this.globalPreTest = null;
nicholas@2498 3780 this.globalPostTest = null;
nicholas@2498 3781 this.testPages = [];
nicholas@2498 3782 this.document = null;
nicholas@2498 3783 this.root = null;
nicholas@2498 3784 this.state = 0;
nicholas@2733 3785 var pFilenamePrefix = "save";
nicholas@2498 3786
nicholas@2498 3787 this.initialise = function (existingStore) {
nicholas@2708 3788 if (existingStore === undefined) {
nicholas@2224 3789 // We need to get the sessionKey
nicholas@2510 3790 this.SessionKey.requestKey();
nicholas@2498 3791 this.document = document.implementation.createDocument(null, "waetresult", null);
nicholas@2224 3792 this.root = this.document.childNodes[0];
nicholas@2224 3793 var projectDocument = specification.projectXML;
nicholas@2708 3794 projectDocument.setAttribute('file-name', specification.url);
nicholas@2708 3795 projectDocument.setAttribute('url', qualifyURL(specification.url));
nicholas@2224 3796 this.root.appendChild(projectDocument);
nicholas@2224 3797 this.root.appendChild(interfaceContext.returnDateNode());
nicholas@2224 3798 this.root.appendChild(interfaceContext.returnNavigator());
nicholas@2224 3799 } else {
nicholas@2224 3800 this.document = existingStore;
nicholas@2294 3801 this.root = existingStore.firstChild;
nicholas@2224 3802 this.SessionKey.key = this.root.getAttribute("key");
nicholas@2224 3803 }
nicholas@2708 3804 if (specification.preTest !== undefined) {
nicholas@2498 3805 this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
nicholas@2498 3806 }
nicholas@2708 3807 if (specification.postTest !== undefined) {
nicholas@2498 3808 this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
nicholas@2498 3809 }
nicholas@2498 3810 };
nicholas@2498 3811
n@2967 3812 this.SessionKey = (function (parent) {
n@2970 3813 var returnURL = "";
n@2970 3814 if (window.returnURL !== undefined) {
n@2970 3815 returnURL = String(window.returnURL);
n@2970 3816 }
n@2970 3817
n@2967 3818 function postUpdate() {
n@2967 3819 // Return a new promise.
n@2967 3820 var hold = document.createElement("div");
n@2967 3821 var clone = parent.root.cloneNode(true);
n@2967 3822 hold.appendChild(clone);
n@2967 3823 return new Promise(function (resolve, reject) {
n@2967 3824 // Do the usual XHR stuff
n@2977 3825 console.log("Requested save...");
n@2967 3826 var req = new XMLHttpRequest();
n@2973 3827 req.open("POST", returnURL + "php/save.php?key=" + sessionKey + "&saveFilenamePrefix=" + parent.filenamePrefix);
n@2967 3828 req.setRequestHeader('Content-Type', 'text/xml');
n@2967 3829
n@2967 3830 req.onload = function () {
n@2967 3831 // This is called even on 404 etc
n@2967 3832 // so check the status
n@2967 3833 if (this.status >= 300) {
n@2967 3834 console.log("WARNING - Could not update at this time");
n@2967 3835 } else {
n@2967 3836 var parser = new DOMParser();
n@2974 3837 var xmlDoc = parser.parseFromString(req.responseText, "application/xml");
n@2967 3838 var response = xmlDoc.getElementsByTagName('response')[0];
n@2967 3839 if (response.getAttribute("state") == "OK") {
n@2967 3840 var file = response.getElementsByTagName("file")[0];
n@2967 3841 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
n@2967 3842 resolve(true);
n@2967 3843 } else {
n@2967 3844 var message = response.getElementsByTagName("message");
n@2967 3845 console.log("Intermediate save: Error! " + message.textContent);
n@2967 3846 reject("Intermediate save: Error! " + message.textContent);
n@2967 3847 }
n@2967 3848 }
n@2967 3849 };
n@2967 3850
n@2967 3851 // Handle network errors
n@2967 3852 req.onerror = function () {
n@2967 3853 reject(Error("Network Error"));
n@2967 3854 };
n@2967 3855
n@2967 3856 // Make the request
n@2967 3857 req.send([hold.innerHTML]);
n@2967 3858 });
n@2979 3859 }
n@2967 3860
n@2967 3861 function keyPromise() {
n@2967 3862 return new Promise(function (resolve, reject) {
n@2967 3863 var req = new XMLHttpRequest();
n@2967 3864 req.open("GET", returnURL + "php/requestKey.php?saveFilenamePrefix=" + parent.filenamePrefix, true);
n@2967 3865 req.onload = function () {
n@2967 3866 // This is called even on 404 etc
n@2967 3867 // so check the status
n@2967 3868 if (req.status == 200) {
n@2967 3869 // Resolve the promise with the response text
n@2967 3870 resolve(req.response);
n@2967 3871 } else {
n@2967 3872 // Otherwise reject with the status text
n@2967 3873 // which will hopefully be a meaningful error
n@2967 3874 reject(Error(req.statusText));
n@2967 3875 }
n@2967 3876 };
n@2967 3877
n@2967 3878 // Handle network errors
n@2967 3879 req.onerror = function () {
n@2967 3880 reject(Error("Network Error"));
n@2967 3881 };
n@2967 3882
n@2967 3883 req.send();
n@2979 3884 });
n@2967 3885 }
n@2967 3886
n@2967 3887 var requestChains = null;
n@2969 3888 var sessionKey = null;
n@2967 3889 var object = {};
n@2967 3890
n@2967 3891 Object.defineProperties(object, {
n@2967 3892 "key": {
n@2967 3893 "get": function () {
n@2969 3894 return sessionKey;
n@2967 3895 },
n@2967 3896 "set": function (a) {
n@2979 3897 throw ("Cannot set read-only property");
n@2967 3898 }
n@2967 3899 },
n@2967 3900 "request": {
n@2967 3901 "value": new XMLHttpRequest()
n@2967 3902 },
n@2967 3903 "parent": {
n@2967 3904 "value": parent
n@2967 3905 },
n@2967 3906 "requestKey": {
n@2967 3907 "value": function () {
n@2967 3908 requestChains = keyPromise().then(function (response) {
n@2967 3909 function throwerror() {
n@2969 3910 sessionKey = null;
n@2967 3911 throw ("An unspecified error occured, no server key could be generated");
n@2967 3912 }
n@2967 3913 var parse = new DOMParser();
n@2967 3914 var xml = parse.parseFromString(response, "text/xml");
n@2971 3915 if (response.length === 0) {
n@2967 3916 throwerror();
n@2967 3917 }
n@2967 3918 if (xml.getElementsByTagName("state").length > 0) {
n@2967 3919 if (xml.getElementsByTagName("state")[0].textContent == "OK") {
n@2969 3920 sessionKey = xml.getAllElementsByTagName("key")[0].textContent;
n@2972 3921 parent.root.setAttribute("key", sessionKey);
n@2972 3922 parent.root.setAttribute("state", "empty");
n@2967 3923 return (true);
n@2967 3924 } else if (xml.getElementsByTagName("state")[0].textContent == "ERROR") {
n@2969 3925 sessionKey = null;
n@2967 3926 console.error("Could not generate server key. Server responded with error message: \"" + xml.getElementsByTagName("message")[0].textContent + "\"");
n@2969 3927 return (false);
n@2967 3928 }
n@2967 3929 } else {
n@2967 3930 throwerror();
n@2967 3931 }
n@2967 3932 return (true);
n@2967 3933 });
n@2967 3934 }
n@2967 3935 },
n@2968 3936 "update": {
n@2968 3937 "value": function () {
n@2979 3938 if (this.key === null || requestChains === undefined) {
n@2968 3939 throw ("Cannot save as key == null");
n@2968 3940 }
n@2968 3941 this.parent.root.setAttribute("state", "update");
n@2978 3942 requestChains = requestChains.then(postUpdate);
n@2967 3943 }
n@2967 3944 },
n@2968 3945 "finish": {
n@2968 3946 "value": function () {
n@2979 3947 if (this.key === null || requestChains === undefined) {
n@2968 3948 throw ("Cannot save as key == null");
n@2968 3949 }
n@2968 3950 this.parent.finish();
n@2975 3951 return requestChains.then(postUpdate()).then(function () {
n@2968 3952 console.log("OK");
n@2968 3953 }, function () {
n@2968 3954 createProjectSave("local");
n@2979 3955 });
n@2967 3956 }
n@2967 3957 }
n@2968 3958 });
n@2967 3959 return object;
n@2967 3960 })(this);
n@2967 3961 /*
nicholas@2224 3962 this.SessionKey = {
nicholas@2224 3963 key: null,
nicholas@2224 3964 request: new XMLHttpRequest(),
nicholas@2224 3965 parent: this,
nicholas@2498 3966 handleEvent: function () {
n@2967 3967
nicholas@2224 3968 },
nicholas@2510 3969 requestKey: function () {
n@2967 3970
nicholas@2510 3971 },
nicholas@2498 3972 update: function () {
nicholas@2708 3973 if (this.key === null) {
nicholas@2357 3974 console.log("Cannot save as key == null");
nicholas@2357 3975 return;
nicholas@2357 3976 }
nicholas@2498 3977 this.parent.root.setAttribute("state", "update");
nicholas@2224 3978 var xmlhttp = new XMLHttpRequest();
nicholas@2302 3979 var returnURL = "";
nicholas@2302 3980 if (typeof specification.projectReturn == "string") {
nicholas@2498 3981 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2302 3982 returnURL = specification.projectReturn;
nicholas@2302 3983 }
nicholas@2302 3984 }
nicholas@2722 3985 xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix);
nicholas@2224 3986 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
nicholas@2498 3987 xmlhttp.onerror = function () {
nicholas@2224 3988 console.log('Error updating file to server!');
nicholas@2224 3989 };
nicholas@2224 3990 var hold = document.createElement("div");
nicholas@2224 3991 var clone = this.parent.root.cloneNode(true);
nicholas@2224 3992 hold.appendChild(clone);
nicholas@2498 3993 xmlhttp.onload = function () {
nicholas@2224 3994 if (this.status >= 300) {
nicholas@2224 3995 console.log("WARNING - Could not update at this time");
nicholas@2224 3996 } else {
nicholas@2224 3997 var parser = new DOMParser();
nicholas@2224 3998 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
nicholas@2224 3999 var response = xmlDoc.getElementsByTagName('response')[0];
nicholas@2224 4000 if (response.getAttribute("state") == "OK") {
nicholas@2224 4001 var file = response.getElementsByTagName("file")[0];
nicholas@2498 4002 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
nicholas@2224 4003 } else {
nicholas@2224 4004 var message = response.getElementsByTagName("message");
nicholas@2498 4005 console.log("Intermediate save: Error! " + message.textContent);
nicholas@2224 4006 }
nicholas@2224 4007 }
nicholas@2708 4008 };
nicholas@2224 4009 xmlhttp.send([hold.innerHTML]);
nicholas@2723 4010 },
nicholas@2723 4011 finish: function () {
nicholas@2723 4012 // Final upload to complete the test
nicholas@2723 4013 this.parent.finish();
nicholas@2723 4014 var hold = document.createElement("div");
nicholas@2723 4015 var clone = this.parent.root.cloneNode(true);
nicholas@2723 4016 hold.appendChild(clone);
nicholas@2733 4017 var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=";
nicholas@2742 4018 if (this.parent.filenamePrefix.length === 0) {
nicholas@2733 4019 saveURL += "save";
nicholas@2733 4020 } else {
nicholas@2733 4021 saveURL += this.parent.filenamePrefix;
nicholas@2733 4022 }
nicholas@2723 4023 return new Promise(function (resolve, reject) {
nicholas@2723 4024 var xmlhttp = new XMLHttpRequest();
nicholas@2723 4025 xmlhttp.open("POST", saveURL);
nicholas@2723 4026 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
nicholas@2723 4027 xmlhttp.onerror = function () {
nicholas@2723 4028 console.log('Error updating file to server!');
nicholas@2723 4029 createProjectSave("local");
nicholas@2723 4030 };
nicholas@2723 4031 xmlhttp.onload = function () {
nicholas@2723 4032 if (this.status >= 300) {
nicholas@2723 4033 console.log("WARNING - Could not update at this time");
nicholas@2723 4034 createProjectSave("local");
nicholas@2723 4035 } else {
nicholas@2723 4036 var parser = new DOMParser();
nicholas@2723 4037 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
nicholas@2723 4038 var response = xmlDoc.getElementsByTagName('response')[0];
nicholas@2723 4039 if (response.getAttribute("state") == "OK") {
nicholas@2723 4040 var file = response.getElementsByTagName("file")[0];
nicholas@2723 4041 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
nicholas@2723 4042 resolve(response);
nicholas@2723 4043 } else {
nicholas@2723 4044 var message = response.getElementsByTagName("message");
nicholas@2723 4045 reject(message);
nicholas@2723 4046 }
nicholas@2723 4047 }
nicholas@2723 4048 };
nicholas@2723 4049 xmlhttp.send([hold.innerHTML]);
nicholas@2723 4050 });
nicholas@2224 4051 }
nicholas@2708 4052 };
n@2967 4053 */
nicholas@2498 4054 this.createTestPageStore = function (specification) {
nicholas@2498 4055 var store = new this.pageNode(this, specification);
nicholas@2498 4056 this.testPages.push(store);
nicholas@2498 4057 return this.testPages[this.testPages.length - 1];
nicholas@2498 4058 };
nicholas@2498 4059
nicholas@2498 4060 this.surveyNode = function (parent, root, specification) {
nicholas@2498 4061 this.specification = specification;
nicholas@2498 4062 this.parent = parent;
nicholas@2224 4063 this.state = "empty";
nicholas@2498 4064 this.XMLDOM = this.parent.document.createElement('survey');
nicholas@2498 4065 this.XMLDOM.setAttribute('location', this.specification.location);
nicholas@2498 4066 this.XMLDOM.setAttribute("state", this.state);
nicholas@2708 4067 this.specification.options.forEach(function (optNode) {
nicholas@2498 4068 if (optNode.type != 'statement') {
nicholas@2498 4069 var node = this.parent.document.createElement('surveyresult');
nicholas@2498 4070 node.setAttribute("ref", optNode.id);
nicholas@2498 4071 node.setAttribute('type', optNode.type);
nicholas@2498 4072 this.XMLDOM.appendChild(node);
nicholas@2498 4073 }
nicholas@2708 4074 }, this);
nicholas@2498 4075 root.appendChild(this.XMLDOM);
nicholas@2498 4076
nicholas@2498 4077 this.postResult = function (node) {
nicholas@2708 4078 function postNumber(doc, value) {
nicholas@2708 4079 var child = doc.createElement("response");
nicholas@2708 4080 child.textContent = value;
nicholas@2708 4081 return child;
nicholas@2708 4082 }
nicholas@2708 4083
nicholas@2708 4084 function postRadio(doc, node) {
nicholas@2708 4085 var child = doc.createElement('response');
nicholas@2708 4086 if (node.response !== null) {
nicholas@2708 4087 child.setAttribute('name', node.response.name);
nicholas@2708 4088 child.textContent = node.response.text;
nicholas@2708 4089 }
nicholas@2708 4090 return child;
nicholas@2708 4091 }
nicholas@2708 4092
nicholas@2708 4093 function postCheckbox(doc, node) {
nicholas@2708 4094 var checkNode = doc.createElement('response');
nicholas@2708 4095 checkNode.setAttribute('name', node.name);
nicholas@2708 4096 checkNode.setAttribute('checked', node.checked);
nicholas@2708 4097 return checkNode;
nicholas@2708 4098 }
nicholas@2498 4099 // From popup: node is the popupOption node containing both spec. and results
nicholas@2498 4100 // ID is the position
nicholas@2498 4101 if (node.specification.type == 'statement') {
nicholas@2498 4102 return;
nicholas@2498 4103 }
nicholas@2498 4104 var surveyresult = this.XMLDOM.firstChild;
nicholas@2708 4105 while (surveyresult !== null) {
nicholas@2498 4106 if (surveyresult.getAttribute("ref") == node.specification.id) {
nicholas@2224 4107 break;
nicholas@2224 4108 }
nicholas@2224 4109 surveyresult = surveyresult.nextElementSibling;
nicholas@2224 4110 }
nicholas@2775 4111 surveyresult.setAttribute("duration", node.elapsedTime);
nicholas@2498 4112 switch (node.specification.type) {
nicholas@2498 4113 case "number":
nicholas@2498 4114 case "question":
n@2583 4115 case "slider":
nicholas@2708 4116 surveyresult.appendChild(postNumber(this.parent.document, node.response));
nicholas@2464 4117 break;
nicholas@2498 4118 case "radio":
nicholas@2708 4119 surveyresult.appendChild(postRadio(this.parent.document, node));
nicholas@2498 4120 break;
nicholas@2498 4121 case "checkbox":
nicholas@2708 4122 if (node.response === undefined) {
nicholas@2498 4123 surveyresult.appendChild(this.parent.document.createElement('response'));
nicholas@2498 4124 break;
nicholas@2498 4125 }
nicholas@2498 4126 for (var i = 0; i < node.response.length; i++) {
nicholas@2708 4127 surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i]));
nicholas@2498 4128 }
nicholas@2498 4129 break;
nicholas@2498 4130 }
nicholas@2498 4131 };
nicholas@2498 4132 this.complete = function () {
nicholas@2498 4133 this.state = "complete";
nicholas@2498 4134 this.XMLDOM.setAttribute("state", this.state);
nicholas@2708 4135 };
nicholas@2498 4136 };
nicholas@2498 4137
nicholas@2498 4138 this.pageNode = function (parent, specification) {
nicholas@2498 4139 // Create one store per test page
nicholas@2498 4140 this.specification = specification;
nicholas@2498 4141 this.parent = parent;
nicholas@2498 4142 this.state = "empty";
nicholas@2498 4143 this.XMLDOM = this.parent.document.createElement('page');
nicholas@2498 4144 this.XMLDOM.setAttribute('ref', specification.id);
nicholas@2498 4145 this.XMLDOM.setAttribute('presentedId', specification.presentedId);
nicholas@2498 4146 this.XMLDOM.setAttribute("state", this.state);
nicholas@2708 4147 if (specification.preTest !== undefined) {
nicholas@2498 4148 this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
nicholas@2498 4149 }
nicholas@2708 4150 if (specification.postTest !== undefined) {
nicholas@2498 4151 this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
nicholas@2498 4152 }
nicholas@2498 4153
nicholas@2498 4154 // Add any page metrics
nicholas@2498 4155 var page_metric = this.parent.document.createElement('metric');
nicholas@2498 4156 this.XMLDOM.appendChild(page_metric);
nicholas@2498 4157
nicholas@2498 4158 // Add the audioelement
nicholas@2708 4159 this.specification.audioElements.forEach(function (element) {
nicholas@2498 4160 var aeNode = this.parent.document.createElement('audioelement');
nicholas@2498 4161 aeNode.setAttribute('ref', element.id);
nicholas@2708 4162 if (element.name !== undefined) {
nicholas@2708 4163 aeNode.setAttribute('name', element.name);
nicholas@2708 4164 }
nicholas@2498 4165 aeNode.setAttribute('type', element.type);
nicholas@2498 4166 aeNode.setAttribute('url', element.url);
nicholas@2498 4167 aeNode.setAttribute('fqurl', qualifyURL(element.url));
nicholas@2498 4168 aeNode.setAttribute('gain', element.gain);
nicholas@2498 4169 if (element.type == 'anchor' || element.type == 'reference') {
nicholas@2498 4170 if (element.marker > 0) {
nicholas@2498 4171 aeNode.setAttribute('marker', element.marker);
nicholas@2464 4172 }
nicholas@2498 4173 }
nicholas@2498 4174 var ae_metric = this.parent.document.createElement('metric');
nicholas@2498 4175 aeNode.appendChild(ae_metric);
nicholas@2498 4176 this.XMLDOM.appendChild(aeNode);
nicholas@2708 4177 }, this);
nicholas@2498 4178
nicholas@2498 4179 this.parent.root.appendChild(this.XMLDOM);
nicholas@2498 4180
nicholas@2498 4181 this.complete = function () {
nicholas@2224 4182 this.state = "complete";
nicholas@2498 4183 this.XMLDOM.setAttribute("state", "complete");
nicholas@2708 4184 };
nicholas@2498 4185 };
nicholas@2498 4186 this.update = function () {
nicholas@2224 4187 this.SessionKey.update();
nicholas@2708 4188 };
nicholas@2498 4189 this.finish = function () {
nicholas@2498 4190 this.state = 1;
nicholas@2498 4191 this.root.setAttribute("state", "complete");
nicholas@2498 4192 return this.root;
nicholas@2498 4193 };
nicholas@2722 4194
nicholas@2722 4195 Object.defineProperties(this, {
nicholas@2722 4196 'filenamePrefix': {
nicholas@2722 4197 'get': function () {
nicholas@2722 4198 return pFilenamePrefix;
nicholas@2722 4199 },
nicholas@2722 4200 'set': function (value) {
nicholas@2722 4201 if (typeof value !== "string") {
nicholas@2722 4202 value = String(value);
nicholas@2722 4203 }
nicholas@2722 4204 pFilenamePrefix = value;
nicholas@2722 4205 return value;
nicholas@2722 4206 }
nicholas@2722 4207 }
nicholas@2725 4208 });
nicholas@2224 4209 }
nicholas@2384 4210
nicholas@2401 4211 var window_depedancy_callback;
nicholas@2498 4212 window_depedancy_callback = window.setInterval(function () {
nicholas@2401 4213 if (check_dependancies()) {
nicholas@2401 4214 window.clearInterval(window_depedancy_callback);
nicholas@2401 4215 onload();
nicholas@2401 4216 } else {
nicholas@2401 4217 document.getElementById("topLevelBody").innerHTML = "<h1>Loading Resources</h1>";
nicholas@2401 4218 }
nicholas@2498 4219 }, 100);