annotate js/core.js @ 3090:385bb2e03ab7

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