annotate js/core.js @ 2682:0a72855de078

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