annotate js/core.js @ 2983:bc33aca9f8f1

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