annotate js/core.js @ 2774:2fc62a50d593

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