annotate js/core.js @ 2499:54782e20bc62

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