annotate js/core.js @ 2833:47f187d4fc71

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