annotate js/core.js @ 2924:4ae62d4c5c6d

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