annotate js/core.js @ 3141:335bc77627e0 tip

fixing discrete interface to allow labels to display
author Dave Moffat <me@davemoffat.com>
date Mon, 26 Jul 2021 12:15:24 +0100
parents bc0ef78bb07a
children
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@2708 1163 var node = this.popupOptions[this.currentIndex],
nicholas@2774 1164 pass = true,
nicholas@2778 1165 timeDelta = (new Date() - lastNodeStart) / 1000.0;
nicholas@3131 1166 if (node == undefined) {
nicholas@3131 1167 advanceState();
nicholas@3131 1168 }
nicholas@2774 1169 if (timeDelta < node.specification.minWait) {
nicholas@2778 1170 interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds");
nicholas@2774 1171 return;
nicholas@2774 1172 }
nicholas@2775 1173 node.elapsedTime = timeDelta;
nicholas@2498 1174 if (node.specification.type == 'question') {
nicholas@2498 1175 // Must extract the question data
nicholas@2708 1176 pass = processQuestion.call(this, node);
nicholas@2498 1177 } else if (node.specification.type == 'checkbox') {
nicholas@2498 1178 // Must extract checkbox data
nicholas@2708 1179 pass = processCheckbox.call(this, node);
nicholas@2708 1180 } else if (node.specification.type == "radio") {
nicholas@2464 1181 // Perform the conditional
nicholas@2708 1182 pass = processRadio.call(this, node);
nicholas@2708 1183 } else if (node.specification.type == "number") {
nicholas@2464 1184 // Perform the conditional
nicholas@2708 1185 pass = processNumber.call(this, node);
n@2583 1186 } else if (node.specification.type == 'slider') {
nicholas@2708 1187 pass = processSlider.call(this, node);
nicholas@2708 1188 }
nicholas@2708 1189 if (pass === false) {
nicholas@2708 1190 return;
nicholas@2498 1191 }
nicholas@2498 1192 this.currentIndex++;
nicholas@2498 1193 if (this.currentIndex < this.popupOptions.length) {
nicholas@2498 1194 this.postNode();
nicholas@2498 1195 } else {
nicholas@2498 1196 // Reached the end of the popupOptions
nicholas@2645 1197 this.popupTitle.innerHTML = "";
nicholas@2498 1198 this.popupResponse.innerHTML = "";
nicholas@2498 1199 this.hidePopup();
nicholas@2708 1200 this.popupOptions.forEach(function (node) {
nicholas@2498 1201 this.store.postResult(node);
nicholas@2708 1202 }, this);
nicholas@2224 1203 this.store.complete();
nicholas@2498 1204 advanceState();
nicholas@2498 1205 }
nicholas@2498 1206 };
nicholas@2498 1207
nicholas@2498 1208 this.previousClick = function () {
nicholas@2498 1209 // Triggered when the 'Back' button is clicked in the survey
nicholas@2498 1210 if (this.currentIndex > 0) {
nicholas@2498 1211 this.currentIndex--;
nicholas@2498 1212 this.postNode();
nicholas@2498 1213 }
nicholas@2498 1214 };
nicholas@2498 1215
nicholas@2498 1216 this.resize = function (event) {
nicholas@2498 1217 // Called on window resize;
nicholas@2708 1218 if (this.popup !== null) {
nicholas@2498 1219 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2498 1220 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
nicholas@2498 1221 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2498 1222 blank.style.width = window.innerWidth;
nicholas@2498 1223 blank.style.height = window.innerHeight;
nicholas@2498 1224 }
nicholas@2498 1225 };
nicholas@2498 1226 this.hideNextButton = function () {
nicholas@2224 1227 this.buttonProceed.style.visibility = "hidden";
nicholas@2708 1228 };
nicholas@2498 1229 this.hidePreviousButton = function () {
nicholas@2224 1230 this.buttonPrevious.style.visibility = "hidden";
nicholas@2708 1231 };
nicholas@2498 1232 this.showNextButton = function () {
nicholas@2224 1233 this.buttonProceed.style.visibility = "visible";
nicholas@2708 1234 };
nicholas@2498 1235 this.showPreviousButton = function () {
nicholas@2224 1236 this.buttonPrevious.style.visibility = "visible";
nicholas@2708 1237 };
nicholas@2224 1238 }
nicholas@2224 1239
nicholas@2498 1240 function advanceState() {
nicholas@2498 1241 // Just for complete clarity
nicholas@2498 1242 testState.advanceState();
nicholas@2224 1243 }
nicholas@2224 1244
nicholas@2498 1245 function stateMachine() {
nicholas@2498 1246 // Object prototype for tracking and managing the test state
nicholas@2722 1247
n@2716 1248 function pickSubPool(pool, numElements) {
n@2716 1249 // Assumes each element of pool has function "alwaysInclude"
n@2716 1250
n@2716 1251 // First extract those excluded from picking process
n@2716 1252 var picked = [];
nicholas@2833 1253 pool.forEach(function (e, i) {
n@2716 1254 if (e.alwaysInclude) {
nicholas@2833 1255 picked.push(pool.splice(i, 1)[0]);
n@2716 1256 }
n@2716 1257 });
n@2716 1258
n@2716 1259 return picked.concat(randomSubArray(pool, numElements - picked.length));
n@2716 1260 }
nicholas@2722 1261
nicholas@2498 1262 this.stateMap = [];
nicholas@2498 1263 this.preTestSurvey = null;
nicholas@2498 1264 this.postTestSurvey = null;
nicholas@2498 1265 this.stateIndex = null;
nicholas@2498 1266 this.currentStateMap = null;
nicholas@2498 1267 this.currentStatePosition = null;
nicholas@2224 1268 this.currentStore = null;
nicholas@2498 1269 this.initialise = function () {
nicholas@2498 1270
n@2909 1271 function randomiseElements(page) {
n@2909 1272 // Get the elements which are fixed / labelled
n@2909 1273 var fixed = [],
n@2909 1274 or = [],
n@2909 1275 remainder = [];
n@2909 1276 page.audioElements.forEach(function (a) {
n@2909 1277 if (a.label.length > 0 || a.postion !== undefined) {
n@2909 1278 fixed.push(a);
n@2909 1279 } else if (a.type === "outside-reference") {
n@2909 1280 or.push(a);
n@2909 1281 } else {
n@2909 1282 remainder.push(a);
n@2909 1283 }
n@2979 1284 });
n@2909 1285 if (page.poolSize > 0 || page.randomiseOrder) {
n@2909 1286 page.randomiseOrder = true;
n@2909 1287 if (page.poolSize === 0) {
n@2909 1288 page.poolSize = page.audioElements.length;
n@2909 1289 }
n@2909 1290 page.poolSize -= fixed.length;
n@2909 1291 remainder = pickSubPool(remainder, page.poolSize);
n@2909 1292 }
n@2909 1293 // Randomise the remainders
n@2909 1294 if (page.randomiseOrder) {
n@2909 1295 remainder = randomiseOrder(remainder);
n@2909 1296 }
n@2909 1297 fixed = fixed.concat(remainder);
n@2909 1298 page.audioElements = fixed.concat(or);
n@2909 1299 page.audioElements.forEach(function (a, i) {
n@2909 1300 a.position = i;
n@2909 1301 });
n@2909 1302 }
n@2909 1303
nicholas@2498 1304 // Get the data from Specification
nicholas@2498 1305 var pagePool = [];
nicholas@2722 1306 specification.pages.forEach(function (page) {
n@2716 1307 if (page.position !== null || page.alwaysInclude) {
n@2716 1308 page.alwaysInclude = true;
n@2716 1309 }
n@2716 1310 pagePool.push(page);
n@2717 1311 });
n@2716 1312 if (specification.numPages > 0) {
n@2716 1313 specification.randomiseOrder = true;
n@2716 1314 pagePool = pickSubPool(pagePool, specification.numPages);
n@2716 1315 }
n@2716 1316
n@2716 1317 // Now get the order of pages
n@2716 1318 var fixed = [];
nicholas@2722 1319 pagePool.forEach(function (page) {
nicholas@2748 1320 if (page.position !== undefined) {
n@2716 1321 fixed.push(page);
n@2716 1322 var i = pagePool.indexOf(page);
n@2716 1323 pagePool.splice(i, 1);
nicholas@2224 1324 }
n@2717 1325 });
nicholas@2498 1326
n@2716 1327 if (specification.randomiseOrder) {
n@2716 1328 pagePool = randomiseOrder(pagePool);
nicholas@2224 1329 }
nicholas@2498 1330
n@2716 1331 // Place in the correct order
nicholas@2722 1332 fixed.forEach(function (page) {
n@2716 1333 pagePool.splice(page.position, 0, page);
n@2717 1334 });
n@2716 1335
n@2716 1336 // Now process the pages
n@2716 1337 pagePool.forEach(function (page, i) {
n@2716 1338 page.presentedId = i;
n@2716 1339 this.stateMap.push(page);
n@2716 1340 var elements = page.audioElements;
n@2909 1341 randomiseElements(page);
n@2716 1342 storage.createTestPageStore(page);
n@2716 1343 audioEngineContext.loadPageData(page);
n@2716 1344 }, this);
nicholas@2674 1345
nicholas@2708 1346 if (specification.preTest !== null) {
nicholas@2498 1347 this.preTestSurvey = specification.preTest;
nicholas@2498 1348 }
nicholas@2708 1349 if (specification.postTest !== null) {
nicholas@2498 1350 this.postTestSurvey = specification.postTest;
nicholas@2498 1351 }
nicholas@2498 1352
nicholas@2498 1353 if (this.stateMap.length > 0) {
nicholas@2708 1354 if (this.stateIndex !== null) {
nicholas@2498 1355 console.log('NOTE - State already initialise');
nicholas@2498 1356 }
nicholas@2498 1357 this.stateIndex = -2;
nicholas@2224 1358 console.log('Starting test...');
nicholas@2498 1359 } else {
nicholas@2498 1360 console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
nicholas@2498 1361 }
nicholas@2498 1362 };
nicholas@2498 1363 this.advanceState = function () {
nicholas@2708 1364 if (this.stateIndex === null) {
nicholas@2498 1365 this.initialise();
nicholas@2498 1366 }
nicholas@2357 1367 if (this.stateIndex > -2) {
nicholas@2357 1368 storage.update();
nicholas@2357 1369 }
nicholas@2498 1370 if (this.stateIndex == -2) {
nicholas@3131 1371 this.stateIndex++;
nicholas@2708 1372 if (this.preTestSurvey !== undefined) {
nicholas@2498 1373 popup.initState(this.preTestSurvey, storage.globalPreTest);
nicholas@2498 1374 } else {
nicholas@2498 1375 this.advanceState();
nicholas@2498 1376 }
nicholas@2498 1377 } else if (this.stateIndex == -1) {
nicholas@3101 1378 if (interfaceContext.calibrationTests.checkFrequencies) {
nicholas@2224 1379 popup.showPopup();
nicholas@3101 1380 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 1381 interfaceContext.calibrationTests.performFrequencyCheck(popup.popupResponse);
nicholas@3101 1382 popup.hidePreviousButton();
nicholas@3101 1383 } else if (interfaceContext.calibrationTests.checkChannels) {
nicholas@3101 1384 popup.showPopup();
nicholas@3101 1385 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 1386 interfaceContext.calibrationTests.performChannelCheck(popup.popupResponse);
nicholas@2224 1387 popup.hidePreviousButton();
nicholas@2224 1388 } else {
nicholas@3101 1389 this.stateIndex++;
nicholas@2224 1390 this.advanceState();
nicholas@2224 1391 }
nicholas@2498 1392 } else if (this.stateIndex == this.stateMap.length) {
nicholas@2498 1393 // All test pages complete, post test
nicholas@2498 1394 console.log('Ending test ...');
nicholas@2498 1395 this.stateIndex++;
nicholas@2708 1396 if (this.postTestSurvey === undefined) {
nicholas@2498 1397 this.advanceState();
nicholas@2498 1398 } else {
nicholas@2498 1399 popup.initState(this.postTestSurvey, storage.globalPostTest);
nicholas@2498 1400 }
nicholas@2498 1401 } else if (this.stateIndex > this.stateMap.length) {
nicholas@2498 1402 createProjectSave(specification.projectReturn);
nicholas@2498 1403 } else {
nicholas@2224 1404 popup.hidePopup();
nicholas@2708 1405 if (this.currentStateMap === null) {
nicholas@2498 1406 this.currentStateMap = this.stateMap[this.stateIndex];
nicholas@2498 1407
nicholas@2224 1408 this.currentStore = storage.testPages[this.stateIndex];
nicholas@2708 1409 if (this.currentStateMap.preTest !== undefined) {
nicholas@2498 1410 this.currentStatePosition = 'pre';
nicholas@2498 1411 popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest);
nicholas@2498 1412 } else {
nicholas@2498 1413 this.currentStatePosition = 'test';
nicholas@2498 1414 }
nicholas@2498 1415 interfaceContext.newPage(this.currentStateMap, storage.testPages[this.stateIndex]);
nicholas@2498 1416 return;
nicholas@2498 1417 }
nicholas@2498 1418 switch (this.currentStatePosition) {
nicholas@2498 1419 case 'pre':
nicholas@2498 1420 this.currentStatePosition = 'test';
nicholas@2498 1421 break;
nicholas@2498 1422 case 'test':
nicholas@2498 1423 this.currentStatePosition = 'post';
nicholas@2498 1424 // Save the data
nicholas@2498 1425 this.testPageCompleted();
nicholas@2708 1426 if (this.currentStateMap.postTest === undefined) {
nicholas@2498 1427 this.advanceState();
nicholas@2498 1428 return;
nicholas@2498 1429 } else {
nicholas@2498 1430 popup.initState(this.currentStateMap.postTest, storage.testPages[this.stateIndex].postTest);
nicholas@2498 1431 }
nicholas@2498 1432 break;
nicholas@2498 1433 case 'post':
nicholas@2498 1434 this.stateIndex++;
nicholas@2498 1435 this.currentStateMap = null;
nicholas@2498 1436 this.advanceState();
nicholas@2498 1437 break;
nicholas@2708 1438 }
nicholas@2498 1439 }
nicholas@2498 1440 };
nicholas@2498 1441
nicholas@2498 1442 this.testPageCompleted = function () {
nicholas@2498 1443 // Function called each time a test page has been completed
nicholas@2498 1444 var storePoint = storage.testPages[this.stateIndex];
nicholas@2498 1445 // First get the test metric
nicholas@2498 1446
nicholas@2498 1447 var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0];
nicholas@2498 1448 if (audioEngineContext.metric.enableTestTimer) {
nicholas@2498 1449 var testTime = storePoint.parent.document.createElement('metricresult');
nicholas@2498 1450 testTime.id = 'testTime';
nicholas@2498 1451 testTime.textContent = audioEngineContext.timer.testDuration;
nicholas@2498 1452 metric.appendChild(testTime);
nicholas@2498 1453 }
nicholas@2498 1454
nicholas@2498 1455 var audioObjects = audioEngineContext.audioObjects;
nicholas@2708 1456 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2498 1457 ao.exportXMLDOM();
nicholas@2708 1458 });
nicholas@2708 1459 interfaceContext.commentQuestions.forEach(function (element) {
nicholas@2498 1460 element.exportXMLDOM(storePoint);
nicholas@2708 1461 });
nicholas@2498 1462 pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
nicholas@2224 1463 storePoint.complete();
nicholas@2498 1464 };
nicholas@2498 1465
nicholas@2498 1466 this.getCurrentTestPage = function () {
nicholas@2498 1467 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
nicholas@2310 1468 return this.currentStateMap;
nicholas@2310 1469 } else {
nicholas@2310 1470 return null;
nicholas@2310 1471 }
nicholas@2708 1472 };
nicholas@2498 1473 this.getCurrentTestPageStore = function () {
nicholas@2498 1474 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
nicholas@2312 1475 return this.currentStore;
nicholas@2312 1476 } else {
nicholas@2312 1477 return null;
nicholas@2312 1478 }
nicholas@2708 1479 };
nicholas@2224 1480 }
nicholas@2224 1481
nicholas@2224 1482 function AudioEngine(specification) {
nicholas@2498 1483
nicholas@2498 1484 // Create two output paths, the main outputGain and fooGain.
nicholas@2498 1485 // Output gain is default to 1 and any items for playback route here
nicholas@2498 1486 // Foo gain is used for analysis to ensure paths get processed, but are not heard
nicholas@2498 1487 // because web audio will optimise and any route which does not go to the destination gets ignored.
nicholas@2498 1488 this.outputGain = audioContext.createGain();
nicholas@2498 1489 this.fooGain = audioContext.createGain();
nicholas@2508 1490 this.fooGain.gain.value = 0;
nicholas@2498 1491
nicholas@2498 1492 // Use this to detect playback state: 0 - stopped, 1 - playing
nicholas@2498 1493 this.status = 0;
nicholas@2498 1494
nicholas@2498 1495 // Connect both gains to output
nicholas@2498 1496 this.outputGain.connect(audioContext.destination);
nicholas@2498 1497 this.fooGain.connect(audioContext.destination);
nicholas@2498 1498
nicholas@2498 1499 // Create the timer Object
nicholas@2498 1500 this.timer = new timer();
nicholas@2498 1501 // Create session metrics
nicholas@2498 1502 this.metric = new sessionMetrics(this, specification);
nicholas@2498 1503
nicholas@2498 1504 this.loopPlayback = false;
nicholas@2351 1505 this.synchPlayback = false;
nicholas@2351 1506 this.pageSpecification = null;
nicholas@2498 1507
nicholas@2498 1508 this.pageStore = null;
nicholas@2498 1509
nicholas@2508 1510 // Chrome 53+ Error solution
nicholas@2508 1511 // Empty buffer for keep-alive
nicholas@2508 1512 var nullBuffer = audioContext.createBuffer(1, audioContext.sampleRate, audioContext.sampleRate);
nicholas@2508 1513 this.nullBufferSource = audioContext.createBufferSource();
nicholas@2508 1514 this.nullBufferSource.buffer = nullBuffer;
nicholas@2508 1515 this.nullBufferSource.loop = true;
nicholas@2508 1516 this.nullBufferSource.start(0);
nicholas@2508 1517
nicholas@2498 1518 // Create store for new audioObjects
nicholas@2498 1519 this.audioObjects = [];
nicholas@2498 1520
nicholas@2498 1521 this.buffers = [];
nicholas@2498 1522 this.bufferObj = function () {
nicholas@2617 1523 var urls = [];
nicholas@2498 1524 this.buffer = null;
nicholas@2498 1525 this.users = [];
nicholas@2224 1526 this.progress = 0;
nicholas@2224 1527 this.status = 0;
nicholas@2498 1528 this.ready = function () {
nicholas@2498 1529 if (this.status >= 2) {
nicholas@2224 1530 this.status = 3;
nicholas@2224 1531 }
nicholas@2498 1532 for (var i = 0; i < this.users.length; i++) {
nicholas@2498 1533 this.users[i].state = 1;
nicholas@2708 1534 if (this.users[i].interfaceDOM !== null) {
nicholas@2498 1535 this.users[i].bufferLoaded(this);
nicholas@2498 1536 }
nicholas@2498 1537 }
nicholas@2498 1538 };
nicholas@2617 1539 this.setUrls = function (obj) {
nicholas@2617 1540 // Obj must be an array of pairs:
nicholas@2617 1541 // [{sampleRate, url}]
nicholas@2617 1542 var localFs = audioContext.sampleRate,
nicholas@2617 1543 list = [],
nicholas@2617 1544 i;
nicholas@2617 1545 for (i = 0; i < obj.length; i++) {
nicholas@2617 1546 if (obj[i].sampleRate == localFs) {
nicholas@2617 1547 list.push(obj.splice(i, 1)[0]);
nicholas@2617 1548 }
nicholas@2617 1549 }
nicholas@2617 1550 list = list.concat(obj);
nicholas@2617 1551 urls = list;
nicholas@2617 1552 };
nicholas@2617 1553 this.hasUrl = function (checkUrl) {
nicholas@2617 1554 var l = urls.length,
nicholas@2617 1555 i;
nicholas@2617 1556 for (i = 0; i < l; i++) {
nicholas@2617 1557 if (urls[i].url == checkUrl) {
nicholas@2617 1558 return true;
nicholas@2617 1559 }
nicholas@2617 1560 }
nicholas@2617 1561 return false;
nicholas@2708 1562 };
nicholas@2617 1563 this.getMedia = function () {
nicholas@2615 1564 var self = this;
nicholas@2616 1565 var currentUrlIndex = 0;
nicholas@2498 1566
nicholas@2615 1567 function get(fqurl) {
nicholas@2615 1568 return new Promise(function (resolve, reject) {
nicholas@2615 1569 var req = new XMLHttpRequest();
nicholas@2615 1570 req.open('GET', fqurl, true);
nicholas@2615 1571 req.responseType = 'arraybuffer';
nicholas@2615 1572 req.onload = function () {
nicholas@2615 1573 if (req.status == 200) {
nicholas@2615 1574 resolve(req.response);
nicholas@2615 1575 }
nicholas@2615 1576 };
nicholas@2615 1577 req.onerror = function () {
nicholas@2615 1578 reject(new Error(req.statusText));
nicholas@2615 1579 };
nicholas@2615 1580
nicholas@2615 1581 req.addEventListener("progress", progressCallback.bind(self));
nicholas@2615 1582 req.send();
nicholas@2615 1583 });
nicholas@2615 1584 }
nicholas@2615 1585
nicholas@2615 1586 function getNextURL() {
nicholas@2615 1587 currentUrlIndex++;
nicholas@2615 1588 var self = this;
nicholas@2617 1589 if (currentUrlIndex >= urls.length) {
nicholas@2615 1590 processError();
nicholas@2615 1591 } else {
nicholas@2617 1592 return get(urls[currentUrlIndex].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
nicholas@2615 1593 }
nicholas@2615 1594 }
nicholas@2498 1595
nicholas@2498 1596 // Create callback to decode the data asynchronously
nicholas@2615 1597 function processAudio(response) {
nicholas@2615 1598 var self = this;
nicholas@2615 1599 return audioContext.decodeAudioData(response, function (decodedData) {
nicholas@2615 1600 self.buffer = decodedData;
nicholas@2615 1601 self.status = 2;
nicholas@2615 1602 calculateLoudness(self, "I");
nicholas@2615 1603 return true;
nicholas@2498 1604 }, function (e) {
nicholas@2403 1605 var waveObj = new WAVE();
nicholas@2708 1606 if (waveObj.open(response) === 0) {
nicholas@2615 1607 self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate);
nicholas@2498 1608 for (var c = 0; c < waveObj.num_channels; c++) {
nicholas@2615 1609 var buffer_ptr = self.buffer.getChannelData(c);
nicholas@2498 1610 for (var n = 0; n < waveObj.num_samples; n++) {
nicholas@2403 1611 buffer_ptr[n] = waveObj.decoded_data[c][n];
nicholas@2224 1612 }
nicholas@2224 1613 }
nicholas@2403 1614 }
nicholas@2708 1615 if (self.buffer !== undefined) {
nicholas@2615 1616 self.status = 2;
nicholas@2615 1617 calculateLoudness(self, "I");
nicholas@2615 1618 return true;
nicholas@2403 1619 }
nicholas@2708 1620 waveObj = undefined;
nicholas@2615 1621 return false;
nicholas@2403 1622 });
nicholas@2615 1623 }
nicholas@2498 1624
nicholas@2224 1625 // Create callback for any error in loading
nicholas@2615 1626 function processError() {
nicholas@2615 1627 this.status = -1;
nicholas@2615 1628 for (var i = 0; i < this.users.length; i++) {
nicholas@2615 1629 this.users[i].state = -1;
nicholas@2708 1630 if (this.users[i].interfaceDOM !== null) {
nicholas@2615 1631 this.users[i].bufferLoaded(this);
nicholas@2224 1632 }
nicholas@2224 1633 }
nicholas@2617 1634 interfaceContext.lightbox.post("Error", "Could not load resource " + urls[currentUrlIndex].url);
nicholas@2224 1635 }
nicholas@2498 1636
nicholas@2615 1637 function progressCallback(event) {
nicholas@2498 1638 if (event.lengthComputable) {
nicholas@2615 1639 this.progress = event.loaded / event.total;
nicholas@2615 1640 for (var i = 0; i < this.users.length; i++) {
nicholas@2708 1641 if (this.users[i].interfaceDOM !== null) {
nicholas@2615 1642 if (typeof this.users[i].interfaceDOM.updateLoading === "function") {
nicholas@2615 1643 this.users[i].interfaceDOM.updateLoading(this.progress * 100);
nicholas@2498 1644 }
nicholas@2498 1645 }
nicholas@2498 1646 }
nicholas@2498 1647 }
nicholas@2708 1648 }
nicholas@2615 1649
nicholas@2615 1650 this.progress = 0;
nicholas@2224 1651 this.status = 1;
nicholas@2617 1652 currentUrlIndex = 0;
nicholas@2617 1653 get(urls[0].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
nicholas@2498 1654 };
nicholas@2498 1655
nicholas@2498 1656 this.registerAudioObject = function (audioObject) {
nicholas@2224 1657 // Called by an audioObject to register to the buffer for use
nicholas@2224 1658 // First check if already in the register pool
nicholas@2708 1659 this.users.forEach(function (object) {
nicholas@2708 1660 if (audioObject.id == object.id) {
nicholas@2498 1661 return 0;
nicholas@2498 1662 }
nicholas@2708 1663 });
nicholas@2224 1664 this.users.push(audioObject);
nicholas@2498 1665 if (this.status == 3 || this.status == -1) {
nicholas@2224 1666 // The buffer is already ready, trigger bufferLoaded
nicholas@2224 1667 audioObject.bufferLoaded(this);
nicholas@2224 1668 }
nicholas@2224 1669 };
nicholas@2498 1670
nicholas@2498 1671 this.copyBuffer = function (preSilenceTime, postSilenceTime) {
nicholas@2224 1672 // Copies the entire bufferObj.
nicholas@2708 1673 if (preSilenceTime === undefined) {
nicholas@2498 1674 preSilenceTime = 0;
nicholas@2498 1675 }
nicholas@2708 1676 if (postSilenceTime === undefined) {
nicholas@2498 1677 postSilenceTime = 0;
nicholas@2498 1678 }
nicholas@2498 1679 var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate);
nicholas@2498 1680 var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate);
nicholas@2498 1681 var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples;
nicholas@2460 1682 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
nicholas@2708 1683 var c;
nicholas@2224 1684 // Now we can use some efficient background copy schemes if we are just padding the end
nicholas@2708 1685 if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") {
nicholas@2708 1686 for (c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2498 1687 copybuffer.copyToChannel(this.buffer.getChannelData(c), c);
nicholas@2224 1688 }
nicholas@2224 1689 } else {
nicholas@2708 1690 for (c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2224 1691 var src = this.buffer.getChannelData(c);
nicholas@2460 1692 var dst = copybuffer.getChannelData(c);
nicholas@2498 1693 for (var n = 0; n < src.length; n++)
nicholas@2498 1694 dst[n + preSilenceSamples] = src[n];
nicholas@2224 1695 }
nicholas@2224 1696 }
nicholas@2224 1697 // Copy in the rest of the buffer information
nicholas@2460 1698 copybuffer.lufs = this.buffer.lufs;
nicholas@2460 1699 copybuffer.playbackGain = this.buffer.playbackGain;
nicholas@2460 1700 return copybuffer;
nicholas@2708 1701 };
nicholas@2498 1702
nicholas@2498 1703 this.cropBuffer = function (startTime, stopTime) {
nicholas@2460 1704 // Copy and return the cropped buffer
nicholas@2498 1705 var start_sample = Math.floor(startTime * this.buffer.sampleRate);
nicholas@2498 1706 var stop_sample = Math.floor(stopTime * this.buffer.sampleRate);
nicholas@2460 1707 var newLength = stop_sample - start_sample;
nicholas@2460 1708 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
nicholas@2460 1709 // Now we can use some efficient background copy schemes if we are just padding the end
nicholas@2498 1710 for (var c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2460 1711 var buffer = this.buffer.getChannelData(c);
nicholas@2498 1712 var sub_frame = buffer.subarray(start_sample, stop_sample);
nicholas@2460 1713 if (typeof copybuffer.copyToChannel == "function") {
nicholas@2498 1714 copybuffer.copyToChannel(sub_frame, c);
nicholas@2460 1715 } else {
nicholas@2460 1716 var dst = copybuffer.getChannelData(c);
nicholas@2498 1717 for (var n = 0; n < newLength; n++)
nicholas@2505 1718 dst[n] = buffer[n + start_sample];
nicholas@2460 1719 }
nicholas@2460 1720 }
nicholas@2460 1721 return copybuffer;
nicholas@2708 1722 };
nicholas@2498 1723 };
nicholas@2498 1724
nicholas@2498 1725 this.loadPageData = function (page) {
nicholas@2224 1726 // Load the URL from pages
nicholas@2708 1727 function loadAudioElementData(element) {
nicholas@2224 1728 var URL = page.hostURL + element.url;
nicholas@2708 1729 var buffer = this.buffers.find(function (buffObj) {
nicholas@2708 1730 return buffObj.hasUrl(URL);
nicholas@2708 1731 });
nicholas@2708 1732 if (buffer === undefined) {
nicholas@2224 1733 buffer = new this.bufferObj();
nicholas@2617 1734 var urls = [{
nicholas@2617 1735 url: URL,
nicholas@2617 1736 sampleRate: element.sampleRate
nicholas@2617 1737 }];
nicholas@2615 1738 element.alternatives.forEach(function (e) {
nicholas@2617 1739 urls.push({
nicholas@2617 1740 url: e.url,
nicholas@2617 1741 sampleRate: e.sampleRate
nicholas@2617 1742 });
nicholas@2615 1743 });
nicholas@2617 1744 buffer.setUrls(urls);
nicholas@2617 1745 buffer.getMedia();
nicholas@2224 1746 this.buffers.push(buffer);
nicholas@2224 1747 }
nicholas@2224 1748 }
nicholas@2708 1749 page.audioElements.forEach(loadAudioElementData, this);
nicholas@2224 1750 };
nicholas@2498 1751
nicholas@2708 1752 function playNormal(id) {
nicholas@2708 1753 var playTime = audioContext.currentTime + 0.1;
nicholas@2708 1754 var stopTime = playTime + specification.crossFade;
nicholas@2708 1755 this.audioObjects.forEach(function (ao) {
nicholas@2708 1756 if (ao.id === id) {
nicholas@2942 1757 ao.setupPlayback();
nicholas@2942 1758 ao.bufferStart(playTime);
nicholas@2942 1759 ao.listenStart(playTime);
nicholas@2708 1760 } else {
nicholas@2942 1761 ao.listenStop(playTime);
nicholas@2942 1762 ao.bufferStop(stopTime);
nicholas@2708 1763 }
nicholas@2708 1764 });
nicholas@2708 1765 }
nicholas@2708 1766
nicholas@2942 1767 function playSync(id) {
nicholas@2708 1768 var playTime = audioContext.currentTime + 0.1;
nicholas@2708 1769 var stopTime = playTime + specification.crossFade;
nicholas@2708 1770 this.audioObjects.forEach(function (ao) {
nicholas@2942 1771 ao.setupPlayback();
nicholas@2942 1772 ao.bufferStart(playTime);
nicholas@2708 1773 if (ao.id === id) {
nicholas@2942 1774 ao.listenStart(playTime);
nicholas@2708 1775 } else {
nicholas@2942 1776 ao.listenStop(playTime);
nicholas@2708 1777 }
nicholas@2708 1778 });
nicholas@2708 1779 }
nicholas@2708 1780
nicholas@2498 1781 this.play = function (id) {
nicholas@2498 1782 // Start the timer and set the audioEngine state to playing (1)
nicholas@2708 1783 if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) {
nicholas@2708 1784 throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
nicholas@2498 1785 }
nicholas@2823 1786 var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays;
nicholas@2823 1787 if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) {
nicholas@2823 1788 interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times");
nicholas@2823 1789 return;
nicholas@2823 1790 }
nicholas@2708 1791 if (this.status === 1) {
nicholas@2498 1792 this.timer.startTest();
nicholas@2708 1793 interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
nicholas@2942 1794 if (this.synchPlayback) {
nicholas@2351 1795 // Traditional looped playback
nicholas@2942 1796 playSync.call(this, id);
nicholas@2708 1797 } else {
nicholas@2708 1798 if (this.bufferReady(id) === false) {
nicholas@2708 1799 console.log("Cannot play. Buffer not ready");
nicholas@2708 1800 return;
nicholas@2498 1801 }
nicholas@2708 1802 playNormal.call(this, id);
nicholas@2498 1803 }
nicholas@2498 1804 interfaceContext.playhead.start();
nicholas@2498 1805 }
nicholas@2498 1806 };
nicholas@2224 1807
nicholas@2498 1808 this.stop = function () {
nicholas@2498 1809 // Send stop and reset command to all playback buffers
nicholas@2498 1810 if (this.status == 1) {
nicholas@2498 1811 var setTime = audioContext.currentTime + 0.1;
nicholas@2708 1812 this.audioObjects.forEach(function (a) {
nicholas@2942 1813 a.listenStop(setTime);
nicholas@2942 1814 a.bufferStop(setTime);
nicholas@2708 1815 });
nicholas@2498 1816 interfaceContext.playhead.stop();
nicholas@2498 1817 }
nicholas@2498 1818 };
nicholas@2498 1819
nicholas@2498 1820 this.newTrack = function (element) {
nicholas@2498 1821 // Pull data from given URL into new audio buffer
nicholas@2498 1822 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
nicholas@2498 1823
nicholas@2498 1824 // Create the audioObject with ID of the new track length;
nicholas@2708 1825 var audioObjectId = this.audioObjects.length;
nicholas@2498 1826 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
nicholas@2498 1827
nicholas@2498 1828 // Check if audioObject buffer is currently stored by full URL
nicholas@2498 1829 var URL = testState.currentStateMap.hostURL + element.url;
nicholas@2708 1830 var buffer = this.buffers.find(function (buffObj) {
nicholas@2708 1831 return buffObj.hasUrl(URL);
nicholas@2708 1832 });
nicholas@2708 1833 if (buffer === undefined) {
nicholas@2498 1834 console.log("[WARN]: Buffer was not loaded in pre-test! " + URL);
nicholas@2498 1835 buffer = new this.bufferObj();
nicholas@2224 1836 this.buffers.push(buffer);
nicholas@2498 1837 buffer.getMedia(URL);
nicholas@2498 1838 }
nicholas@2498 1839 this.audioObjects[audioObjectId].specification = element;
nicholas@2498 1840 this.audioObjects[audioObjectId].url = URL;
nicholas@2498 1841 // Obtain store node
nicholas@2498 1842 var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
nicholas@2498 1843 for (var i = 0; i < aeNodes.length; i++) {
nicholas@2498 1844 if (aeNodes[i].getAttribute("ref") == element.id) {
nicholas@2498 1845 this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
nicholas@2498 1846 break;
nicholas@2498 1847 }
nicholas@2498 1848 }
nicholas@2224 1849 buffer.registerAudioObject(this.audioObjects[audioObjectId]);
nicholas@2498 1850 return this.audioObjects[audioObjectId];
nicholas@2498 1851 };
nicholas@2498 1852
nicholas@2498 1853 this.newTestPage = function (audioHolderObject, store) {
nicholas@2498 1854 this.pageStore = store;
nicholas@2351 1855 this.pageSpecification = audioHolderObject;
nicholas@2498 1856 this.status = 0;
nicholas@2498 1857 this.audioObjectsReady = false;
nicholas@2498 1858 this.metric.reset();
nicholas@2708 1859 this.buffers.forEach(function (buffer) {
nicholas@2708 1860 buffer.users = [];
nicholas@2708 1861 });
nicholas@2498 1862 this.audioObjects = [];
nicholas@2224 1863 this.timer = new timer();
nicholas@2224 1864 this.loopPlayback = audioHolderObject.loop;
nicholas@2351 1865 this.synchPlayback = audioHolderObject.synchronous;
nicholas@2955 1866 interfaceContext.keyboardInterface.resetKeyBindings();
nicholas@2498 1867 };
nicholas@2498 1868
nicholas@2498 1869 this.checkAllPlayed = function () {
nicholas@2708 1870 var arr = [];
nicholas@2498 1871 for (var id = 0; id < this.audioObjects.length; id++) {
nicholas@2708 1872 if (this.audioObjects[id].metric.wasListenedTo === false) {
nicholas@2498 1873 arr.push(this.audioObjects[id].id);
nicholas@2498 1874 }
nicholas@2498 1875 }
nicholas@2498 1876 return arr;
nicholas@2498 1877 };
nicholas@2498 1878
nicholas@2498 1879 this.checkAllReady = function () {
nicholas@2498 1880 var ready = true;
nicholas@2498 1881 for (var i = 0; i < this.audioObjects.length; i++) {
nicholas@2708 1882 if (this.audioObjects[i].state === 0) {
nicholas@2498 1883 // Track not ready
nicholas@2498 1884 console.log('WAIT -- audioObject ' + i + ' not ready yet!');
nicholas@2498 1885 ready = false;
nicholas@2708 1886 }
nicholas@2498 1887 }
nicholas@2498 1888 return ready;
nicholas@2498 1889 };
nicholas@2498 1890
nicholas@2498 1891 this.setSynchronousLoop = function () {
nicholas@2570 1892 // Pads the signals so they are all exactly the same duration
nicholas@2570 1893 // Get the duration of the longest signal.
nicholas@2570 1894 var duration = 0;
nicholas@2498 1895 var maxId;
nicholas@2498 1896 for (var i = 0; i < this.audioObjects.length; i++) {
nicholas@2570 1897 if (duration < this.audioObjects[i].buffer.buffer.duration) {
nicholas@2570 1898 duration = this.audioObjects[i].buffer.buffer.duration;
nicholas@2498 1899 maxId = i;
nicholas@2498 1900 }
nicholas@2498 1901 }
nicholas@2498 1902 // Extract the audio and zero-pad
nicholas@2708 1903 this.audioObjects.forEach(function (ao) {
nicholas@2570 1904 if (ao.buffer.buffer.duration !== duration) {
nicholas@2570 1905 ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration);
nicholas@2500 1906 }
nicholas@2708 1907 });
nicholas@2498 1908 };
nicholas@2498 1909
nicholas@2498 1910 this.bufferReady = function (id) {
nicholas@2498 1911 if (this.checkAllReady()) {
nicholas@2498 1912 if (this.synchPlayback) {
nicholas@2498 1913 this.setSynchronousLoop();
nicholas@2498 1914 }
nicholas@2460 1915 this.status = 1;
nicholas@2460 1916 return true;
nicholas@2460 1917 }
nicholas@2460 1918 return false;
nicholas@2224 1919 };
nicholas@2498 1920
nicholas@2224 1921 }
nicholas@2224 1922
nicholas@2224 1923 function audioObject(id) {
nicholas@2498 1924 // The main buffer object with common control nodes to the AudioEngine
nicholas@2498 1925
nicholas@2823 1926 var playCounter = 0;
nicholas@2823 1927
nicholas@2708 1928 this.specification = undefined;
nicholas@2498 1929 this.id = id;
nicholas@2498 1930 this.state = 0; // 0 - no data, 1 - ready
nicholas@2498 1931 this.url = null; // Hold the URL given for the output back to the results.
nicholas@2498 1932 this.metric = new metricTracker(this);
nicholas@2498 1933 this.storeDOM = null;
nicholas@2949 1934 this.playing = false;
nicholas@2498 1935
nicholas@2498 1936 // Bindings for GUI
nicholas@2498 1937 this.interfaceDOM = null;
nicholas@2498 1938 this.commentDOM = null;
nicholas@2498 1939
nicholas@2498 1940 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
nicholas@2498 1941 this.bufferNode = undefined;
nicholas@2498 1942 this.outputGain = audioContext.createGain();
nicholas@2942 1943 this.outputGain.gain.value = 0.0;
nicholas@2498 1944
nicholas@2498 1945 this.onplayGain = 1.0;
nicholas@2498 1946
nicholas@2498 1947 // Connect buffer to the audio graph
nicholas@2498 1948 this.outputGain.connect(audioEngineContext.outputGain);
nicholas@2508 1949 audioEngineContext.nullBufferSource.connect(this.outputGain);
nicholas@2498 1950
nicholas@2498 1951 // the audiobuffer is not designed for multi-start playback
nicholas@2498 1952 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
nicholas@2708 1953 this.buffer = undefined;
nicholas@2498 1954
nicholas@2498 1955 this.bufferLoaded = function (callee) {
nicholas@2498 1956 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
nicholas@2498 1957 // audioObject and trigger the interfaceDOM.enable() function for user feedback
nicholas@2224 1958 if (callee.status == -1) {
nicholas@2224 1959 // ERROR
nicholas@2224 1960 this.state = -1;
nicholas@2708 1961 if (this.interfaceDOM !== null) {
nicholas@2498 1962 this.interfaceDOM.error();
nicholas@2498 1963 }
nicholas@2224 1964 this.buffer = callee;
nicholas@2224 1965 return;
nicholas@2224 1966 }
nicholas@2224 1967 this.buffer = callee;
nicholas@2224 1968 var preSilenceTime = this.specification.preSilence || this.specification.parent.preSilence || specification.preSilence || 0.0;
nicholas@2224 1969 var postSilenceTime = this.specification.postSilence || this.specification.parent.postSilence || specification.postSilence || 0.0;
nicholas@2460 1970 var startTime = this.specification.startTime;
nicholas@2460 1971 var stopTime = this.specification.stopTime;
nicholas@2460 1972 var copybuffer = new callee.constructor();
nicholas@2500 1973
nicholas@2500 1974 copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration);
nicholas@2708 1975 if (preSilenceTime !== 0 || postSilenceTime !== 0) {
nicholas@2500 1976 copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime);
nicholas@2460 1977 }
nicholas@2500 1978
nicholas@2660 1979 copybuffer.buffer.lufs = callee.buffer.lufs;
nicholas@2500 1980 this.buffer = copybuffer;
nicholas@2498 1981
nicholas@2661 1982 var targetLUFS = this.specification.loudness || this.specification.parent.loudness || specification.loudness;
nicholas@2498 1983 if (typeof targetLUFS === "number" && isFinite(targetLUFS)) {
nicholas@2498 1984 this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
nicholas@2498 1985 } else {
nicholas@2498 1986 this.buffer.buffer.playbackGain = 1.0;
nicholas@2498 1987 }
nicholas@2708 1988 if (this.interfaceDOM !== null) {
nicholas@2498 1989 this.interfaceDOM.enable();
nicholas@2498 1990 }
nicholas@2498 1991 this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0);
nicholas@2498 1992 this.storeDOM.setAttribute('playGain', linearToDecibel(this.onplayGain));
nicholas@2460 1993 this.state = 1;
nicholas@2460 1994 audioEngineContext.bufferReady(id);
nicholas@2498 1995 };
nicholas@2498 1996
nicholas@2498 1997 this.bindInterface = function (interfaceObject) {
nicholas@2498 1998 this.interfaceDOM = interfaceObject;
nicholas@2498 1999 this.metric.initialise(interfaceObject.getValue());
nicholas@2498 2000 if (this.state == 1) {
nicholas@2498 2001 this.interfaceDOM.enable();
nicholas@2498 2002 } else if (this.state == -1) {
nicholas@2224 2003 // ERROR
nicholas@2224 2004 this.interfaceDOM.error();
nicholas@2224 2005 return;
nicholas@2224 2006 }
nicholas@2955 2007 var presentedId = interfaceObject.getPresentedId();
nicholas@2955 2008 this.storeDOM.setAttribute('presentedId', presentedId);
nicholas@2955 2009
nicholas@2955 2010 // Key-bindings
nicholas@2955 2011 if (presentedId.length == 1) {
nicholas@2955 2012 interfaceContext.keyboardInterface.registerKeyBinding(presentedId, this);
nicholas@2955 2013 }
nicholas@2498 2014 };
nicholas@2498 2015
nicholas@2942 2016 this.listenStart = function (setTime) {
nicholas@2949 2017 if (this.playing === false) {
nicholas@2942 2018 playCounter++;
nicholas@2942 2019 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, setTime);
nicholas@2942 2020 this.metric.startListening(audioEngineContext.timer.getTestTime());
nicholas@2942 2021 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
nicholas@2942 2022 this.interfaceDOM.startPlayback();
nicholas@2949 2023 this.playing = true;
nicholas@2942 2024 }
nicholas@2498 2025 };
nicholas@2498 2026
nicholas@2942 2027 this.listenStop = function (setTime) {
nicholas@2949 2028 if (this.playing === true) {
nicholas@2498 2029 this.outputGain.gain.linearRampToValueAtTime(0.0, setTime);
nicholas@2942 2030 this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
nicholas@2498 2031 }
nicholas@2224 2032 this.interfaceDOM.stopPlayback();
nicholas@2949 2033 this.playing = false;
nicholas@2498 2034 };
nicholas@2498 2035
nicholas@2942 2036 this.setupPlayback = function () {
nicholas@2708 2037 if (this.bufferNode === undefined && this.buffer.buffer !== undefined) {
nicholas@2498 2038 this.bufferNode = audioContext.createBufferSource();
nicholas@2498 2039 this.bufferNode.owner = this;
nicholas@2498 2040 this.bufferNode.connect(this.outputGain);
nicholas@2498 2041 this.bufferNode.buffer = this.buffer.buffer;
nicholas@2942 2042 if (audioEngineContext.loopPlayback) {
nicholas@2942 2043 this.bufferNode.loopStart = this.specification.startTime || 0;
nicholas@2942 2044 this.bufferNode.loopEnd = this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration;
nicholas@2942 2045 this.bufferNode.loop = true;
nicholas@2942 2046 }
nicholas@2498 2047 this.bufferNode.onended = function (event) {
nicholas@2498 2048 // Safari does not like using 'this' to reference the calling object!
nicholas@2498 2049 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
nicholas@2708 2050 if (event.currentTarget !== null) {
nicholas@2942 2051 event.currentTarget.owner.bufferStop(audioContext.currentTime + 0.1);
nicholas@2950 2052 event.currentTarget.owner.listenStop(audioContext.currentTime + 0.1);
nicholas@2224 2053 }
nicholas@2498 2054 };
nicholas@2942 2055 this.bufferNode.state = 0;
nicholas@2942 2056 }
nicholas@2942 2057 };
nicholas@2942 2058
nicholas@2942 2059 this.bufferStart = function (startTime) {
nicholas@2942 2060 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
n@2979 2061 if (this.bufferNode && this.bufferNode.state === 0) {
nicholas@2942 2062 this.bufferNode.state = 1;
n@2979 2063 if (this.bufferNode.loop === true) {
nicholas@2499 2064 this.bufferNode.start(startTime);
nicholas@2499 2065 } else {
nicholas@2499 2066 this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration);
nicholas@2499 2067 }
nicholas@2498 2068 }
nicholas@2498 2069 };
nicholas@2498 2070
nicholas@2942 2071 this.bufferStop = function (stopTime) {
nicholas@2224 2072 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
nicholas@2942 2073 if (this.bufferNode !== undefined && this.bufferNode.state > 0) {
nicholas@2498 2074 this.bufferNode.stop(stopTime);
nicholas@2498 2075 this.bufferNode = undefined;
nicholas@2498 2076 }
nicholas@2529 2077 this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime);
nicholas@2224 2078 this.interfaceDOM.stopPlayback();
nicholas@2498 2079 };
nicholas@2498 2080
nicholas@2498 2081 this.getCurrentPosition = function () {
nicholas@2498 2082 var time = audioEngineContext.timer.getTestTime();
nicholas@2708 2083 if (this.bufferNode !== undefined) {
nicholas@2498 2084 var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
nicholas@2498 2085 if (isNaN(position)) {
nicholas@2498 2086 return 0;
nicholas@2498 2087 }
nicholas@2224 2088 return position;
nicholas@2498 2089 } else {
nicholas@2498 2090 return 0;
nicholas@2498 2091 }
nicholas@2498 2092 };
nicholas@2498 2093
nicholas@2498 2094 this.exportXMLDOM = function () {
nicholas@2498 2095 var file = storage.document.createElement('file');
nicholas@2498 2096 file.setAttribute('sampleRate', this.buffer.buffer.sampleRate);
nicholas@2498 2097 file.setAttribute('channels', this.buffer.buffer.numberOfChannels);
nicholas@2498 2098 file.setAttribute('sampleCount', this.buffer.buffer.length);
nicholas@2498 2099 file.setAttribute('duration', this.buffer.buffer.duration);
nicholas@2498 2100 this.storeDOM.appendChild(file);
nicholas@2498 2101 if (this.specification.type != 'outside-reference') {
nicholas@2498 2102 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
nicholas@2708 2103 if (interfaceXML !== null) {
nicholas@2708 2104 if (interfaceXML.length === undefined) {
nicholas@2498 2105 this.storeDOM.appendChild(interfaceXML);
nicholas@2498 2106 } else {
nicholas@2498 2107 for (var i = 0; i < interfaceXML.length; i++) {
nicholas@2498 2108 this.storeDOM.appendChild(interfaceXML[i]);
nicholas@2498 2109 }
nicholas@2498 2110 }
nicholas@2498 2111 }
nicholas@2708 2112 if (this.commentDOM !== null) {
nicholas@2498 2113 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
nicholas@2498 2114 }
nicholas@2498 2115 }
nicholas@2708 2116 this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]);
nicholas@2498 2117 };
nicholas@2823 2118
nicholas@2823 2119 Object.defineProperties(this, {
nicholas@2823 2120 "numberOfPlays": {
nicholas@2823 2121 'get': function () {
nicholas@2823 2122 return playCounter;
nicholas@2823 2123 },
nicholas@2823 2124 'set': function () {
nicholas@2823 2125 return playCounter;
nicholas@2823 2126 }
nicholas@2823 2127 }
nicholas@2823 2128 });
nicholas@2224 2129 }
nicholas@2224 2130
nicholas@2498 2131 function timer() {
nicholas@2498 2132 /* Timer object used in audioEngine to keep track of session timings
nicholas@2498 2133 * Uses the timer of the web audio API, so sample resolution
nicholas@2498 2134 */
nicholas@2498 2135 this.testStarted = false;
nicholas@2498 2136 this.testStartTime = 0;
nicholas@2498 2137 this.testDuration = 0;
nicholas@2498 2138 this.minimumTestTime = 0; // No minimum test time
nicholas@2498 2139 this.startTest = function () {
nicholas@2708 2140 if (this.testStarted === false) {
nicholas@2498 2141 this.testStartTime = audioContext.currentTime;
nicholas@2498 2142 this.testStarted = true;
nicholas@2498 2143 this.updateTestTime();
nicholas@2498 2144 audioEngineContext.metric.initialiseTest();
nicholas@2498 2145 }
nicholas@2498 2146 };
nicholas@2498 2147 this.stopTest = function () {
nicholas@2498 2148 if (this.testStarted) {
nicholas@2498 2149 this.testDuration = this.getTestTime();
nicholas@2498 2150 this.testStarted = false;
nicholas@2498 2151 } else {
nicholas@2498 2152 console.log('ERR: Test tried to end before beginning');
nicholas@2498 2153 }
nicholas@2498 2154 };
nicholas@2498 2155 this.updateTestTime = function () {
nicholas@2498 2156 if (this.testStarted) {
nicholas@2498 2157 this.testDuration = audioContext.currentTime - this.testStartTime;
nicholas@2498 2158 }
nicholas@2498 2159 };
nicholas@2498 2160 this.getTestTime = function () {
nicholas@2498 2161 this.updateTestTime();
nicholas@2498 2162 return this.testDuration;
nicholas@2498 2163 };
nicholas@2224 2164 }
nicholas@2224 2165
nicholas@2498 2166 function sessionMetrics(engine, specification) {
nicholas@2498 2167 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
nicholas@2498 2168 */
nicholas@2498 2169 this.engine = engine;
nicholas@2498 2170 this.lastClicked = -1;
nicholas@2498 2171 this.data = -1;
nicholas@2498 2172 this.reset = function () {
nicholas@2498 2173 this.lastClicked = -1;
nicholas@2498 2174 this.data = -1;
nicholas@2498 2175 };
nicholas@2498 2176
nicholas@2498 2177 this.enableElementInitialPosition = false;
nicholas@2498 2178 this.enableElementListenTracker = false;
nicholas@2498 2179 this.enableElementTimer = false;
nicholas@2498 2180 this.enableElementTracker = false;
nicholas@2498 2181 this.enableFlagListenedTo = false;
nicholas@2498 2182 this.enableFlagMoved = false;
nicholas@2498 2183 this.enableTestTimer = false;
nicholas@2498 2184 // Obtain the metrics enabled
nicholas@2498 2185 for (var i = 0; i < specification.metrics.enabled.length; i++) {
nicholas@2498 2186 var node = specification.metrics.enabled[i];
nicholas@2498 2187 switch (node) {
nicholas@2498 2188 case 'testTimer':
nicholas@2498 2189 this.enableTestTimer = true;
nicholas@2498 2190 break;
nicholas@2498 2191 case 'elementTimer':
nicholas@2498 2192 this.enableElementTimer = true;
nicholas@2498 2193 break;
nicholas@2498 2194 case 'elementTracker':
nicholas@2498 2195 this.enableElementTracker = true;
nicholas@2498 2196 break;
nicholas@2498 2197 case 'elementListenTracker':
nicholas@2498 2198 this.enableElementListenTracker = true;
nicholas@2498 2199 break;
nicholas@2498 2200 case 'elementInitialPosition':
nicholas@2498 2201 this.enableElementInitialPosition = true;
nicholas@2498 2202 break;
nicholas@2498 2203 case 'elementFlagListenedTo':
nicholas@2498 2204 this.enableFlagListenedTo = true;
nicholas@2498 2205 break;
nicholas@2498 2206 case 'elementFlagMoved':
nicholas@2498 2207 this.enableFlagMoved = true;
nicholas@2498 2208 break;
nicholas@2498 2209 case 'elementFlagComments':
nicholas@2498 2210 this.enableFlagComments = true;
nicholas@2498 2211 break;
nicholas@2498 2212 }
nicholas@2498 2213 }
nicholas@2498 2214 this.initialiseTest = function () {};
nicholas@2224 2215 }
nicholas@2224 2216
nicholas@2498 2217 function metricTracker(caller) {
nicholas@2498 2218 /* Custom object to track and collect metric data
nicholas@2498 2219 * Used only inside the audioObjects object.
nicholas@2498 2220 */
nicholas@2498 2221
nicholas@2498 2222 this.listenedTimer = 0;
nicholas@2498 2223 this.listenStart = 0;
nicholas@2498 2224 this.listenHold = false;
nicholas@2498 2225 this.initialPosition = -1;
nicholas@2498 2226 this.movementTracker = [];
nicholas@2498 2227 this.listenTracker = [];
nicholas@2498 2228 this.wasListenedTo = false;
nicholas@2498 2229 this.wasMoved = false;
nicholas@2498 2230 this.hasComments = false;
nicholas@2498 2231 this.parent = caller;
nicholas@2498 2232
nicholas@2498 2233 this.initialise = function (position) {
nicholas@2498 2234 if (this.initialPosition == -1) {
nicholas@2498 2235 this.initialPosition = position;
nicholas@2498 2236 this.moved(0, position);
nicholas@2498 2237 }
nicholas@2498 2238 };
nicholas@2498 2239
nicholas@2498 2240 this.moved = function (time, position) {
nicholas@3064 2241 var last;
nicholas@2498 2242 if (time > 0) {
nicholas@2498 2243 this.wasMoved = true;
nicholas@2498 2244 }
nicholas@3064 2245 // Get the last entry
nicholas@3064 2246 if (this.movementTracker.length > 0) {
nicholas@3064 2247 last = this.movementTracker[this.movementTracker.length - 1];
nicholas@3064 2248 } else {
nicholas@3064 2249 last = -1;
nicholas@3064 2250 }
nicholas@3064 2251 if (position != last[1]) {
nicholas@3064 2252 this.movementTracker[this.movementTracker.length] = [time, position];
nicholas@3064 2253 }
nicholas@2498 2254 };
nicholas@2498 2255
nicholas@2498 2256 this.startListening = function (time) {
nicholas@2708 2257 if (this.listenHold === false) {
nicholas@2498 2258 this.wasListenedTo = true;
nicholas@2498 2259 this.listenStart = time;
nicholas@2498 2260 this.listenHold = true;
nicholas@2498 2261
nicholas@2498 2262 var evnt = document.createElement('event');
nicholas@2498 2263 var testTime = document.createElement('testTime');
nicholas@2498 2264 testTime.setAttribute('start', time);
nicholas@2498 2265 var bufferTime = document.createElement('bufferTime');
nicholas@2498 2266 bufferTime.setAttribute('start', this.parent.getCurrentPosition());
nicholas@2498 2267 evnt.appendChild(testTime);
nicholas@2498 2268 evnt.appendChild(bufferTime);
nicholas@2498 2269 this.listenTracker.push(evnt);
nicholas@2498 2270
nicholas@2498 2271 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2272 }
nicholas@2498 2273 };
nicholas@2498 2274
nicholas@2498 2275 this.stopListening = function (time, bufferStopTime) {
nicholas@2708 2276 if (this.listenHold === true) {
nicholas@2498 2277 var diff = time - this.listenStart;
nicholas@2498 2278 this.listenedTimer += (diff);
nicholas@2498 2279 this.listenStart = 0;
nicholas@2498 2280 this.listenHold = false;
nicholas@2498 2281
nicholas@2498 2282 var evnt = this.listenTracker[this.listenTracker.length - 1];
nicholas@2498 2283 var testTime = evnt.getElementsByTagName('testTime')[0];
nicholas@2498 2284 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
nicholas@2498 2285 testTime.setAttribute('stop', time);
nicholas@2708 2286 if (bufferStopTime === undefined) {
nicholas@2498 2287 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
nicholas@2498 2288 } else {
nicholas@2498 2289 bufferTime.setAttribute('stop', bufferStopTime);
nicholas@2498 2290 }
nicholas@2498 2291 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2292 }
nicholas@2498 2293 };
nicholas@2498 2294
nicholas@2708 2295 function exportElementTimer(parentElement) {
nicholas@2708 2296 var mElementTimer = storage.document.createElement('metricresult');
nicholas@2708 2297 mElementTimer.setAttribute('name', 'enableElementTimer');
nicholas@2708 2298 mElementTimer.textContent = this.listenedTimer;
nicholas@2708 2299 parentElement.appendChild(mElementTimer);
nicholas@2708 2300 return mElementTimer;
nicholas@2708 2301 }
nicholas@2708 2302
nicholas@2708 2303 function exportElementTrack(parentElement) {
nicholas@2708 2304 var elementTrackerFull = storage.document.createElement('metricresult');
nicholas@2708 2305 elementTrackerFull.setAttribute('name', 'elementTrackerFull');
nicholas@2708 2306 for (var k = 0; k < this.movementTracker.length; k++) {
nicholas@2708 2307 var timePos = storage.document.createElement('movement');
nicholas@2708 2308 timePos.setAttribute("time", this.movementTracker[k][0]);
nicholas@2708 2309 timePos.setAttribute("value", this.movementTracker[k][1]);
nicholas@2708 2310 elementTrackerFull.appendChild(timePos);
nicholas@2708 2311 }
nicholas@2708 2312 parentElement.appendChild(elementTrackerFull);
nicholas@2708 2313 return elementTrackerFull;
nicholas@2708 2314 }
nicholas@2708 2315
nicholas@2708 2316 function exportElementListenTracker(parentElement) {
nicholas@2708 2317 var elementListenTracker = storage.document.createElement('metricresult');
nicholas@2708 2318 elementListenTracker.setAttribute('name', 'elementListenTracker');
nicholas@2708 2319 for (var k = 0; k < this.listenTracker.length; k++) {
nicholas@2708 2320 elementListenTracker.appendChild(this.listenTracker[k]);
nicholas@2708 2321 }
nicholas@2708 2322 parentElement.appendChild(elementListenTracker);
nicholas@2708 2323 return elementListenTracker;
nicholas@2708 2324 }
nicholas@2708 2325
nicholas@2708 2326 function exportElementInitialPosition(parentElement) {
nicholas@2708 2327 var elementInitial = storage.document.createElement('metricresult');
nicholas@2708 2328 elementInitial.setAttribute('name', 'elementInitialPosition');
nicholas@2708 2329 elementInitial.textContent = this.initialPosition;
nicholas@2708 2330 parentElement.appendChild(elementInitial);
nicholas@2708 2331 return elementInitial;
nicholas@2708 2332 }
nicholas@2708 2333
nicholas@2708 2334 function exportFlagListenedTo(parentElement) {
nicholas@2708 2335 var flagListenedTo = storage.document.createElement('metricresult');
nicholas@2708 2336 flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
nicholas@2708 2337 flagListenedTo.textContent = this.wasListenedTo;
nicholas@2708 2338 parentElement.appendChild(flagListenedTo);
nicholas@2708 2339 return flagListenedTo;
nicholas@2708 2340 }
nicholas@2708 2341
nicholas@2708 2342 function exportFlagMoved(parentElement) {
nicholas@2708 2343 var flagMoved = storage.document.createElement('metricresult');
nicholas@2708 2344 flagMoved.setAttribute('name', 'elementFlagMoved');
nicholas@2708 2345 flagMoved.textContent = this.wasMoved;
nicholas@2708 2346 parentElement.appendChild(flagMoved);
nicholas@2708 2347 return flagMoved;
nicholas@2708 2348 }
nicholas@2708 2349
nicholas@2708 2350 function exportFlagComments(parentElement) {
nicholas@2708 2351 var flagComments = storage.document.createElement('metricresult');
nicholas@2708 2352 flagComments.setAttribute('name', 'elementFlagComments');
nicholas@2708 2353 if (this.parent.commentDOM === null) {
nicholas@2708 2354 flagComments.textContent = 'false';
nicholas@2708 2355 } else if (this.parent.commentDOM.textContent.length === 0) {
nicholas@2708 2356 flagComments.textContent = 'false';
nicholas@2708 2357 } else {
nicholas@2708 2358 flagComments.textContet = 'true';
nicholas@2708 2359 }
nicholas@2708 2360 parentElement.appendChild(flagComments);
nicholas@2708 2361 return flagComments;
nicholas@2708 2362 }
nicholas@2708 2363
nicholas@2708 2364 this.exportXMLDOM = function (parentElement) {
nicholas@2708 2365 var elems = [];
nicholas@2498 2366 if (audioEngineContext.metric.enableElementTimer) {
nicholas@2708 2367 elems.push(exportElementTimer.call(this, parentElement));
nicholas@2498 2368 }
nicholas@2498 2369 if (audioEngineContext.metric.enableElementTracker) {
nicholas@2708 2370 elems.push(exportElementTrack.call(this, parentElement));
nicholas@2498 2371 }
nicholas@2498 2372 if (audioEngineContext.metric.enableElementListenTracker) {
nicholas@2708 2373 elems.push(exportElementListenTracker.call(this, parentElement));
nicholas@2498 2374 }
nicholas@2498 2375 if (audioEngineContext.metric.enableElementInitialPosition) {
nicholas@2708 2376 elems.push(exportElementInitialPosition.call(this, parentElement));
nicholas@2498 2377 }
nicholas@2498 2378 if (audioEngineContext.metric.enableFlagListenedTo) {
nicholas@2708 2379 elems.push(exportFlagListenedTo.call(this, parentElement));
nicholas@2498 2380 }
nicholas@2498 2381 if (audioEngineContext.metric.enableFlagMoved) {
nicholas@2708 2382 elems.push(exportFlagMoved.call(this, parentElement));
nicholas@2498 2383 }
nicholas@2498 2384 if (audioEngineContext.metric.enableFlagComments) {
nicholas@2708 2385 elems.push(exportFlagComments.call(this, parentElement));
nicholas@2498 2386 }
nicholas@2708 2387 return elems;
nicholas@2498 2388 };
nicholas@2224 2389 }
nicholas@2498 2390
nicholas@2224 2391 function Interface(specificationObject) {
nicholas@2498 2392 // This handles the bindings between the interface and the audioEngineContext;
nicholas@2498 2393 this.specification = specificationObject;
nicholas@2498 2394 this.insertPoint = document.getElementById("topLevelBody");
nicholas@2498 2395
nicholas@2498 2396 this.newPage = function (audioHolderObject, store) {
nicholas@2498 2397 audioEngineContext.newTestPage(audioHolderObject, store);
nicholas@2498 2398 interfaceContext.commentBoxes.deleteCommentBoxes();
nicholas@2498 2399 interfaceContext.deleteCommentQuestions();
nicholas@2498 2400 loadTest(audioHolderObject, store);
nicholas@2498 2401 };
nicholas@2498 2402
nicholas@2955 2403 this.keyboardInterface = (function () {
nicholas@2955 2404 var keyboardInterfaceController = {
nicholas@2955 2405 keys: [],
nicholas@2955 2406 registerKeyBinding: function (key, audioObject) {
nicholas@2955 2407 if (typeof key != "string" || key.length != 1) {
nicholas@2955 2408 throw ("Key must be a singular character");
nicholas@2955 2409 }
nicholas@2955 2410 var included = this.keys.findIndex(function (k) {
n@2979 2411 return k.key == key;
nicholas@2955 2412 }) >= 0;
nicholas@2955 2413 if (included) {
nicholas@2955 2414 throw ("Key " + key + " already bounded!");
nicholas@2955 2415 }
nicholas@2955 2416 this.keys.push({
nicholas@2955 2417 key: key,
nicholas@2955 2418 audioObject: audioObject
nicholas@2955 2419 });
nicholas@2955 2420 return true;
nicholas@2955 2421 },
nicholas@2955 2422 deregisterKeyBinding: function (key) {
nicholas@2955 2423 var index = this.keys.findIndex(function (k) {
nicholas@2955 2424 return k.key == key;
nicholas@2955 2425 });
nicholas@2955 2426 if (index == -1) {
nicholas@2955 2427 throw ("Key " + key + " not bounded!");
nicholas@2955 2428 }
nicholas@2955 2429 this.keys.splice(index, 1);
nicholas@2955 2430 return true;
nicholas@2955 2431 },
nicholas@2955 2432 resetKeyBindings: function () {
nicholas@2955 2433 this.keys = [];
nicholas@2955 2434 },
nicholas@2955 2435 handleEvent: function (e) {
nicholas@2955 2436 function isPlaying() {
nicholas@2955 2437 return audioEngineContext.audioObjects.some(function (a) {
nicholas@2955 2438 return a.playing;
nicholas@2955 2439 });
nicholas@2955 2440 }
nicholas@2955 2441
nicholas@2955 2442 function keypress(key) {
nicholas@2955 2443 var index = this.keys.findIndex(function (k) {
n@2979 2444 return k.key == key;
nicholas@2955 2445 });
nicholas@2955 2446 if (index >= 0) {
nicholas@2955 2447 audioEngineContext.play(this.keys[index].audioObject.id);
nicholas@2955 2448 }
nicholas@2955 2449 }
n@2965 2450
n@2965 2451 function trackCommentFocus() {
n@2965 2452 return document.activeElement.className.indexOf("trackComment") >= 0;
n@2965 2453 }
nicholas@3119 2454 if (testState.currentStatePosition != "test") {
nicholas@3119 2455 return;
nicholas@3119 2456 }
nicholas@2982 2457 if (trackCommentFocus()) {
nicholas@2982 2458 return;
nicholas@2982 2459 }
nicholas@2955 2460 if (e.key === " ") {
nicholas@2982 2461 if (isPlaying()) {
n@2965 2462 e.preventDefault();
nicholas@2955 2463 audioEngineContext.stop();
nicholas@2955 2464 }
nicholas@2955 2465 } else {
nicholas@2955 2466 keypress.call(this, e.key);
nicholas@2955 2467 }
nicholas@2955 2468 }
nicholas@2955 2469 };
nicholas@2955 2470 document.addEventListener("keydown", keyboardInterfaceController, false);
nicholas@2955 2471 return keyboardInterfaceController;
nicholas@2955 2472 })();
nicholas@2955 2473
nicholas@2498 2474 // Bounded by interface!!
nicholas@2498 2475 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
nicholas@2498 2476 // For example, APE returns the slider position normalised in a <value> tag.
nicholas@2498 2477 this.interfaceObjects = [];
nicholas@2498 2478 this.interfaceObject = function () {};
nicholas@2498 2479
nicholas@2498 2480 this.resizeWindow = function (event) {
nicholas@2498 2481 popup.resize(event);
nicholas@2352 2482 this.volume.resize();
nicholas@2360 2483 this.lightbox.resize();
n@2718 2484 this.commentBoxes.boxes.forEach(function (elem) {
nicholas@2708 2485 elem.resize();
nicholas@2708 2486 });
nicholas@2708 2487 this.commentQuestions.forEach(function (elem) {
nicholas@2708 2488 elem.resize();
nicholas@2708 2489 });
nicholas@2498 2490 try {
nicholas@2498 2491 resizeWindow(event);
nicholas@2498 2492 } catch (err) {
nicholas@2498 2493 console.log("Warning - Interface does not have Resize option");
nicholas@2498 2494 console.log(err);
nicholas@2498 2495 }
nicholas@2498 2496 };
nicholas@2498 2497
nicholas@2498 2498 this.returnNavigator = function () {
nicholas@2498 2499 var node = storage.document.createElement("navigator");
nicholas@2498 2500 var platform = storage.document.createElement("platform");
nicholas@2498 2501 platform.textContent = navigator.platform;
nicholas@2498 2502 var vendor = storage.document.createElement("vendor");
nicholas@2498 2503 vendor.textContent = navigator.vendor;
nicholas@2498 2504 var userAgent = storage.document.createElement("uagent");
nicholas@2498 2505 userAgent.textContent = navigator.userAgent;
nicholas@2224 2506 var screen = storage.document.createElement("window");
nicholas@2498 2507 screen.setAttribute('innerWidth', window.innerWidth);
nicholas@2498 2508 screen.setAttribute('innerHeight', window.innerHeight);
nicholas@2498 2509 node.appendChild(platform);
nicholas@2498 2510 node.appendChild(vendor);
nicholas@2498 2511 node.appendChild(userAgent);
nicholas@2224 2512 node.appendChild(screen);
nicholas@2498 2513 return node;
nicholas@2498 2514 };
nicholas@2498 2515
nicholas@2498 2516 this.returnDateNode = function () {
nicholas@2224 2517 // Create an XML Node for the Date and Time a test was conducted
nicholas@2224 2518 // Structure is
nicholas@3113 2519 // <datetime>
nicholas@2224 2520 // <date year="##" month="##" day="##">DD/MM/YY</date>
nicholas@2224 2521 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
nicholas@2224 2522 // </datetime>
nicholas@2224 2523 var dateTime = new Date();
nicholas@2224 2524 var hold = storage.document.createElement("datetime");
nicholas@2224 2525 var date = storage.document.createElement("date");
nicholas@2224 2526 var time = storage.document.createElement("time");
nicholas@2498 2527 date.setAttribute('year', dateTime.getFullYear());
nicholas@2498 2528 date.setAttribute('month', dateTime.getMonth() + 1);
nicholas@2498 2529 date.setAttribute('day', dateTime.getDate());
nicholas@2498 2530 time.setAttribute('hour', dateTime.getHours());
nicholas@2498 2531 time.setAttribute('minute', dateTime.getMinutes());
nicholas@2498 2532 time.setAttribute('secs', dateTime.getSeconds());
nicholas@2498 2533
nicholas@2224 2534 hold.appendChild(date);
nicholas@2224 2535 hold.appendChild(time);
nicholas@2224 2536 return hold;
nicholas@2224 2537
nicholas@2708 2538 };
nicholas@2498 2539
nicholas@2360 2540 this.lightbox = {
nicholas@2360 2541 parent: this,
nicholas@2360 2542 root: document.createElement("div"),
nicholas@2360 2543 content: document.createElement("div"),
nicholas@2360 2544 accept: document.createElement("button"),
nicholas@2360 2545 blanker: document.createElement("div"),
nicholas@2498 2546 post: function (type, message) {
nicholas@2498 2547 switch (type) {
nicholas@2360 2548 case "Error":
nicholas@2360 2549 this.content.className = "lightbox-error";
nicholas@2360 2550 break;
nicholas@2360 2551 case "Warning":
nicholas@2360 2552 this.content.className = "lightbox-warning";
nicholas@2360 2553 break;
nicholas@2360 2554 default:
nicholas@2360 2555 this.content.className = "lightbox-message";
nicholas@2360 2556 break;
nicholas@2360 2557 }
nicholas@2360 2558 var msg = document.createElement("p");
nicholas@2360 2559 msg.textContent = message;
nicholas@2360 2560 this.content.appendChild(msg);
nicholas@2360 2561 this.show();
nicholas@2360 2562 },
nicholas@2498 2563 show: function () {
nicholas@2360 2564 this.root.style.visibility = "visible";
nicholas@2360 2565 this.blanker.style.visibility = "visible";
n@2914 2566 this.accept.focus();
nicholas@2360 2567 },
nicholas@2498 2568 clear: function () {
nicholas@2360 2569 this.root.style.visibility = "";
nicholas@2360 2570 this.blanker.style.visibility = "";
nicholas@2360 2571 this.content.textContent = "";
nicholas@2360 2572 },
nicholas@2498 2573 handleEvent: function (event) {
nicholas@2360 2574 if (event.currentTarget == this.accept) {
nicholas@2360 2575 this.clear();
nicholas@2360 2576 }
nicholas@2360 2577 },
nicholas@2498 2578 resize: function (event) {
nicholas@2498 2579 this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
n@2915 2580 },
n@2915 2581 isVisible: function () {
n@2915 2582 return this.root.style.visibility == "visible";
nicholas@2360 2583 }
nicholas@2708 2584 };
nicholas@2498 2585
nicholas@2360 2586 this.lightbox.root.appendChild(this.lightbox.content);
nicholas@2360 2587 this.lightbox.root.appendChild(this.lightbox.accept);
nicholas@2360 2588 this.lightbox.root.className = "popupHolder";
nicholas@2360 2589 this.lightbox.root.id = "lightbox-root";
nicholas@2360 2590 this.lightbox.accept.className = "popupButton";
nicholas@2360 2591 this.lightbox.accept.style.bottom = "10px";
nicholas@2360 2592 this.lightbox.accept.textContent = "OK";
nicholas@2360 2593 this.lightbox.accept.style.left = "237.5px";
nicholas@2498 2594 this.lightbox.accept.addEventListener("click", this.lightbox);
nicholas@2360 2595 this.lightbox.blanker.className = "testHalt";
nicholas@2360 2596 this.lightbox.blanker.id = "lightbox-blanker";
nicholas@2360 2597 document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
nicholas@2360 2598 document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
nicholas@2498 2599
nicholas@2712 2600 this.commentBoxes = (function () {
nicholas@2712 2601 var commentBoxes = {};
nicholas@2712 2602 commentBoxes.boxes = [];
nicholas@2712 2603 commentBoxes.injectPoint = null;
nicholas@2712 2604 commentBoxes.elementCommentBox = function (audioObject) {
nicholas@2224 2605 var element = audioObject.specification;
nicholas@2224 2606 this.audioObject = audioObject;
nicholas@2224 2607 this.id = audioObject.id;
nicholas@2224 2608 var audioHolderObject = audioObject.specification.parent;
nicholas@2224 2609 // Create document objects to hold the comment boxes
nicholas@2224 2610 this.trackComment = document.createElement('div');
nicholas@2224 2611 this.trackComment.className = 'comment-div';
nicholas@2498 2612 this.trackComment.id = 'comment-div-' + audioObject.id;
nicholas@2224 2613 // Create a string next to each comment asking for a comment
nicholas@2224 2614 this.trackString = document.createElement('span');
nicholas@2498 2615 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId();
nicholas@2224 2616 // Create the HTML5 comment box 'textarea'
nicholas@2224 2617 this.trackCommentBox = document.createElement('textarea');
nicholas@2224 2618 this.trackCommentBox.rows = '4';
nicholas@2224 2619 this.trackCommentBox.cols = '100';
nicholas@2498 2620 this.trackCommentBox.name = 'trackComment' + audioObject.id;
nicholas@2224 2621 this.trackCommentBox.className = 'trackComment';
nicholas@2224 2622 var br = document.createElement('br');
nicholas@2224 2623 // Add to the holder.
nicholas@2224 2624 this.trackComment.appendChild(this.trackString);
nicholas@2224 2625 this.trackComment.appendChild(br);
nicholas@2224 2626 this.trackComment.appendChild(this.trackCommentBox);
nicholas@2224 2627
nicholas@2498 2628 this.exportXMLDOM = function () {
nicholas@2224 2629 var root = document.createElement('comment');
nicholas@2224 2630 var question = document.createElement('question');
nicholas@2224 2631 question.textContent = this.trackString.textContent;
nicholas@2224 2632 var response = document.createElement('response');
nicholas@2224 2633 response.textContent = this.trackCommentBox.value;
nicholas@2498 2634 console.log("Comment frag-" + this.id + ": " + response.textContent);
nicholas@2224 2635 root.appendChild(question);
nicholas@2224 2636 root.appendChild(response);
nicholas@2224 2637 return root;
nicholas@2224 2638 };
nicholas@2498 2639 this.resize = function () {
nicholas@2498 2640 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2641 if (boxwidth >= 600) {
nicholas@2224 2642 boxwidth = 600;
nicholas@2498 2643 } else if (boxwidth < 400) {
nicholas@2224 2644 boxwidth = 400;
nicholas@2224 2645 }
nicholas@2498 2646 this.trackComment.style.width = boxwidth + "px";
nicholas@2498 2647 this.trackCommentBox.style.width = boxwidth - 6 + "px";
nicholas@2224 2648 };
nicholas@2224 2649 this.resize();
nicholas@2725 2650 this.highlight = function (state) {
nicholas@2725 2651 if (state === true) {
nicholas@2725 2652 $(this.trackComment).addClass("comment-box-playing");
nicholas@2725 2653 } else {
nicholas@2725 2654 $(this.trackComment).removeClass("comment-box-playing");
nicholas@2725 2655 }
nicholas@2725 2656 };
nicholas@2224 2657 };
nicholas@2712 2658 commentBoxes.createCommentBox = function (audioObject) {
nicholas@2224 2659 var node = new this.elementCommentBox(audioObject);
nicholas@2224 2660 this.boxes.push(node);
nicholas@2224 2661 audioObject.commentDOM = node;
nicholas@2224 2662 return node;
nicholas@2224 2663 };
nicholas@2712 2664 commentBoxes.sortCommentBoxes = function () {
nicholas@2498 2665 this.boxes.sort(function (a, b) {
nicholas@2498 2666 return a.id - b.id;
nicholas@2498 2667 });
nicholas@2224 2668 };
nicholas@2224 2669
nicholas@2712 2670 commentBoxes.showCommentBoxes = function (inject, sort) {
nicholas@2224 2671 this.injectPoint = inject;
nicholas@2498 2672 if (sort) {
nicholas@2498 2673 this.sortCommentBoxes();
nicholas@2498 2674 }
nicholas@2708 2675 this.boxes.forEach(function (box) {
nicholas@2224 2676 inject.appendChild(box.trackComment);
nicholas@2708 2677 });
nicholas@2224 2678 };
nicholas@2224 2679
nicholas@2712 2680 commentBoxes.deleteCommentBoxes = function () {
nicholas@2708 2681 if (this.injectPoint !== null) {
nicholas@2708 2682 this.boxes.forEach(function (box) {
nicholas@2224 2683 this.injectPoint.removeChild(box.trackComment);
nicholas@2708 2684 }, this);
nicholas@2224 2685 this.injectPoint = null;
nicholas@2224 2686 }
nicholas@2224 2687 this.boxes = [];
nicholas@2224 2688 };
nicholas@2725 2689 commentBoxes.highlightById = function (id) {
nicholas@2725 2690 if (id === undefined || typeof id !== "number" || id >= this.boxes.length) {
nicholas@2725 2691 console.log("Error - Invalid id");
nicholas@2725 2692 id = -1;
nicholas@2725 2693 }
nicholas@2725 2694 this.boxes.forEach(function (a) {
nicholas@2725 2695 if (a.id === id) {
nicholas@2725 2696 a.highlight(true);
nicholas@2725 2697 } else {
nicholas@2725 2698 a.highlight(false);
nicholas@2725 2699 }
nicholas@2725 2700 });
nicholas@2725 2701 };
nicholas@2712 2702 return commentBoxes;
nicholas@2712 2703 })();
nicholas@2498 2704
nicholas@2498 2705 this.commentQuestions = [];
nicholas@2498 2706
nicholas@2498 2707 this.commentBox = function (commentQuestion) {
nicholas@2498 2708 this.specification = commentQuestion;
nicholas@2498 2709 // Create document objects to hold the comment boxes
nicholas@2498 2710 this.holder = document.createElement('div');
nicholas@2498 2711 this.holder.className = 'comment-div';
nicholas@2498 2712 // Create a string next to each comment asking for a comment
nicholas@2498 2713 this.string = document.createElement('span');
nicholas@2498 2714 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2715 // Create the HTML5 comment box 'textarea'
nicholas@2498 2716 this.textArea = document.createElement('textarea');
nicholas@2498 2717 this.textArea.rows = '4';
nicholas@2498 2718 this.textArea.cols = '100';
nicholas@2498 2719 this.textArea.className = 'trackComment';
nicholas@2498 2720 var br = document.createElement('br');
nicholas@2498 2721 // Add to the holder.
nicholas@2498 2722 this.holder.appendChild(this.string);
nicholas@2498 2723 this.holder.appendChild(br);
nicholas@2498 2724 this.holder.appendChild(this.textArea);
nicholas@2498 2725
nicholas@2498 2726 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2727 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2728 root.id = this.specification.id;
nicholas@2498 2729 root.setAttribute('type', this.specification.type);
nicholas@2498 2730 console.log("Question: " + this.string.textContent);
nicholas@2498 2731 console.log("Response: " + root.textContent);
nicholas@2224 2732 var question = storePoint.parent.document.createElement('question');
nicholas@2224 2733 question.textContent = this.string.textContent;
nicholas@2224 2734 var response = storePoint.parent.document.createElement('response');
nicholas@2224 2735 response.textContent = this.textArea.value;
nicholas@2224 2736 root.appendChild(question);
nicholas@2224 2737 root.appendChild(response);
nicholas@2224 2738 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2739 return root;
nicholas@2498 2740 };
nicholas@2498 2741 this.resize = function () {
nicholas@2498 2742 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2743 if (boxwidth >= 600) {
nicholas@2498 2744 boxwidth = 600;
nicholas@2498 2745 } else if (boxwidth < 400) {
nicholas@2498 2746 boxwidth = 400;
nicholas@2498 2747 }
nicholas@2498 2748 this.holder.style.width = boxwidth + "px";
nicholas@2498 2749 this.textArea.style.width = boxwidth - 6 + "px";
nicholas@2498 2750 };
nicholas@2498 2751 this.resize();
nicholas@3063 2752 this.check = function () {
n@3095 2753 if (this.specification.mandatory && this.textArea.value.length === 0) {
nicholas@3063 2754 return false;
nicholas@3063 2755 }
nicholas@3063 2756 return true;
n@3095 2757 };
nicholas@2498 2758 };
nicholas@2498 2759
nicholas@2498 2760 this.radioBox = function (commentQuestion) {
nicholas@2498 2761 this.specification = commentQuestion;
nicholas@2498 2762 // Create document objects to hold the comment boxes
nicholas@2498 2763 this.holder = document.createElement('div');
nicholas@2498 2764 this.holder.className = 'comment-div';
nicholas@2498 2765 // Create a string next to each comment asking for a comment
nicholas@2498 2766 this.string = document.createElement('span');
nicholas@2498 2767 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2768 // Add to the holder.
nicholas@2498 2769 this.holder.appendChild(this.string);
nicholas@2498 2770 this.options = [];
nicholas@2498 2771 this.inputs = document.createElement('div');
nicholas@2711 2772 this.inputs.className = "comment-checkbox-inputs-holder";
nicholas@2498 2773
nicholas@2498 2774 var optCount = commentQuestion.options.length;
nicholas@2711 2775 for (var i = 0; i < optCount; i++) {
nicholas@2498 2776 var div = document.createElement('div');
nicholas@2711 2777 div.className = "comment-checkbox-inputs-flex";
nicholas@2722 2778
nicholas@2711 2779 var span = document.createElement('span');
nicholas@2711 2780 span.textContent = commentQuestion.options[i].text;
nicholas@2711 2781 span.className = 'comment-radio-span';
nicholas@2711 2782 div.appendChild(span);
nicholas@2722 2783
nicholas@2498 2784 var input = document.createElement('input');
nicholas@2498 2785 input.type = 'radio';
nicholas@2498 2786 input.name = commentQuestion.id;
nicholas@2711 2787 input.setAttribute('setvalue', commentQuestion.options[i].name);
nicholas@2498 2788 input.className = 'comment-radio';
nicholas@2498 2789 div.appendChild(input);
nicholas@2722 2790
nicholas@2498 2791 this.inputs.appendChild(div);
nicholas@2498 2792 this.options.push(input);
nicholas@2498 2793 }
nicholas@2498 2794 this.holder.appendChild(this.inputs);
nicholas@2498 2795
nicholas@2498 2796 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2797 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2798 root.id = this.specification.id;
nicholas@2498 2799 root.setAttribute('type', this.specification.type);
nicholas@2498 2800 var question = document.createElement('question');
nicholas@2498 2801 question.textContent = this.string.textContent;
nicholas@2498 2802 var response = document.createElement('response');
nicholas@2498 2803 var i = 0;
nicholas@2708 2804 while (this.options[i].checked === false) {
nicholas@2498 2805 i++;
nicholas@2498 2806 if (i >= this.options.length) {
nicholas@2498 2807 break;
nicholas@2498 2808 }
nicholas@2498 2809 }
nicholas@2498 2810 if (i >= this.options.length) {
nicholas@2498 2811 response.textContent = 'null';
nicholas@2498 2812 } else {
nicholas@2498 2813 response.textContent = this.options[i].getAttribute('setvalue');
nicholas@2498 2814 response.setAttribute('number', i);
nicholas@2498 2815 }
nicholas@2498 2816 console.log('Comment: ' + question.textContent);
nicholas@2498 2817 console.log('Response: ' + response.textContent);
nicholas@2498 2818 root.appendChild(question);
nicholas@2498 2819 root.appendChild(response);
nicholas@2224 2820 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2821 return root;
nicholas@2498 2822 };
nicholas@2498 2823 this.resize = function () {
nicholas@2498 2824 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2825 if (boxwidth >= 600) {
nicholas@2498 2826 boxwidth = 600;
nicholas@2498 2827 } else if (boxwidth < 400) {
nicholas@2498 2828 boxwidth = 400;
nicholas@2498 2829 }
nicholas@2498 2830 this.holder.style.width = boxwidth + "px";
nicholas@2498 2831 };
nicholas@3063 2832 this.check = function () {
nicholas@3063 2833 var anyChecked = this.options.some(function (a) {
nicholas@3063 2834 return a.checked;
nicholas@3063 2835 });
n@3095 2836 if (this.specification.mandatory && anyChecked === false) {
nicholas@3063 2837 return false;
nicholas@3063 2838 }
nicholas@3063 2839 return true;
n@3095 2840 };
nicholas@2498 2841 this.resize();
nicholas@2498 2842 };
nicholas@2498 2843
nicholas@2498 2844 this.checkboxBox = function (commentQuestion) {
nicholas@2498 2845 this.specification = commentQuestion;
nicholas@2498 2846 // Create document objects to hold the comment boxes
nicholas@2498 2847 this.holder = document.createElement('div');
nicholas@2498 2848 this.holder.className = 'comment-div';
nicholas@2498 2849 // Create a string next to each comment asking for a comment
nicholas@2498 2850 this.string = document.createElement('span');
nicholas@2498 2851 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2852 // Add to the holder.
nicholas@2498 2853 this.holder.appendChild(this.string);
nicholas@2498 2854 this.options = [];
nicholas@2498 2855 this.inputs = document.createElement('div');
nicholas@2294 2856 this.inputs.className = "comment-checkbox-inputs-holder";
nicholas@2498 2857
nicholas@2498 2858 var optCount = commentQuestion.options.length;
nicholas@2498 2859 for (var i = 0; i < optCount; i++) {
nicholas@2498 2860 var div = document.createElement('div');
nicholas@2711 2861 div.className = "comment-checkbox-inputs-flex";
nicholas@2722 2862
nicholas@2711 2863 var span = document.createElement('span');
nicholas@2711 2864 span.textContent = commentQuestion.options[i].text;
nicholas@2711 2865 span.className = 'comment-radio-span';
nicholas@2711 2866 div.appendChild(span);
nicholas@2722 2867
nicholas@2498 2868 var input = document.createElement('input');
nicholas@2498 2869 input.type = 'checkbox';
nicholas@2498 2870 input.name = commentQuestion.id;
nicholas@2498 2871 input.setAttribute('setvalue', commentQuestion.options[i].name);
nicholas@2498 2872 input.className = 'comment-radio';
nicholas@2498 2873 div.appendChild(input);
nicholas@2722 2874
nicholas@2498 2875 this.inputs.appendChild(div);
nicholas@2498 2876 this.options.push(input);
nicholas@2498 2877 }
nicholas@2498 2878 this.holder.appendChild(this.inputs);
nicholas@2498 2879
nicholas@2498 2880 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2881 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2882 root.id = this.specification.id;
nicholas@2498 2883 root.setAttribute('type', this.specification.type);
nicholas@2498 2884 var question = document.createElement('question');
nicholas@2498 2885 question.textContent = this.string.textContent;
nicholas@2498 2886 root.appendChild(question);
nicholas@2498 2887 console.log('Comment: ' + question.textContent);
nicholas@2498 2888 for (var i = 0; i < this.options.length; i++) {
nicholas@2498 2889 var response = document.createElement('response');
nicholas@2498 2890 response.textContent = this.options[i].checked;
nicholas@2498 2891 response.setAttribute('name', this.options[i].getAttribute('setvalue'));
nicholas@2498 2892 root.appendChild(response);
nicholas@2498 2893 console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent);
nicholas@2498 2894 }
nicholas@2224 2895 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2896 return root;
nicholas@2498 2897 };
nicholas@2498 2898 this.resize = function () {
nicholas@2498 2899 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2900 if (boxwidth >= 600) {
nicholas@2498 2901 boxwidth = 600;
nicholas@2498 2902 } else if (boxwidth < 400) {
nicholas@2498 2903 boxwidth = 400;
nicholas@2498 2904 }
nicholas@2498 2905 this.holder.style.width = boxwidth + "px";
nicholas@2498 2906 };
nicholas@3063 2907 this.check = function () {
nicholas@3063 2908 var anyChecked = this.options.some(function (a) {
nicholas@3063 2909 return a.checked;
nicholas@3063 2910 });
n@3095 2911 if (this.specification.mandatory && anyChecked === false) {
nicholas@3063 2912 return false;
nicholas@3063 2913 }
nicholas@3063 2914 return true;
nicholas@3063 2915 };
nicholas@2498 2916 this.resize();
nicholas@2498 2917 };
nicholas@2498 2918
n@2579 2919 this.sliderBox = function (commentQuestion) {
n@2579 2920 this.specification = commentQuestion;
n@2579 2921 this.holder = document.createElement("div");
n@2579 2922 this.holder.className = 'comment-div';
n@2579 2923 this.string = document.createElement("span");
n@2579 2924 this.string.innerHTML = commentQuestion.statement;
n@2579 2925 this.slider = document.createElement("input");
n@2579 2926 this.slider.type = "range";
n@2579 2927 this.slider.min = commentQuestion.min;
n@2579 2928 this.slider.max = commentQuestion.max;
n@2579 2929 this.slider.step = commentQuestion.step;
n@2579 2930 this.slider.value = commentQuestion.value;
n@2579 2931 var br = document.createElement('br');
n@2579 2932
n@2580 2933 var textHolder = document.createElement("div");
n@2580 2934 textHolder.className = "comment-slider-text-holder";
n@2580 2935
n@2580 2936 this.leftText = document.createElement("span");
n@2580 2937 this.leftText.textContent = commentQuestion.leftText;
n@2580 2938 this.rightText = document.createElement("span");
n@2580 2939 this.rightText.textContent = commentQuestion.rightText;
n@2580 2940 textHolder.appendChild(this.leftText);
n@2580 2941 textHolder.appendChild(this.rightText);
n@2580 2942
n@2579 2943 this.holder.appendChild(this.string);
n@2579 2944 this.holder.appendChild(br);
n@2579 2945 this.holder.appendChild(this.slider);
n@2580 2946 this.holder.appendChild(textHolder);
n@2579 2947
n@2579 2948 this.exportXMLDOM = function (storePoint) {
n@2579 2949 var root = storePoint.parent.document.createElement('comment');
n@2579 2950 root.id = this.specification.id;
n@2579 2951 root.setAttribute('type', this.specification.type);
n@2579 2952 console.log("Question: " + this.string.textContent);
n@2579 2953 console.log("Response: " + this.slider.value);
n@2579 2954 var question = storePoint.parent.document.createElement('question');
n@2579 2955 question.textContent = this.string.textContent;
n@2579 2956 var response = storePoint.parent.document.createElement('response');
n@2579 2957 response.textContent = this.slider.value;
n@2579 2958 root.appendChild(question);
n@2579 2959 root.appendChild(response);
n@2579 2960 storePoint.XMLDOM.appendChild(root);
n@2579 2961 return root;
n@2579 2962 };
n@2579 2963 this.resize = function () {
n@2579 2964 var boxwidth = (window.innerWidth - 100) / 2;
n@2579 2965 if (boxwidth >= 600) {
n@2579 2966 boxwidth = 600;
n@2579 2967 } else if (boxwidth < 400) {
n@2579 2968 boxwidth = 400;
n@2579 2969 }
n@2579 2970 this.holder.style.width = boxwidth + "px";
n@2579 2971 this.slider.style.width = boxwidth - 24 + "px";
n@2579 2972 };
nicholas@3063 2973 this.check = function () {
nicholas@3063 2974 return true;
n@3095 2975 };
n@2579 2976 this.resize();
n@2579 2977 };
n@2579 2978
nicholas@2498 2979 this.createCommentQuestion = function (element) {
nicholas@2498 2980 var node;
nicholas@2498 2981 if (element.type == 'question') {
nicholas@2498 2982 node = new this.commentBox(element);
nicholas@2498 2983 } else if (element.type == 'radio') {
nicholas@2498 2984 node = new this.radioBox(element);
nicholas@2498 2985 } else if (element.type == 'checkbox') {
nicholas@2498 2986 node = new this.checkboxBox(element);
n@2579 2987 } else if (element.type == 'slider') {
n@2579 2988 node = new this.sliderBox(element);
nicholas@2498 2989 }
nicholas@2498 2990 this.commentQuestions.push(node);
nicholas@2498 2991 return node;
nicholas@2498 2992 };
nicholas@2498 2993
nicholas@2498 2994 this.deleteCommentQuestions = function () {
nicholas@2498 2995 this.commentQuestions = [];
nicholas@2498 2996 };
nicholas@2498 2997
nicholas@3063 2998 this.checkCommentQuestions = function () {
nicholas@3063 2999 var errored = this.commentQuestions.reduce(function (a, cq) {
n@3095 3000 if (cq.check() === false) {
nicholas@3063 3001 a.push(cq);
nicholas@3063 3002 }
nicholas@3063 3003 return a;
nicholas@3063 3004 }, []);
n@3095 3005 if (errored.length === 0) {
nicholas@3063 3006 return true;
nicholas@3063 3007 }
nicholas@3063 3008 interfaceContext.lightbox.post("Message", "Not all the mandatory comment boxes below have been filled.");
n@3095 3009 };
nicholas@3063 3010
nicholas@2498 3011 this.outsideReferenceDOM = function (audioObject, index, inject) {
nicholas@2224 3012 this.parent = audioObject;
nicholas@2224 3013 this.outsideReferenceHolder = document.createElement('button');
nicholas@2224 3014 this.outsideReferenceHolder.className = 'outside-reference';
nicholas@2498 3015 this.outsideReferenceHolder.setAttribute('track-id', index);
nicholas@2409 3016 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 3017 this.outsideReferenceHolder.disabled = true;
nicholas@2708 3018 this.handleEvent = function (event) {
nicholas@2708 3019 audioEngineContext.play(this.parent.id);
nicholas@2224 3020 };
nicholas@2708 3021 this.outsideReferenceHolder.addEventListener("click", this);
nicholas@2224 3022 inject.appendChild(this.outsideReferenceHolder);
nicholas@2498 3023 this.enable = function () {
nicholas@2498 3024 if (this.parent.state == 1) {
nicholas@2224 3025 this.outsideReferenceHolder.disabled = false;
nicholas@2224 3026 }
nicholas@2224 3027 };
nicholas@2498 3028 this.updateLoading = function (progress) {
nicholas@2498 3029 if (progress != 100) {
nicholas@2224 3030 progress = String(progress);
nicholas@2224 3031 progress = progress.split('.')[0];
nicholas@2498 3032 this.outsideReferenceHolder.textContent = progress + '%';
nicholas@2224 3033 } else {
nicholas@2409 3034 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 3035 }
nicholas@2224 3036 };
nicholas@2498 3037 this.startPlayback = function () {
nicholas@2224 3038 // Called when playback has begun
nicholas@2224 3039 $('.track-slider').removeClass('track-slider-playing');
nicholas@2224 3040 $('.comment-div').removeClass('comment-box-playing');
nicholas@2224 3041 this.outsideReferenceHolder.style.backgroundColor = "#FDD";
nicholas@2224 3042 };
nicholas@2498 3043 this.stopPlayback = function () {
nicholas@2224 3044 // Called when playback has stopped. This gets called even if playback never started!
nicholas@2224 3045 this.outsideReferenceHolder.style.backgroundColor = "";
nicholas@2224 3046 };
nicholas@2498 3047 this.exportXMLDOM = function (audioObject) {
nicholas@2224 3048 return null;
nicholas@2224 3049 };
nicholas@2498 3050 this.getValue = function () {
nicholas@2224 3051 return 0;
nicholas@2224 3052 };
nicholas@2498 3053 this.getPresentedId = function () {
nicholas@2409 3054 return this.parent.specification.label || "Reference";
nicholas@2224 3055 };
nicholas@2498 3056 this.canMove = function () {
nicholas@2224 3057 return false;
nicholas@2224 3058 };
nicholas@2498 3059 this.error = function () {
nicholas@2498 3060 // audioObject has an error!!
nicholas@2224 3061 this.outsideReferenceHolder.textContent = "Error";
nicholas@2224 3062 this.outsideReferenceHolder.style.backgroundColor = "#F00";
nicholas@2708 3063 };
nicholas@2708 3064 };
nicholas@2498 3065
nicholas@2712 3066 this.playhead = (function () {
nicholas@2722 3067 var playhead = {};
nicholas@2712 3068 playhead.object = document.createElement('div');
nicholas@2712 3069 playhead.object.className = 'playhead';
nicholas@2712 3070 playhead.object.align = 'left';
nicholas@2498 3071 var curTime = document.createElement('div');
nicholas@2498 3072 curTime.style.width = '50px';
nicholas@2712 3073 playhead.curTimeSpan = document.createElement('span');
nicholas@2712 3074 playhead.curTimeSpan.textContent = '00:00';
nicholas@2712 3075 curTime.appendChild(playhead.curTimeSpan);
nicholas@2712 3076 playhead.object.appendChild(curTime);
nicholas@2712 3077 playhead.scrubberTrack = document.createElement('div');
nicholas@2712 3078 playhead.scrubberTrack.className = 'playhead-scrub-track';
nicholas@2498 3079
nicholas@2712 3080 playhead.scrubberHead = document.createElement('div');
nicholas@2712 3081 playhead.scrubberHead.id = 'playhead-scrubber';
nicholas@2712 3082 playhead.scrubberTrack.appendChild(playhead.scrubberHead);
nicholas@2712 3083 playhead.object.appendChild(playhead.scrubberTrack);
nicholas@2498 3084
nicholas@2712 3085 playhead.timePerPixel = 0;
nicholas@2712 3086 playhead.maxTime = 0;
nicholas@2498 3087
nicholas@2712 3088 playhead.playbackObject = undefined;
nicholas@2498 3089
nicholas@2712 3090 playhead.setTimePerPixel = function (audioObject) {
nicholas@2498 3091 //maxTime must be in seconds
nicholas@2498 3092 this.playbackObject = audioObject;
nicholas@2498 3093 this.maxTime = audioObject.buffer.buffer.duration;
nicholas@2498 3094 var width = 490; //500 - 10, 5 each side of the tracker head
nicholas@2498 3095 this.timePerPixel = this.maxTime / 490;
nicholas@2498 3096 if (this.maxTime < 60) {
nicholas@2498 3097 this.curTimeSpan.textContent = '0.00';
nicholas@2498 3098 } else {
nicholas@2498 3099 this.curTimeSpan.textContent = '00:00';
nicholas@2498 3100 }
nicholas@2498 3101 };
nicholas@2498 3102
nicholas@2712 3103 playhead.update = function () {
nicholas@2498 3104 // Update the playhead position, startPlay must be called
nicholas@2498 3105 if (this.timePerPixel > 0) {
nicholas@2498 3106 var time = this.playbackObject.getCurrentPosition();
nicholas@2498 3107 if (time > 0 && time < this.maxTime) {
nicholas@2498 3108 var width = 490;
nicholas@2498 3109 var pix = Math.floor(time / this.timePerPixel);
nicholas@2498 3110 this.scrubberHead.style.left = pix + 'px';
nicholas@2498 3111 if (this.maxTime > 60.0) {
nicholas@2498 3112 var secs = time % 60;
nicholas@2498 3113 var mins = Math.floor((time - secs) / 60);
nicholas@2498 3114 secs = secs.toString();
nicholas@2498 3115 secs = secs.substr(0, 2);
nicholas@2498 3116 mins = mins.toString();
nicholas@2498 3117 this.curTimeSpan.textContent = mins + ':' + secs;
nicholas@2498 3118 } else {
nicholas@2498 3119 time = time.toString();
nicholas@2498 3120 this.curTimeSpan.textContent = time.substr(0, 4);
nicholas@2498 3121 }
nicholas@2498 3122 } else {
nicholas@2498 3123 this.scrubberHead.style.left = '0px';
nicholas@2498 3124 if (this.maxTime < 60) {
nicholas@2498 3125 this.curTimeSpan.textContent = '0.00';
nicholas@2498 3126 } else {
nicholas@2498 3127 this.curTimeSpan.textContent = '00:00';
nicholas@2498 3128 }
nicholas@2498 3129 }
nicholas@2498 3130 }
nicholas@2817 3131 if (this.playbackObject !== undefined && this.interval === undefined) {
nicholas@2817 3132 window.requestAnimationFrame(this.update.bind(this));
nicholas@2817 3133 }
nicholas@2498 3134 };
nicholas@2498 3135
nicholas@2712 3136 playhead.interval = undefined;
nicholas@2498 3137
nicholas@2712 3138 playhead.start = function () {
nicholas@2708 3139 if (this.playbackObject !== undefined && this.interval === undefined) {
nicholas@2817 3140 window.requestAnimationFrame(this.update.bind(this));
nicholas@2498 3141 }
nicholas@2498 3142 };
nicholas@2712 3143 playhead.stop = function () {
nicholas@2817 3144 this.timePerPixel = 0;
nicholas@2498 3145 };
nicholas@2712 3146 return playhead;
nicholas@2712 3147 })();
nicholas@2498 3148
nicholas@2712 3149 this.volume = (function () {
nicholas@2224 3150 // An in-built volume module which can be viewed on page
nicholas@2224 3151 // Includes trackers on page-by-page data
nicholas@2224 3152 // Volume does NOT reset to 0dB on each page load
nicholas@2712 3153 var volume = {};
nicholas@2712 3154 volume.valueLin = 1.0;
nicholas@2712 3155 volume.valueDB = 0.0;
nicholas@2712 3156 volume.root = document.createElement('div');
nicholas@2712 3157 volume.root.id = 'master-volume-root';
nicholas@2712 3158 volume.object = document.createElement('div');
nicholas@2712 3159 volume.object.className = 'master-volume-holder-float';
nicholas@2712 3160 volume.object.appendChild(volume.root);
nicholas@2712 3161 volume.slider = document.createElement('input');
nicholas@2712 3162 volume.slider.id = 'master-volume-control';
nicholas@2712 3163 volume.slider.type = 'range';
nicholas@2712 3164 volume.valueText = document.createElement('span');
nicholas@2712 3165 volume.valueText.id = 'master-volume-feedback';
nicholas@2712 3166 volume.valueText.textContent = '0dB';
nicholas@2498 3167
nicholas@2712 3168 volume.slider.min = -60;
nicholas@2712 3169 volume.slider.max = 12;
nicholas@2712 3170 volume.slider.value = 0;
nicholas@2712 3171 volume.slider.step = 1;
nicholas@2712 3172 volume.handleEvent = function (event) {
nicholas@2951 3173 if (event.type == "mousemove" || event.type == "mouseup") {
nicholas@2669 3174 this.valueDB = Number(this.slider.value);
nicholas@2669 3175 this.valueLin = decibelToLinear(this.valueDB);
nicholas@2669 3176 this.valueText.textContent = this.valueDB + 'dB';
nicholas@2669 3177 audioEngineContext.outputGain.gain.value = this.valueLin;
nicholas@2951 3178 }
nicholas@2951 3179 if (event.type == "mouseup") {
nicholas@2669 3180 this.onmouseup();
nicholas@2669 3181 }
nicholas@2669 3182 this.slider.value = this.valueDB;
nicholas@2669 3183
nicholas@2669 3184 if (event.stopPropagation) {
nicholas@2669 3185 event.stopPropagation();
nicholas@2669 3186 }
nicholas@2711 3187 };
nicholas@2712 3188 volume.onmouseup = function () {
nicholas@2224 3189 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
nicholas@2708 3190 if (storePoint.length === 0) {
nicholas@2224 3191 storePoint = storage.document.createElement('metricresult');
nicholas@2498 3192 storePoint.setAttribute('name', 'volumeTracker');
nicholas@2224 3193 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
nicholas@2498 3194 } else {
nicholas@2224 3195 storePoint = storePoint[0];
nicholas@2224 3196 }
nicholas@2224 3197 var node = storage.document.createElement('movement');
nicholas@2498 3198 node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
nicholas@2669 3199 node.setAttribute('volume', this.valueDB);
nicholas@2498 3200 node.setAttribute('format', 'dBFS');
nicholas@2224 3201 storePoint.appendChild(node);
nicholas@2711 3202 };
nicholas@2712 3203 volume.slider.addEventListener("mousemove", volume);
nicholas@2712 3204 volume.root.addEventListener("mouseup", volume);
nicholas@2498 3205
nicholas@2224 3206 var title = document.createElement('div');
nicholas@2224 3207 title.innerHTML = '<span>Master Volume Control</span>';
nicholas@2224 3208 title.style.fontSize = '0.75em';
nicholas@2224 3209 title.style.width = "100%";
nicholas@2224 3210 title.align = 'center';
nicholas@2712 3211 volume.root.appendChild(title);
nicholas@2498 3212
nicholas@2712 3213 volume.root.appendChild(volume.slider);
nicholas@2712 3214 volume.root.appendChild(volume.valueText);
nicholas@2498 3215
nicholas@2712 3216 volume.resize = function (event) {
nicholas@2352 3217 if (window.innerWidth < 1000) {
nicholas@2708 3218 this.object.className = "master-volume-holder-inline";
nicholas@2352 3219 } else {
nicholas@2352 3220 this.object.className = 'master-volume-holder-float';
nicholas@2352 3221 }
nicholas@2708 3222 };
nicholas@2712 3223 return volume;
nicholas@2712 3224 })();
nicholas@2498 3225
nicholas@2778 3226 this.imageHolder = (function () {
nicholas@2778 3227 var imageController = {};
nicholas@2778 3228 imageController.root = document.createElement("div");
nicholas@2778 3229 imageController.root.id = "imageController";
nicholas@2778 3230 imageController.img = document.createElement("img");
nicholas@2778 3231 imageController.root.appendChild(imageController.img);
nicholas@2778 3232 imageController.setImage = function (src) {
nicholas@2778 3233 imageController.img.src = "";
n@2785 3234 if (typeof src !== "string" || src.length === undefined) {
nicholas@2778 3235 return;
nicholas@2778 3236 }
nicholas@2778 3237 imageController.img.src = src;
n@2785 3238 };
nicholas@2778 3239 return imageController;
nicholas@2778 3240 })();
nicholas@2778 3241
nicholas@3101 3242 this.calibrationTests = (function () {
nicholas@3101 3243 function readonly(t) {
nicholas@3101 3244 throw ("Cannot set read-only variable");
nicholas@3101 3245 }
nicholas@3101 3246
nicholas@3101 3247 function getStorageRoot() {
nicholas@3101 3248 var storageRoot = storage.root.querySelector("calibration");
nicholas@3101 3249 if (storageRoot === undefined) {
nicholas@3101 3250 storageRoot = storage.document.createElement("calibration");
nicholas@3101 3251 storage.root.appendChild(storageRoot);
nicholas@3101 3252 }
nicholas@3101 3253 return storageRoot;
nicholas@3101 3254 }
n@3102 3255 var calibrationObject,
nicholas@3101 3256 _checkedFrequency = false,
nicholas@3101 3257 _checkedChannels = false;
nicholas@3101 3258
nicholas@3101 3259 // Define the checkFrequencies test!
nicholas@3101 3260 var checkFrequencyUnit = function (htmlRoot, storageRoot) {
nicholas@3101 3261
nicholas@3101 3262 function createFrequencyElement(frequency) {
nicholas@3101 3263 return (function (frequency) {
n@3102 3264 var hold = document.createElement("div");
n@3102 3265 hold.className = "calibration-slider";
nicholas@3101 3266 var range = document.createElement("input");
nicholas@3101 3267 range.type = "range";
nicholas@3101 3268 range.min = "-24";
nicholas@3101 3269 range.max = "24";
nicholas@3101 3270 range.step = "0.5";
nicholas@3101 3271 range.setAttribute("orient", "vertical");
nicholas@3101 3272 range.value = (Math.random() - 0.5) * 24;
nicholas@3101 3273 range.setAttribute("frequency", frequency);
n@3102 3274 hold.appendChild(range);
n@3102 3275 htmlRoot.appendChild(hold);
nicholas@3101 3276
nicholas@3101 3277 var gain = audioContext.createGain();
nicholas@3101 3278 gain.connect(outputGain);
nicholas@3101 3279 gain.gain.value = Math.pow(10, Number(range.value) / 20.0);
n@3102 3280 var osc;
nicholas@3101 3281
nicholas@3101 3282 var store = storage.document.createElement("response");
nicholas@3101 3283 store.setAttribute("frequency", frequency);
nicholas@3101 3284 storageHook.appendChild(store);
nicholas@3101 3285 var interface = {};
nicholas@3101 3286 Object.defineProperties(interface, {
nicholas@3101 3287 "handleEvent": {
nicholas@3101 3288 "value": function (e) {
nicholas@3101 3289 if (e.type == "mouseenter") {
nicholas@3101 3290 osc = audioContext.createOscillator();
nicholas@3101 3291 osc.frequency.value = frequency;
nicholas@3101 3292 osc.connect(gain);
nicholas@3101 3293 osc.start();
nicholas@3101 3294 console.log("start " + frequency);
nicholas@3101 3295 } else if (e.type == "mouseleave") {
nicholas@3101 3296 console.log("stop " + frequency);
nicholas@3101 3297 osc.stop();
nicholas@3101 3298 osc = undefined;
nicholas@2224 3299 }
nicholas@3101 3300 store.textContent = e.currentTarget.value;
nicholas@3101 3301 gain.gain.value = Math.pow(10, Number(e.currentTarget.value) / 20.0);
nicholas@3101 3302 }
nicholas@2224 3303 }
nicholas@3101 3304 });
nicholas@3101 3305 range.addEventListener("mousemove", interface);
nicholas@3101 3306 range.addEventListener("mouseenter", interface);
nicholas@3101 3307 range.addEventListener("mouseleave", interface);
nicholas@3101 3308 return interface;
nicholas@3101 3309 })(frequency);
nicholas@3101 3310 }
nicholas@3101 3311 var htmlHook = document.createElement("div");
nicholas@3101 3312 htmlRoot.appendChild(htmlHook);
nicholas@3101 3313 var storageHook = storage.document.createElement("frequency");
nicholas@3101 3314 storageRoot.appendChild(storageHook);
nicholas@3101 3315 var frequencies = [100, 200, 400, 800, 1200, 1600, 2000, 4000, 8000, 12000];
nicholas@3101 3316 var outputGain = audioContext.createGain();
nicholas@3101 3317 outputGain.gain.value = 0.25;
nicholas@3101 3318 outputGain.connect(audioContext.destination);
nicholas@3101 3319 this.sliders = frequencies.map(createFrequencyElement);
n@3102 3320 };
nicholas@3101 3321
nicholas@3101 3322 var checkChannelsUnit = function (htmlRoot, storageRoot) {
nicholas@3101 3323
nicholas@3101 3324 function onclick(ev) {
nicholas@3101 3325 var storageHook = storage.document.querySelector("calibration").querySelector("channels");
nicholas@3101 3326 storageHook.setAttribute("selected", ev.currentTarget.value);
nicholas@3101 3327 storageHook.setAttribute("selectedText", ev.currentTarget.textContent);
nicholas@3101 3328 osc.stop();
nicholas@3101 3329 gainL = undefined;
nicholas@3101 3330 gainR = undefined;
nicholas@3101 3331 cmerge = undefined;
nicholas@3101 3332 popup.proceedClicked();
nicholas@3101 3333 }
nicholas@3101 3334 var osc = audioContext.createOscillator();
nicholas@3101 3335 var gainL = audioContext.createGain();
nicholas@3101 3336 var gainR = audioContext.createGain();
nicholas@3101 3337 gainL.channelCount = 1;
nicholas@3101 3338 gainR.channelCount = 1;
nicholas@3101 3339 var cmerge = audioContext.createChannelMerger(2);
nicholas@3101 3340 osc.connect(gainL, 0, 0);
nicholas@3101 3341 osc.connect(gainR, 0, 0);
nicholas@3101 3342 gainL.connect(cmerge, 0, 0);
nicholas@3101 3343 gainR.connect(cmerge, 0, 1);
nicholas@3101 3344 cmerge.connect(audioContext.destination);
nicholas@3101 3345 var play = document.createElement("button");
nicholas@3101 3346 play.textContent = "Play Audio";
nicholas@3101 3347 play.onclick = function () {
nicholas@3101 3348 osc.start();
nicholas@3101 3349 play.disabled = true;
n@3102 3350 };
n@3102 3351 play.className = "calibration-button";
nicholas@3101 3352 htmlRoot.appendChild(play);
nicholas@3101 3353 var choiceHolder = document.createElement("div");
nicholas@3101 3354 var leftButton = document.createElement("button");
nicholas@3101 3355 leftButton.textContent = "Left";
nicholas@3101 3356 leftButton.value = "-1";
n@3102 3357 leftButton.className = "calibration-button";
nicholas@3101 3358 var centerButton = document.createElement("button");
nicholas@3101 3359 centerButton.textContent = "Middle";
nicholas@3101 3360 centerButton.value = "0";
n@3102 3361 centerButton.className = "calibration-button";
nicholas@3101 3362 var rightButton = document.createElement("button");
nicholas@3101 3363 rightButton.textContent = "Right";
nicholas@3101 3364 rightButton.value = "1";
n@3102 3365 rightButton.className = "calibration-button";
nicholas@3101 3366 choiceHolder.appendChild(leftButton);
nicholas@3101 3367 choiceHolder.appendChild(centerButton);
nicholas@3101 3368 choiceHolder.appendChild(rightButton);
nicholas@3101 3369 htmlRoot.appendChild(choiceHolder);
nicholas@3101 3370 leftButton.addEventListener("click", onclick);
nicholas@3101 3371 centerButton.addEventListener("click", onclick);
nicholas@3101 3372 rightButton.addEventListener("click", onclick);
nicholas@3101 3373
nicholas@3101 3374 var storageHook = storage.document.createElement("channels");
nicholas@3101 3375 storageRoot.appendChild(storageHook);
nicholas@3101 3376
nicholas@3101 3377 var pan;
nicholas@3101 3378 if (Math.random() > 0.5) {
nicholas@3101 3379 pan = 1;
nicholas@3101 3380 gainL.gain.value = 0.0;
nicholas@3101 3381 gainR.gain.value = 0.25;
nicholas@3101 3382 storageHook.setAttribute("presented", pan);
nicholas@3101 3383 storageHook.setAttribute("presentedText", "Right");
nicholas@3101 3384 } else {
nicholas@3101 3385 pan = -1;
nicholas@3101 3386 gainL.gain.value = 0.25;
nicholas@3101 3387 gainR.gain.value = 0.0;
nicholas@3101 3388 storageHook.setAttribute("presented", pan);
nicholas@3101 3389 storageHook.setAttribute("presentedText", "Left");
nicholas@3101 3390 }
n@3102 3391 };
nicholas@3101 3392
nicholas@3101 3393 var interface = {};
nicholas@3101 3394 Object.defineProperties(interface, {
nicholas@3101 3395 "calibrationObject": {
nicholas@3101 3396 "get": function () {
n@3102 3397 return calibrationObject;
nicholas@3101 3398 },
nicholas@3101 3399 "set": readonly
nicholas@3101 3400 },
nicholas@3101 3401 "checkFrequencies": {
nicholas@3101 3402 "get": function () {
nicholas@3101 3403 if (specification.calibration.checkFrequencies && _checkedFrequency === false) {
nicholas@3101 3404 return true;
nicholas@2224 3405 }
nicholas@3101 3406 return false;
nicholas@3101 3407 },
nicholas@3101 3408 "set": readonly
nicholas@3101 3409 },
nicholas@3101 3410 "checkChannels": {
nicholas@3101 3411 "get": function () {
nicholas@3101 3412 if (specification.calibration.checkChannels && _checkedChannels === false) {
nicholas@3101 3413 return true;
nicholas@3101 3414 }
nicholas@3101 3415 return false;
nicholas@3101 3416 },
nicholas@3101 3417 "set": readonly
nicholas@3101 3418 },
nicholas@3101 3419 "performFrequencyCheck": {
nicholas@3101 3420 "value": function (htmlRoot) {
nicholas@3101 3421 htmlRoot.innerHTML = "";
nicholas@3101 3422 calibrationObject = new checkFrequencyUnit(htmlRoot, getStorageRoot());
nicholas@3101 3423 _checkedFrequency = true;
nicholas@2224 3424 }
nicholas@3101 3425 },
nicholas@3101 3426 "performChannelCheck": {
nicholas@3101 3427 "value": function (htmlRoot) {
nicholas@3101 3428 htmlRoot.innerHTML = "";
nicholas@3101 3429 calibrationObject = new checkChannelsUnit(htmlRoot, getStorageRoot());
nicholas@3101 3430 _checkedChannels = true;
nicholas@3101 3431 }
nicholas@2224 3432 }
n@3102 3433 });
nicholas@3101 3434 return interface;
nicholas@3101 3435 })();
nicholas@2498 3436
nicholas@2498 3437
nicholas@2498 3438 // Global Checkers
nicholas@2498 3439 // These functions will help enforce the checkers
n@2789 3440 this.checkHiddenAnchor = function (message) {
nicholas@2708 3441 var anchors = audioEngineContext.audioObjects.filter(function (ao) {
nicholas@2708 3442 return ao.specification.type === "anchor";
nicholas@2708 3443 });
nicholas@2708 3444 var state = anchors.some(function (ao) {
nicholas@2708 3445 return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0);
nicholas@2708 3446 });
nicholas@2708 3447 if (state) {
nicholas@2708 3448 console.log('Anchor node not below marker value');
n@2789 3449 if (message) {
n@2789 3450 interfaceContext.lightbox.post("Message", message);
n@2789 3451 } else {
n@2789 3452 interfaceContext.lightbox.post("Message", 'Please keep listening');
n@2789 3453 }
nicholas@2708 3454 this.storeErrorNode('Anchor node not below marker value');
nicholas@2708 3455 return false;
nicholas@2498 3456 }
nicholas@2498 3457 return true;
nicholas@2498 3458 };
nicholas@2498 3459
n@2789 3460 this.checkHiddenReference = function (message) {
nicholas@2708 3461 var references = audioEngineContext.audioObjects.filter(function (ao) {
nicholas@2708 3462 return ao.specification.type === "reference";
nicholas@2708 3463 });
nicholas@2708 3464 var state = references.some(function (ao) {
nicholas@2708 3465 return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0);
nicholas@2708 3466 });
nicholas@2708 3467 if (state) {
nicholas@2708 3468 console.log('Reference node not below marker value');
n@2789 3469 if (message) {
n@2789 3470 interfaceContext.lightbox.post("Message", message);
n@2789 3471 } else {
n@2789 3472 interfaceContext.lightbox.post("Message", 'Please keep listening');
n@2789 3473 }
nicholas@2708 3474 this.storeErrorNode('Reference node not below marker value');
nicholas@2708 3475 return false;
nicholas@2498 3476 }
nicholas@2498 3477 return true;
nicholas@2498 3478 };
nicholas@2498 3479
n@2789 3480 this.checkFragmentsFullyPlayed = function (message) {
nicholas@2498 3481 // Checks the entire file has been played back
nicholas@2498 3482 // NOTE ! This will return true IF playback is Looped!!!
nicholas@2498 3483 if (audioEngineContext.loopPlayback) {
nicholas@2498 3484 console.log("WARNING - Looped source: Cannot check fragments are fully played");
nicholas@2498 3485 return true;
nicholas@2498 3486 }
nicholas@2498 3487 var check_pass = true;
nicholas@2708 3488 var error_obj = [],
nicholas@2708 3489 i;
nicholas@2708 3490 for (i = 0; i < audioEngineContext.audioObjects.length; i++) {
nicholas@2498 3491 var object = audioEngineContext.audioObjects[i];
nicholas@2498 3492 var time = object.buffer.buffer.duration;
nicholas@2498 3493 var metric = object.metric;
nicholas@2498 3494 var passed = false;
nicholas@2498 3495 for (var j = 0; j < metric.listenTracker.length; j++) {
nicholas@2498 3496 var bt = metric.listenTracker[j].getElementsByTagName('testtime');
nicholas@2498 3497 var start_time = Number(bt[0].getAttribute('start'));
nicholas@2498 3498 var stop_time = Number(bt[0].getAttribute('stop'));
nicholas@2498 3499 var delta = stop_time - start_time;
nicholas@2498 3500 if (delta >= time) {
nicholas@2498 3501 passed = true;
nicholas@2498 3502 break;
nicholas@2498 3503 }
nicholas@2498 3504 }
nicholas@2708 3505 if (passed === false) {
nicholas@2498 3506 check_pass = false;
nicholas@2498 3507 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
nicholas@2498 3508 error_obj.push(object.interfaceDOM.getPresentedId());
nicholas@2498 3509 }
nicholas@2498 3510 }
nicholas@2708 3511 if (check_pass === false) {
nicholas@2498 3512 var str_start = "You have not completely listened to fragments ";
nicholas@2708 3513 for (i = 0; i < error_obj.length; i++) {
nicholas@2498 3514 str_start += error_obj[i];
nicholas@2498 3515 if (i != error_obj.length - 1) {
nicholas@2498 3516 str_start += ', ';
nicholas@2498 3517 }
nicholas@2498 3518 }
nicholas@2498 3519 str_start += ". Please keep listening";
n@2789 3520 console.log(str_start);
n@2789 3521 this.storeErrorNode(str_start);
n@2789 3522 if (message) {
n@2789 3523 str_start = message;
n@2789 3524 }
nicholas@2498 3525 interfaceContext.lightbox.post("Error", str_start);
nicholas@2444 3526 return false;
nicholas@2498 3527 }
nicholas@2444 3528 return true;
nicholas@2498 3529 };
n@2789 3530 this.checkAllMoved = function (message) {
nicholas@2498 3531 var str = "You have not moved ";
nicholas@2498 3532 var failed = [];
nicholas@2708 3533 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2708 3534 if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) {
nicholas@2498 3535 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3536 }
nicholas@2708 3537 }, this);
nicholas@2708 3538 if (failed.length === 0) {
nicholas@2498 3539 return true;
nicholas@2498 3540 } else if (failed.length == 1) {
nicholas@2498 3541 str += 'track ' + failed[0];
nicholas@2498 3542 } else {
nicholas@2498 3543 str += 'tracks ';
nicholas@2498 3544 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3545 str += failed[i] + ', ';
nicholas@2498 3546 }
nicholas@2498 3547 str += 'and ' + failed[i];
nicholas@2498 3548 }
nicholas@2498 3549 str += '.';
nicholas@2498 3550 console.log(str);
nicholas@2224 3551 this.storeErrorNode(str);
n@2789 3552 if (message) {
n@2789 3553 str = message;
n@2789 3554 }
n@2789 3555 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3556 return false;
nicholas@2498 3557 };
n@2789 3558 this.checkAllPlayed = function (message) {
nicholas@2498 3559 var str = "You have not played ";
nicholas@2498 3560 var failed = [];
nicholas@2708 3561 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2708 3562 if (ao.metric.wasListenedTo === false) {
nicholas@2498 3563 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3564 }
nicholas@2708 3565 }, this);
nicholas@2708 3566 if (failed.length === 0) {
nicholas@2498 3567 return true;
nicholas@2498 3568 } else if (failed.length == 1) {
nicholas@2498 3569 str += 'track ' + failed[0];
nicholas@2498 3570 } else {
nicholas@2498 3571 str += 'tracks ';
nicholas@2498 3572 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3573 str += failed[i] + ', ';
nicholas@2498 3574 }
nicholas@2498 3575 str += 'and ' + failed[i];
nicholas@2498 3576 }
nicholas@2498 3577 str += '.';
nicholas@2498 3578 console.log(str);
nicholas@2224 3579 this.storeErrorNode(str);
n@2789 3580 if (message) {
n@2789 3581 str = message;
n@2789 3582 }
n@2789 3583 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3584 return false;
nicholas@2498 3585 };
n@2789 3586 this.checkAllCommented = function (message) {
nicholas@2540 3587 var str = "You have not commented on all the fragments.";
nicholas@2540 3588 var cont = true,
nicholas@2540 3589 boxes = this.commentBoxes.boxes,
nicholas@2540 3590 numBoxes = boxes.length,
nicholas@2540 3591 i;
nicholas@2540 3592 for (i = 0; i < numBoxes; i++) {
nicholas@2540 3593 if (boxes[i].trackCommentBox.value === "") {
nicholas@2540 3594 console.log(str);
nicholas@2540 3595 this.storeErrorNode(str);
n@2789 3596 if (message) {
n@2789 3597 str = message;
n@2789 3598 }
n@2789 3599 interfaceContext.lightbox.post("Error", str);
nicholas@2540 3600 return false;
nicholas@2540 3601 }
nicholas@2540 3602 }
nicholas@2540 3603 return true;
nicholas@2708 3604 };
n@2789 3605 this.checkScaleRange = function (message) {
nicholas@2310 3606 var page = testState.getCurrentTestPage();
nicholas@2708 3607 var interfaceObject = page.interfaces;
nicholas@2310 3608 var state = true;
nicholas@2310 3609 var str = "Please keep listening. ";
nicholas@2708 3610 if (interfaceObject === undefined) {
nicholas@2708 3611 return true;
nicholas@2310 3612 }
nicholas@2708 3613 interfaceObject = interfaceObject[0];
nicholas@2708 3614 var scales = (function () {
nicholas@2708 3615 var scaleRange = interfaceObject.options.find(function (a) {
nicholas@2708 3616 return a.name == "scalerange";
nicholas@2708 3617 });
nicholas@2708 3618 return {
nicholas@2708 3619 min: scaleRange.min,
nicholas@2708 3620 max: scaleRange.max
nicholas@2708 3621 };
nicholas@2708 3622 })();
nicholas@2708 3623 var range = audioEngineContext.audioObjects.reduce(function (a, b) {
nicholas@2742 3624 var v = b.interfaceDOM.getValue() * 100.0;
nicholas@2708 3625 return {
nicholas@2708 3626 min: Math.min(a.min, v),
nicholas@2708 3627 max: Math.max(a.max, v)
nicholas@2712 3628 };
nicholas@2708 3629 }, {
nicholas@2708 3630 min: 100,
nicholas@2708 3631 max: 0
nicholas@2708 3632 });
nicholas@2708 3633 if (range.min > scales.min) {
nicholas@2742 3634 str += "At least one fragment must be below the " + scales.min + " mark.";
nicholas@2708 3635 state = false;
nicholas@2712 3636 } else if (range.max < scales.max) {
nicholas@2742 3637 str += "At least one fragment must be above the " + scales.max + " mark.";
nicholas@2310 3638 state = false;
nicholas@2310 3639 }
nicholas@2708 3640 if (state === false) {
nicholas@2310 3641 console.log(str);
nicholas@2310 3642 this.storeErrorNode(str);
n@2789 3643 if (message) {
n@2789 3644 str = message;
n@2789 3645 }
nicholas@2498 3646 interfaceContext.lightbox.post("Error", str);
nicholas@2310 3647 }
nicholas@2310 3648 return state;
nicholas@2708 3649 };
nicholas@2826 3650 this.checkFragmentMinPlays = function () {
nicholas@2826 3651 var failedObjects = audioEngineContext.audioObjects.filter(function (a) {
nicholas@2826 3652 var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays;
nicholas@2826 3653 if (minPlays === undefined || a.numberOfPlays >= minPlays) {
nicholas@2826 3654 return false;
nicholas@2826 3655 }
nicholas@2826 3656 return true;
nicholas@2826 3657 });
nicholas@2826 3658 if (failedObjects.length === 0) {
nicholas@2827 3659 return true;
nicholas@2826 3660 }
nicholas@2826 3661 var failedString = [];
nicholas@2826 3662 failedObjects.forEach(function (a) {
nicholas@2826 3663 failedString.push(a.interfaceDOM.getPresentedId());
nicholas@2826 3664 });
nicholas@2826 3665 var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening";
nicholas@2826 3666 interfaceContext.lightbox.post("Message", str);
nicholas@2826 3667 this.storeErrorNode(str);
nicholas@2827 3668 return false;
nicholas@2826 3669 };
nicholas@2826 3670
nicholas@2498 3671
nicholas@2849 3672 this.sortFragmentsByScore = function () {
nicholas@2849 3673 var elements = audioEngineContext.audioObjects.filter(function (elem) {
nicholas@2849 3674 return elem.specification.type !== "outside-reference";
nicholas@2849 3675 });
nicholas@2849 3676 var indexes = [];
nicholas@2849 3677 var i = 0;
nicholas@2849 3678 while (indexes.push(i++) < elements.length);
nicholas@2849 3679 return indexes.sort(function (x, y) {
nicholas@2849 3680 var a = elements[x].interfaceDOM.getValue();
nicholas@2849 3681 var b = elements[y].interfaceDOM.getValue();
nicholas@2849 3682 if (a > b) {
nicholas@2849 3683 return 1;
nicholas@2849 3684 } else if (a < b) {
nicholas@2849 3685 return -1;
nicholas@2849 3686 }
nicholas@2849 3687 return 0;
nicholas@2849 3688 }, elements[0].interfaceDOM.getValue());
nicholas@2849 3689 };
nicholas@2849 3690
nicholas@2498 3691 this.storeErrorNode = function (errorMessage) {
nicholas@2224 3692 var time = audioEngineContext.timer.getTestTime();
nicholas@2224 3693 var node = storage.document.createElement('error');
nicholas@2498 3694 node.setAttribute('time', time);
nicholas@2224 3695 node.textContent = errorMessage;
nicholas@2224 3696 testState.currentStore.XMLDOM.appendChild(node);
nicholas@2224 3697 };
nicholas@2595 3698
nicholas@2595 3699 this.getLabel = function (labelType, index, labelStart) {
nicholas@2595 3700 /*
nicholas@2595 3701 Get the correct label based on type, index and offset
nicholas@2595 3702 */
nicholas@2595 3703
nicholas@2595 3704 function calculateLabel(labelType, index, offset) {
nicholas@2595 3705 if (labelType == "none") {
nicholas@2595 3706 return "";
nicholas@2595 3707 }
nicholas@2595 3708 switch (labelType) {
nicholas@2595 3709 case "letter":
nicholas@2596 3710 return String.fromCharCode((index + offset) % 26 + 97);
nicholas@2595 3711 case "capital":
nicholas@2607 3712 return String.fromCharCode((index + offset) % 26 + 65);
nicholas@2625 3713 case "samediff":
nicholas@2708 3714 if (index === 0) {
nicholas@2625 3715 return "Same";
nicholas@2625 3716 } else if (index == 1) {
nicholas@2625 3717 return "Difference";
nicholas@2625 3718 }
nicholas@2708 3719 return "";
nicholas@2595 3720 case "number":
nicholas@2595 3721 return String(index + offset);
nicholas@2595 3722 default:
nicholas@2595 3723 return "";
nicholas@2595 3724 }
nicholas@2595 3725 }
nicholas@2595 3726
nicholas@2708 3727 if (typeof labelStart !== "string" || labelStart.length === 0) {
nicholas@2595 3728 labelStart = String.fromCharCode(0);
nicholas@2595 3729 }
nicholas@2595 3730
nicholas@2595 3731 switch (labelType) {
nicholas@2595 3732 case "letter":
nicholas@2595 3733 labelStart = labelStart.charCodeAt(0);
nicholas@2596 3734 if (labelStart < 97 || labelStart > 122) {
nicholas@2595 3735 labelStart = 97;
nicholas@2595 3736 }
nicholas@2595 3737 labelStart -= 97;
nicholas@2595 3738 break;
nicholas@2595 3739 case "capital":
nicholas@2595 3740 labelStart = labelStart.charCodeAt(0);
nicholas@2596 3741 if (labelStart < 65 || labelStart > 90) {
nicholas@2595 3742 labelStart = 65;
nicholas@2595 3743 }
nicholas@2595 3744 labelStart -= 65;
nicholas@2595 3745 break;
nicholas@2595 3746 case "number":
nicholas@2608 3747 labelStart = Number(labelStart);
nicholas@2608 3748 if (!isFinite(labelStart)) {
nicholas@2595 3749 labelStart = 1;
nicholas@2595 3750 }
nicholas@2595 3751 break;
nicholas@2595 3752 default:
nicholas@2596 3753 labelStart = 0;
nicholas@2595 3754 }
nicholas@2595 3755 if (typeof index == "number") {
nicholas@2595 3756 return calculateLabel(labelType, index, labelStart);
nicholas@2595 3757 } else if (index.length && index.length > 0) {
nicholas@2595 3758 var a = [],
nicholas@2595 3759 l = index.length,
nicholas@2595 3760 i;
nicholas@2595 3761 for (i = 0; i < l; i++) {
nicholas@2595 3762 a[i] = calculateLabel(labelType, index[i], labelStart);
nicholas@2595 3763 }
nicholas@2595 3764 return a;
nicholas@2595 3765 } else {
nicholas@2595 3766 throw ("Invalid arguments");
nicholas@2595 3767 }
nicholas@2708 3768 };
nicholas@2649 3769
nicholas@2649 3770 this.getCombinedInterfaces = function (page) {
nicholas@2649 3771 // Combine the interfaces with the global interface nodes
nicholas@2649 3772 var global = specification.interfaces,
nicholas@2649 3773 local = page.interfaces;
nicholas@2649 3774 local.forEach(function (locInt) {
nicholas@2649 3775 // Iterate through the options nodes
nicholas@2649 3776 var addList = [];
nicholas@2649 3777 global.options.forEach(function (gopt) {
nicholas@2649 3778 var lopt = locInt.options.find(function (lopt) {
nicholas@2649 3779 return (lopt.name == gopt.name) && (lopt.type == gopt.type);
nicholas@2649 3780 });
nicholas@2649 3781 if (!lopt) {
nicholas@2649 3782 // Global option doesn't exist locally
nicholas@2649 3783 addList.push(gopt);
nicholas@2649 3784 }
nicholas@2649 3785 });
nicholas@2649 3786 locInt.options = locInt.options.concat(addList);
nicholas@2649 3787 if (!locInt.scales && global.scales) {
nicholas@2649 3788 // Use the global default scales
nicholas@2649 3789 locInt.scales = global.scales;
nicholas@2649 3790 }
nicholas@2649 3791 });
nicholas@2649 3792 return local;
nicholas@2708 3793 };
nicholas@2224 3794 }
nicholas@2224 3795
nicholas@2498 3796 function Storage() {
nicholas@2498 3797 // Holds results in XML format until ready for collection
nicholas@2498 3798 this.globalPreTest = null;
nicholas@2498 3799 this.globalPostTest = null;
nicholas@2498 3800 this.testPages = [];
nicholas@2498 3801 this.document = null;
nicholas@2498 3802 this.root = null;
nicholas@2498 3803 this.state = 0;
nicholas@3113 3804 var linkedID = undefined;
nicholas@2733 3805 var pFilenamePrefix = "save";
nicholas@2498 3806
nicholas@2498 3807 this.initialise = function (existingStore) {
nicholas@2708 3808 if (existingStore === undefined) {
nicholas@2224 3809 // We need to get the sessionKey
nicholas@2510 3810 this.SessionKey.requestKey();
nicholas@2498 3811 this.document = document.implementation.createDocument(null, "waetresult", null);
nicholas@2224 3812 this.root = this.document.childNodes[0];
nicholas@2224 3813 var projectDocument = specification.projectXML;
nicholas@2708 3814 projectDocument.setAttribute('file-name', specification.url);
nicholas@2708 3815 projectDocument.setAttribute('url', qualifyURL(specification.url));
nicholas@2224 3816 this.root.appendChild(projectDocument);
nicholas@2224 3817 this.root.appendChild(interfaceContext.returnDateNode());
nicholas@2224 3818 this.root.appendChild(interfaceContext.returnNavigator());
nicholas@2224 3819 } else {
nicholas@2224 3820 this.document = existingStore;
nicholas@2294 3821 this.root = existingStore.firstChild;
nicholas@2224 3822 this.SessionKey.key = this.root.getAttribute("key");
nicholas@2224 3823 }
nicholas@2708 3824 if (specification.preTest !== undefined) {
nicholas@2498 3825 this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
nicholas@2498 3826 }
nicholas@2708 3827 if (specification.postTest !== undefined) {
nicholas@2498 3828 this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
nicholas@2498 3829 }
nicholas@3113 3830 if (linkedID) {
nicholas@3113 3831 this.root.setAttribute("linked", linkedID);
nicholas@3113 3832 }
nicholas@2498 3833 };
nicholas@2498 3834
n@2967 3835 this.SessionKey = (function (parent) {
n@2970 3836 var returnURL = "";
n@2970 3837 if (window.returnURL !== undefined) {
n@2970 3838 returnURL = String(window.returnURL);
n@2970 3839 }
nicholas@3129 3840
nicholas@3118 3841 var chainCount = 0;
nicholas@3118 3842 var chainPosition = chainCount;
n@2970 3843
n@2967 3844 function postUpdate() {
n@2967 3845 return new Promise(function (resolve, reject) {
nicholas@3118 3846 // Return a new promise.
nicholas@3118 3847 chainPosition+=1;
nicholas@3118 3848 var hold = document.createElement("div");
nicholas@3118 3849 var clone = parent.root.cloneNode(true);
nicholas@3118 3850 hold.appendChild(clone);
n@2967 3851 // Do the usual XHR stuff
n@2977 3852 console.log("Requested save...");
n@2967 3853 var req = new XMLHttpRequest();
n@2973 3854 req.open("POST", returnURL + "php/save.php?key=" + sessionKey + "&saveFilenamePrefix=" + parent.filenamePrefix);
n@2967 3855 req.setRequestHeader('Content-Type', 'text/xml');
n@2967 3856
n@2967 3857 req.onload = function () {
n@2967 3858 // This is called even on 404 etc
n@2967 3859 // so check the status
n@2967 3860 if (this.status >= 300) {
n@2967 3861 console.log("WARNING - Could not update at this time");
n@2967 3862 } else {
n@2967 3863 var parser = new DOMParser();
n@2974 3864 var xmlDoc = parser.parseFromString(req.responseText, "application/xml");
n@2967 3865 var response = xmlDoc.getElementsByTagName('response')[0];
n@2967 3866 if (response.getAttribute("state") == "OK") {
n@2967 3867 var file = response.getElementsByTagName("file")[0];
n@2967 3868 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
n@2967 3869 resolve(true);
n@2967 3870 } else {
n@2967 3871 var message = response.getElementsByTagName("message");
n@2967 3872 console.log("Intermediate save: Error! " + message.textContent);
n@2967 3873 reject("Intermediate save: Error! " + message.textContent);
n@2967 3874 }
n@2967 3875 }
n@2967 3876 };
n@2967 3877
n@2967 3878 // Handle network errors
n@2967 3879 req.onerror = function () {
n@2967 3880 reject(Error("Network Error"));
n@2967 3881 };
n@2967 3882
n@2967 3883 // Make the request
nicholas@3118 3884 if (chainCount > chainPosition) {
nicholas@3118 3885 // We have items downstream that will upload for us
nicholas@3118 3886 resolve(true);
nicholas@3118 3887 } else {
nicholas@3118 3888 req.send([hold.innerHTML]);
nicholas@3118 3889 }
n@2967 3890 });
n@2979 3891 }
n@2967 3892
n@2967 3893 function keyPromise() {
n@2967 3894 return new Promise(function (resolve, reject) {
n@2967 3895 var req = new XMLHttpRequest();
nicholas@3139 3896 req.open("POST", returnURL + "php/requestKey.php?saveFilenamePrefix=" + parent.filenamePrefix, true);
n@2967 3897 req.onload = function () {
n@2967 3898 // This is called even on 404 etc
n@2967 3899 // so check the status
n@2967 3900 if (req.status == 200) {
n@2967 3901 // Resolve the promise with the response text
n@2967 3902 resolve(req.response);
n@2967 3903 } else {
n@2967 3904 // Otherwise reject with the status text
n@2967 3905 // which will hopefully be a meaningful error
n@2967 3906 reject(Error(req.statusText));
n@2967 3907 }
n@2967 3908 };
n@2967 3909
n@2967 3910 // Handle network errors
n@2967 3911 req.onerror = function () {
n@2967 3912 reject(Error("Network Error"));
n@2967 3913 };
n@2967 3914
n@2967 3915 req.send();
n@2979 3916 });
n@2967 3917 }
n@2967 3918
n@2967 3919 var requestChains = null;
n@2969 3920 var sessionKey = null;
n@2967 3921 var object = {};
n@2967 3922
n@2967 3923 Object.defineProperties(object, {
n@2967 3924 "key": {
n@2967 3925 "get": function () {
n@2969 3926 return sessionKey;
n@2967 3927 },
n@2967 3928 "set": function (a) {
n@2979 3929 throw ("Cannot set read-only property");
n@2967 3930 }
n@2967 3931 },
n@2967 3932 "request": {
n@2967 3933 "value": new XMLHttpRequest()
n@2967 3934 },
n@2967 3935 "parent": {
n@2967 3936 "value": parent
n@2967 3937 },
n@2967 3938 "requestKey": {
n@2967 3939 "value": function () {
n@2967 3940 requestChains = keyPromise().then(function (response) {
n@2967 3941 function throwerror() {
n@2969 3942 sessionKey = null;
n@2967 3943 throw ("An unspecified error occured, no server key could be generated");
n@2967 3944 }
n@2967 3945 var parse = new DOMParser();
n@2967 3946 var xml = parse.parseFromString(response, "text/xml");
n@2971 3947 if (response.length === 0) {
n@2967 3948 throwerror();
n@2967 3949 }
n@2967 3950 if (xml.getElementsByTagName("state").length > 0) {
n@2967 3951 if (xml.getElementsByTagName("state")[0].textContent == "OK") {
n@2969 3952 sessionKey = xml.getAllElementsByTagName("key")[0].textContent;
n@2972 3953 parent.root.setAttribute("key", sessionKey);
n@2972 3954 parent.root.setAttribute("state", "empty");
n@2967 3955 return (true);
n@2967 3956 } else if (xml.getElementsByTagName("state")[0].textContent == "ERROR") {
n@2969 3957 sessionKey = null;
n@2967 3958 console.error("Could not generate server key. Server responded with error message: \"" + xml.getElementsByTagName("message")[0].textContent + "\"");
n@2969 3959 return (false);
n@2967 3960 }
n@2967 3961 } else {
n@2967 3962 throwerror();
n@2967 3963 }
n@2967 3964 return (true);
n@2967 3965 });
n@2967 3966 }
n@2967 3967 },
n@2968 3968 "update": {
n@2968 3969 "value": function () {
n@3104 3970 if (requestChains === undefined) {
n@3104 3971 throw ("Initiate key exchange first");
n@2968 3972 }
nicholas@3118 3973 chainCount += 1;
n@2968 3974 this.parent.root.setAttribute("state", "update");
n@2978 3975 requestChains = requestChains.then(postUpdate);
n@2967 3976 }
n@2967 3977 },
n@2968 3978 "finish": {
n@2968 3979 "value": function () {
n@2979 3980 if (this.key === null || requestChains === undefined) {
n@2968 3981 throw ("Cannot save as key == null");
n@2968 3982 }
n@2968 3983 this.parent.finish();
n@2975 3984 return requestChains.then(postUpdate()).then(function () {
n@2968 3985 console.log("OK");
nicholas@3113 3986 return true;
n@2968 3987 }, function () {
n@2968 3988 createProjectSave("local");
n@2979 3989 });
n@2967 3990 }
n@2967 3991 }
n@2968 3992 });
n@2967 3993 return object;
n@2967 3994 })(this);
n@2967 3995 /*
nicholas@2224 3996 this.SessionKey = {
nicholas@2224 3997 key: null,
nicholas@2224 3998 request: new XMLHttpRequest(),
nicholas@2224 3999 parent: this,
nicholas@2498 4000 handleEvent: function () {
n@2967 4001
nicholas@2224 4002 },
nicholas@2510 4003 requestKey: function () {
n@2967 4004
nicholas@2510 4005 },
nicholas@2498 4006 update: function () {
nicholas@2708 4007 if (this.key === null) {
nicholas@2357 4008 console.log("Cannot save as key == null");
nicholas@2357 4009 return;
nicholas@2357 4010 }
nicholas@2498 4011 this.parent.root.setAttribute("state", "update");
nicholas@2224 4012 var xmlhttp = new XMLHttpRequest();
nicholas@2302 4013 var returnURL = "";
nicholas@2302 4014 if (typeof specification.projectReturn == "string") {
nicholas@2498 4015 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2302 4016 returnURL = specification.projectReturn;
nicholas@2302 4017 }
nicholas@2302 4018 }
nicholas@2722 4019 xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix);
nicholas@2224 4020 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
nicholas@2498 4021 xmlhttp.onerror = function () {
nicholas@2224 4022 console.log('Error updating file to server!');
nicholas@2224 4023 };
nicholas@2224 4024 var hold = document.createElement("div");
nicholas@2224 4025 var clone = this.parent.root.cloneNode(true);
nicholas@2224 4026 hold.appendChild(clone);
nicholas@2498 4027 xmlhttp.onload = function () {
nicholas@2224 4028 if (this.status >= 300) {
nicholas@2224 4029 console.log("WARNING - Could not update at this time");
nicholas@2224 4030 } else {
nicholas@2224 4031 var parser = new DOMParser();
nicholas@2224 4032 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
nicholas@2224 4033 var response = xmlDoc.getElementsByTagName('response')[0];
nicholas@2224 4034 if (response.getAttribute("state") == "OK") {
nicholas@2224 4035 var file = response.getElementsByTagName("file")[0];
nicholas@2498 4036 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
nicholas@2224 4037 } else {
nicholas@2224 4038 var message = response.getElementsByTagName("message");
nicholas@2498 4039 console.log("Intermediate save: Error! " + message.textContent);
nicholas@2224 4040 }
nicholas@2224 4041 }
nicholas@2708 4042 };
nicholas@2224 4043 xmlhttp.send([hold.innerHTML]);
nicholas@2723 4044 },
nicholas@2723 4045 finish: function () {
nicholas@2723 4046 // Final upload to complete the test
nicholas@2723 4047 this.parent.finish();
nicholas@2723 4048 var hold = document.createElement("div");
nicholas@2723 4049 var clone = this.parent.root.cloneNode(true);
nicholas@2723 4050 hold.appendChild(clone);
nicholas@2733 4051 var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=";
nicholas@2742 4052 if (this.parent.filenamePrefix.length === 0) {
nicholas@2733 4053 saveURL += "save";
nicholas@2733 4054 } else {
nicholas@2733 4055 saveURL += this.parent.filenamePrefix;
nicholas@2733 4056 }
nicholas@2723 4057 return new Promise(function (resolve, reject) {
nicholas@2723 4058 var xmlhttp = new XMLHttpRequest();
nicholas@2723 4059 xmlhttp.open("POST", saveURL);
nicholas@2723 4060 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
nicholas@2723 4061 xmlhttp.onerror = function () {
nicholas@2723 4062 console.log('Error updating file to server!');
nicholas@2723 4063 createProjectSave("local");
nicholas@2723 4064 };
nicholas@2723 4065 xmlhttp.onload = function () {
nicholas@2723 4066 if (this.status >= 300) {
nicholas@2723 4067 console.log("WARNING - Could not update at this time");
nicholas@2723 4068 createProjectSave("local");
nicholas@2723 4069 } else {
nicholas@2723 4070 var parser = new DOMParser();
nicholas@2723 4071 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
nicholas@2723 4072 var response = xmlDoc.getElementsByTagName('response')[0];
nicholas@2723 4073 if (response.getAttribute("state") == "OK") {
nicholas@2723 4074 var file = response.getElementsByTagName("file")[0];
nicholas@2723 4075 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
nicholas@2723 4076 resolve(response);
nicholas@2723 4077 } else {
nicholas@2723 4078 var message = response.getElementsByTagName("message");
nicholas@2723 4079 reject(message);
nicholas@2723 4080 }
nicholas@2723 4081 }
nicholas@2723 4082 };
nicholas@2723 4083 xmlhttp.send([hold.innerHTML]);
nicholas@2723 4084 });
nicholas@2224 4085 }
nicholas@2708 4086 };
n@2967 4087 */
nicholas@2498 4088 this.createTestPageStore = function (specification) {
nicholas@2498 4089 var store = new this.pageNode(this, specification);
nicholas@2498 4090 this.testPages.push(store);
nicholas@2498 4091 return this.testPages[this.testPages.length - 1];
nicholas@2498 4092 };
nicholas@2498 4093
nicholas@2498 4094 this.surveyNode = function (parent, root, specification) {
nicholas@2498 4095 this.specification = specification;
nicholas@2498 4096 this.parent = parent;
nicholas@2224 4097 this.state = "empty";
nicholas@2498 4098 this.XMLDOM = this.parent.document.createElement('survey');
nicholas@2498 4099 this.XMLDOM.setAttribute('location', this.specification.location);
nicholas@2498 4100 this.XMLDOM.setAttribute("state", this.state);
nicholas@2708 4101 this.specification.options.forEach(function (optNode) {
nicholas@2498 4102 if (optNode.type != 'statement') {
nicholas@2498 4103 var node = this.parent.document.createElement('surveyresult');
nicholas@2498 4104 node.setAttribute("ref", optNode.id);
nicholas@2498 4105 node.setAttribute('type', optNode.type);
nicholas@2498 4106 this.XMLDOM.appendChild(node);
nicholas@2498 4107 }
nicholas@2708 4108 }, this);
nicholas@2498 4109 root.appendChild(this.XMLDOM);
nicholas@2498 4110
nicholas@2498 4111 this.postResult = function (node) {
nicholas@2708 4112 function postNumber(doc, value) {
nicholas@2708 4113 var child = doc.createElement("response");
nicholas@2708 4114 child.textContent = value;
nicholas@2708 4115 return child;
nicholas@2708 4116 }
nicholas@2708 4117
nicholas@2708 4118 function postRadio(doc, node) {
nicholas@2708 4119 var child = doc.createElement('response');
nicholas@2708 4120 if (node.response !== null) {
nicholas@2708 4121 child.setAttribute('name', node.response.name);
nicholas@2708 4122 child.textContent = node.response.text;
nicholas@2708 4123 }
nicholas@2708 4124 return child;
nicholas@2708 4125 }
nicholas@2708 4126
nicholas@2708 4127 function postCheckbox(doc, node) {
nicholas@2708 4128 var checkNode = doc.createElement('response');
nicholas@2708 4129 checkNode.setAttribute('name', node.name);
nicholas@2708 4130 checkNode.setAttribute('checked', node.checked);
nicholas@2708 4131 return checkNode;
nicholas@2708 4132 }
nicholas@2498 4133 // From popup: node is the popupOption node containing both spec. and results
nicholas@2498 4134 // ID is the position
nicholas@2498 4135 if (node.specification.type == 'statement') {
nicholas@2498 4136 return;
nicholas@2498 4137 }
nicholas@2498 4138 var surveyresult = this.XMLDOM.firstChild;
nicholas@2708 4139 while (surveyresult !== null) {
nicholas@2498 4140 if (surveyresult.getAttribute("ref") == node.specification.id) {
nicholas@2224 4141 break;
nicholas@2224 4142 }
nicholas@2224 4143 surveyresult = surveyresult.nextElementSibling;
nicholas@2224 4144 }
nicholas@2775 4145 surveyresult.setAttribute("duration", node.elapsedTime);
nicholas@2498 4146 switch (node.specification.type) {
nicholas@2498 4147 case "number":
nicholas@2498 4148 case "question":
n@2583 4149 case "slider":
nicholas@2708 4150 surveyresult.appendChild(postNumber(this.parent.document, node.response));
nicholas@2464 4151 break;
nicholas@2498 4152 case "radio":
nicholas@2708 4153 surveyresult.appendChild(postRadio(this.parent.document, node));
nicholas@2498 4154 break;
nicholas@2498 4155 case "checkbox":
nicholas@2708 4156 if (node.response === undefined) {
nicholas@2498 4157 surveyresult.appendChild(this.parent.document.createElement('response'));
nicholas@2498 4158 break;
nicholas@2498 4159 }
nicholas@2498 4160 for (var i = 0; i < node.response.length; i++) {
nicholas@2708 4161 surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i]));
nicholas@2498 4162 }
nicholas@2498 4163 break;
nicholas@2498 4164 }
nicholas@2498 4165 };
nicholas@2498 4166 this.complete = function () {
nicholas@2498 4167 this.state = "complete";
nicholas@2498 4168 this.XMLDOM.setAttribute("state", this.state);
nicholas@2708 4169 };
nicholas@2498 4170 };
nicholas@2498 4171
nicholas@2498 4172 this.pageNode = function (parent, specification) {
nicholas@2498 4173 // Create one store per test page
nicholas@2498 4174 this.specification = specification;
nicholas@2498 4175 this.parent = parent;
nicholas@2498 4176 this.state = "empty";
nicholas@2498 4177 this.XMLDOM = this.parent.document.createElement('page');
nicholas@2498 4178 this.XMLDOM.setAttribute('ref', specification.id);
nicholas@2498 4179 this.XMLDOM.setAttribute('presentedId', specification.presentedId);
nicholas@2498 4180 this.XMLDOM.setAttribute("state", this.state);
nicholas@2708 4181 if (specification.preTest !== undefined) {
nicholas@2498 4182 this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
nicholas@2498 4183 }
nicholas@2708 4184 if (specification.postTest !== undefined) {
nicholas@2498 4185 this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
nicholas@2498 4186 }
nicholas@2498 4187
nicholas@2498 4188 // Add any page metrics
nicholas@2498 4189 var page_metric = this.parent.document.createElement('metric');
nicholas@2498 4190 this.XMLDOM.appendChild(page_metric);
nicholas@2498 4191
nicholas@2498 4192 // Add the audioelement
nicholas@2708 4193 this.specification.audioElements.forEach(function (element) {
nicholas@2498 4194 var aeNode = this.parent.document.createElement('audioelement');
nicholas@2498 4195 aeNode.setAttribute('ref', element.id);
nicholas@2708 4196 if (element.name !== undefined) {
nicholas@2708 4197 aeNode.setAttribute('name', element.name);
nicholas@2708 4198 }
nicholas@2498 4199 aeNode.setAttribute('type', element.type);
nicholas@2498 4200 aeNode.setAttribute('url', element.url);
nicholas@2498 4201 aeNode.setAttribute('fqurl', qualifyURL(element.url));
nicholas@2498 4202 aeNode.setAttribute('gain', element.gain);
nicholas@2498 4203 if (element.type == 'anchor' || element.type == 'reference') {
nicholas@2498 4204 if (element.marker > 0) {
nicholas@2498 4205 aeNode.setAttribute('marker', element.marker);
nicholas@2464 4206 }
nicholas@2498 4207 }
nicholas@2498 4208 var ae_metric = this.parent.document.createElement('metric');
nicholas@2498 4209 aeNode.appendChild(ae_metric);
nicholas@2498 4210 this.XMLDOM.appendChild(aeNode);
nicholas@2708 4211 }, this);
nicholas@2498 4212
nicholas@2498 4213 this.parent.root.appendChild(this.XMLDOM);
nicholas@2498 4214
nicholas@2498 4215 this.complete = function () {
nicholas@2224 4216 this.state = "complete";
nicholas@2498 4217 this.XMLDOM.setAttribute("state", "complete");
nicholas@2708 4218 };
nicholas@2498 4219 };
nicholas@2498 4220 this.update = function () {
nicholas@2224 4221 this.SessionKey.update();
nicholas@2708 4222 };
nicholas@2498 4223 this.finish = function () {
nicholas@2498 4224 this.state = 1;
nicholas@2498 4225 this.root.setAttribute("state", "complete");
nicholas@2498 4226 return this.root;
nicholas@2498 4227 };
nicholas@2722 4228
nicholas@2722 4229 Object.defineProperties(this, {
nicholas@2722 4230 'filenamePrefix': {
nicholas@2722 4231 'get': function () {
nicholas@2722 4232 return pFilenamePrefix;
nicholas@2722 4233 },
nicholas@2722 4234 'set': function (value) {
nicholas@2722 4235 if (typeof value !== "string") {
nicholas@2722 4236 value = String(value);
nicholas@2722 4237 }
nicholas@2722 4238 pFilenamePrefix = value;
nicholas@2722 4239 return value;
nicholas@2722 4240 }
nicholas@3113 4241 },
nicholas@3113 4242 "sessionLinked": {
nicholas@3113 4243 'get': function () {
nicholas@3113 4244 return linkedID;
nicholas@3113 4245 },
nicholas@3113 4246 'set': function(s) {
nicholas@3113 4247 if (typeof s == "string") {
nicholas@3113 4248 linkedID = s;
nicholas@3113 4249 if (this.root) {
nicholas@3113 4250 this.root.setAttribute("linked", s);
nicholas@3113 4251 }
nicholas@3113 4252 }
nicholas@3113 4253 return linkedID;
nicholas@3113 4254 }
nicholas@2722 4255 }
nicholas@2725 4256 });
nicholas@2224 4257 }
nicholas@2384 4258
nicholas@2401 4259 var window_depedancy_callback;
nicholas@2498 4260 window_depedancy_callback = window.setInterval(function () {
nicholas@2401 4261 if (check_dependancies()) {
nicholas@2401 4262 window.clearInterval(window_depedancy_callback);
nicholas@2401 4263 onload();
nicholas@2401 4264 } else {
nicholas@2401 4265 document.getElementById("topLevelBody").innerHTML = "<h1>Loading Resources</h1>";
nicholas@2401 4266 }
nicholas@2498 4267 }, 100);