annotate js/core.js @ 2571:161d63a60b9e

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