annotate js/core.js @ 3119:aa4503f8c630

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