annotate js/core.js @ 2939:5d7e33fd00d8

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