annotate js/core.js @ 2687:66a24a6a0358

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