annotate js/core.js @ 2498:309ed146c2e6

Fix for #148
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Tue, 20 Sep 2016 17:19:39 +0100
parents f0b0809c7ee5
children 54782e20bc62
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@2498 1843 this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration);
nicholas@2224 1844 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
nicholas@2498 1845 }
nicholas@2498 1846 };
nicholas@2498 1847
nicholas@2498 1848 this.stop = function (stopTime) {
nicholas@2224 1849 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
nicholas@2498 1850 if (this.bufferNode != undefined) {
nicholas@2498 1851 this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
nicholas@2498 1852 this.bufferNode.stop(stopTime);
nicholas@2498 1853 this.bufferNode = undefined;
nicholas@2498 1854 }
nicholas@2224 1855 this.outputGain.gain.value = 0.0;
nicholas@2224 1856 this.interfaceDOM.stopPlayback();
nicholas@2498 1857 };
nicholas@2498 1858
nicholas@2498 1859 this.getCurrentPosition = function () {
nicholas@2498 1860 var time = audioEngineContext.timer.getTestTime();
nicholas@2498 1861 if (this.bufferNode != undefined) {
nicholas@2498 1862 var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
nicholas@2498 1863 if (isNaN(position)) {
nicholas@2498 1864 return 0;
nicholas@2498 1865 }
nicholas@2224 1866 return position;
nicholas@2498 1867 } else {
nicholas@2498 1868 return 0;
nicholas@2498 1869 }
nicholas@2498 1870 };
nicholas@2498 1871
nicholas@2498 1872 this.exportXMLDOM = function () {
nicholas@2498 1873 var file = storage.document.createElement('file');
nicholas@2498 1874 file.setAttribute('sampleRate', this.buffer.buffer.sampleRate);
nicholas@2498 1875 file.setAttribute('channels', this.buffer.buffer.numberOfChannels);
nicholas@2498 1876 file.setAttribute('sampleCount', this.buffer.buffer.length);
nicholas@2498 1877 file.setAttribute('duration', this.buffer.buffer.duration);
nicholas@2498 1878 this.storeDOM.appendChild(file);
nicholas@2498 1879 if (this.specification.type != 'outside-reference') {
nicholas@2498 1880 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
nicholas@2498 1881 if (interfaceXML != null) {
nicholas@2498 1882 if (interfaceXML.length == undefined) {
nicholas@2498 1883 this.storeDOM.appendChild(interfaceXML);
nicholas@2498 1884 } else {
nicholas@2498 1885 for (var i = 0; i < interfaceXML.length; i++) {
nicholas@2498 1886 this.storeDOM.appendChild(interfaceXML[i]);
nicholas@2498 1887 }
nicholas@2498 1888 }
nicholas@2498 1889 }
nicholas@2498 1890 if (this.commentDOM != null) {
nicholas@2498 1891 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
nicholas@2498 1892 }
nicholas@2498 1893 }
nicholas@2498 1894 var nodes = this.metric.exportXMLDOM();
nicholas@2498 1895 var mroot = this.storeDOM.getElementsByTagName('metric')[0];
nicholas@2498 1896 for (var i = 0; i < nodes.length; i++) {
nicholas@2498 1897 mroot.appendChild(nodes[i]);
nicholas@2498 1898 }
nicholas@2498 1899 };
nicholas@2224 1900 }
nicholas@2224 1901
nicholas@2498 1902 function timer() {
nicholas@2498 1903 /* Timer object used in audioEngine to keep track of session timings
nicholas@2498 1904 * Uses the timer of the web audio API, so sample resolution
nicholas@2498 1905 */
nicholas@2498 1906 this.testStarted = false;
nicholas@2498 1907 this.testStartTime = 0;
nicholas@2498 1908 this.testDuration = 0;
nicholas@2498 1909 this.minimumTestTime = 0; // No minimum test time
nicholas@2498 1910 this.startTest = function () {
nicholas@2498 1911 if (this.testStarted == false) {
nicholas@2498 1912 this.testStartTime = audioContext.currentTime;
nicholas@2498 1913 this.testStarted = true;
nicholas@2498 1914 this.updateTestTime();
nicholas@2498 1915 audioEngineContext.metric.initialiseTest();
nicholas@2498 1916 }
nicholas@2498 1917 };
nicholas@2498 1918 this.stopTest = function () {
nicholas@2498 1919 if (this.testStarted) {
nicholas@2498 1920 this.testDuration = this.getTestTime();
nicholas@2498 1921 this.testStarted = false;
nicholas@2498 1922 } else {
nicholas@2498 1923 console.log('ERR: Test tried to end before beginning');
nicholas@2498 1924 }
nicholas@2498 1925 };
nicholas@2498 1926 this.updateTestTime = function () {
nicholas@2498 1927 if (this.testStarted) {
nicholas@2498 1928 this.testDuration = audioContext.currentTime - this.testStartTime;
nicholas@2498 1929 }
nicholas@2498 1930 };
nicholas@2498 1931 this.getTestTime = function () {
nicholas@2498 1932 this.updateTestTime();
nicholas@2498 1933 return this.testDuration;
nicholas@2498 1934 };
nicholas@2224 1935 }
nicholas@2224 1936
nicholas@2498 1937 function sessionMetrics(engine, specification) {
nicholas@2498 1938 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
nicholas@2498 1939 */
nicholas@2498 1940 this.engine = engine;
nicholas@2498 1941 this.lastClicked = -1;
nicholas@2498 1942 this.data = -1;
nicholas@2498 1943 this.reset = function () {
nicholas@2498 1944 this.lastClicked = -1;
nicholas@2498 1945 this.data = -1;
nicholas@2498 1946 };
nicholas@2498 1947
nicholas@2498 1948 this.enableElementInitialPosition = false;
nicholas@2498 1949 this.enableElementListenTracker = false;
nicholas@2498 1950 this.enableElementTimer = false;
nicholas@2498 1951 this.enableElementTracker = false;
nicholas@2498 1952 this.enableFlagListenedTo = false;
nicholas@2498 1953 this.enableFlagMoved = false;
nicholas@2498 1954 this.enableTestTimer = false;
nicholas@2498 1955 // Obtain the metrics enabled
nicholas@2498 1956 for (var i = 0; i < specification.metrics.enabled.length; i++) {
nicholas@2498 1957 var node = specification.metrics.enabled[i];
nicholas@2498 1958 switch (node) {
nicholas@2498 1959 case 'testTimer':
nicholas@2498 1960 this.enableTestTimer = true;
nicholas@2498 1961 break;
nicholas@2498 1962 case 'elementTimer':
nicholas@2498 1963 this.enableElementTimer = true;
nicholas@2498 1964 break;
nicholas@2498 1965 case 'elementTracker':
nicholas@2498 1966 this.enableElementTracker = true;
nicholas@2498 1967 break;
nicholas@2498 1968 case 'elementListenTracker':
nicholas@2498 1969 this.enableElementListenTracker = true;
nicholas@2498 1970 break;
nicholas@2498 1971 case 'elementInitialPosition':
nicholas@2498 1972 this.enableElementInitialPosition = true;
nicholas@2498 1973 break;
nicholas@2498 1974 case 'elementFlagListenedTo':
nicholas@2498 1975 this.enableFlagListenedTo = true;
nicholas@2498 1976 break;
nicholas@2498 1977 case 'elementFlagMoved':
nicholas@2498 1978 this.enableFlagMoved = true;
nicholas@2498 1979 break;
nicholas@2498 1980 case 'elementFlagComments':
nicholas@2498 1981 this.enableFlagComments = true;
nicholas@2498 1982 break;
nicholas@2498 1983 }
nicholas@2498 1984 }
nicholas@2498 1985 this.initialiseTest = function () {};
nicholas@2224 1986 }
nicholas@2224 1987
nicholas@2498 1988 function metricTracker(caller) {
nicholas@2498 1989 /* Custom object to track and collect metric data
nicholas@2498 1990 * Used only inside the audioObjects object.
nicholas@2498 1991 */
nicholas@2498 1992
nicholas@2498 1993 this.listenedTimer = 0;
nicholas@2498 1994 this.listenStart = 0;
nicholas@2498 1995 this.listenHold = false;
nicholas@2498 1996 this.initialPosition = -1;
nicholas@2498 1997 this.movementTracker = [];
nicholas@2498 1998 this.listenTracker = [];
nicholas@2498 1999 this.wasListenedTo = false;
nicholas@2498 2000 this.wasMoved = false;
nicholas@2498 2001 this.hasComments = false;
nicholas@2498 2002 this.parent = caller;
nicholas@2498 2003
nicholas@2498 2004 this.initialise = function (position) {
nicholas@2498 2005 if (this.initialPosition == -1) {
nicholas@2498 2006 this.initialPosition = position;
nicholas@2498 2007 this.moved(0, position);
nicholas@2498 2008 }
nicholas@2498 2009 };
nicholas@2498 2010
nicholas@2498 2011 this.moved = function (time, position) {
nicholas@2498 2012 if (time > 0) {
nicholas@2498 2013 this.wasMoved = true;
nicholas@2498 2014 }
nicholas@2498 2015 this.movementTracker[this.movementTracker.length] = [time, position];
nicholas@2498 2016 };
nicholas@2498 2017
nicholas@2498 2018 this.startListening = function (time) {
nicholas@2498 2019 if (this.listenHold == false) {
nicholas@2498 2020 this.wasListenedTo = true;
nicholas@2498 2021 this.listenStart = time;
nicholas@2498 2022 this.listenHold = true;
nicholas@2498 2023
nicholas@2498 2024 var evnt = document.createElement('event');
nicholas@2498 2025 var testTime = document.createElement('testTime');
nicholas@2498 2026 testTime.setAttribute('start', time);
nicholas@2498 2027 var bufferTime = document.createElement('bufferTime');
nicholas@2498 2028 bufferTime.setAttribute('start', this.parent.getCurrentPosition());
nicholas@2498 2029 evnt.appendChild(testTime);
nicholas@2498 2030 evnt.appendChild(bufferTime);
nicholas@2498 2031 this.listenTracker.push(evnt);
nicholas@2498 2032
nicholas@2498 2033 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2034 }
nicholas@2498 2035 };
nicholas@2498 2036
nicholas@2498 2037 this.stopListening = function (time, bufferStopTime) {
nicholas@2498 2038 if (this.listenHold == true) {
nicholas@2498 2039 var diff = time - this.listenStart;
nicholas@2498 2040 this.listenedTimer += (diff);
nicholas@2498 2041 this.listenStart = 0;
nicholas@2498 2042 this.listenHold = false;
nicholas@2498 2043
nicholas@2498 2044 var evnt = this.listenTracker[this.listenTracker.length - 1];
nicholas@2498 2045 var testTime = evnt.getElementsByTagName('testTime')[0];
nicholas@2498 2046 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
nicholas@2498 2047 testTime.setAttribute('stop', time);
nicholas@2498 2048 if (bufferStopTime == undefined) {
nicholas@2498 2049 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
nicholas@2498 2050 } else {
nicholas@2498 2051 bufferTime.setAttribute('stop', bufferStopTime);
nicholas@2498 2052 }
nicholas@2498 2053 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2054 }
nicholas@2498 2055 };
nicholas@2498 2056
nicholas@2498 2057 this.exportXMLDOM = function () {
nicholas@2498 2058 var storeDOM = [];
nicholas@2498 2059 if (audioEngineContext.metric.enableElementTimer) {
nicholas@2498 2060 var mElementTimer = storage.document.createElement('metricresult');
nicholas@2498 2061 mElementTimer.setAttribute('name', 'enableElementTimer');
nicholas@2498 2062 mElementTimer.textContent = this.listenedTimer;
nicholas@2498 2063 storeDOM.push(mElementTimer);
nicholas@2498 2064 }
nicholas@2498 2065 if (audioEngineContext.metric.enableElementTracker) {
nicholas@2498 2066 var elementTrackerFull = storage.document.createElement('metricresult');
nicholas@2498 2067 elementTrackerFull.setAttribute('name', 'elementTrackerFull');
nicholas@2498 2068 for (var k = 0; k < this.movementTracker.length; k++) {
nicholas@2498 2069 var timePos = storage.document.createElement('movement');
nicholas@2498 2070 timePos.setAttribute("time", this.movementTracker[k][0]);
nicholas@2498 2071 timePos.setAttribute("value", this.movementTracker[k][1]);
nicholas@2498 2072 elementTrackerFull.appendChild(timePos);
nicholas@2498 2073 }
nicholas@2498 2074 storeDOM.push(elementTrackerFull);
nicholas@2498 2075 }
nicholas@2498 2076 if (audioEngineContext.metric.enableElementListenTracker) {
nicholas@2498 2077 var elementListenTracker = storage.document.createElement('metricresult');
nicholas@2498 2078 elementListenTracker.setAttribute('name', 'elementListenTracker');
nicholas@2498 2079 for (var k = 0; k < this.listenTracker.length; k++) {
nicholas@2498 2080 elementListenTracker.appendChild(this.listenTracker[k]);
nicholas@2498 2081 }
nicholas@2498 2082 storeDOM.push(elementListenTracker);
nicholas@2498 2083 }
nicholas@2498 2084 if (audioEngineContext.metric.enableElementInitialPosition) {
nicholas@2498 2085 var elementInitial = storage.document.createElement('metricresult');
nicholas@2498 2086 elementInitial.setAttribute('name', 'elementInitialPosition');
nicholas@2498 2087 elementInitial.textContent = this.initialPosition;
nicholas@2498 2088 storeDOM.push(elementInitial);
nicholas@2498 2089 }
nicholas@2498 2090 if (audioEngineContext.metric.enableFlagListenedTo) {
nicholas@2498 2091 var flagListenedTo = storage.document.createElement('metricresult');
nicholas@2498 2092 flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
nicholas@2498 2093 flagListenedTo.textContent = this.wasListenedTo;
nicholas@2498 2094 storeDOM.push(flagListenedTo);
nicholas@2498 2095 }
nicholas@2498 2096 if (audioEngineContext.metric.enableFlagMoved) {
nicholas@2498 2097 var flagMoved = storage.document.createElement('metricresult');
nicholas@2498 2098 flagMoved.setAttribute('name', 'elementFlagMoved');
nicholas@2498 2099 flagMoved.textContent = this.wasMoved;
nicholas@2498 2100 storeDOM.push(flagMoved);
nicholas@2498 2101 }
nicholas@2498 2102 if (audioEngineContext.metric.enableFlagComments) {
nicholas@2498 2103 var flagComments = storage.document.createElement('metricresult');
nicholas@2498 2104 flagComments.setAttribute('name', 'elementFlagComments');
nicholas@2498 2105 if (this.parent.commentDOM == null) {
nicholas@2498 2106 flag.textContent = 'false';
nicholas@2498 2107 } else if (this.parent.commentDOM.textContent.length == 0) {
nicholas@2498 2108 flag.textContent = 'false';
nicholas@2498 2109 } else {
nicholas@2498 2110 flag.textContet = 'true';
nicholas@2498 2111 }
nicholas@2498 2112 storeDOM.push(flagComments);
nicholas@2498 2113 }
nicholas@2498 2114 return storeDOM;
nicholas@2498 2115 };
nicholas@2224 2116 }
nicholas@2498 2117
nicholas@2224 2118 function Interface(specificationObject) {
nicholas@2498 2119 // This handles the bindings between the interface and the audioEngineContext;
nicholas@2498 2120 this.specification = specificationObject;
nicholas@2498 2121 this.insertPoint = document.getElementById("topLevelBody");
nicholas@2498 2122
nicholas@2498 2123 this.newPage = function (audioHolderObject, store) {
nicholas@2498 2124 audioEngineContext.newTestPage(audioHolderObject, store);
nicholas@2498 2125 interfaceContext.commentBoxes.deleteCommentBoxes();
nicholas@2498 2126 interfaceContext.deleteCommentQuestions();
nicholas@2498 2127 loadTest(audioHolderObject, store);
nicholas@2498 2128 };
nicholas@2498 2129
nicholas@2498 2130 // Bounded by interface!!
nicholas@2498 2131 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
nicholas@2498 2132 // For example, APE returns the slider position normalised in a <value> tag.
nicholas@2498 2133 this.interfaceObjects = [];
nicholas@2498 2134 this.interfaceObject = function () {};
nicholas@2498 2135
nicholas@2498 2136 this.resizeWindow = function (event) {
nicholas@2498 2137 popup.resize(event);
nicholas@2352 2138 this.volume.resize();
nicholas@2360 2139 this.lightbox.resize();
nicholas@2498 2140 for (var i = 0; i < this.commentBoxes.length; i++) {
nicholas@2498 2141 this.commentBoxes[i].resize();
nicholas@2498 2142 }
nicholas@2498 2143 for (var i = 0; i < this.commentQuestions.length; i++) {
nicholas@2498 2144 this.commentQuestions[i].resize();
nicholas@2498 2145 }
nicholas@2498 2146 try {
nicholas@2498 2147 resizeWindow(event);
nicholas@2498 2148 } catch (err) {
nicholas@2498 2149 console.log("Warning - Interface does not have Resize option");
nicholas@2498 2150 console.log(err);
nicholas@2498 2151 }
nicholas@2498 2152 };
nicholas@2498 2153
nicholas@2498 2154 this.returnNavigator = function () {
nicholas@2498 2155 var node = storage.document.createElement("navigator");
nicholas@2498 2156 var platform = storage.document.createElement("platform");
nicholas@2498 2157 platform.textContent = navigator.platform;
nicholas@2498 2158 var vendor = storage.document.createElement("vendor");
nicholas@2498 2159 vendor.textContent = navigator.vendor;
nicholas@2498 2160 var userAgent = storage.document.createElement("uagent");
nicholas@2498 2161 userAgent.textContent = navigator.userAgent;
nicholas@2224 2162 var screen = storage.document.createElement("window");
nicholas@2498 2163 screen.setAttribute('innerWidth', window.innerWidth);
nicholas@2498 2164 screen.setAttribute('innerHeight', window.innerHeight);
nicholas@2498 2165 node.appendChild(platform);
nicholas@2498 2166 node.appendChild(vendor);
nicholas@2498 2167 node.appendChild(userAgent);
nicholas@2224 2168 node.appendChild(screen);
nicholas@2498 2169 return node;
nicholas@2498 2170 };
nicholas@2498 2171
nicholas@2498 2172 this.returnDateNode = function () {
nicholas@2224 2173 // Create an XML Node for the Date and Time a test was conducted
nicholas@2224 2174 // Structure is
nicholas@2224 2175 // <datetime>
nicholas@2224 2176 // <date year="##" month="##" day="##">DD/MM/YY</date>
nicholas@2224 2177 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
nicholas@2224 2178 // </datetime>
nicholas@2224 2179 var dateTime = new Date();
nicholas@2224 2180 var hold = storage.document.createElement("datetime");
nicholas@2224 2181 var date = storage.document.createElement("date");
nicholas@2224 2182 var time = storage.document.createElement("time");
nicholas@2498 2183 date.setAttribute('year', dateTime.getFullYear());
nicholas@2498 2184 date.setAttribute('month', dateTime.getMonth() + 1);
nicholas@2498 2185 date.setAttribute('day', dateTime.getDate());
nicholas@2498 2186 time.setAttribute('hour', dateTime.getHours());
nicholas@2498 2187 time.setAttribute('minute', dateTime.getMinutes());
nicholas@2498 2188 time.setAttribute('secs', dateTime.getSeconds());
nicholas@2498 2189
nicholas@2224 2190 hold.appendChild(date);
nicholas@2224 2191 hold.appendChild(time);
nicholas@2224 2192 return hold;
nicholas@2224 2193
nicholas@2224 2194 }
nicholas@2498 2195
nicholas@2360 2196 this.lightbox = {
nicholas@2360 2197 parent: this,
nicholas@2360 2198 root: document.createElement("div"),
nicholas@2360 2199 content: document.createElement("div"),
nicholas@2360 2200 accept: document.createElement("button"),
nicholas@2360 2201 blanker: document.createElement("div"),
nicholas@2498 2202 post: function (type, message) {
nicholas@2498 2203 switch (type) {
nicholas@2360 2204 case "Error":
nicholas@2360 2205 this.content.className = "lightbox-error";
nicholas@2360 2206 break;
nicholas@2360 2207 case "Warning":
nicholas@2360 2208 this.content.className = "lightbox-warning";
nicholas@2360 2209 break;
nicholas@2360 2210 default:
nicholas@2360 2211 this.content.className = "lightbox-message";
nicholas@2360 2212 break;
nicholas@2360 2213 }
nicholas@2360 2214 var msg = document.createElement("p");
nicholas@2360 2215 msg.textContent = message;
nicholas@2360 2216 this.content.appendChild(msg);
nicholas@2360 2217 this.show();
nicholas@2360 2218 },
nicholas@2498 2219 show: function () {
nicholas@2360 2220 this.root.style.visibility = "visible";
nicholas@2360 2221 this.blanker.style.visibility = "visible";
nicholas@2360 2222 },
nicholas@2498 2223 clear: function () {
nicholas@2360 2224 this.root.style.visibility = "";
nicholas@2360 2225 this.blanker.style.visibility = "";
nicholas@2360 2226 this.content.textContent = "";
nicholas@2360 2227 },
nicholas@2498 2228 handleEvent: function (event) {
nicholas@2360 2229 if (event.currentTarget == this.accept) {
nicholas@2360 2230 this.clear();
nicholas@2360 2231 }
nicholas@2360 2232 },
nicholas@2498 2233 resize: function (event) {
nicholas@2498 2234 this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2360 2235 }
nicholas@2360 2236 }
nicholas@2498 2237
nicholas@2360 2238 this.lightbox.root.appendChild(this.lightbox.content);
nicholas@2360 2239 this.lightbox.root.appendChild(this.lightbox.accept);
nicholas@2360 2240 this.lightbox.root.className = "popupHolder";
nicholas@2360 2241 this.lightbox.root.id = "lightbox-root";
nicholas@2360 2242 this.lightbox.accept.className = "popupButton";
nicholas@2360 2243 this.lightbox.accept.style.bottom = "10px";
nicholas@2360 2244 this.lightbox.accept.textContent = "OK";
nicholas@2360 2245 this.lightbox.accept.style.left = "237.5px";
nicholas@2498 2246 this.lightbox.accept.addEventListener("click", this.lightbox);
nicholas@2360 2247 this.lightbox.blanker.className = "testHalt";
nicholas@2360 2248 this.lightbox.blanker.id = "lightbox-blanker";
nicholas@2360 2249 document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
nicholas@2360 2250 document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
nicholas@2498 2251
nicholas@2498 2252 this.commentBoxes = new function () {
nicholas@2224 2253 this.boxes = [];
nicholas@2224 2254 this.injectPoint = null;
nicholas@2498 2255 this.elementCommentBox = function (audioObject) {
nicholas@2224 2256 var element = audioObject.specification;
nicholas@2224 2257 this.audioObject = audioObject;
nicholas@2224 2258 this.id = audioObject.id;
nicholas@2224 2259 var audioHolderObject = audioObject.specification.parent;
nicholas@2224 2260 // Create document objects to hold the comment boxes
nicholas@2224 2261 this.trackComment = document.createElement('div');
nicholas@2224 2262 this.trackComment.className = 'comment-div';
nicholas@2498 2263 this.trackComment.id = 'comment-div-' + audioObject.id;
nicholas@2224 2264 // Create a string next to each comment asking for a comment
nicholas@2224 2265 this.trackString = document.createElement('span');
nicholas@2498 2266 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId();
nicholas@2224 2267 // Create the HTML5 comment box 'textarea'
nicholas@2224 2268 this.trackCommentBox = document.createElement('textarea');
nicholas@2224 2269 this.trackCommentBox.rows = '4';
nicholas@2224 2270 this.trackCommentBox.cols = '100';
nicholas@2498 2271 this.trackCommentBox.name = 'trackComment' + audioObject.id;
nicholas@2224 2272 this.trackCommentBox.className = 'trackComment';
nicholas@2224 2273 var br = document.createElement('br');
nicholas@2224 2274 // Add to the holder.
nicholas@2224 2275 this.trackComment.appendChild(this.trackString);
nicholas@2224 2276 this.trackComment.appendChild(br);
nicholas@2224 2277 this.trackComment.appendChild(this.trackCommentBox);
nicholas@2224 2278
nicholas@2498 2279 this.exportXMLDOM = function () {
nicholas@2224 2280 var root = document.createElement('comment');
nicholas@2224 2281 var question = document.createElement('question');
nicholas@2224 2282 question.textContent = this.trackString.textContent;
nicholas@2224 2283 var response = document.createElement('response');
nicholas@2224 2284 response.textContent = this.trackCommentBox.value;
nicholas@2498 2285 console.log("Comment frag-" + this.id + ": " + response.textContent);
nicholas@2224 2286 root.appendChild(question);
nicholas@2224 2287 root.appendChild(response);
nicholas@2224 2288 return root;
nicholas@2224 2289 };
nicholas@2498 2290 this.resize = function () {
nicholas@2498 2291 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2292 if (boxwidth >= 600) {
nicholas@2224 2293 boxwidth = 600;
nicholas@2498 2294 } else if (boxwidth < 400) {
nicholas@2224 2295 boxwidth = 400;
nicholas@2224 2296 }
nicholas@2498 2297 this.trackComment.style.width = boxwidth + "px";
nicholas@2498 2298 this.trackCommentBox.style.width = boxwidth - 6 + "px";
nicholas@2224 2299 };
nicholas@2224 2300 this.resize();
nicholas@2224 2301 };
nicholas@2498 2302 this.createCommentBox = function (audioObject) {
nicholas@2224 2303 var node = new this.elementCommentBox(audioObject);
nicholas@2224 2304 this.boxes.push(node);
nicholas@2224 2305 audioObject.commentDOM = node;
nicholas@2224 2306 return node;
nicholas@2224 2307 };
nicholas@2498 2308 this.sortCommentBoxes = function () {
nicholas@2498 2309 this.boxes.sort(function (a, b) {
nicholas@2498 2310 return a.id - b.id;
nicholas@2498 2311 });
nicholas@2224 2312 };
nicholas@2224 2313
nicholas@2498 2314 this.showCommentBoxes = function (inject, sort) {
nicholas@2224 2315 this.injectPoint = inject;
nicholas@2498 2316 if (sort) {
nicholas@2498 2317 this.sortCommentBoxes();
nicholas@2498 2318 }
nicholas@2224 2319 for (var box of this.boxes) {
nicholas@2224 2320 inject.appendChild(box.trackComment);
nicholas@2224 2321 }
nicholas@2224 2322 };
nicholas@2224 2323
nicholas@2498 2324 this.deleteCommentBoxes = function () {
nicholas@2224 2325 if (this.injectPoint != null) {
nicholas@2224 2326 for (var box of this.boxes) {
nicholas@2224 2327 this.injectPoint.removeChild(box.trackComment);
nicholas@2224 2328 }
nicholas@2224 2329 this.injectPoint = null;
nicholas@2224 2330 }
nicholas@2224 2331 this.boxes = [];
nicholas@2224 2332 };
nicholas@2224 2333 }
nicholas@2498 2334
nicholas@2498 2335 this.commentQuestions = [];
nicholas@2498 2336
nicholas@2498 2337 this.commentBox = function (commentQuestion) {
nicholas@2498 2338 this.specification = commentQuestion;
nicholas@2498 2339 // Create document objects to hold the comment boxes
nicholas@2498 2340 this.holder = document.createElement('div');
nicholas@2498 2341 this.holder.className = 'comment-div';
nicholas@2498 2342 // Create a string next to each comment asking for a comment
nicholas@2498 2343 this.string = document.createElement('span');
nicholas@2498 2344 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2345 // Create the HTML5 comment box 'textarea'
nicholas@2498 2346 this.textArea = document.createElement('textarea');
nicholas@2498 2347 this.textArea.rows = '4';
nicholas@2498 2348 this.textArea.cols = '100';
nicholas@2498 2349 this.textArea.className = 'trackComment';
nicholas@2498 2350 var br = document.createElement('br');
nicholas@2498 2351 // Add to the holder.
nicholas@2498 2352 this.holder.appendChild(this.string);
nicholas@2498 2353 this.holder.appendChild(br);
nicholas@2498 2354 this.holder.appendChild(this.textArea);
nicholas@2498 2355
nicholas@2498 2356 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2357 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2358 root.id = this.specification.id;
nicholas@2498 2359 root.setAttribute('type', this.specification.type);
nicholas@2498 2360 console.log("Question: " + this.string.textContent);
nicholas@2498 2361 console.log("Response: " + root.textContent);
nicholas@2224 2362 var question = storePoint.parent.document.createElement('question');
nicholas@2224 2363 question.textContent = this.string.textContent;
nicholas@2224 2364 var response = storePoint.parent.document.createElement('response');
nicholas@2224 2365 response.textContent = this.textArea.value;
nicholas@2224 2366 root.appendChild(question);
nicholas@2224 2367 root.appendChild(response);
nicholas@2224 2368 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2369 return root;
nicholas@2498 2370 };
nicholas@2498 2371 this.resize = function () {
nicholas@2498 2372 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2373 if (boxwidth >= 600) {
nicholas@2498 2374 boxwidth = 600;
nicholas@2498 2375 } else if (boxwidth < 400) {
nicholas@2498 2376 boxwidth = 400;
nicholas@2498 2377 }
nicholas@2498 2378 this.holder.style.width = boxwidth + "px";
nicholas@2498 2379 this.textArea.style.width = boxwidth - 6 + "px";
nicholas@2498 2380 };
nicholas@2498 2381 this.resize();
nicholas@2498 2382 };
nicholas@2498 2383
nicholas@2498 2384 this.radioBox = function (commentQuestion) {
nicholas@2498 2385 this.specification = commentQuestion;
nicholas@2498 2386 // Create document objects to hold the comment boxes
nicholas@2498 2387 this.holder = document.createElement('div');
nicholas@2498 2388 this.holder.className = 'comment-div';
nicholas@2498 2389 // Create a string next to each comment asking for a comment
nicholas@2498 2390 this.string = document.createElement('span');
nicholas@2498 2391 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2392 var br = document.createElement('br');
nicholas@2498 2393 // Add to the holder.
nicholas@2498 2394 this.holder.appendChild(this.string);
nicholas@2498 2395 this.holder.appendChild(br);
nicholas@2498 2396 this.options = [];
nicholas@2498 2397 this.inputs = document.createElement('div');
nicholas@2498 2398 this.span = document.createElement('div');
nicholas@2498 2399 this.inputs.align = 'center';
nicholas@2498 2400 this.inputs.style.marginLeft = '12px';
nicholas@2294 2401 this.inputs.className = "comment-radio-inputs-holder";
nicholas@2498 2402 this.span.style.marginLeft = '12px';
nicholas@2498 2403 this.span.align = 'center';
nicholas@2498 2404 this.span.style.marginTop = '15px';
nicholas@2294 2405 this.span.className = "comment-radio-span-holder";
nicholas@2498 2406
nicholas@2498 2407 var optCount = commentQuestion.options.length;
nicholas@2498 2408 for (var optNode of commentQuestion.options) {
nicholas@2498 2409 var div = document.createElement('div');
nicholas@2498 2410 div.style.width = '80px';
nicholas@2498 2411 div.style.float = 'left';
nicholas@2498 2412 var input = document.createElement('input');
nicholas@2498 2413 input.type = 'radio';
nicholas@2498 2414 input.name = commentQuestion.id;
nicholas@2498 2415 input.setAttribute('setvalue', optNode.name);
nicholas@2498 2416 input.className = 'comment-radio';
nicholas@2498 2417 div.appendChild(input);
nicholas@2498 2418 this.inputs.appendChild(div);
nicholas@2498 2419
nicholas@2498 2420
nicholas@2498 2421 div = document.createElement('div');
nicholas@2498 2422 div.style.width = '80px';
nicholas@2498 2423 div.style.float = 'left';
nicholas@2498 2424 div.align = 'center';
nicholas@2498 2425 var span = document.createElement('span');
nicholas@2498 2426 span.textContent = optNode.text;
nicholas@2498 2427 span.className = 'comment-radio-span';
nicholas@2498 2428 div.appendChild(span);
nicholas@2498 2429 this.span.appendChild(div);
nicholas@2498 2430 this.options.push(input);
nicholas@2498 2431 }
nicholas@2498 2432 this.holder.appendChild(this.span);
nicholas@2498 2433 this.holder.appendChild(this.inputs);
nicholas@2498 2434
nicholas@2498 2435 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2436 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2437 root.id = this.specification.id;
nicholas@2498 2438 root.setAttribute('type', this.specification.type);
nicholas@2498 2439 var question = document.createElement('question');
nicholas@2498 2440 question.textContent = this.string.textContent;
nicholas@2498 2441 var response = document.createElement('response');
nicholas@2498 2442 var i = 0;
nicholas@2498 2443 while (this.options[i].checked == false) {
nicholas@2498 2444 i++;
nicholas@2498 2445 if (i >= this.options.length) {
nicholas@2498 2446 break;
nicholas@2498 2447 }
nicholas@2498 2448 }
nicholas@2498 2449 if (i >= this.options.length) {
nicholas@2498 2450 response.textContent = 'null';
nicholas@2498 2451 } else {
nicholas@2498 2452 response.textContent = this.options[i].getAttribute('setvalue');
nicholas@2498 2453 response.setAttribute('number', i);
nicholas@2498 2454 }
nicholas@2498 2455 console.log('Comment: ' + question.textContent);
nicholas@2498 2456 console.log('Response: ' + response.textContent);
nicholas@2498 2457 root.appendChild(question);
nicholas@2498 2458 root.appendChild(response);
nicholas@2224 2459 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2460 return root;
nicholas@2498 2461 };
nicholas@2498 2462 this.resize = function () {
nicholas@2498 2463 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2464 if (boxwidth >= 600) {
nicholas@2498 2465 boxwidth = 600;
nicholas@2498 2466 } else if (boxwidth < 400) {
nicholas@2498 2467 boxwidth = 400;
nicholas@2498 2468 }
nicholas@2498 2469 this.holder.style.width = boxwidth + "px";
nicholas@2498 2470 var text = this.holder.getElementsByClassName("comment-radio-span-holder")[0];
nicholas@2498 2471 var options = this.holder.getElementsByClassName("comment-radio-inputs-holder")[0];
nicholas@2498 2472 var optCount = options.childElementCount;
nicholas@2498 2473 var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px';
nicholas@2498 2474 var options = options.firstChild;
nicholas@2498 2475 var text = text.firstChild;
nicholas@2498 2476 options.style.marginRight = spanMargin;
nicholas@2498 2477 options.style.marginLeft = spanMargin;
nicholas@2498 2478 text.style.marginRight = spanMargin;
nicholas@2498 2479 text.style.marginLeft = spanMargin;
nicholas@2498 2480 while (options.nextSibling != undefined) {
nicholas@2498 2481 options = options.nextSibling;
nicholas@2498 2482 text = text.nextSibling;
nicholas@2498 2483 options.style.marginRight = spanMargin;
nicholas@2498 2484 options.style.marginLeft = spanMargin;
nicholas@2498 2485 text.style.marginRight = spanMargin;
nicholas@2498 2486 text.style.marginLeft = spanMargin;
nicholas@2498 2487 }
nicholas@2498 2488 };
nicholas@2498 2489 this.resize();
nicholas@2498 2490 };
nicholas@2498 2491
nicholas@2498 2492 this.checkboxBox = function (commentQuestion) {
nicholas@2498 2493 this.specification = commentQuestion;
nicholas@2498 2494 // Create document objects to hold the comment boxes
nicholas@2498 2495 this.holder = document.createElement('div');
nicholas@2498 2496 this.holder.className = 'comment-div';
nicholas@2498 2497 // Create a string next to each comment asking for a comment
nicholas@2498 2498 this.string = document.createElement('span');
nicholas@2498 2499 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2500 var br = document.createElement('br');
nicholas@2498 2501 // Add to the holder.
nicholas@2498 2502 this.holder.appendChild(this.string);
nicholas@2498 2503 this.holder.appendChild(br);
nicholas@2498 2504 this.options = [];
nicholas@2498 2505 this.inputs = document.createElement('div');
nicholas@2498 2506 this.span = document.createElement('div');
nicholas@2498 2507 this.inputs.align = 'center';
nicholas@2498 2508 this.inputs.style.marginLeft = '12px';
nicholas@2294 2509 this.inputs.className = "comment-checkbox-inputs-holder";
nicholas@2498 2510 this.span.style.marginLeft = '12px';
nicholas@2498 2511 this.span.align = 'center';
nicholas@2498 2512 this.span.style.marginTop = '15px';
nicholas@2294 2513 this.span.className = "comment-checkbox-span-holder";
nicholas@2498 2514
nicholas@2498 2515 var optCount = commentQuestion.options.length;
nicholas@2498 2516 for (var i = 0; i < optCount; i++) {
nicholas@2498 2517 var div = document.createElement('div');
nicholas@2498 2518 div.style.width = '80px';
nicholas@2498 2519 div.style.float = 'left';
nicholas@2498 2520 var input = document.createElement('input');
nicholas@2498 2521 input.type = 'checkbox';
nicholas@2498 2522 input.name = commentQuestion.id;
nicholas@2498 2523 input.setAttribute('setvalue', commentQuestion.options[i].name);
nicholas@2498 2524 input.className = 'comment-radio';
nicholas@2498 2525 div.appendChild(input);
nicholas@2498 2526 this.inputs.appendChild(div);
nicholas@2498 2527
nicholas@2498 2528
nicholas@2498 2529 div = document.createElement('div');
nicholas@2498 2530 div.style.width = '80px';
nicholas@2498 2531 div.style.float = 'left';
nicholas@2498 2532 div.align = 'center';
nicholas@2498 2533 var span = document.createElement('span');
nicholas@2498 2534 span.textContent = commentQuestion.options[i].text;
nicholas@2498 2535 span.className = 'comment-radio-span';
nicholas@2498 2536 div.appendChild(span);
nicholas@2498 2537 this.span.appendChild(div);
nicholas@2498 2538 this.options.push(input);
nicholas@2498 2539 }
nicholas@2498 2540 this.holder.appendChild(this.span);
nicholas@2498 2541 this.holder.appendChild(this.inputs);
nicholas@2498 2542
nicholas@2498 2543 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2544 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2545 root.id = this.specification.id;
nicholas@2498 2546 root.setAttribute('type', this.specification.type);
nicholas@2498 2547 var question = document.createElement('question');
nicholas@2498 2548 question.textContent = this.string.textContent;
nicholas@2498 2549 root.appendChild(question);
nicholas@2498 2550 console.log('Comment: ' + question.textContent);
nicholas@2498 2551 for (var i = 0; i < this.options.length; i++) {
nicholas@2498 2552 var response = document.createElement('response');
nicholas@2498 2553 response.textContent = this.options[i].checked;
nicholas@2498 2554 response.setAttribute('name', this.options[i].getAttribute('setvalue'));
nicholas@2498 2555 root.appendChild(response);
nicholas@2498 2556 console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent);
nicholas@2498 2557 }
nicholas@2224 2558 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2559 return root;
nicholas@2498 2560 };
nicholas@2498 2561 this.resize = function () {
nicholas@2498 2562 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2563 if (boxwidth >= 600) {
nicholas@2498 2564 boxwidth = 600;
nicholas@2498 2565 } else if (boxwidth < 400) {
nicholas@2498 2566 boxwidth = 400;
nicholas@2498 2567 }
nicholas@2498 2568 this.holder.style.width = boxwidth + "px";
nicholas@2498 2569 var text = this.holder.getElementsByClassName("comment-checkbox-span-holder")[0];
nicholas@2498 2570 var options = this.holder.getElementsByClassName("comment-checkbox-inputs-holder")[0];
nicholas@2498 2571 var optCount = options.childElementCount;
nicholas@2498 2572 var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px';
nicholas@2498 2573 var options = options.firstChild;
nicholas@2498 2574 var text = text.firstChild;
nicholas@2498 2575 options.style.marginRight = spanMargin;
nicholas@2498 2576 options.style.marginLeft = spanMargin;
nicholas@2498 2577 text.style.marginRight = spanMargin;
nicholas@2498 2578 text.style.marginLeft = spanMargin;
nicholas@2498 2579 while (options.nextSibling != undefined) {
nicholas@2498 2580 options = options.nextSibling;
nicholas@2498 2581 text = text.nextSibling;
nicholas@2498 2582 options.style.marginRight = spanMargin;
nicholas@2498 2583 options.style.marginLeft = spanMargin;
nicholas@2498 2584 text.style.marginRight = spanMargin;
nicholas@2498 2585 text.style.marginLeft = spanMargin;
nicholas@2498 2586 }
nicholas@2498 2587 };
nicholas@2498 2588 this.resize();
nicholas@2498 2589 };
nicholas@2498 2590
nicholas@2498 2591 this.createCommentQuestion = function (element) {
nicholas@2498 2592 var node;
nicholas@2498 2593 if (element.type == 'question') {
nicholas@2498 2594 node = new this.commentBox(element);
nicholas@2498 2595 } else if (element.type == 'radio') {
nicholas@2498 2596 node = new this.radioBox(element);
nicholas@2498 2597 } else if (element.type == 'checkbox') {
nicholas@2498 2598 node = new this.checkboxBox(element);
nicholas@2498 2599 }
nicholas@2498 2600 this.commentQuestions.push(node);
nicholas@2498 2601 return node;
nicholas@2498 2602 };
nicholas@2498 2603
nicholas@2498 2604 this.deleteCommentQuestions = function () {
nicholas@2498 2605 this.commentQuestions = [];
nicholas@2498 2606 };
nicholas@2498 2607
nicholas@2498 2608 this.outsideReferenceDOM = function (audioObject, index, inject) {
nicholas@2224 2609 this.parent = audioObject;
nicholas@2224 2610 this.outsideReferenceHolder = document.createElement('button');
nicholas@2224 2611 this.outsideReferenceHolder.className = 'outside-reference';
nicholas@2498 2612 this.outsideReferenceHolder.setAttribute('track-id', index);
nicholas@2409 2613 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 2614 this.outsideReferenceHolder.disabled = true;
nicholas@2224 2615
nicholas@2498 2616 this.outsideReferenceHolder.onclick = function (event) {
nicholas@2224 2617 audioEngineContext.play(event.currentTarget.getAttribute('track-id'));
nicholas@2224 2618 };
nicholas@2224 2619 inject.appendChild(this.outsideReferenceHolder);
nicholas@2498 2620 this.enable = function () {
nicholas@2498 2621 if (this.parent.state == 1) {
nicholas@2224 2622 this.outsideReferenceHolder.disabled = false;
nicholas@2224 2623 }
nicholas@2224 2624 };
nicholas@2498 2625 this.updateLoading = function (progress) {
nicholas@2498 2626 if (progress != 100) {
nicholas@2224 2627 progress = String(progress);
nicholas@2224 2628 progress = progress.split('.')[0];
nicholas@2498 2629 this.outsideReferenceHolder.textContent = progress + '%';
nicholas@2224 2630 } else {
nicholas@2409 2631 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 2632 }
nicholas@2224 2633 };
nicholas@2498 2634 this.startPlayback = function () {
nicholas@2224 2635 // Called when playback has begun
nicholas@2224 2636 $('.track-slider').removeClass('track-slider-playing');
nicholas@2224 2637 $('.comment-div').removeClass('comment-box-playing');
nicholas@2224 2638 this.outsideReferenceHolder.style.backgroundColor = "#FDD";
nicholas@2224 2639 };
nicholas@2498 2640 this.stopPlayback = function () {
nicholas@2224 2641 // Called when playback has stopped. This gets called even if playback never started!
nicholas@2224 2642 this.outsideReferenceHolder.style.backgroundColor = "";
nicholas@2224 2643 };
nicholas@2498 2644 this.exportXMLDOM = function (audioObject) {
nicholas@2224 2645 return null;
nicholas@2224 2646 };
nicholas@2498 2647 this.getValue = function () {
nicholas@2224 2648 return 0;
nicholas@2224 2649 };
nicholas@2498 2650 this.getPresentedId = function () {
nicholas@2409 2651 return this.parent.specification.label || "Reference";
nicholas@2224 2652 };
nicholas@2498 2653 this.canMove = function () {
nicholas@2224 2654 return false;
nicholas@2224 2655 };
nicholas@2498 2656 this.error = function () {
nicholas@2498 2657 // audioObject has an error!!
nicholas@2224 2658 this.outsideReferenceHolder.textContent = "Error";
nicholas@2224 2659 this.outsideReferenceHolder.style.backgroundColor = "#F00";
nicholas@2224 2660 }
nicholas@2224 2661 }
nicholas@2498 2662
nicholas@2498 2663 this.playhead = new function () {
nicholas@2498 2664 this.object = document.createElement('div');
nicholas@2498 2665 this.object.className = 'playhead';
nicholas@2498 2666 this.object.align = 'left';
nicholas@2498 2667 var curTime = document.createElement('div');
nicholas@2498 2668 curTime.style.width = '50px';
nicholas@2498 2669 this.curTimeSpan = document.createElement('span');
nicholas@2498 2670 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2671 curTime.appendChild(this.curTimeSpan);
nicholas@2498 2672 this.object.appendChild(curTime);
nicholas@2498 2673 this.scrubberTrack = document.createElement('div');
nicholas@2498 2674 this.scrubberTrack.className = 'playhead-scrub-track';
nicholas@2498 2675
nicholas@2498 2676 this.scrubberHead = document.createElement('div');
nicholas@2498 2677 this.scrubberHead.id = 'playhead-scrubber';
nicholas@2498 2678 this.scrubberTrack.appendChild(this.scrubberHead);
nicholas@2498 2679 this.object.appendChild(this.scrubberTrack);
nicholas@2498 2680
nicholas@2498 2681 this.timePerPixel = 0;
nicholas@2498 2682 this.maxTime = 0;
nicholas@2498 2683
nicholas@2498 2684 this.playbackObject;
nicholas@2498 2685
nicholas@2498 2686 this.setTimePerPixel = function (audioObject) {
nicholas@2498 2687 //maxTime must be in seconds
nicholas@2498 2688 this.playbackObject = audioObject;
nicholas@2498 2689 this.maxTime = audioObject.buffer.buffer.duration;
nicholas@2498 2690 var width = 490; //500 - 10, 5 each side of the tracker head
nicholas@2498 2691 this.timePerPixel = this.maxTime / 490;
nicholas@2498 2692 if (this.maxTime < 60) {
nicholas@2498 2693 this.curTimeSpan.textContent = '0.00';
nicholas@2498 2694 } else {
nicholas@2498 2695 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2696 }
nicholas@2498 2697 };
nicholas@2498 2698
nicholas@2498 2699 this.update = function () {
nicholas@2498 2700 // Update the playhead position, startPlay must be called
nicholas@2498 2701 if (this.timePerPixel > 0) {
nicholas@2498 2702 var time = this.playbackObject.getCurrentPosition();
nicholas@2498 2703 if (time > 0 && time < this.maxTime) {
nicholas@2498 2704 var width = 490;
nicholas@2498 2705 var pix = Math.floor(time / this.timePerPixel);
nicholas@2498 2706 this.scrubberHead.style.left = pix + 'px';
nicholas@2498 2707 if (this.maxTime > 60.0) {
nicholas@2498 2708 var secs = time % 60;
nicholas@2498 2709 var mins = Math.floor((time - secs) / 60);
nicholas@2498 2710 secs = secs.toString();
nicholas@2498 2711 secs = secs.substr(0, 2);
nicholas@2498 2712 mins = mins.toString();
nicholas@2498 2713 this.curTimeSpan.textContent = mins + ':' + secs;
nicholas@2498 2714 } else {
nicholas@2498 2715 time = time.toString();
nicholas@2498 2716 this.curTimeSpan.textContent = time.substr(0, 4);
nicholas@2498 2717 }
nicholas@2498 2718 } else {
nicholas@2498 2719 this.scrubberHead.style.left = '0px';
nicholas@2498 2720 if (this.maxTime < 60) {
nicholas@2498 2721 this.curTimeSpan.textContent = '0.00';
nicholas@2498 2722 } else {
nicholas@2498 2723 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2724 }
nicholas@2498 2725 }
nicholas@2498 2726 }
nicholas@2498 2727 };
nicholas@2498 2728
nicholas@2498 2729 this.interval = undefined;
nicholas@2498 2730
nicholas@2498 2731 this.start = function () {
nicholas@2498 2732 if (this.playbackObject != undefined && this.interval == undefined) {
nicholas@2498 2733 if (this.maxTime < 60) {
nicholas@2498 2734 this.interval = setInterval(function () {
nicholas@2498 2735 interfaceContext.playhead.update();
nicholas@2498 2736 }, 10);
nicholas@2498 2737 } else {
nicholas@2498 2738 this.interval = setInterval(function () {
nicholas@2498 2739 interfaceContext.playhead.update();
nicholas@2498 2740 }, 100);
nicholas@2498 2741 }
nicholas@2498 2742 }
nicholas@2498 2743 };
nicholas@2498 2744 this.stop = function () {
nicholas@2498 2745 clearInterval(this.interval);
nicholas@2498 2746 this.interval = undefined;
nicholas@2224 2747 this.scrubberHead.style.left = '0px';
nicholas@2498 2748 if (this.maxTime < 60) {
nicholas@2498 2749 this.curTimeSpan.textContent = '0.00';
nicholas@2498 2750 } else {
nicholas@2498 2751 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2752 }
nicholas@2498 2753 };
nicholas@2498 2754 };
nicholas@2498 2755
nicholas@2498 2756 this.volume = new function () {
nicholas@2224 2757 // An in-built volume module which can be viewed on page
nicholas@2224 2758 // Includes trackers on page-by-page data
nicholas@2224 2759 // Volume does NOT reset to 0dB on each page load
nicholas@2224 2760 this.valueLin = 1.0;
nicholas@2224 2761 this.valueDB = 0.0;
nicholas@2352 2762 this.root = document.createElement('div');
nicholas@2352 2763 this.root.id = 'master-volume-root';
nicholas@2224 2764 this.object = document.createElement('div');
nicholas@2352 2765 this.object.className = 'master-volume-holder-float';
nicholas@2352 2766 this.object.appendChild(this.root);
nicholas@2224 2767 this.slider = document.createElement('input');
nicholas@2224 2768 this.slider.id = 'master-volume-control';
nicholas@2224 2769 this.slider.type = 'range';
nicholas@2224 2770 this.valueText = document.createElement('span');
nicholas@2224 2771 this.valueText.id = 'master-volume-feedback';
nicholas@2224 2772 this.valueText.textContent = '0dB';
nicholas@2498 2773
nicholas@2224 2774 this.slider.min = -60;
nicholas@2224 2775 this.slider.max = 12;
nicholas@2224 2776 this.slider.value = 0;
nicholas@2224 2777 this.slider.step = 1;
nicholas@2498 2778 this.slider.onmousemove = function (event) {
nicholas@2224 2779 interfaceContext.volume.valueDB = event.currentTarget.value;
nicholas@2224 2780 interfaceContext.volume.valueLin = decibelToLinear(interfaceContext.volume.valueDB);
nicholas@2498 2781 interfaceContext.volume.valueText.textContent = interfaceContext.volume.valueDB + 'dB';
nicholas@2224 2782 audioEngineContext.outputGain.gain.value = interfaceContext.volume.valueLin;
nicholas@2224 2783 }
nicholas@2498 2784 this.slider.onmouseup = function (event) {
nicholas@2224 2785 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
nicholas@2498 2786 if (storePoint.length == 0) {
nicholas@2224 2787 storePoint = storage.document.createElement('metricresult');
nicholas@2498 2788 storePoint.setAttribute('name', 'volumeTracker');
nicholas@2224 2789 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
nicholas@2498 2790 } else {
nicholas@2224 2791 storePoint = storePoint[0];
nicholas@2224 2792 }
nicholas@2224 2793 var node = storage.document.createElement('movement');
nicholas@2498 2794 node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
nicholas@2498 2795 node.setAttribute('volume', interfaceContext.volume.valueDB);
nicholas@2498 2796 node.setAttribute('format', 'dBFS');
nicholas@2224 2797 storePoint.appendChild(node);
nicholas@2224 2798 }
nicholas@2498 2799
nicholas@2224 2800 var title = document.createElement('div');
nicholas@2224 2801 title.innerHTML = '<span>Master Volume Control</span>';
nicholas@2224 2802 title.style.fontSize = '0.75em';
nicholas@2224 2803 title.style.width = "100%";
nicholas@2224 2804 title.align = 'center';
nicholas@2352 2805 this.root.appendChild(title);
nicholas@2498 2806
nicholas@2352 2807 this.root.appendChild(this.slider);
nicholas@2352 2808 this.root.appendChild(this.valueText);
nicholas@2498 2809
nicholas@2498 2810 this.resize = function (event) {
nicholas@2352 2811 if (window.innerWidth < 1000) {
nicholas@2352 2812 this.object.className = "master-volume-holder-inline"
nicholas@2352 2813 } else {
nicholas@2352 2814 this.object.className = 'master-volume-holder-float';
nicholas@2352 2815 }
nicholas@2352 2816 }
nicholas@2224 2817 }
nicholas@2498 2818
nicholas@2224 2819 this.calibrationModuleObject = null;
nicholas@2498 2820 this.calibrationModule = function () {
nicholas@2224 2821 // This creates an on-page calibration module
nicholas@2224 2822 this.storeDOM = storage.document.createElement("calibration");
nicholas@2224 2823 storage.root.appendChild(this.storeDOM);
nicholas@2224 2824 // The calibration is a fixed state module
nicholas@2224 2825 this.calibrationNodes = [];
nicholas@2224 2826 this.holder = null;
nicholas@2498 2827 this.build = function (inject) {
nicholas@2224 2828 var f0 = 62.5;
nicholas@2224 2829 this.holder = document.createElement("div");
nicholas@2224 2830 this.holder.className = "calibration-holder";
nicholas@2224 2831 this.calibrationNodes = [];
nicholas@2498 2832 while (f0 < 20000) {
nicholas@2224 2833 var obj = {
nicholas@2224 2834 root: document.createElement("div"),
nicholas@2224 2835 input: document.createElement("input"),
nicholas@2224 2836 oscillator: audioContext.createOscillator(),
nicholas@2224 2837 gain: audioContext.createGain(),
nicholas@2224 2838 f: f0,
nicholas@2224 2839 parent: this,
nicholas@2498 2840 handleEvent: function (event) {
nicholas@2498 2841 switch (event.type) {
nicholas@2224 2842 case "mouseenter":
nicholas@2224 2843 this.oscillator.start(0);
nicholas@2224 2844 break;
nicholas@2224 2845 case "mouseleave":
nicholas@2224 2846 this.oscillator.stop(0);
nicholas@2224 2847 this.oscillator = audioContext.createOscillator();
nicholas@2224 2848 this.oscillator.connect(this.gain);
nicholas@2224 2849 this.oscillator.frequency.value = this.f;
nicholas@2224 2850 break;
nicholas@2224 2851 case "mousemove":
nicholas@2498 2852 var value = Math.pow(10, this.input.value / 20);
nicholas@2224 2853 if (this.f == 1000) {
nicholas@2224 2854 audioEngineContext.outputGain.gain.value = value;
nicholas@2224 2855 interfaceContext.volume.slider.value = this.input.value;
nicholas@2224 2856 } else {
nicholas@2224 2857 this.gain.gain.value = value
nicholas@2224 2858 }
nicholas@2224 2859 break;
nicholas@2224 2860 }
nicholas@2224 2861 },
nicholas@2498 2862 disconnect: function () {
nicholas@2224 2863 this.gain.disconnect();
nicholas@2224 2864 }
nicholas@2224 2865 }
nicholas@2224 2866 obj.root.className = "calibration-slider";
nicholas@2224 2867 obj.root.appendChild(obj.input);
nicholas@2224 2868 obj.oscillator.connect(obj.gain);
nicholas@2224 2869 obj.gain.connect(audioEngineContext.outputGain);
nicholas@2498 2870 obj.gain.gain.value = Math.random() * 2;
nicholas@2224 2871 obj.input.value = obj.gain.gain.value;
nicholas@2498 2872 obj.input.setAttribute('orient', 'vertical');
nicholas@2224 2873 obj.input.type = "range";
nicholas@2224 2874 obj.input.min = -6;
nicholas@2224 2875 obj.input.max = 6;
nicholas@2224 2876 obj.input.step = 0.25;
nicholas@2224 2877 if (f0 != 1000) {
nicholas@2498 2878 obj.input.value = (Math.random() * 12) - 6;
nicholas@2224 2879 } else {
nicholas@2224 2880 obj.input.value = 0;
nicholas@2498 2881 obj.root.style.backgroundColor = "rgb(255,125,125)";
nicholas@2224 2882 }
nicholas@2498 2883 obj.input.addEventListener("mousemove", obj);
nicholas@2498 2884 obj.input.addEventListener("mouseenter", obj);
nicholas@2498 2885 obj.input.addEventListener("mouseleave", obj);
nicholas@2498 2886 obj.gain.gain.value = Math.pow(10, obj.input.value / 20);
nicholas@2224 2887 obj.oscillator.frequency.value = f0;
nicholas@2224 2888 this.calibrationNodes.push(obj);
nicholas@2224 2889 this.holder.appendChild(obj.root);
nicholas@2224 2890 f0 *= 2;
nicholas@2224 2891 }
nicholas@2224 2892 inject.appendChild(this.holder);
nicholas@2224 2893 }
nicholas@2498 2894 this.collect = function () {
nicholas@2224 2895 for (var obj of this.calibrationNodes) {
nicholas@2224 2896 var node = storage.document.createElement("calibrationresult");
nicholas@2498 2897 node.setAttribute("frequency", obj.f);
nicholas@2498 2898 node.setAttribute("range-min", obj.input.min);
nicholas@2498 2899 node.setAttribute("range-max", obj.input.max);
nicholas@2498 2900 node.setAttribute("gain-lin", obj.gain.gain.value);
nicholas@2224 2901 this.storeDOM.appendChild(node);
nicholas@2224 2902 }
nicholas@2224 2903 }
nicholas@2224 2904 }
nicholas@2498 2905
nicholas@2498 2906
nicholas@2498 2907 // Global Checkers
nicholas@2498 2908 // These functions will help enforce the checkers
nicholas@2498 2909 this.checkHiddenAnchor = function () {
nicholas@2498 2910 for (var ao of audioEngineContext.audioObjects) {
nicholas@2498 2911 if (ao.specification.type == "anchor") {
nicholas@2498 2912 if (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0) {
nicholas@2498 2913 // Anchor is not set below
nicholas@2498 2914 console.log('Anchor node not below marker value');
nicholas@2498 2915 interfaceContext.lightbox.post("Message", 'Please keep listening');
nicholas@2224 2916 this.storeErrorNode('Anchor node not below marker value');
nicholas@2498 2917 return false;
nicholas@2498 2918 }
nicholas@2498 2919 }
nicholas@2498 2920 }
nicholas@2498 2921 return true;
nicholas@2498 2922 };
nicholas@2498 2923
nicholas@2498 2924 this.checkHiddenReference = function () {
nicholas@2498 2925 for (var ao of audioEngineContext.audioObjects) {
nicholas@2498 2926 if (ao.specification.type == "reference") {
nicholas@2498 2927 if (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0) {
nicholas@2498 2928 // Anchor is not set below
nicholas@2498 2929 console.log('Reference node not above marker value');
nicholas@2224 2930 this.storeErrorNode('Reference node not above marker value');
nicholas@2498 2931 interfaceContext.lightbox.post("Message", 'Please keep listening');
nicholas@2498 2932 return false;
nicholas@2498 2933 }
nicholas@2498 2934 }
nicholas@2498 2935 }
nicholas@2498 2936 return true;
nicholas@2498 2937 };
nicholas@2498 2938
nicholas@2498 2939 this.checkFragmentsFullyPlayed = function () {
nicholas@2498 2940 // Checks the entire file has been played back
nicholas@2498 2941 // NOTE ! This will return true IF playback is Looped!!!
nicholas@2498 2942 if (audioEngineContext.loopPlayback) {
nicholas@2498 2943 console.log("WARNING - Looped source: Cannot check fragments are fully played");
nicholas@2498 2944 return true;
nicholas@2498 2945 }
nicholas@2498 2946 var check_pass = true;
nicholas@2498 2947 var error_obj = [];
nicholas@2498 2948 for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
nicholas@2498 2949 var object = audioEngineContext.audioObjects[i];
nicholas@2498 2950 var time = object.buffer.buffer.duration;
nicholas@2498 2951 var metric = object.metric;
nicholas@2498 2952 var passed = false;
nicholas@2498 2953 for (var j = 0; j < metric.listenTracker.length; j++) {
nicholas@2498 2954 var bt = metric.listenTracker[j].getElementsByTagName('testtime');
nicholas@2498 2955 var start_time = Number(bt[0].getAttribute('start'));
nicholas@2498 2956 var stop_time = Number(bt[0].getAttribute('stop'));
nicholas@2498 2957 var delta = stop_time - start_time;
nicholas@2498 2958 if (delta >= time) {
nicholas@2498 2959 passed = true;
nicholas@2498 2960 break;
nicholas@2498 2961 }
nicholas@2498 2962 }
nicholas@2498 2963 if (passed == false) {
nicholas@2498 2964 check_pass = false;
nicholas@2498 2965 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
nicholas@2498 2966 error_obj.push(object.interfaceDOM.getPresentedId());
nicholas@2498 2967 }
nicholas@2498 2968 }
nicholas@2498 2969 if (check_pass == false) {
nicholas@2498 2970 var str_start = "You have not completely listened to fragments ";
nicholas@2498 2971 for (var i = 0; i < error_obj.length; i++) {
nicholas@2498 2972 str_start += error_obj[i];
nicholas@2498 2973 if (i != error_obj.length - 1) {
nicholas@2498 2974 str_start += ', ';
nicholas@2498 2975 }
nicholas@2498 2976 }
nicholas@2498 2977 str_start += ". Please keep listening";
nicholas@2498 2978 console.log("[ALERT]: " + str_start);
nicholas@2498 2979 this.storeErrorNode("[ALERT]: " + str_start);
nicholas@2498 2980 interfaceContext.lightbox.post("Error", str_start);
nicholas@2444 2981 return false;
nicholas@2498 2982 }
nicholas@2444 2983 return true;
nicholas@2498 2984 };
nicholas@2498 2985 this.checkAllMoved = function () {
nicholas@2498 2986 var str = "You have not moved ";
nicholas@2498 2987 var failed = [];
nicholas@2498 2988 for (var ao of audioEngineContext.audioObjects) {
nicholas@2498 2989 if (ao.metric.wasMoved == false && ao.interfaceDOM.canMove() == true) {
nicholas@2498 2990 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 2991 }
nicholas@2498 2992 }
nicholas@2498 2993 if (failed.length == 0) {
nicholas@2498 2994 return true;
nicholas@2498 2995 } else if (failed.length == 1) {
nicholas@2498 2996 str += 'track ' + failed[0];
nicholas@2498 2997 } else {
nicholas@2498 2998 str += 'tracks ';
nicholas@2498 2999 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3000 str += failed[i] + ', ';
nicholas@2498 3001 }
nicholas@2498 3002 str += 'and ' + failed[i];
nicholas@2498 3003 }
nicholas@2498 3004 str += '.';
nicholas@2498 3005 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3006 console.log(str);
nicholas@2224 3007 this.storeErrorNode(str);
nicholas@2498 3008 return false;
nicholas@2498 3009 };
nicholas@2498 3010 this.checkAllPlayed = function () {
nicholas@2498 3011 var str = "You have not played ";
nicholas@2498 3012 var failed = [];
nicholas@2498 3013 for (var ao of audioEngineContext.audioObjects) {
nicholas@2498 3014 if (ao.metric.wasListenedTo == false) {
nicholas@2498 3015 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3016 }
nicholas@2498 3017 }
nicholas@2498 3018 if (failed.length == 0) {
nicholas@2498 3019 return true;
nicholas@2498 3020 } else if (failed.length == 1) {
nicholas@2498 3021 str += 'track ' + failed[0];
nicholas@2498 3022 } else {
nicholas@2498 3023 str += 'tracks ';
nicholas@2498 3024 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3025 str += failed[i] + ', ';
nicholas@2498 3026 }
nicholas@2498 3027 str += 'and ' + failed[i];
nicholas@2498 3028 }
nicholas@2498 3029 str += '.';
nicholas@2498 3030 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3031 console.log(str);
nicholas@2224 3032 this.storeErrorNode(str);
nicholas@2498 3033 return false;
nicholas@2498 3034 };
nicholas@2498 3035 this.checkScaleRange = function (min, max) {
nicholas@2310 3036 var page = testState.getCurrentTestPage();
nicholas@2310 3037 var audioObjects = audioEngineContext.audioObjects;
nicholas@2310 3038 var state = true;
nicholas@2310 3039 var str = "Please keep listening. ";
nicholas@2310 3040 var minRanking = Infinity;
nicholas@2310 3041 var maxRanking = -Infinity;
nicholas@2310 3042 for (var ao of audioObjects) {
nicholas@2310 3043 var rank = ao.interfaceDOM.getValue();
nicholas@2498 3044 if (rank < minRanking) {
nicholas@2498 3045 minRanking = rank;
nicholas@2498 3046 }
nicholas@2498 3047 if (rank > maxRanking) {
nicholas@2498 3048 maxRanking = rank;
nicholas@2498 3049 }
nicholas@2310 3050 }
nicholas@2498 3051 if (minRanking * 100 > min) {
nicholas@2498 3052 str += "At least one fragment must be below the " + min + " mark.";
nicholas@2310 3053 state = false;
nicholas@2310 3054 }
nicholas@2498 3055 if (maxRanking * 100 < max) {
nicholas@2498 3056 str += "At least one fragment must be above the " + max + " mark."
nicholas@2310 3057 state = false;
nicholas@2310 3058 }
nicholas@2310 3059 if (!state) {
nicholas@2310 3060 console.log(str);
nicholas@2310 3061 this.storeErrorNode(str);
nicholas@2498 3062 interfaceContext.lightbox.post("Error", str);
nicholas@2310 3063 }
nicholas@2310 3064 return state;
nicholas@2310 3065 }
nicholas@2498 3066
nicholas@2498 3067 this.storeErrorNode = function (errorMessage) {
nicholas@2224 3068 var time = audioEngineContext.timer.getTestTime();
nicholas@2224 3069 var node = storage.document.createElement('error');
nicholas@2498 3070 node.setAttribute('time', time);
nicholas@2224 3071 node.textContent = errorMessage;
nicholas@2224 3072 testState.currentStore.XMLDOM.appendChild(node);
nicholas@2224 3073 };
nicholas@2224 3074 }
nicholas@2224 3075
nicholas@2498 3076 function Storage() {
nicholas@2498 3077 // Holds results in XML format until ready for collection
nicholas@2498 3078 this.globalPreTest = null;
nicholas@2498 3079 this.globalPostTest = null;
nicholas@2498 3080 this.testPages = [];
nicholas@2498 3081 this.document = null;
nicholas@2498 3082 this.root = null;
nicholas@2498 3083 this.state = 0;
nicholas@2498 3084
nicholas@2498 3085 this.initialise = function (existingStore) {
nicholas@2224 3086 if (existingStore == undefined) {
nicholas@2224 3087 // We need to get the sessionKey
nicholas@2224 3088 this.SessionKey.generateKey();
nicholas@2498 3089 this.document = document.implementation.createDocument(null, "waetresult", null);
nicholas@2224 3090 this.root = this.document.childNodes[0];
nicholas@2224 3091 var projectDocument = specification.projectXML;
nicholas@2498 3092 projectDocument.setAttribute('file-name', url);
nicholas@2498 3093 projectDocument.setAttribute('url', qualifyURL(url));
nicholas@2224 3094 this.root.appendChild(projectDocument);
nicholas@2224 3095 this.root.appendChild(interfaceContext.returnDateNode());
nicholas@2224 3096 this.root.appendChild(interfaceContext.returnNavigator());
nicholas@2224 3097 } else {
nicholas@2224 3098 this.document = existingStore;
nicholas@2294 3099 this.root = existingStore.firstChild;
nicholas@2224 3100 this.SessionKey.key = this.root.getAttribute("key");
nicholas@2224 3101 }
nicholas@2498 3102 if (specification.preTest != undefined) {
nicholas@2498 3103 this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
nicholas@2498 3104 }
nicholas@2498 3105 if (specification.postTest != undefined) {
nicholas@2498 3106 this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
nicholas@2498 3107 }
nicholas@2498 3108 };
nicholas@2498 3109
nicholas@2224 3110 this.SessionKey = {
nicholas@2224 3111 key: null,
nicholas@2224 3112 request: new XMLHttpRequest(),
nicholas@2224 3113 parent: this,
nicholas@2498 3114 handleEvent: function () {
nicholas@2224 3115 var parse = new DOMParser();
nicholas@2498 3116 var xml = parse.parseFromString(this.request.response, "text/xml");
giuliomoro@2301 3117 var shouldGenerateKey = true;
nicholas@2376 3118 if (this.request.response.length == 0) {
nicholas@2376 3119 console.log("Error: Server did not respond");
nicholas@2376 3120 return;
nicholas@2376 3121 }
nicholas@2498 3122 if (xml.getElementsByTagName("state").length > 0) {
nicholas@2498 3123 if (xml.getElementsByTagName("state")[0].textContent == "OK") {
nicholas@2498 3124 this.key = xml.getAllElementsByTagName("key")[0].textContent;
nicholas@2498 3125 this.parent.root.setAttribute("key", this.key);
nicholas@2498 3126 this.parent.root.setAttribute("state", "empty");
nicholas@2498 3127 shouldGenerateKey = false;
nicholas@2498 3128 }
nicholas@2498 3129 }
nicholas@2498 3130 if (shouldGenerateKey === true) {
nicholas@2498 3131 this.generateKey();
nicholas@2498 3132 }
nicholas@2224 3133 },
nicholas@2498 3134 generateKey: function () {
nicholas@2224 3135 var temp_key = randomString(32);
nicholas@2302 3136 var returnURL = "";
nicholas@2302 3137 if (typeof specification.projectReturn == "string") {
nicholas@2498 3138 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2302 3139 returnURL = specification.projectReturn;
nicholas@2302 3140 }
nicholas@2302 3141 }
nicholas@2498 3142 this.request.open("GET", returnURL + "php/keygen.php?key=" + temp_key, true);
nicholas@2498 3143 this.request.addEventListener("load", this);
nicholas@2224 3144 this.request.send();
nicholas@2224 3145 },
nicholas@2498 3146 update: function () {
nicholas@2357 3147 if (this.key == null) {
nicholas@2357 3148 console.log("Cannot save as key == null");
nicholas@2357 3149 return;
nicholas@2357 3150 }
nicholas@2498 3151 this.parent.root.setAttribute("state", "update");
nicholas@2224 3152 var xmlhttp = new XMLHttpRequest();
nicholas@2302 3153 var returnURL = "";
nicholas@2302 3154 if (typeof specification.projectReturn == "string") {
nicholas@2498 3155 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2302 3156 returnURL = specification.projectReturn;
nicholas@2302 3157 }
nicholas@2302 3158 }
nicholas@2498 3159 xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key);
nicholas@2224 3160 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
nicholas@2498 3161 xmlhttp.onerror = function () {
nicholas@2224 3162 console.log('Error updating file to server!');
nicholas@2224 3163 };
nicholas@2224 3164 var hold = document.createElement("div");
nicholas@2224 3165 var clone = this.parent.root.cloneNode(true);
nicholas@2224 3166 hold.appendChild(clone);
nicholas@2498 3167 xmlhttp.onload = function () {
nicholas@2224 3168 if (this.status >= 300) {
nicholas@2224 3169 console.log("WARNING - Could not update at this time");
nicholas@2224 3170 } else {
nicholas@2224 3171 var parser = new DOMParser();
nicholas@2224 3172 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
nicholas@2224 3173 var response = xmlDoc.getElementsByTagName('response')[0];
nicholas@2224 3174 if (response.getAttribute("state") == "OK") {
nicholas@2224 3175 var file = response.getElementsByTagName("file")[0];
nicholas@2498 3176 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
nicholas@2224 3177 } else {
nicholas@2224 3178 var message = response.getElementsByTagName("message");
nicholas@2498 3179 console.log("Intermediate save: Error! " + message.textContent);
nicholas@2224 3180 }
nicholas@2224 3181 }
nicholas@2224 3182 }
nicholas@2224 3183 xmlhttp.send([hold.innerHTML]);
nicholas@2224 3184 }
nicholas@2224 3185 }
nicholas@2498 3186
nicholas@2498 3187 this.createTestPageStore = function (specification) {
nicholas@2498 3188 var store = new this.pageNode(this, specification);
nicholas@2498 3189 this.testPages.push(store);
nicholas@2498 3190 return this.testPages[this.testPages.length - 1];
nicholas@2498 3191 };
nicholas@2498 3192
nicholas@2498 3193 this.surveyNode = function (parent, root, specification) {
nicholas@2498 3194 this.specification = specification;
nicholas@2498 3195 this.parent = parent;
nicholas@2224 3196 this.state = "empty";
nicholas@2498 3197 this.XMLDOM = this.parent.document.createElement('survey');
nicholas@2498 3198 this.XMLDOM.setAttribute('location', this.specification.location);
nicholas@2498 3199 this.XMLDOM.setAttribute("state", this.state);
nicholas@2498 3200 for (var optNode of this.specification.options) {
nicholas@2498 3201 if (optNode.type != 'statement') {
nicholas@2498 3202 var node = this.parent.document.createElement('surveyresult');
nicholas@2498 3203 node.setAttribute("ref", optNode.id);
nicholas@2498 3204 node.setAttribute('type', optNode.type);
nicholas@2498 3205 this.XMLDOM.appendChild(node);
nicholas@2498 3206 }
nicholas@2498 3207 }
nicholas@2498 3208 root.appendChild(this.XMLDOM);
nicholas@2498 3209
nicholas@2498 3210 this.postResult = function (node) {
nicholas@2498 3211 // From popup: node is the popupOption node containing both spec. and results
nicholas@2498 3212 // ID is the position
nicholas@2498 3213 if (node.specification.type == 'statement') {
nicholas@2498 3214 return;
nicholas@2498 3215 }
nicholas@2498 3216 var surveyresult = this.XMLDOM.firstChild;
nicholas@2498 3217 while (surveyresult != null) {
nicholas@2498 3218 if (surveyresult.getAttribute("ref") == node.specification.id) {
nicholas@2224 3219 break;
nicholas@2224 3220 }
nicholas@2224 3221 surveyresult = surveyresult.nextElementSibling;
nicholas@2224 3222 }
nicholas@2498 3223 switch (node.specification.type) {
nicholas@2498 3224 case "number":
nicholas@2498 3225 case "question":
nicholas@2498 3226 var child = this.parent.document.createElement('response');
nicholas@2498 3227 child.textContent = node.response;
nicholas@2498 3228 surveyresult.appendChild(child);
nicholas@2464 3229 break;
nicholas@2498 3230 case "radio":
nicholas@2498 3231 var child = this.parent.document.createElement('response');
nicholas@2498 3232 child.setAttribute('name', node.response.name);
nicholas@2498 3233 child.textContent = node.response.text;
nicholas@2498 3234 surveyresult.appendChild(child);
nicholas@2498 3235 break;
nicholas@2498 3236 case "checkbox":
nicholas@2498 3237 if (node.response == undefined) {
nicholas@2498 3238 surveyresult.appendChild(this.parent.document.createElement('response'));
nicholas@2498 3239 break;
nicholas@2498 3240 }
nicholas@2498 3241 for (var i = 0; i < node.response.length; i++) {
nicholas@2498 3242 var checkNode = this.parent.document.createElement('response');
nicholas@2498 3243 checkNode.setAttribute('name', node.response[i].name);
nicholas@2498 3244 checkNode.setAttribute('checked', node.response[i].checked);
nicholas@2498 3245 surveyresult.appendChild(checkNode);
nicholas@2498 3246 }
nicholas@2498 3247 break;
nicholas@2498 3248 }
nicholas@2498 3249 };
nicholas@2498 3250 this.complete = function () {
nicholas@2498 3251 this.state = "complete";
nicholas@2498 3252 this.XMLDOM.setAttribute("state", this.state);
nicholas@2498 3253 }
nicholas@2498 3254 };
nicholas@2498 3255
nicholas@2498 3256 this.pageNode = function (parent, specification) {
nicholas@2498 3257 // Create one store per test page
nicholas@2498 3258 this.specification = specification;
nicholas@2498 3259 this.parent = parent;
nicholas@2498 3260 this.state = "empty";
nicholas@2498 3261 this.XMLDOM = this.parent.document.createElement('page');
nicholas@2498 3262 this.XMLDOM.setAttribute('ref', specification.id);
nicholas@2498 3263 this.XMLDOM.setAttribute('presentedId', specification.presentedId);
nicholas@2498 3264 this.XMLDOM.setAttribute("state", this.state);
nicholas@2498 3265 if (specification.preTest != undefined) {
nicholas@2498 3266 this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
nicholas@2498 3267 }
nicholas@2498 3268 if (specification.postTest != undefined) {
nicholas@2498 3269 this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
nicholas@2498 3270 }
nicholas@2498 3271
nicholas@2498 3272 // Add any page metrics
nicholas@2498 3273 var page_metric = this.parent.document.createElement('metric');
nicholas@2498 3274 this.XMLDOM.appendChild(page_metric);
nicholas@2498 3275
nicholas@2498 3276 // Add the audioelement
nicholas@2498 3277 for (var element of this.specification.audioElements) {
nicholas@2498 3278 var aeNode = this.parent.document.createElement('audioelement');
nicholas@2498 3279 aeNode.setAttribute('ref', element.id);
nicholas@2498 3280 if (element.name != undefined) {
nicholas@2498 3281 aeNode.setAttribute('name', element.name)
nicholas@2498 3282 };
nicholas@2498 3283 aeNode.setAttribute('type', element.type);
nicholas@2498 3284 aeNode.setAttribute('url', element.url);
nicholas@2498 3285 aeNode.setAttribute('fqurl', qualifyURL(element.url));
nicholas@2498 3286 aeNode.setAttribute('gain', element.gain);
nicholas@2498 3287 if (element.type == 'anchor' || element.type == 'reference') {
nicholas@2498 3288 if (element.marker > 0) {
nicholas@2498 3289 aeNode.setAttribute('marker', element.marker);
nicholas@2464 3290 }
nicholas@2498 3291 }
nicholas@2498 3292 var ae_metric = this.parent.document.createElement('metric');
nicholas@2498 3293 aeNode.appendChild(ae_metric);
nicholas@2498 3294 this.XMLDOM.appendChild(aeNode);
nicholas@2498 3295 }
nicholas@2498 3296
nicholas@2498 3297 this.parent.root.appendChild(this.XMLDOM);
nicholas@2498 3298
nicholas@2498 3299 this.complete = function () {
nicholas@2224 3300 this.state = "complete";
nicholas@2498 3301 this.XMLDOM.setAttribute("state", "complete");
nicholas@2224 3302 }
nicholas@2498 3303 };
nicholas@2498 3304 this.update = function () {
nicholas@2224 3305 this.SessionKey.update();
nicholas@2224 3306 }
nicholas@2498 3307 this.finish = function () {
nicholas@2498 3308 if (this.state == 0) {
nicholas@2224 3309 this.update();
nicholas@2498 3310 }
nicholas@2498 3311 this.state = 1;
nicholas@2498 3312 this.root.setAttribute("state", "complete");
nicholas@2498 3313 return this.root;
nicholas@2498 3314 };
nicholas@2224 3315 }
nicholas@2384 3316
nicholas@2401 3317 var window_depedancy_callback;
nicholas@2498 3318 window_depedancy_callback = window.setInterval(function () {
nicholas@2401 3319 if (check_dependancies()) {
nicholas@2401 3320 window.clearInterval(window_depedancy_callback);
nicholas@2401 3321 onload();
nicholas@2401 3322 } else {
nicholas@2401 3323 document.getElementById("topLevelBody").innerHTML = "<h1>Loading Resources</h1>";
nicholas@2401 3324 }
nicholas@2498 3325 }, 100);