annotate js/core.js @ 2934:260efd43fe52

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