annotate js/core.js @ 2917:60e5116da71a

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