annotate js/core.js @ 2703:536cb44c7292

Tidied up check on submits for AB/ABX/APE.
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Mon, 13 Mar 2017 11:53:36 +0000
parents d29471d114cf
children 3a0be105ee85
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@2689 331 if (isFinite(specification.sampleRate)) {
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@2689 612 } else if (node.specification.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@2689 804 if (node.response === null) {
nicholas@2677 805 node.response = {
nicholas@2677 806 name: "",
nicholas@2677 807 text: ""
nicholas@2677 808 };
nicholas@2677 809 }
nicholas@2677 810 var table = document.createElement("table");
nicholas@2677 811 table.className = "popup-option-list";
nicholas@2677 812 table.border = "0";
nicholas@2689 813 if (node.response === null || node.response.length === 0) {
nicholas@2689 814 node.response = [];
nicholas@2689 815 }
nicholas@2689 816 node.specification.options.forEach(function (option, index) {
nicholas@2677 817 var tr = document.createElement("tr");
nicholas@2677 818 table.appendChild(tr);
nicholas@2677 819 var td = document.createElement("td");
nicholas@2677 820 tr.appendChild(td);
nicholas@2677 821 var input = document.createElement('input');
nicholas@2677 822 input.id = option.name;
nicholas@2677 823 input.type = 'radio';
nicholas@2677 824 input.name = node.specification.id;
nicholas@2677 825 td.appendChild(input);
nicholas@2677 826
nicholas@2677 827 td = document.createElement("td");
nicholas@2677 828 tr.appendChild(td);
nicholas@2677 829 var span = document.createElement('span');
nicholas@2677 830 span.textContent = option.text;
nicholas@2677 831 td.appendChild(span);
nicholas@2677 832 tr = document.createElement('div');
nicholas@2677 833 tr.setAttribute('name', 'option');
nicholas@2677 834 tr.className = "popup-option-checbox";
nicholas@2689 835 table.appendChild(tr);
nicholas@2677 836 });
nicholas@2677 837 this.popupResponse.appendChild(table);
nicholas@2677 838 }
nicholas@2677 839
nicholas@2677 840 function processRadio(node) {
nicholas@2677 841 var optHold = this.popupResponse;
nicholas@2677 842 console.log("Radio: " + node.specification.statement);
nicholas@2677 843 node.response = null;
nicholas@2677 844 var i = 0;
nicholas@2677 845 var inputs = optHold.getElementsByTagName('input');
nicholas@2677 846 while (node.response === null) {
nicholas@2677 847 if (i == inputs.length) {
nicholas@2677 848 if (node.specification.mandatory === true) {
nicholas@2677 849 interfaceContext.lightbox.post("Error", "Please select one option");
nicholas@2679 850 return false;
nicholas@2677 851 }
nicholas@2677 852 break;
nicholas@2677 853 }
nicholas@2677 854 if (inputs[i].checked === true) {
nicholas@2677 855 node.response = node.specification.options[i];
nicholas@2677 856 console.log("Selected: " + node.specification.options[i].name);
nicholas@2677 857 }
nicholas@2677 858 i++;
nicholas@2677 859 }
nicholas@2679 860 processConditional.call(this, node, node.response);
nicholas@2679 861 return true;
nicholas@2677 862 }
nicholas@2677 863
nicholas@2677 864 function processRadioConditional(condition, response) {
nicholas@2677 865 switch (condition.check) {
nicholas@2677 866 case "equals":
nicholas@2682 867 if (response === condition.value) {
nicholas@2677 868 return true;
nicholas@2677 869 }
nicholas@2677 870 break;
nicholas@2677 871 case "contains":
nicholas@2677 872 case "greaterThan":
nicholas@2677 873 case "lessThan":
nicholas@2677 874 console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING");
nicholas@2677 875 break;
nicholas@2677 876 default:
nicholas@2677 877 console.log("Unknown condition. IGNORING");
nicholas@2677 878 break;
nicholas@2677 879 }
nicholas@2677 880 return false;
nicholas@2677 881 }
nicholas@2677 882
nicholas@2677 883 function postNumber(node) {
nicholas@2677 884 var input = document.createElement('input');
nicholas@2677 885 input.type = 'textarea';
nicholas@2677 886 if (node.specification.min !== null) {
nicholas@2677 887 input.min = node.specification.min;
nicholas@2677 888 }
nicholas@2677 889 if (node.specification.max !== null) {
nicholas@2677 890 input.max = node.specification.max;
nicholas@2677 891 }
nicholas@2677 892 if (node.specification.step !== null) {
nicholas@2677 893 input.step = node.specification.step;
nicholas@2677 894 }
nicholas@2677 895 if (node.response !== undefined) {
nicholas@2677 896 input.value = node.response;
nicholas@2677 897 }
nicholas@2677 898 this.popupResponse.appendChild(input);
nicholas@2677 899 this.popupResponse.style.textAlign = "center";
nicholas@2677 900 this.popupResponse.style.left = "0%";
nicholas@2677 901 }
nicholas@2677 902
nicholas@2677 903 function processNumber(node) {
nicholas@2677 904 var input = this.popupContent.getElementsByTagName('input')[0];
nicholas@2677 905 if (node.mandatory === true && input.value.length === 0) {
nicholas@2677 906 interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number');
nicholas@2679 907 return false;
nicholas@2677 908 }
nicholas@2677 909 var enteredNumber = Number(input.value);
nicholas@2677 910 if (isNaN(enteredNumber)) {
nicholas@2677 911 interfaceContext.lightbox.post("Error", 'Please enter a valid number');
nicholas@2679 912 return false;
nicholas@2677 913 }
nicholas@2677 914 if (enteredNumber < node.min && node.min !== null) {
nicholas@2677 915 interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.min);
nicholas@2679 916 return false;
nicholas@2677 917 }
nicholas@2677 918 if (enteredNumber > node.max && node.max !== null) {
nicholas@2677 919 interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.max);
nicholas@2679 920 return false;
nicholas@2677 921 }
nicholas@2677 922 node.response = input.value;
nicholas@2679 923 processConditional.call(this, node, node.response);
nicholas@2679 924 return true;
nicholas@2677 925 }
nicholas@2677 926
nicholas@2677 927 function processNumberConditional(condtion, value) {
nicholas@2682 928 var condition = condition;
nicholas@2677 929 switch (condition.check) {
nicholas@2677 930 case "greaterThan":
nicholas@2682 931 if (value > Number(condition.value)) {
nicholas@2677 932 return true;
nicholas@2677 933 }
nicholas@2677 934 break;
nicholas@2677 935 case "lessThan":
nicholas@2682 936 if (value < Number(condition.value)) {
nicholas@2677 937 return true;
nicholas@2677 938 }
nicholas@2677 939 break;
nicholas@2677 940 case "equals":
nicholas@2682 941 if (value == condition.value) {
nicholas@2677 942 return true;
nicholas@2677 943 }
nicholas@2677 944 break;
nicholas@2677 945 case "contains":
nicholas@2677 946 console.log("Survey Element of type 'number' cannot interpret \"contains\" conditions. IGNORING");
nicholas@2677 947 break;
nicholas@2677 948 default:
nicholas@2677 949 console.log("Unknown condition. IGNORING");
nicholas@2677 950 break;
nicholas@2677 951 }
nicholas@2677 952 return false;
nicholas@2677 953 }
nicholas@2677 954
nicholas@2677 955 function postVideo(node) {
nicholas@2677 956 var video = document.createElement("video");
nicholas@2677 957 video.src = node.specification.url;
nicholas@2677 958 this.popupResponse.appendChild(video);
nicholas@2677 959 }
nicholas@2677 960
nicholas@2677 961 function postYoutube(node) {
nicholas@2677 962 var iframe = document.createElement("iframe");
nicholas@2677 963 iframe.className = "youtube";
nicholas@2677 964 iframe.src = node.specification.url;
nicholas@2677 965 this.popupResponse.appendChild(iframe);
nicholas@2677 966 }
nicholas@2677 967
nicholas@2677 968 function postSlider(node) {
nicholas@2677 969 var hold = document.createElement('div');
nicholas@2677 970 var input = document.createElement('input');
nicholas@2677 971 input.type = 'range';
nicholas@2677 972 input.style.width = "90%";
nicholas@2677 973 if (node.specification.min !== null) {
nicholas@2677 974 input.min = node.specification.min;
nicholas@2677 975 }
nicholas@2677 976 if (node.specification.max !== null) {
nicholas@2677 977 input.max = node.specification.max;
nicholas@2677 978 }
nicholas@2677 979 if (node.response !== undefined) {
nicholas@2677 980 input.value = node.response;
nicholas@2677 981 }
nicholas@2677 982 hold.className = "survey-slider-text-holder";
nicholas@2677 983 var minText = document.createElement('span');
nicholas@2677 984 var maxText = document.createElement('span');
nicholas@2677 985 minText.textContent = node.specification.leftText;
nicholas@2677 986 maxText.textContent = node.specification.rightText;
nicholas@2677 987 hold.appendChild(minText);
nicholas@2677 988 hold.appendChild(maxText);
nicholas@2677 989 this.popupResponse.appendChild(input);
nicholas@2677 990 this.popupResponse.appendChild(hold);
nicholas@2677 991 this.popupResponse.style.textAlign = "center";
nicholas@2677 992 }
nicholas@2677 993
nicholas@2677 994 function processSlider(node) {
nicholas@2677 995 var input = this.popupContent.getElementsByTagName('input')[0];
nicholas@2677 996 node.response = input.value;
nicholas@2679 997 processConditional.call(this, node, node.response);
nicholas@2679 998 return true;
nicholas@2677 999 }
nicholas@2677 1000
nicholas@2677 1001 function processSliderConditional(condition, value) {
nicholas@2677 1002 switch (condition.check) {
nicholas@2677 1003 case "contains":
nicholas@2677 1004 console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING");
nicholas@2677 1005 break;
nicholas@2677 1006 case "greaterThan":
nicholas@2682 1007 if (value > Number(condition.value)) {
nicholas@2677 1008 return true;
nicholas@2677 1009 }
nicholas@2677 1010 break;
nicholas@2677 1011 case "lessThan":
nicholas@2682 1012 if (value < Number(condition.value)) {
nicholas@2677 1013 return true;
nicholas@2677 1014 }
nicholas@2677 1015 break;
nicholas@2677 1016 case "equals":
nicholas@2682 1017 if (value == condition.value) {
nicholas@2677 1018 return true;
nicholas@2677 1019 }
nicholas@2677 1020 break;
nicholas@2677 1021 default:
nicholas@2677 1022 console.log("Unknown condition. IGNORING");
nicholas@2677 1023 break;
nicholas@2677 1024 }
nicholas@2677 1025 return false;
nicholas@2677 1026 }
nicholas@2498 1027
nicholas@2498 1028 this.createPopup = function () {
nicholas@2498 1029 // Create popup window interface
nicholas@2498 1030 var insertPoint = document.getElementById("topLevelBody");
nicholas@2498 1031
nicholas@2498 1032 this.popup = document.getElementById('popupHolder');
nicholas@2498 1033 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2498 1034 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
nicholas@2498 1035
nicholas@2498 1036 this.popupContent = document.getElementById('popupContent');
nicholas@2498 1037
nicholas@2645 1038 this.popupTitle = document.getElementById('popupTitleHolder');
nicholas@2498 1039
nicholas@2498 1040 this.popupResponse = document.getElementById('popupResponse');
nicholas@2498 1041
nicholas@2498 1042 this.buttonProceed = document.getElementById('popup-proceed');
nicholas@2498 1043 this.buttonProceed.onclick = function () {
nicholas@2498 1044 popup.proceedClicked();
nicholas@2498 1045 };
nicholas@2498 1046
nicholas@2498 1047 this.buttonPrevious = document.getElementById('popup-previous');
nicholas@2498 1048 this.buttonPrevious.onclick = function () {
nicholas@2498 1049 popup.previousClick();
nicholas@2498 1050 };
nicholas@2498 1051
nicholas@2224 1052 this.hidePopup();
nicholas@2498 1053 this.popup.style.visibility = 'hidden';
nicholas@2498 1054 };
nicholas@2498 1055
nicholas@2498 1056 this.showPopup = function () {
nicholas@2677 1057 if (this.popup === null) {
nicholas@2498 1058 this.createPopup();
nicholas@2498 1059 }
nicholas@2498 1060 this.popup.style.visibility = 'visible';
nicholas@2498 1061 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2498 1062 blank.style.visibility = 'visible';
nicholas@2498 1063 this.popupResponse.style.left = "0%";
nicholas@2498 1064 };
nicholas@2498 1065
nicholas@2498 1066 this.hidePopup = function () {
nicholas@2224 1067 if (this.popup) {
nicholas@2224 1068 this.popup.style.visibility = 'hidden';
nicholas@2224 1069 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2224 1070 blank.style.visibility = 'hidden';
nicholas@2224 1071 this.buttonPrevious.style.visibility = 'inherit';
nicholas@2224 1072 }
nicholas@2498 1073 };
nicholas@2498 1074
nicholas@2498 1075 this.postNode = function () {
nicholas@2498 1076 // This will take the node from the popupOptions and display it
nicholas@2645 1077 var node = this.popupOptions[this.currentIndex],
nicholas@2646 1078 converter = new showdown.Converter(),
nicholas@2646 1079 p = new DOMParser();
nicholas@2498 1080 this.popupResponse.innerHTML = "";
nicholas@2648 1081 this.popupTitle.innerHTML = "";
nicholas@2647 1082 this.popupTitle.appendChild(p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").getElementsByTagName("body")[0].firstElementChild);
nicholas@2498 1083 if (node.specification.type == 'question') {
nicholas@2679 1084 postQuestion.call(this, node);
nicholas@2498 1085 } else if (node.specification.type == 'checkbox') {
nicholas@2679 1086 postCheckbox.call(this, node);
nicholas@2498 1087 } else if (node.specification.type == 'radio') {
nicholas@2679 1088 postRadio.call(this, node);
nicholas@2498 1089 } else if (node.specification.type == 'number') {
nicholas@2679 1090 postNumber.call(this, node);
nicholas@2498 1091 } else if (node.specification.type == "video") {
nicholas@2679 1092 postVideo.call(this, node);
nicholas@2491 1093 } else if (node.specification.type == "youtube") {
nicholas@2679 1094 postYoutube.call(this, node);
n@2583 1095 } else if (node.specification.type == "slider") {
nicholas@2679 1096 postSlider.call(this, node);
nicholas@2491 1097 }
nicholas@2498 1098 if (this.currentIndex + 1 == this.popupOptions.length) {
nicholas@2498 1099 if (this.node.location == "pre") {
nicholas@2498 1100 this.buttonProceed.textContent = 'Start';
nicholas@2498 1101 } else {
nicholas@2498 1102 this.buttonProceed.textContent = 'Submit';
nicholas@2498 1103 }
nicholas@2498 1104 } else {
nicholas@2498 1105 this.buttonProceed.textContent = 'Next';
nicholas@2498 1106 }
nicholas@2498 1107 if (this.currentIndex > 0)
nicholas@2498 1108 this.buttonPrevious.style.visibility = 'visible';
nicholas@2498 1109 else
nicholas@2498 1110 this.buttonPrevious.style.visibility = 'hidden';
nicholas@2498 1111 };
nicholas@2498 1112
nicholas@2498 1113 this.initState = function (node, store) {
nicholas@2498 1114 //Call this with your preTest and postTest nodes when needed to
nicholas@2498 1115 // initialise the popup procedure.
nicholas@2498 1116 if (node.options.length > 0) {
nicholas@2498 1117 this.popupOptions = [];
nicholas@2498 1118 this.node = node;
nicholas@2498 1119 this.store = store;
nicholas@2677 1120 node.options.forEach(function (opt) {
nicholas@2498 1121 this.popupOptions.push({
nicholas@2498 1122 specification: opt,
nicholas@2498 1123 response: null
nicholas@2498 1124 });
nicholas@2677 1125 }, this);
nicholas@2498 1126 this.currentIndex = 0;
nicholas@2498 1127 this.showPopup();
nicholas@2498 1128 this.postNode();
nicholas@2498 1129 } else {
nicholas@2498 1130 advanceState();
nicholas@2498 1131 }
nicholas@2498 1132 };
nicholas@2498 1133
nicholas@2498 1134 this.proceedClicked = function () {
nicholas@2498 1135 // Each time the popup button is clicked!
nicholas@2677 1136 if (testState.stateIndex === 0 && specification.calibration) {
nicholas@2224 1137 interfaceContext.calibrationModuleObject.collect();
nicholas@2224 1138 advanceState();
nicholas@2224 1139 return;
nicholas@2224 1140 }
nicholas@2679 1141 var node = this.popupOptions[this.currentIndex],
nicholas@2679 1142 pass = true;
nicholas@2498 1143 if (node.specification.type == 'question') {
nicholas@2498 1144 // Must extract the question data
nicholas@2679 1145 pass = processQuestion.call(this, node);
nicholas@2498 1146 } else if (node.specification.type == 'checkbox') {
nicholas@2498 1147 // Must extract checkbox data
nicholas@2679 1148 pass = processCheckbox.call(this, node);
nicholas@2677 1149 } else if (node.specification.type == "radio") {
nicholas@2464 1150 // Perform the conditional
nicholas@2679 1151 pass = processRadio.call(this, node);
nicholas@2677 1152 } else if (node.specification.type == "number") {
nicholas@2464 1153 // Perform the conditional
nicholas@2679 1154 pass = processNumber.call(this, node);
n@2583 1155 } else if (node.specification.type == 'slider') {
nicholas@2679 1156 pass = processSlider.call(this, node);
nicholas@2679 1157 }
nicholas@2679 1158 if (pass === false) {
nicholas@2679 1159 return;
nicholas@2498 1160 }
nicholas@2498 1161 this.currentIndex++;
nicholas@2498 1162 if (this.currentIndex < this.popupOptions.length) {
nicholas@2498 1163 this.postNode();
nicholas@2498 1164 } else {
nicholas@2498 1165 // Reached the end of the popupOptions
nicholas@2645 1166 this.popupTitle.innerHTML = "";
nicholas@2498 1167 this.popupResponse.innerHTML = "";
nicholas@2498 1168 this.hidePopup();
nicholas@2677 1169 this.popupOptions.forEach(function (node) {
nicholas@2498 1170 this.store.postResult(node);
nicholas@2677 1171 }, this);
nicholas@2224 1172 this.store.complete();
nicholas@2498 1173 advanceState();
nicholas@2498 1174 }
nicholas@2498 1175 };
nicholas@2498 1176
nicholas@2498 1177 this.previousClick = function () {
nicholas@2498 1178 // Triggered when the 'Back' button is clicked in the survey
nicholas@2498 1179 if (this.currentIndex > 0) {
nicholas@2498 1180 this.currentIndex--;
nicholas@2498 1181 this.postNode();
nicholas@2498 1182 }
nicholas@2498 1183 };
nicholas@2498 1184
nicholas@2498 1185 this.resize = function (event) {
nicholas@2498 1186 // Called on window resize;
nicholas@2677 1187 if (this.popup !== null) {
nicholas@2498 1188 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2498 1189 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
nicholas@2498 1190 var blank = document.getElementsByClassName('testHalt')[0];
nicholas@2498 1191 blank.style.width = window.innerWidth;
nicholas@2498 1192 blank.style.height = window.innerHeight;
nicholas@2498 1193 }
nicholas@2498 1194 };
nicholas@2498 1195 this.hideNextButton = function () {
nicholas@2224 1196 this.buttonProceed.style.visibility = "hidden";
nicholas@2677 1197 };
nicholas@2498 1198 this.hidePreviousButton = function () {
nicholas@2224 1199 this.buttonPrevious.style.visibility = "hidden";
nicholas@2677 1200 };
nicholas@2498 1201 this.showNextButton = function () {
nicholas@2224 1202 this.buttonProceed.style.visibility = "visible";
nicholas@2677 1203 };
nicholas@2498 1204 this.showPreviousButton = function () {
nicholas@2224 1205 this.buttonPrevious.style.visibility = "visible";
nicholas@2677 1206 };
nicholas@2224 1207 }
nicholas@2224 1208
nicholas@2498 1209 function advanceState() {
nicholas@2498 1210 // Just for complete clarity
nicholas@2498 1211 testState.advanceState();
nicholas@2224 1212 }
nicholas@2224 1213
nicholas@2498 1214 function stateMachine() {
nicholas@2498 1215 // Object prototype for tracking and managing the test state
nicholas@2498 1216 this.stateMap = [];
nicholas@2498 1217 this.preTestSurvey = null;
nicholas@2498 1218 this.postTestSurvey = null;
nicholas@2498 1219 this.stateIndex = null;
nicholas@2498 1220 this.currentStateMap = null;
nicholas@2498 1221 this.currentStatePosition = null;
nicholas@2224 1222 this.currentStore = null;
nicholas@2498 1223 this.initialise = function () {
nicholas@2498 1224
nicholas@2498 1225 // Get the data from Specification
nicholas@2498 1226 var pagePool = [];
nicholas@2224 1227 var pageInclude = [];
nicholas@2681 1228 var i;
nicholas@2681 1229 for (i = 0; i < specification.pages.length; i++) {
nicholas@2678 1230 var page = specification.pages[i];
nicholas@2224 1231 if (page.alwaysInclude) {
nicholas@2224 1232 pageInclude.push(page);
nicholas@2224 1233 } else {
nicholas@2224 1234 pagePool.push(page);
nicholas@2224 1235 }
nicholas@2498 1236 }
nicholas@2498 1237
nicholas@2224 1238 // Find how many are left to get
nicholas@2224 1239 var numPages = specification.poolSize;
nicholas@2224 1240 if (numPages > pagePool.length) {
nicholas@2224 1241 console.log("WARNING - You have specified more pages in <setup poolSize> than you have created!!");
nicholas@2224 1242 numPages = specification.pages.length;
nicholas@2224 1243 }
nicholas@2678 1244 if (specification.poolSize === 0) {
nicholas@2224 1245 numPages = specification.pages.length;
nicholas@2224 1246 }
nicholas@2224 1247 numPages -= pageInclude.length;
nicholas@2498 1248
nicholas@2224 1249 if (numPages > 0) {
nicholas@2224 1250 // Go find the rest of the pages from the pool
nicholas@2224 1251 var subarr = null;
nicholas@2224 1252 if (specification.randomiseOrder) {
nicholas@2224 1253 // Append a random sub-array
nicholas@2498 1254 subarr = randomSubArray(pagePool, numPages);
nicholas@2224 1255 } else {
nicholas@2224 1256 // Append the matching number
nicholas@2498 1257 subarr = pagePool.slice(0, numPages);
nicholas@2224 1258 }
nicholas@2224 1259 pageInclude = pageInclude.concat(subarr);
nicholas@2224 1260 }
nicholas@2498 1261
nicholas@2224 1262 // We now have our selected pages in pageInclude array
nicholas@2498 1263 if (specification.randomiseOrder) {
nicholas@2498 1264 pageInclude = randomiseOrder(pageInclude);
nicholas@2498 1265 }
nicholas@2681 1266 for (i = 0; i < pageInclude.length; i++) {
nicholas@2224 1267 pageInclude[i].presentedId = i;
nicholas@2498 1268 this.stateMap.push(pageInclude[i]);
nicholas@2224 1269 // For each selected page, we must get the sub pool
nicholas@2678 1270 if (pageInclude[i].poolSize !== 0 && pageInclude[i].poolSize !== pageInclude[i].audioElements.length) {
nicholas@2224 1271 var elemInclude = [];
nicholas@2224 1272 var elemPool = [];
nicholas@2681 1273 for (var j = 0; j < pageInclude[i].audioElements.length; j++) {
nicholas@2681 1274 var elem = pageInclude[i].audioElements[j];
nicholas@2559 1275 if (elem.alwaysInclude || elem.type != "normal") {
nicholas@2224 1276 elemInclude.push(elem);
nicholas@2224 1277 } else {
nicholas@2224 1278 elemPool.push(elem);
nicholas@2224 1279 }
nicholas@2224 1280 }
nicholas@2224 1281 var numElems = pageInclude[i].poolSize - elemInclude.length;
nicholas@2498 1282 pageInclude[i].audioElements = elemInclude.concat(randomSubArray(elemPool, numElems));
nicholas@2224 1283 }
nicholas@2224 1284 storage.createTestPageStore(pageInclude[i]);
nicholas@2224 1285 audioEngineContext.loadPageData(pageInclude[i]);
nicholas@2498 1286 }
nicholas@2498 1287
nicholas@2678 1288 if (specification.preTest !== null) {
nicholas@2498 1289 this.preTestSurvey = specification.preTest;
nicholas@2498 1290 }
nicholas@2678 1291 if (specification.postTest !== null) {
nicholas@2498 1292 this.postTestSurvey = specification.postTest;
nicholas@2498 1293 }
nicholas@2498 1294
nicholas@2498 1295 if (this.stateMap.length > 0) {
nicholas@2678 1296 if (this.stateIndex !== null) {
nicholas@2498 1297 console.log('NOTE - State already initialise');
nicholas@2498 1298 }
nicholas@2498 1299 this.stateIndex = -2;
nicholas@2224 1300 console.log('Starting test...');
nicholas@2498 1301 } else {
nicholas@2498 1302 console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
nicholas@2498 1303 }
nicholas@2498 1304 };
nicholas@2498 1305 this.advanceState = function () {
nicholas@2678 1306 if (this.stateIndex === null) {
nicholas@2498 1307 this.initialise();
nicholas@2498 1308 }
nicholas@2357 1309 if (this.stateIndex > -2) {
nicholas@2357 1310 storage.update();
nicholas@2357 1311 }
nicholas@2498 1312 if (this.stateIndex == -2) {
nicholas@2224 1313 this.stateIndex++;
n@2695 1314 if (this.preTestSurvey !== undefined) {
nicholas@2498 1315 popup.initState(this.preTestSurvey, storage.globalPreTest);
nicholas@2498 1316 } else {
nicholas@2498 1317 this.advanceState();
nicholas@2498 1318 }
nicholas@2498 1319 } else if (this.stateIndex == -1) {
nicholas@2224 1320 this.stateIndex++;
nicholas@2224 1321 if (specification.calibration) {
nicholas@2224 1322 popup.showPopup();
nicholas@2224 1323 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 1324 interfaceContext.calibrationModuleObject = new interfaceContext.calibrationModule();
nicholas@2224 1325 interfaceContext.calibrationModuleObject.build(popup.popupResponse);
nicholas@2224 1326 popup.hidePreviousButton();
nicholas@2224 1327 } else {
nicholas@2224 1328 this.advanceState();
nicholas@2224 1329 }
nicholas@2498 1330 } else if (this.stateIndex == this.stateMap.length) {
nicholas@2498 1331 // All test pages complete, post test
nicholas@2498 1332 console.log('Ending test ...');
nicholas@2498 1333 this.stateIndex++;
n@2695 1334 if (this.postTestSurvey === undefined) {
nicholas@2498 1335 this.advanceState();
nicholas@2498 1336 } else {
nicholas@2498 1337 popup.initState(this.postTestSurvey, storage.globalPostTest);
nicholas@2498 1338 }
nicholas@2498 1339 } else if (this.stateIndex > this.stateMap.length) {
nicholas@2498 1340 createProjectSave(specification.projectReturn);
nicholas@2498 1341 } else {
nicholas@2224 1342 popup.hidePopup();
nicholas@2678 1343 if (this.currentStateMap === null) {
nicholas@2498 1344 this.currentStateMap = this.stateMap[this.stateIndex];
nicholas@2349 1345 // Find and extract the outside reference
nicholas@2498 1346 var elements = [],
nicholas@2498 1347 ref = [];
nicholas@2681 1348 var elem = this.currentStateMap.audioElements.pop();
nicholas@2681 1349 while (elem) {
nicholas@2399 1350 if (elem.type == "outside-reference") {
nicholas@2399 1351 ref.push(elem);
nicholas@2498 1352 } else {
nicholas@2399 1353 elements.push(elem);
nicholas@2399 1354 }
nicholas@2681 1355 elem = this.currentStateMap.audioElements.pop();
nicholas@2349 1356 }
nicholas@2443 1357 elements = elements.reverse();
nicholas@2498 1358 if (this.currentStateMap.randomiseOrder) {
nicholas@2498 1359 elements = randomiseOrder(elements);
nicholas@2498 1360 }
nicholas@2399 1361 this.currentStateMap.audioElements = elements.concat(ref);
nicholas@2498 1362
nicholas@2224 1363 this.currentStore = storage.testPages[this.stateIndex];
n@2695 1364 if (this.currentStateMap.preTest !== undefined) {
nicholas@2498 1365 this.currentStatePosition = 'pre';
nicholas@2498 1366 popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest);
nicholas@2498 1367 } else {
nicholas@2498 1368 this.currentStatePosition = 'test';
nicholas@2498 1369 }
nicholas@2498 1370 interfaceContext.newPage(this.currentStateMap, storage.testPages[this.stateIndex]);
nicholas@2498 1371 return;
nicholas@2498 1372 }
nicholas@2498 1373 switch (this.currentStatePosition) {
nicholas@2498 1374 case 'pre':
nicholas@2498 1375 this.currentStatePosition = 'test';
nicholas@2498 1376 break;
nicholas@2498 1377 case 'test':
nicholas@2498 1378 this.currentStatePosition = 'post';
nicholas@2498 1379 // Save the data
nicholas@2498 1380 this.testPageCompleted();
nicholas@2698 1381 if (this.currentStateMap.postTest === undefined) {
nicholas@2498 1382 this.advanceState();
nicholas@2498 1383 return;
nicholas@2498 1384 } else {
nicholas@2498 1385 popup.initState(this.currentStateMap.postTest, storage.testPages[this.stateIndex].postTest);
nicholas@2498 1386 }
nicholas@2498 1387 break;
nicholas@2498 1388 case 'post':
nicholas@2498 1389 this.stateIndex++;
nicholas@2498 1390 this.currentStateMap = null;
nicholas@2498 1391 this.advanceState();
nicholas@2498 1392 break;
nicholas@2678 1393 }
nicholas@2498 1394 }
nicholas@2498 1395 };
nicholas@2498 1396
nicholas@2498 1397 this.testPageCompleted = function () {
nicholas@2498 1398 // Function called each time a test page has been completed
nicholas@2498 1399 var storePoint = storage.testPages[this.stateIndex];
nicholas@2498 1400 // First get the test metric
nicholas@2498 1401
nicholas@2498 1402 var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0];
nicholas@2498 1403 if (audioEngineContext.metric.enableTestTimer) {
nicholas@2498 1404 var testTime = storePoint.parent.document.createElement('metricresult');
nicholas@2498 1405 testTime.id = 'testTime';
nicholas@2498 1406 testTime.textContent = audioEngineContext.timer.testDuration;
nicholas@2498 1407 metric.appendChild(testTime);
nicholas@2498 1408 }
nicholas@2498 1409
nicholas@2498 1410 var audioObjects = audioEngineContext.audioObjects;
nicholas@2681 1411 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2498 1412 ao.exportXMLDOM();
nicholas@2681 1413 });
nicholas@2681 1414 interfaceContext.commentQuestions.forEach(function (element) {
nicholas@2498 1415 element.exportXMLDOM(storePoint);
nicholas@2681 1416 });
nicholas@2498 1417 pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
nicholas@2224 1418 storePoint.complete();
nicholas@2498 1419 };
nicholas@2498 1420
nicholas@2498 1421 this.getCurrentTestPage = function () {
nicholas@2498 1422 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
nicholas@2310 1423 return this.currentStateMap;
nicholas@2310 1424 } else {
nicholas@2310 1425 return null;
nicholas@2310 1426 }
nicholas@2678 1427 };
nicholas@2498 1428 this.getCurrentTestPageStore = function () {
nicholas@2498 1429 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
nicholas@2312 1430 return this.currentStore;
nicholas@2312 1431 } else {
nicholas@2312 1432 return null;
nicholas@2312 1433 }
nicholas@2678 1434 };
nicholas@2224 1435 }
nicholas@2224 1436
nicholas@2224 1437 function AudioEngine(specification) {
nicholas@2498 1438
nicholas@2498 1439 // Create two output paths, the main outputGain and fooGain.
nicholas@2498 1440 // Output gain is default to 1 and any items for playback route here
nicholas@2498 1441 // Foo gain is used for analysis to ensure paths get processed, but are not heard
nicholas@2498 1442 // because web audio will optimise and any route which does not go to the destination gets ignored.
nicholas@2498 1443 this.outputGain = audioContext.createGain();
nicholas@2498 1444 this.fooGain = audioContext.createGain();
nicholas@2508 1445 this.fooGain.gain.value = 0;
nicholas@2498 1446
nicholas@2498 1447 // Use this to detect playback state: 0 - stopped, 1 - playing
nicholas@2498 1448 this.status = 0;
nicholas@2498 1449
nicholas@2498 1450 // Connect both gains to output
nicholas@2498 1451 this.outputGain.connect(audioContext.destination);
nicholas@2498 1452 this.fooGain.connect(audioContext.destination);
nicholas@2498 1453
nicholas@2498 1454 // Create the timer Object
nicholas@2498 1455 this.timer = new timer();
nicholas@2498 1456 // Create session metrics
nicholas@2498 1457 this.metric = new sessionMetrics(this, specification);
nicholas@2498 1458
nicholas@2498 1459 this.loopPlayback = false;
nicholas@2351 1460 this.synchPlayback = false;
nicholas@2351 1461 this.pageSpecification = null;
nicholas@2498 1462
nicholas@2498 1463 this.pageStore = null;
nicholas@2498 1464
nicholas@2508 1465 // Chrome 53+ Error solution
nicholas@2508 1466 // Empty buffer for keep-alive
nicholas@2508 1467 var nullBuffer = audioContext.createBuffer(1, audioContext.sampleRate, audioContext.sampleRate);
nicholas@2508 1468 this.nullBufferSource = audioContext.createBufferSource();
nicholas@2508 1469 this.nullBufferSource.buffer = nullBuffer;
nicholas@2508 1470 this.nullBufferSource.loop = true;
nicholas@2508 1471 this.nullBufferSource.start(0);
nicholas@2508 1472
nicholas@2498 1473 // Create store for new audioObjects
nicholas@2498 1474 this.audioObjects = [];
nicholas@2498 1475
nicholas@2498 1476 this.buffers = [];
nicholas@2498 1477 this.bufferObj = function () {
nicholas@2617 1478 var urls = [];
nicholas@2498 1479 this.buffer = null;
nicholas@2498 1480 this.users = [];
nicholas@2224 1481 this.progress = 0;
nicholas@2224 1482 this.status = 0;
nicholas@2498 1483 this.ready = function () {
nicholas@2498 1484 if (this.status >= 2) {
nicholas@2224 1485 this.status = 3;
nicholas@2224 1486 }
nicholas@2498 1487 for (var i = 0; i < this.users.length; i++) {
nicholas@2498 1488 this.users[i].state = 1;
nicholas@2678 1489 if (this.users[i].interfaceDOM !== null) {
nicholas@2498 1490 this.users[i].bufferLoaded(this);
nicholas@2498 1491 }
nicholas@2498 1492 }
nicholas@2498 1493 };
nicholas@2617 1494 this.setUrls = function (obj) {
nicholas@2617 1495 // Obj must be an array of pairs:
nicholas@2617 1496 // [{sampleRate, url}]
nicholas@2617 1497 var localFs = audioContext.sampleRate,
nicholas@2617 1498 list = [],
nicholas@2617 1499 i;
nicholas@2617 1500 for (i = 0; i < obj.length; i++) {
nicholas@2617 1501 if (obj[i].sampleRate == localFs) {
nicholas@2617 1502 list.push(obj.splice(i, 1)[0]);
nicholas@2617 1503 }
nicholas@2617 1504 }
nicholas@2617 1505 list = list.concat(obj);
nicholas@2617 1506 urls = list;
nicholas@2617 1507 };
nicholas@2617 1508 this.hasUrl = function (checkUrl) {
nicholas@2617 1509 var l = urls.length,
nicholas@2617 1510 i;
nicholas@2617 1511 for (i = 0; i < l; i++) {
nicholas@2617 1512 if (urls[i].url == checkUrl) {
nicholas@2617 1513 return true;
nicholas@2617 1514 }
nicholas@2617 1515 }
nicholas@2617 1516 return false;
nicholas@2678 1517 };
nicholas@2617 1518 this.getMedia = function () {
nicholas@2615 1519 var self = this;
nicholas@2616 1520 var currentUrlIndex = 0;
nicholas@2498 1521
nicholas@2615 1522 function get(fqurl) {
nicholas@2615 1523 return new Promise(function (resolve, reject) {
nicholas@2615 1524 var req = new XMLHttpRequest();
nicholas@2615 1525 req.open('GET', fqurl, true);
nicholas@2615 1526 req.responseType = 'arraybuffer';
nicholas@2615 1527 req.onload = function () {
nicholas@2615 1528 if (req.status == 200) {
nicholas@2615 1529 resolve(req.response);
nicholas@2615 1530 }
nicholas@2615 1531 };
nicholas@2615 1532 req.onerror = function () {
nicholas@2615 1533 reject(new Error(req.statusText));
nicholas@2615 1534 };
nicholas@2615 1535
nicholas@2615 1536 req.addEventListener("progress", progressCallback.bind(self));
nicholas@2615 1537 req.send();
nicholas@2615 1538 });
nicholas@2615 1539 }
nicholas@2615 1540
nicholas@2615 1541 function getNextURL() {
nicholas@2615 1542 currentUrlIndex++;
nicholas@2615 1543 var self = this;
nicholas@2617 1544 if (currentUrlIndex >= urls.length) {
nicholas@2615 1545 processError();
nicholas@2615 1546 } else {
nicholas@2617 1547 return get(urls[currentUrlIndex].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
nicholas@2615 1548 }
nicholas@2615 1549 }
nicholas@2498 1550
nicholas@2498 1551 // Create callback to decode the data asynchronously
nicholas@2615 1552 function processAudio(response) {
nicholas@2615 1553 var self = this;
nicholas@2615 1554 return audioContext.decodeAudioData(response, function (decodedData) {
nicholas@2615 1555 self.buffer = decodedData;
nicholas@2615 1556 self.status = 2;
nicholas@2615 1557 calculateLoudness(self, "I");
nicholas@2615 1558 return true;
nicholas@2498 1559 }, function (e) {
nicholas@2403 1560 var waveObj = new WAVE();
nicholas@2678 1561 if (waveObj.open(response) === 0) {
nicholas@2615 1562 self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate);
nicholas@2498 1563 for (var c = 0; c < waveObj.num_channels; c++) {
nicholas@2615 1564 var buffer_ptr = self.buffer.getChannelData(c);
nicholas@2498 1565 for (var n = 0; n < waveObj.num_samples; n++) {
nicholas@2403 1566 buffer_ptr[n] = waveObj.decoded_data[c][n];
nicholas@2224 1567 }
nicholas@2224 1568 }
nicholas@2403 1569 }
nicholas@2678 1570 if (self.buffer !== undefined) {
nicholas@2615 1571 self.status = 2;
nicholas@2615 1572 calculateLoudness(self, "I");
nicholas@2615 1573 return true;
nicholas@2403 1574 }
nicholas@2678 1575 waveObj = undefined;
nicholas@2615 1576 return false;
nicholas@2403 1577 });
nicholas@2615 1578 }
nicholas@2498 1579
nicholas@2224 1580 // Create callback for any error in loading
nicholas@2615 1581 function processError() {
nicholas@2615 1582 this.status = -1;
nicholas@2615 1583 for (var i = 0; i < this.users.length; i++) {
nicholas@2615 1584 this.users[i].state = -1;
nicholas@2678 1585 if (this.users[i].interfaceDOM !== null) {
nicholas@2615 1586 this.users[i].bufferLoaded(this);
nicholas@2224 1587 }
nicholas@2224 1588 }
nicholas@2617 1589 interfaceContext.lightbox.post("Error", "Could not load resource " + urls[currentUrlIndex].url);
nicholas@2224 1590 }
nicholas@2498 1591
nicholas@2615 1592 function progressCallback(event) {
nicholas@2498 1593 if (event.lengthComputable) {
nicholas@2615 1594 this.progress = event.loaded / event.total;
nicholas@2615 1595 for (var i = 0; i < this.users.length; i++) {
nicholas@2678 1596 if (this.users[i].interfaceDOM !== null) {
nicholas@2615 1597 if (typeof this.users[i].interfaceDOM.updateLoading === "function") {
nicholas@2615 1598 this.users[i].interfaceDOM.updateLoading(this.progress * 100);
nicholas@2498 1599 }
nicholas@2498 1600 }
nicholas@2498 1601 }
nicholas@2498 1602 }
nicholas@2681 1603 }
nicholas@2615 1604
nicholas@2615 1605 this.progress = 0;
nicholas@2224 1606 this.status = 1;
nicholas@2617 1607 currentUrlIndex = 0;
nicholas@2617 1608 get(urls[0].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
nicholas@2498 1609 };
nicholas@2498 1610
nicholas@2498 1611 this.registerAudioObject = function (audioObject) {
nicholas@2224 1612 // Called by an audioObject to register to the buffer for use
nicholas@2224 1613 // First check if already in the register pool
nicholas@2681 1614 this.users.forEach(function (object) {
nicholas@2682 1615 if (audioObject.id == object.id) {
nicholas@2498 1616 return 0;
nicholas@2498 1617 }
nicholas@2681 1618 });
nicholas@2224 1619 this.users.push(audioObject);
nicholas@2498 1620 if (this.status == 3 || this.status == -1) {
nicholas@2224 1621 // The buffer is already ready, trigger bufferLoaded
nicholas@2224 1622 audioObject.bufferLoaded(this);
nicholas@2224 1623 }
nicholas@2224 1624 };
nicholas@2498 1625
nicholas@2498 1626 this.copyBuffer = function (preSilenceTime, postSilenceTime) {
nicholas@2224 1627 // Copies the entire bufferObj.
nicholas@2678 1628 if (preSilenceTime === undefined) {
nicholas@2498 1629 preSilenceTime = 0;
nicholas@2498 1630 }
nicholas@2678 1631 if (postSilenceTime === undefined) {
nicholas@2498 1632 postSilenceTime = 0;
nicholas@2498 1633 }
nicholas@2498 1634 var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate);
nicholas@2498 1635 var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate);
nicholas@2498 1636 var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples;
nicholas@2460 1637 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
nicholas@2681 1638 var c;
nicholas@2224 1639 // Now we can use some efficient background copy schemes if we are just padding the end
nicholas@2678 1640 if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") {
nicholas@2681 1641 for (c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2498 1642 copybuffer.copyToChannel(this.buffer.getChannelData(c), c);
nicholas@2224 1643 }
nicholas@2224 1644 } else {
nicholas@2681 1645 for (c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2224 1646 var src = this.buffer.getChannelData(c);
nicholas@2460 1647 var dst = copybuffer.getChannelData(c);
nicholas@2498 1648 for (var n = 0; n < src.length; n++)
nicholas@2498 1649 dst[n + preSilenceSamples] = src[n];
nicholas@2224 1650 }
nicholas@2224 1651 }
nicholas@2224 1652 // Copy in the rest of the buffer information
nicholas@2460 1653 copybuffer.lufs = this.buffer.lufs;
nicholas@2460 1654 copybuffer.playbackGain = this.buffer.playbackGain;
nicholas@2460 1655 return copybuffer;
nicholas@2678 1656 };
nicholas@2498 1657
nicholas@2498 1658 this.cropBuffer = function (startTime, stopTime) {
nicholas@2460 1659 // Copy and return the cropped buffer
nicholas@2498 1660 var start_sample = Math.floor(startTime * this.buffer.sampleRate);
nicholas@2498 1661 var stop_sample = Math.floor(stopTime * this.buffer.sampleRate);
nicholas@2460 1662 var newLength = stop_sample - start_sample;
nicholas@2460 1663 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
nicholas@2460 1664 // Now we can use some efficient background copy schemes if we are just padding the end
nicholas@2498 1665 for (var c = 0; c < this.buffer.numberOfChannels; c++) {
nicholas@2460 1666 var buffer = this.buffer.getChannelData(c);
nicholas@2498 1667 var sub_frame = buffer.subarray(start_sample, stop_sample);
nicholas@2460 1668 if (typeof copybuffer.copyToChannel == "function") {
nicholas@2498 1669 copybuffer.copyToChannel(sub_frame, c);
nicholas@2460 1670 } else {
nicholas@2460 1671 var dst = copybuffer.getChannelData(c);
nicholas@2498 1672 for (var n = 0; n < newLength; n++)
nicholas@2505 1673 dst[n] = buffer[n + start_sample];
nicholas@2460 1674 }
nicholas@2460 1675 }
nicholas@2460 1676 return copybuffer;
nicholas@2678 1677 };
nicholas@2498 1678 };
nicholas@2498 1679
nicholas@2498 1680 this.loadPageData = function (page) {
nicholas@2224 1681 // Load the URL from pages
nicholas@2681 1682 function loadAudioElementData(element) {
nicholas@2224 1683 var URL = page.hostURL + element.url;
nicholas@2681 1684 var buffer = this.buffers.find(function (buffObj) {
nicholas@2681 1685 return buffObj.hasUrl(URL);
nicholas@2681 1686 });
nicholas@2681 1687 if (buffer === undefined) {
nicholas@2224 1688 buffer = new this.bufferObj();
nicholas@2617 1689 var urls = [{
nicholas@2617 1690 url: URL,
nicholas@2617 1691 sampleRate: element.sampleRate
nicholas@2617 1692 }];
nicholas@2615 1693 element.alternatives.forEach(function (e) {
nicholas@2617 1694 urls.push({
nicholas@2617 1695 url: e.url,
nicholas@2617 1696 sampleRate: e.sampleRate
nicholas@2617 1697 });
nicholas@2615 1698 });
nicholas@2617 1699 buffer.setUrls(urls);
nicholas@2617 1700 buffer.getMedia();
nicholas@2224 1701 this.buffers.push(buffer);
nicholas@2224 1702 }
nicholas@2224 1703 }
nicholas@2681 1704 page.audioElements.forEach(loadAudioElementData, this);
nicholas@2224 1705 };
nicholas@2498 1706
nicholas@2681 1707 function playNormal(id) {
nicholas@2681 1708 var playTime = audioContext.currentTime + 0.1;
nicholas@2681 1709 var stopTime = playTime + specification.crossFade;
nicholas@2681 1710 this.audioObjects.forEach(function (ao) {
nicholas@2681 1711 if (ao.id === id) {
nicholas@2681 1712 ao.play(playTime);
nicholas@2681 1713 } else {
nicholas@2681 1714 ao.stop(stopTime);
nicholas@2681 1715 }
nicholas@2681 1716 });
nicholas@2681 1717 }
nicholas@2681 1718
nicholas@2681 1719 function playLoopSync(id) {
nicholas@2681 1720 var playTime = audioContext.currentTime + 0.1;
nicholas@2681 1721 var stopTime = playTime + specification.crossFade;
nicholas@2681 1722 this.audioObjects.forEach(function (ao) {
nicholas@2681 1723 ao.play(playTime);
nicholas@2681 1724 if (ao.id === id) {
nicholas@2681 1725 ao.loopStart(playTime);
nicholas@2681 1726 } else {
nicholas@2681 1727 ao.loopStop(stopTime);
nicholas@2681 1728 }
nicholas@2681 1729 });
nicholas@2681 1730 }
nicholas@2681 1731
nicholas@2498 1732 this.play = function (id) {
nicholas@2498 1733 // Start the timer and set the audioEngine state to playing (1)
nicholas@2681 1734 if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) {
nicholas@2681 1735 throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
nicholas@2498 1736 }
nicholas@2678 1737 if (this.status === 1) {
nicholas@2498 1738 this.timer.startTest();
nicholas@2681 1739 interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
nicholas@2498 1740 if (this.synchPlayback && this.loopPlayback) {
nicholas@2351 1741 // Traditional looped playback
nicholas@2681 1742 playLoopSync.call(this, id);
nicholas@2681 1743 } else {
nicholas@2681 1744 if (this.bufferReady(id) === false) {
nicholas@2681 1745 console.log("Cannot play. Buffer not ready");
nicholas@2681 1746 return;
nicholas@2498 1747 }
nicholas@2681 1748 playNormal.call(this, id);
nicholas@2498 1749 }
nicholas@2498 1750 interfaceContext.playhead.start();
nicholas@2498 1751 }
nicholas@2498 1752 };
nicholas@2224 1753
nicholas@2498 1754 this.stop = function () {
nicholas@2498 1755 // Send stop and reset command to all playback buffers
nicholas@2498 1756 if (this.status == 1) {
nicholas@2498 1757 var setTime = audioContext.currentTime + 0.1;
nicholas@2681 1758 this.audioObjects.forEach(function (a) {
nicholas@2681 1759 a.stop(setTime);
nicholas@2681 1760 });
nicholas@2498 1761 interfaceContext.playhead.stop();
nicholas@2498 1762 }
nicholas@2498 1763 };
nicholas@2498 1764
nicholas@2498 1765 this.newTrack = function (element) {
nicholas@2498 1766 // Pull data from given URL into new audio buffer
nicholas@2498 1767 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
nicholas@2498 1768
nicholas@2498 1769 // Create the audioObject with ID of the new track length;
nicholas@2682 1770 var audioObjectId = this.audioObjects.length;
nicholas@2498 1771 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
nicholas@2498 1772
nicholas@2498 1773 // Check if audioObject buffer is currently stored by full URL
nicholas@2498 1774 var URL = testState.currentStateMap.hostURL + element.url;
nicholas@2681 1775 var buffer = this.buffers.find(function (buffObj) {
nicholas@2681 1776 return buffObj.hasUrl(URL);
nicholas@2681 1777 });
nicholas@2681 1778 if (buffer === undefined) {
nicholas@2498 1779 console.log("[WARN]: Buffer was not loaded in pre-test! " + URL);
nicholas@2498 1780 buffer = new this.bufferObj();
nicholas@2224 1781 this.buffers.push(buffer);
nicholas@2498 1782 buffer.getMedia(URL);
nicholas@2498 1783 }
nicholas@2498 1784 this.audioObjects[audioObjectId].specification = element;
nicholas@2498 1785 this.audioObjects[audioObjectId].url = URL;
nicholas@2498 1786 // Obtain store node
nicholas@2498 1787 var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
nicholas@2498 1788 for (var i = 0; i < aeNodes.length; i++) {
nicholas@2498 1789 if (aeNodes[i].getAttribute("ref") == element.id) {
nicholas@2498 1790 this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
nicholas@2498 1791 break;
nicholas@2498 1792 }
nicholas@2498 1793 }
nicholas@2224 1794 buffer.registerAudioObject(this.audioObjects[audioObjectId]);
nicholas@2498 1795 return this.audioObjects[audioObjectId];
nicholas@2498 1796 };
nicholas@2498 1797
nicholas@2498 1798 this.newTestPage = function (audioHolderObject, store) {
nicholas@2498 1799 this.pageStore = store;
nicholas@2351 1800 this.pageSpecification = audioHolderObject;
nicholas@2498 1801 this.status = 0;
nicholas@2498 1802 this.audioObjectsReady = false;
nicholas@2498 1803 this.metric.reset();
nicholas@2681 1804 this.buffers.forEach(function (buffer) {
nicholas@2681 1805 buffer.users = [];
nicholas@2681 1806 });
nicholas@2498 1807 this.audioObjects = [];
nicholas@2224 1808 this.timer = new timer();
nicholas@2224 1809 this.loopPlayback = audioHolderObject.loop;
nicholas@2351 1810 this.synchPlayback = audioHolderObject.synchronous;
nicholas@2498 1811 };
nicholas@2498 1812
nicholas@2498 1813 this.checkAllPlayed = function () {
nicholas@2682 1814 var arr = [];
nicholas@2498 1815 for (var id = 0; id < this.audioObjects.length; id++) {
nicholas@2678 1816 if (this.audioObjects[id].metric.wasListenedTo === false) {
nicholas@2498 1817 arr.push(this.audioObjects[id].id);
nicholas@2498 1818 }
nicholas@2498 1819 }
nicholas@2498 1820 return arr;
nicholas@2498 1821 };
nicholas@2498 1822
nicholas@2498 1823 this.checkAllReady = function () {
nicholas@2498 1824 var ready = true;
nicholas@2498 1825 for (var i = 0; i < this.audioObjects.length; i++) {
nicholas@2678 1826 if (this.audioObjects[i].state === 0) {
nicholas@2498 1827 // Track not ready
nicholas@2498 1828 console.log('WAIT -- audioObject ' + i + ' not ready yet!');
nicholas@2498 1829 ready = false;
nicholas@2678 1830 }
nicholas@2498 1831 }
nicholas@2498 1832 return ready;
nicholas@2498 1833 };
nicholas@2498 1834
nicholas@2498 1835 this.setSynchronousLoop = function () {
nicholas@2570 1836 // Pads the signals so they are all exactly the same duration
nicholas@2570 1837 // Get the duration of the longest signal.
nicholas@2570 1838 var duration = 0;
nicholas@2498 1839 var maxId;
nicholas@2498 1840 for (var i = 0; i < this.audioObjects.length; i++) {
nicholas@2570 1841 if (duration < this.audioObjects[i].buffer.buffer.duration) {
nicholas@2570 1842 duration = this.audioObjects[i].buffer.buffer.duration;
nicholas@2498 1843 maxId = i;
nicholas@2498 1844 }
nicholas@2498 1845 }
nicholas@2498 1846 // Extract the audio and zero-pad
nicholas@2681 1847 this.audioObjects.forEach(function (ao) {
nicholas@2570 1848 if (ao.buffer.buffer.duration !== duration) {
nicholas@2570 1849 ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration);
nicholas@2500 1850 }
nicholas@2681 1851 });
nicholas@2498 1852 };
nicholas@2498 1853
nicholas@2498 1854 this.bufferReady = function (id) {
nicholas@2498 1855 if (this.checkAllReady()) {
nicholas@2498 1856 if (this.synchPlayback) {
nicholas@2498 1857 this.setSynchronousLoop();
nicholas@2498 1858 }
nicholas@2460 1859 this.status = 1;
nicholas@2460 1860 return true;
nicholas@2460 1861 }
nicholas@2460 1862 return false;
nicholas@2678 1863 };
nicholas@2498 1864
nicholas@2224 1865 }
nicholas@2224 1866
nicholas@2224 1867 function audioObject(id) {
nicholas@2498 1868 // The main buffer object with common control nodes to the AudioEngine
nicholas@2498 1869
nicholas@2681 1870 this.specification = undefined;
nicholas@2498 1871 this.id = id;
nicholas@2498 1872 this.state = 0; // 0 - no data, 1 - ready
nicholas@2498 1873 this.url = null; // Hold the URL given for the output back to the results.
nicholas@2498 1874 this.metric = new metricTracker(this);
nicholas@2498 1875 this.storeDOM = null;
nicholas@2498 1876
nicholas@2498 1877 // Bindings for GUI
nicholas@2498 1878 this.interfaceDOM = null;
nicholas@2498 1879 this.commentDOM = null;
nicholas@2498 1880
nicholas@2498 1881 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
nicholas@2498 1882 this.bufferNode = undefined;
nicholas@2498 1883 this.outputGain = audioContext.createGain();
nicholas@2498 1884
nicholas@2498 1885 this.onplayGain = 1.0;
nicholas@2498 1886
nicholas@2498 1887 // Connect buffer to the audio graph
nicholas@2498 1888 this.outputGain.connect(audioEngineContext.outputGain);
nicholas@2508 1889 audioEngineContext.nullBufferSource.connect(this.outputGain);
nicholas@2498 1890
nicholas@2498 1891 // the audiobuffer is not designed for multi-start playback
nicholas@2498 1892 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
nicholas@2681 1893 this.buffer = undefined;
nicholas@2498 1894
nicholas@2498 1895 this.bufferLoaded = function (callee) {
nicholas@2498 1896 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
nicholas@2498 1897 // audioObject and trigger the interfaceDOM.enable() function for user feedback
nicholas@2224 1898 if (callee.status == -1) {
nicholas@2224 1899 // ERROR
nicholas@2224 1900 this.state = -1;
nicholas@2678 1901 if (this.interfaceDOM !== null) {
nicholas@2498 1902 this.interfaceDOM.error();
nicholas@2498 1903 }
nicholas@2224 1904 this.buffer = callee;
nicholas@2224 1905 return;
nicholas@2224 1906 }
nicholas@2224 1907 this.buffer = callee;
nicholas@2224 1908 var preSilenceTime = this.specification.preSilence || this.specification.parent.preSilence || specification.preSilence || 0.0;
nicholas@2224 1909 var postSilenceTime = this.specification.postSilence || this.specification.parent.postSilence || specification.postSilence || 0.0;
nicholas@2460 1910 var startTime = this.specification.startTime;
nicholas@2460 1911 var stopTime = this.specification.stopTime;
nicholas@2460 1912 var copybuffer = new callee.constructor();
nicholas@2500 1913
nicholas@2500 1914 copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration);
nicholas@2678 1915 if (preSilenceTime !== 0 || postSilenceTime !== 0) {
nicholas@2500 1916 copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime);
nicholas@2460 1917 }
nicholas@2500 1918
nicholas@2660 1919 copybuffer.buffer.lufs = callee.buffer.lufs;
nicholas@2500 1920 this.buffer = copybuffer;
nicholas@2498 1921
nicholas@2661 1922 var targetLUFS = this.specification.loudness || this.specification.parent.loudness || specification.loudness;
nicholas@2498 1923 if (typeof targetLUFS === "number" && isFinite(targetLUFS)) {
nicholas@2498 1924 this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
nicholas@2498 1925 } else {
nicholas@2498 1926 this.buffer.buffer.playbackGain = 1.0;
nicholas@2498 1927 }
nicholas@2678 1928 if (this.interfaceDOM !== null) {
nicholas@2498 1929 this.interfaceDOM.enable();
nicholas@2498 1930 }
nicholas@2498 1931 this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0);
nicholas@2498 1932 this.storeDOM.setAttribute('playGain', linearToDecibel(this.onplayGain));
nicholas@2460 1933 this.state = 1;
nicholas@2460 1934 audioEngineContext.bufferReady(id);
nicholas@2498 1935 };
nicholas@2498 1936
nicholas@2498 1937 this.bindInterface = function (interfaceObject) {
nicholas@2498 1938 this.interfaceDOM = interfaceObject;
nicholas@2498 1939 this.metric.initialise(interfaceObject.getValue());
nicholas@2498 1940 if (this.state == 1) {
nicholas@2498 1941 this.interfaceDOM.enable();
nicholas@2498 1942 } else if (this.state == -1) {
nicholas@2224 1943 // ERROR
nicholas@2224 1944 this.interfaceDOM.error();
nicholas@2224 1945 return;
nicholas@2224 1946 }
nicholas@2498 1947 this.storeDOM.setAttribute('presentedId', interfaceObject.getPresentedId());
nicholas@2498 1948 };
nicholas@2498 1949
nicholas@2498 1950 this.loopStart = function (setTime) {
nicholas@2498 1951 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, setTime);
nicholas@2498 1952 this.metric.startListening(audioEngineContext.timer.getTestTime());
nicholas@2224 1953 this.interfaceDOM.startPlayback();
nicholas@2498 1954 };
nicholas@2498 1955
nicholas@2498 1956 this.loopStop = function (setTime) {
nicholas@2678 1957 if (this.outputGain.gain.value !== 0.0) {
nicholas@2498 1958 this.outputGain.gain.linearRampToValueAtTime(0.0, setTime);
nicholas@2498 1959 this.metric.stopListening(audioEngineContext.timer.getTestTime());
nicholas@2498 1960 }
nicholas@2224 1961 this.interfaceDOM.stopPlayback();
nicholas@2498 1962 };
nicholas@2498 1963
nicholas@2498 1964 this.play = function (startTime) {
nicholas@2678 1965 if (this.bufferNode === undefined && this.buffer.buffer !== undefined) {
nicholas@2498 1966 this.bufferNode = audioContext.createBufferSource();
nicholas@2498 1967 this.bufferNode.owner = this;
nicholas@2498 1968 this.bufferNode.connect(this.outputGain);
nicholas@2498 1969 this.bufferNode.buffer = this.buffer.buffer;
nicholas@2498 1970 this.bufferNode.loop = audioEngineContext.loopPlayback;
nicholas@2498 1971 this.bufferNode.onended = function (event) {
nicholas@2498 1972 // Safari does not like using 'this' to reference the calling object!
nicholas@2498 1973 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
nicholas@2678 1974 if (event.currentTarget !== null) {
nicholas@2498 1975 event.currentTarget.owner.stop(audioContext.currentTime + 1);
nicholas@2224 1976 }
nicholas@2498 1977 };
nicholas@2508 1978 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
nicholas@2498 1979 if (!audioEngineContext.loopPlayback || !audioEngineContext.synchPlayback) {
nicholas@2498 1980 this.metric.startListening(audioEngineContext.timer.getTestTime());
nicholas@2529 1981 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, startTime + specification.crossFade);
nicholas@2224 1982 this.interfaceDOM.startPlayback();
nicholas@2498 1983 } else {
nicholas@2529 1984 this.outputGain.gain.linearRampToValueAtTime(0.0, startTime);
nicholas@2224 1985 }
nicholas@2499 1986 if (audioEngineContext.loopPlayback) {
nicholas@2499 1987 this.bufferNode.loopStart = this.specification.startTime || 0;
nicholas@2499 1988 this.bufferNode.loopEnd = this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration;
nicholas@2499 1989 this.bufferNode.start(startTime);
nicholas@2499 1990 } else {
nicholas@2499 1991 this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration);
nicholas@2499 1992 }
nicholas@2224 1993 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
nicholas@2498 1994 }
nicholas@2498 1995 };
nicholas@2498 1996
nicholas@2498 1997 this.stop = function (stopTime) {
nicholas@2224 1998 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
nicholas@2678 1999 if (this.bufferNode !== undefined) {
nicholas@2498 2000 this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
nicholas@2498 2001 this.bufferNode.stop(stopTime);
nicholas@2498 2002 this.bufferNode = undefined;
nicholas@2498 2003 }
nicholas@2529 2004 this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime);
nicholas@2224 2005 this.interfaceDOM.stopPlayback();
nicholas@2498 2006 };
nicholas@2498 2007
nicholas@2498 2008 this.getCurrentPosition = function () {
nicholas@2498 2009 var time = audioEngineContext.timer.getTestTime();
nicholas@2678 2010 if (this.bufferNode !== undefined) {
nicholas@2498 2011 var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
nicholas@2498 2012 if (isNaN(position)) {
nicholas@2498 2013 return 0;
nicholas@2498 2014 }
nicholas@2224 2015 return position;
nicholas@2498 2016 } else {
nicholas@2498 2017 return 0;
nicholas@2498 2018 }
nicholas@2498 2019 };
nicholas@2498 2020
nicholas@2498 2021 this.exportXMLDOM = function () {
nicholas@2498 2022 var file = storage.document.createElement('file');
nicholas@2498 2023 file.setAttribute('sampleRate', this.buffer.buffer.sampleRate);
nicholas@2498 2024 file.setAttribute('channels', this.buffer.buffer.numberOfChannels);
nicholas@2498 2025 file.setAttribute('sampleCount', this.buffer.buffer.length);
nicholas@2498 2026 file.setAttribute('duration', this.buffer.buffer.duration);
nicholas@2498 2027 this.storeDOM.appendChild(file);
nicholas@2498 2028 if (this.specification.type != 'outside-reference') {
nicholas@2498 2029 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
nicholas@2678 2030 if (interfaceXML !== null) {
nicholas@2678 2031 if (interfaceXML.length === undefined) {
nicholas@2498 2032 this.storeDOM.appendChild(interfaceXML);
nicholas@2498 2033 } else {
nicholas@2498 2034 for (var i = 0; i < interfaceXML.length; i++) {
nicholas@2498 2035 this.storeDOM.appendChild(interfaceXML[i]);
nicholas@2498 2036 }
nicholas@2498 2037 }
nicholas@2498 2038 }
nicholas@2678 2039 if (this.commentDOM !== null) {
nicholas@2498 2040 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
nicholas@2498 2041 }
nicholas@2498 2042 }
nicholas@2682 2043 this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]);
nicholas@2498 2044 };
nicholas@2224 2045 }
nicholas@2224 2046
nicholas@2498 2047 function timer() {
nicholas@2498 2048 /* Timer object used in audioEngine to keep track of session timings
nicholas@2498 2049 * Uses the timer of the web audio API, so sample resolution
nicholas@2498 2050 */
nicholas@2498 2051 this.testStarted = false;
nicholas@2498 2052 this.testStartTime = 0;
nicholas@2498 2053 this.testDuration = 0;
nicholas@2498 2054 this.minimumTestTime = 0; // No minimum test time
nicholas@2498 2055 this.startTest = function () {
nicholas@2678 2056 if (this.testStarted === false) {
nicholas@2498 2057 this.testStartTime = audioContext.currentTime;
nicholas@2498 2058 this.testStarted = true;
nicholas@2498 2059 this.updateTestTime();
nicholas@2498 2060 audioEngineContext.metric.initialiseTest();
nicholas@2498 2061 }
nicholas@2498 2062 };
nicholas@2498 2063 this.stopTest = function () {
nicholas@2498 2064 if (this.testStarted) {
nicholas@2498 2065 this.testDuration = this.getTestTime();
nicholas@2498 2066 this.testStarted = false;
nicholas@2498 2067 } else {
nicholas@2498 2068 console.log('ERR: Test tried to end before beginning');
nicholas@2498 2069 }
nicholas@2498 2070 };
nicholas@2498 2071 this.updateTestTime = function () {
nicholas@2498 2072 if (this.testStarted) {
nicholas@2498 2073 this.testDuration = audioContext.currentTime - this.testStartTime;
nicholas@2498 2074 }
nicholas@2498 2075 };
nicholas@2498 2076 this.getTestTime = function () {
nicholas@2498 2077 this.updateTestTime();
nicholas@2498 2078 return this.testDuration;
nicholas@2498 2079 };
nicholas@2224 2080 }
nicholas@2224 2081
nicholas@2498 2082 function sessionMetrics(engine, specification) {
nicholas@2498 2083 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
nicholas@2498 2084 */
nicholas@2498 2085 this.engine = engine;
nicholas@2498 2086 this.lastClicked = -1;
nicholas@2498 2087 this.data = -1;
nicholas@2498 2088 this.reset = function () {
nicholas@2498 2089 this.lastClicked = -1;
nicholas@2498 2090 this.data = -1;
nicholas@2498 2091 };
nicholas@2498 2092
nicholas@2498 2093 this.enableElementInitialPosition = false;
nicholas@2498 2094 this.enableElementListenTracker = false;
nicholas@2498 2095 this.enableElementTimer = false;
nicholas@2498 2096 this.enableElementTracker = false;
nicholas@2498 2097 this.enableFlagListenedTo = false;
nicholas@2498 2098 this.enableFlagMoved = false;
nicholas@2498 2099 this.enableTestTimer = false;
nicholas@2498 2100 // Obtain the metrics enabled
nicholas@2498 2101 for (var i = 0; i < specification.metrics.enabled.length; i++) {
nicholas@2498 2102 var node = specification.metrics.enabled[i];
nicholas@2498 2103 switch (node) {
nicholas@2498 2104 case 'testTimer':
nicholas@2498 2105 this.enableTestTimer = true;
nicholas@2498 2106 break;
nicholas@2498 2107 case 'elementTimer':
nicholas@2498 2108 this.enableElementTimer = true;
nicholas@2498 2109 break;
nicholas@2498 2110 case 'elementTracker':
nicholas@2498 2111 this.enableElementTracker = true;
nicholas@2498 2112 break;
nicholas@2498 2113 case 'elementListenTracker':
nicholas@2498 2114 this.enableElementListenTracker = true;
nicholas@2498 2115 break;
nicholas@2498 2116 case 'elementInitialPosition':
nicholas@2498 2117 this.enableElementInitialPosition = true;
nicholas@2498 2118 break;
nicholas@2498 2119 case 'elementFlagListenedTo':
nicholas@2498 2120 this.enableFlagListenedTo = true;
nicholas@2498 2121 break;
nicholas@2498 2122 case 'elementFlagMoved':
nicholas@2498 2123 this.enableFlagMoved = true;
nicholas@2498 2124 break;
nicholas@2498 2125 case 'elementFlagComments':
nicholas@2498 2126 this.enableFlagComments = true;
nicholas@2498 2127 break;
nicholas@2498 2128 }
nicholas@2498 2129 }
nicholas@2498 2130 this.initialiseTest = function () {};
nicholas@2224 2131 }
nicholas@2224 2132
nicholas@2498 2133 function metricTracker(caller) {
nicholas@2498 2134 /* Custom object to track and collect metric data
nicholas@2498 2135 * Used only inside the audioObjects object.
nicholas@2498 2136 */
nicholas@2498 2137
nicholas@2498 2138 this.listenedTimer = 0;
nicholas@2498 2139 this.listenStart = 0;
nicholas@2498 2140 this.listenHold = false;
nicholas@2498 2141 this.initialPosition = -1;
nicholas@2498 2142 this.movementTracker = [];
nicholas@2498 2143 this.listenTracker = [];
nicholas@2498 2144 this.wasListenedTo = false;
nicholas@2498 2145 this.wasMoved = false;
nicholas@2498 2146 this.hasComments = false;
nicholas@2498 2147 this.parent = caller;
nicholas@2498 2148
nicholas@2498 2149 this.initialise = function (position) {
nicholas@2498 2150 if (this.initialPosition == -1) {
nicholas@2498 2151 this.initialPosition = position;
nicholas@2498 2152 this.moved(0, position);
nicholas@2498 2153 }
nicholas@2498 2154 };
nicholas@2498 2155
nicholas@2498 2156 this.moved = function (time, position) {
nicholas@2498 2157 if (time > 0) {
nicholas@2498 2158 this.wasMoved = true;
nicholas@2498 2159 }
nicholas@2498 2160 this.movementTracker[this.movementTracker.length] = [time, position];
nicholas@2498 2161 };
nicholas@2498 2162
nicholas@2498 2163 this.startListening = function (time) {
nicholas@2678 2164 if (this.listenHold === false) {
nicholas@2498 2165 this.wasListenedTo = true;
nicholas@2498 2166 this.listenStart = time;
nicholas@2498 2167 this.listenHold = true;
nicholas@2498 2168
nicholas@2498 2169 var evnt = document.createElement('event');
nicholas@2498 2170 var testTime = document.createElement('testTime');
nicholas@2498 2171 testTime.setAttribute('start', time);
nicholas@2498 2172 var bufferTime = document.createElement('bufferTime');
nicholas@2498 2173 bufferTime.setAttribute('start', this.parent.getCurrentPosition());
nicholas@2498 2174 evnt.appendChild(testTime);
nicholas@2498 2175 evnt.appendChild(bufferTime);
nicholas@2498 2176 this.listenTracker.push(evnt);
nicholas@2498 2177
nicholas@2498 2178 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2179 }
nicholas@2498 2180 };
nicholas@2498 2181
nicholas@2498 2182 this.stopListening = function (time, bufferStopTime) {
nicholas@2678 2183 if (this.listenHold === true) {
nicholas@2498 2184 var diff = time - this.listenStart;
nicholas@2498 2185 this.listenedTimer += (diff);
nicholas@2498 2186 this.listenStart = 0;
nicholas@2498 2187 this.listenHold = false;
nicholas@2498 2188
nicholas@2498 2189 var evnt = this.listenTracker[this.listenTracker.length - 1];
nicholas@2498 2190 var testTime = evnt.getElementsByTagName('testTime')[0];
nicholas@2498 2191 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
nicholas@2498 2192 testTime.setAttribute('stop', time);
nicholas@2678 2193 if (bufferStopTime === undefined) {
nicholas@2498 2194 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
nicholas@2498 2195 } else {
nicholas@2498 2196 bufferTime.setAttribute('stop', bufferStopTime);
nicholas@2498 2197 }
nicholas@2498 2198 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
nicholas@2498 2199 }
nicholas@2498 2200 };
nicholas@2498 2201
nicholas@2682 2202 function exportElementTimer(parentElement) {
nicholas@2682 2203 var mElementTimer = storage.document.createElement('metricresult');
nicholas@2682 2204 mElementTimer.setAttribute('name', 'enableElementTimer');
nicholas@2682 2205 mElementTimer.textContent = this.listenedTimer;
nicholas@2682 2206 parentElement.appendChild(mElementTimer);
nicholas@2689 2207 return mElementTimer;
nicholas@2682 2208 }
nicholas@2682 2209
nicholas@2682 2210 function exportElementTrack(parentElement) {
nicholas@2682 2211 var elementTrackerFull = storage.document.createElement('metricresult');
nicholas@2682 2212 elementTrackerFull.setAttribute('name', 'elementTrackerFull');
nicholas@2682 2213 for (var k = 0; k < this.movementTracker.length; k++) {
nicholas@2682 2214 var timePos = storage.document.createElement('movement');
nicholas@2682 2215 timePos.setAttribute("time", this.movementTracker[k][0]);
nicholas@2682 2216 timePos.setAttribute("value", this.movementTracker[k][1]);
nicholas@2682 2217 elementTrackerFull.appendChild(timePos);
nicholas@2682 2218 }
nicholas@2682 2219 parentElement.appendChild(elementTrackerFull);
nicholas@2689 2220 return elementTrackerFull;
nicholas@2682 2221 }
nicholas@2682 2222
nicholas@2682 2223 function exportElementListenTracker(parentElement) {
nicholas@2682 2224 var elementListenTracker = storage.document.createElement('metricresult');
nicholas@2682 2225 elementListenTracker.setAttribute('name', 'elementListenTracker');
nicholas@2682 2226 for (var k = 0; k < this.listenTracker.length; k++) {
nicholas@2682 2227 elementListenTracker.appendChild(this.listenTracker[k]);
nicholas@2682 2228 }
nicholas@2682 2229 parentElement.appendChild(elementListenTracker);
nicholas@2689 2230 return 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@2689 2238 return elementInitial;
nicholas@2682 2239 }
nicholas@2682 2240
nicholas@2682 2241 function exportFlagListenedTo(parentElement) {
nicholas@2682 2242 var flagListenedTo = storage.document.createElement('metricresult');
nicholas@2682 2243 flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
nicholas@2682 2244 flagListenedTo.textContent = this.wasListenedTo;
nicholas@2682 2245 parentElement.appendChild(flagListenedTo);
nicholas@2689 2246 return flagListenedTo;
nicholas@2682 2247 }
nicholas@2682 2248
nicholas@2682 2249 function exportFlagMoved(parentElement) {
nicholas@2682 2250 var flagMoved = storage.document.createElement('metricresult');
nicholas@2682 2251 flagMoved.setAttribute('name', 'elementFlagMoved');
nicholas@2682 2252 flagMoved.textContent = this.wasMoved;
nicholas@2682 2253 parentElement.appendChild(flagMoved);
nicholas@2689 2254 return flagMoved;
nicholas@2682 2255 }
nicholas@2682 2256
nicholas@2682 2257 function exportFlagComments(parentElement) {
nicholas@2682 2258 var flagComments = storage.document.createElement('metricresult');
nicholas@2682 2259 flagComments.setAttribute('name', 'elementFlagComments');
nicholas@2682 2260 if (this.parent.commentDOM === null) {
nicholas@2682 2261 flagComments.textContent = 'false';
nicholas@2682 2262 } else if (this.parent.commentDOM.textContent.length === 0) {
nicholas@2682 2263 flagComments.textContent = 'false';
nicholas@2682 2264 } else {
nicholas@2682 2265 flagComments.textContet = 'true';
nicholas@2682 2266 }
nicholas@2682 2267 parentElement.appendChild(flagComments);
nicholas@2689 2268 return flagComments;
nicholas@2682 2269 }
nicholas@2682 2270
nicholas@2682 2271 this.exportXMLDOM = function (parentElement) {
nicholas@2690 2272 var elems = [];
nicholas@2498 2273 if (audioEngineContext.metric.enableElementTimer) {
nicholas@2689 2274 elems.push(exportElementTimer.call(this, parentElement));
nicholas@2498 2275 }
nicholas@2498 2276 if (audioEngineContext.metric.enableElementTracker) {
nicholas@2689 2277 elems.push(exportElementTrack.call(this, parentElement));
nicholas@2498 2278 }
nicholas@2498 2279 if (audioEngineContext.metric.enableElementListenTracker) {
nicholas@2689 2280 elems.push(exportElementListenTracker.call(this, parentElement));
nicholas@2498 2281 }
nicholas@2498 2282 if (audioEngineContext.metric.enableElementInitialPosition) {
nicholas@2689 2283 elems.push(exportElementInitialPosition.call(this, parentElement));
nicholas@2498 2284 }
nicholas@2498 2285 if (audioEngineContext.metric.enableFlagListenedTo) {
nicholas@2689 2286 elems.push(exportFlagListenedTo.call(this, parentElement));
nicholas@2498 2287 }
nicholas@2498 2288 if (audioEngineContext.metric.enableFlagMoved) {
nicholas@2689 2289 elems.push(exportFlagMoved.call(this, parentElement));
nicholas@2498 2290 }
nicholas@2498 2291 if (audioEngineContext.metric.enableFlagComments) {
nicholas@2689 2292 elems.push(exportFlagComments.call(this, parentElement));
nicholas@2498 2293 }
nicholas@2689 2294 return elems;
nicholas@2498 2295 };
nicholas@2224 2296 }
nicholas@2498 2297
nicholas@2224 2298 function Interface(specificationObject) {
nicholas@2498 2299 // This handles the bindings between the interface and the audioEngineContext;
nicholas@2498 2300 this.specification = specificationObject;
nicholas@2498 2301 this.insertPoint = document.getElementById("topLevelBody");
nicholas@2498 2302
nicholas@2498 2303 this.newPage = function (audioHolderObject, store) {
nicholas@2498 2304 audioEngineContext.newTestPage(audioHolderObject, store);
nicholas@2498 2305 interfaceContext.commentBoxes.deleteCommentBoxes();
nicholas@2498 2306 interfaceContext.deleteCommentQuestions();
nicholas@2498 2307 loadTest(audioHolderObject, store);
nicholas@2498 2308 };
nicholas@2498 2309
nicholas@2498 2310 // Bounded by interface!!
nicholas@2498 2311 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
nicholas@2498 2312 // For example, APE returns the slider position normalised in a <value> tag.
nicholas@2498 2313 this.interfaceObjects = [];
nicholas@2498 2314 this.interfaceObject = function () {};
nicholas@2498 2315
nicholas@2498 2316 this.resizeWindow = function (event) {
nicholas@2498 2317 popup.resize(event);
nicholas@2352 2318 this.volume.resize();
nicholas@2360 2319 this.lightbox.resize();
nicholas@2682 2320 this.commentBoxes.forEach(function (elem) {
nicholas@2682 2321 elem.resize();
nicholas@2682 2322 });
nicholas@2682 2323 this.commentQuestions.forEach(function (elem) {
nicholas@2682 2324 elem.resize();
nicholas@2682 2325 });
nicholas@2498 2326 try {
nicholas@2498 2327 resizeWindow(event);
nicholas@2498 2328 } catch (err) {
nicholas@2498 2329 console.log("Warning - Interface does not have Resize option");
nicholas@2498 2330 console.log(err);
nicholas@2498 2331 }
nicholas@2498 2332 };
nicholas@2498 2333
nicholas@2498 2334 this.returnNavigator = function () {
nicholas@2498 2335 var node = storage.document.createElement("navigator");
nicholas@2498 2336 var platform = storage.document.createElement("platform");
nicholas@2498 2337 platform.textContent = navigator.platform;
nicholas@2498 2338 var vendor = storage.document.createElement("vendor");
nicholas@2498 2339 vendor.textContent = navigator.vendor;
nicholas@2498 2340 var userAgent = storage.document.createElement("uagent");
nicholas@2498 2341 userAgent.textContent = navigator.userAgent;
nicholas@2224 2342 var screen = storage.document.createElement("window");
nicholas@2498 2343 screen.setAttribute('innerWidth', window.innerWidth);
nicholas@2498 2344 screen.setAttribute('innerHeight', window.innerHeight);
nicholas@2498 2345 node.appendChild(platform);
nicholas@2498 2346 node.appendChild(vendor);
nicholas@2498 2347 node.appendChild(userAgent);
nicholas@2224 2348 node.appendChild(screen);
nicholas@2498 2349 return node;
nicholas@2498 2350 };
nicholas@2498 2351
nicholas@2498 2352 this.returnDateNode = function () {
nicholas@2224 2353 // Create an XML Node for the Date and Time a test was conducted
nicholas@2224 2354 // Structure is
nicholas@2224 2355 // <datetime>
nicholas@2224 2356 // <date year="##" month="##" day="##">DD/MM/YY</date>
nicholas@2224 2357 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
nicholas@2224 2358 // </datetime>
nicholas@2224 2359 var dateTime = new Date();
nicholas@2224 2360 var hold = storage.document.createElement("datetime");
nicholas@2224 2361 var date = storage.document.createElement("date");
nicholas@2224 2362 var time = storage.document.createElement("time");
nicholas@2498 2363 date.setAttribute('year', dateTime.getFullYear());
nicholas@2498 2364 date.setAttribute('month', dateTime.getMonth() + 1);
nicholas@2498 2365 date.setAttribute('day', dateTime.getDate());
nicholas@2498 2366 time.setAttribute('hour', dateTime.getHours());
nicholas@2498 2367 time.setAttribute('minute', dateTime.getMinutes());
nicholas@2498 2368 time.setAttribute('secs', dateTime.getSeconds());
nicholas@2498 2369
nicholas@2224 2370 hold.appendChild(date);
nicholas@2224 2371 hold.appendChild(time);
nicholas@2224 2372 return hold;
nicholas@2224 2373
nicholas@2678 2374 };
nicholas@2498 2375
nicholas@2360 2376 this.lightbox = {
nicholas@2360 2377 parent: this,
nicholas@2360 2378 root: document.createElement("div"),
nicholas@2360 2379 content: document.createElement("div"),
nicholas@2360 2380 accept: document.createElement("button"),
nicholas@2360 2381 blanker: document.createElement("div"),
nicholas@2498 2382 post: function (type, message) {
nicholas@2498 2383 switch (type) {
nicholas@2360 2384 case "Error":
nicholas@2360 2385 this.content.className = "lightbox-error";
nicholas@2360 2386 break;
nicholas@2360 2387 case "Warning":
nicholas@2360 2388 this.content.className = "lightbox-warning";
nicholas@2360 2389 break;
nicholas@2360 2390 default:
nicholas@2360 2391 this.content.className = "lightbox-message";
nicholas@2360 2392 break;
nicholas@2360 2393 }
nicholas@2360 2394 var msg = document.createElement("p");
nicholas@2360 2395 msg.textContent = message;
nicholas@2360 2396 this.content.appendChild(msg);
nicholas@2360 2397 this.show();
nicholas@2360 2398 },
nicholas@2498 2399 show: function () {
nicholas@2360 2400 this.root.style.visibility = "visible";
nicholas@2360 2401 this.blanker.style.visibility = "visible";
nicholas@2360 2402 },
nicholas@2498 2403 clear: function () {
nicholas@2360 2404 this.root.style.visibility = "";
nicholas@2360 2405 this.blanker.style.visibility = "";
nicholas@2360 2406 this.content.textContent = "";
nicholas@2360 2407 },
nicholas@2498 2408 handleEvent: function (event) {
nicholas@2360 2409 if (event.currentTarget == this.accept) {
nicholas@2360 2410 this.clear();
nicholas@2360 2411 }
nicholas@2360 2412 },
nicholas@2498 2413 resize: function (event) {
nicholas@2498 2414 this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
nicholas@2360 2415 }
nicholas@2678 2416 };
nicholas@2498 2417
nicholas@2360 2418 this.lightbox.root.appendChild(this.lightbox.content);
nicholas@2360 2419 this.lightbox.root.appendChild(this.lightbox.accept);
nicholas@2360 2420 this.lightbox.root.className = "popupHolder";
nicholas@2360 2421 this.lightbox.root.id = "lightbox-root";
nicholas@2360 2422 this.lightbox.accept.className = "popupButton";
nicholas@2360 2423 this.lightbox.accept.style.bottom = "10px";
nicholas@2360 2424 this.lightbox.accept.textContent = "OK";
nicholas@2360 2425 this.lightbox.accept.style.left = "237.5px";
nicholas@2498 2426 this.lightbox.accept.addEventListener("click", this.lightbox);
nicholas@2360 2427 this.lightbox.blanker.className = "testHalt";
nicholas@2360 2428 this.lightbox.blanker.id = "lightbox-blanker";
nicholas@2360 2429 document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
nicholas@2360 2430 document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
nicholas@2498 2431
nicholas@2498 2432 this.commentBoxes = new function () {
nicholas@2224 2433 this.boxes = [];
nicholas@2224 2434 this.injectPoint = null;
nicholas@2498 2435 this.elementCommentBox = function (audioObject) {
nicholas@2224 2436 var element = audioObject.specification;
nicholas@2224 2437 this.audioObject = audioObject;
nicholas@2224 2438 this.id = audioObject.id;
nicholas@2224 2439 var audioHolderObject = audioObject.specification.parent;
nicholas@2224 2440 // Create document objects to hold the comment boxes
nicholas@2224 2441 this.trackComment = document.createElement('div');
nicholas@2224 2442 this.trackComment.className = 'comment-div';
nicholas@2498 2443 this.trackComment.id = 'comment-div-' + audioObject.id;
nicholas@2224 2444 // Create a string next to each comment asking for a comment
nicholas@2224 2445 this.trackString = document.createElement('span');
nicholas@2498 2446 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId();
nicholas@2224 2447 // Create the HTML5 comment box 'textarea'
nicholas@2224 2448 this.trackCommentBox = document.createElement('textarea');
nicholas@2224 2449 this.trackCommentBox.rows = '4';
nicholas@2224 2450 this.trackCommentBox.cols = '100';
nicholas@2498 2451 this.trackCommentBox.name = 'trackComment' + audioObject.id;
nicholas@2224 2452 this.trackCommentBox.className = 'trackComment';
nicholas@2224 2453 var br = document.createElement('br');
nicholas@2224 2454 // Add to the holder.
nicholas@2224 2455 this.trackComment.appendChild(this.trackString);
nicholas@2224 2456 this.trackComment.appendChild(br);
nicholas@2224 2457 this.trackComment.appendChild(this.trackCommentBox);
nicholas@2224 2458
nicholas@2498 2459 this.exportXMLDOM = function () {
nicholas@2224 2460 var root = document.createElement('comment');
nicholas@2224 2461 var question = document.createElement('question');
nicholas@2224 2462 question.textContent = this.trackString.textContent;
nicholas@2224 2463 var response = document.createElement('response');
nicholas@2224 2464 response.textContent = this.trackCommentBox.value;
nicholas@2498 2465 console.log("Comment frag-" + this.id + ": " + response.textContent);
nicholas@2224 2466 root.appendChild(question);
nicholas@2224 2467 root.appendChild(response);
nicholas@2224 2468 return root;
nicholas@2224 2469 };
nicholas@2498 2470 this.resize = function () {
nicholas@2498 2471 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2472 if (boxwidth >= 600) {
nicholas@2224 2473 boxwidth = 600;
nicholas@2498 2474 } else if (boxwidth < 400) {
nicholas@2224 2475 boxwidth = 400;
nicholas@2224 2476 }
nicholas@2498 2477 this.trackComment.style.width = boxwidth + "px";
nicholas@2498 2478 this.trackCommentBox.style.width = boxwidth - 6 + "px";
nicholas@2224 2479 };
nicholas@2224 2480 this.resize();
nicholas@2224 2481 };
nicholas@2498 2482 this.createCommentBox = function (audioObject) {
nicholas@2224 2483 var node = new this.elementCommentBox(audioObject);
nicholas@2224 2484 this.boxes.push(node);
nicholas@2224 2485 audioObject.commentDOM = node;
nicholas@2224 2486 return node;
nicholas@2224 2487 };
nicholas@2498 2488 this.sortCommentBoxes = function () {
nicholas@2498 2489 this.boxes.sort(function (a, b) {
nicholas@2498 2490 return a.id - b.id;
nicholas@2498 2491 });
nicholas@2224 2492 };
nicholas@2224 2493
nicholas@2498 2494 this.showCommentBoxes = function (inject, sort) {
nicholas@2224 2495 this.injectPoint = inject;
nicholas@2498 2496 if (sort) {
nicholas@2498 2497 this.sortCommentBoxes();
nicholas@2498 2498 }
nicholas@2682 2499 this.boxes.forEach(function (box) {
nicholas@2224 2500 inject.appendChild(box.trackComment);
nicholas@2682 2501 });
nicholas@2224 2502 };
nicholas@2224 2503
nicholas@2498 2504 this.deleteCommentBoxes = function () {
nicholas@2678 2505 if (this.injectPoint !== null) {
nicholas@2682 2506 this.boxes.forEach(function (box) {
nicholas@2224 2507 this.injectPoint.removeChild(box.trackComment);
nicholas@2682 2508 }, this);
nicholas@2224 2509 this.injectPoint = null;
nicholas@2224 2510 }
nicholas@2224 2511 this.boxes = [];
nicholas@2224 2512 };
nicholas@2678 2513 };
nicholas@2498 2514
nicholas@2498 2515 this.commentQuestions = [];
nicholas@2498 2516
nicholas@2498 2517 this.commentBox = function (commentQuestion) {
nicholas@2498 2518 this.specification = commentQuestion;
nicholas@2498 2519 // Create document objects to hold the comment boxes
nicholas@2498 2520 this.holder = document.createElement('div');
nicholas@2498 2521 this.holder.className = 'comment-div';
nicholas@2498 2522 // Create a string next to each comment asking for a comment
nicholas@2498 2523 this.string = document.createElement('span');
nicholas@2498 2524 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2525 // Create the HTML5 comment box 'textarea'
nicholas@2498 2526 this.textArea = document.createElement('textarea');
nicholas@2498 2527 this.textArea.rows = '4';
nicholas@2498 2528 this.textArea.cols = '100';
nicholas@2498 2529 this.textArea.className = 'trackComment';
nicholas@2498 2530 var br = document.createElement('br');
nicholas@2498 2531 // Add to the holder.
nicholas@2498 2532 this.holder.appendChild(this.string);
nicholas@2498 2533 this.holder.appendChild(br);
nicholas@2498 2534 this.holder.appendChild(this.textArea);
nicholas@2498 2535
nicholas@2498 2536 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2537 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2538 root.id = this.specification.id;
nicholas@2498 2539 root.setAttribute('type', this.specification.type);
nicholas@2498 2540 console.log("Question: " + this.string.textContent);
nicholas@2498 2541 console.log("Response: " + root.textContent);
nicholas@2224 2542 var question = storePoint.parent.document.createElement('question');
nicholas@2224 2543 question.textContent = this.string.textContent;
nicholas@2224 2544 var response = storePoint.parent.document.createElement('response');
nicholas@2224 2545 response.textContent = this.textArea.value;
nicholas@2224 2546 root.appendChild(question);
nicholas@2224 2547 root.appendChild(response);
nicholas@2224 2548 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2549 return root;
nicholas@2498 2550 };
nicholas@2498 2551 this.resize = function () {
nicholas@2498 2552 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2553 if (boxwidth >= 600) {
nicholas@2498 2554 boxwidth = 600;
nicholas@2498 2555 } else if (boxwidth < 400) {
nicholas@2498 2556 boxwidth = 400;
nicholas@2498 2557 }
nicholas@2498 2558 this.holder.style.width = boxwidth + "px";
nicholas@2498 2559 this.textArea.style.width = boxwidth - 6 + "px";
nicholas@2498 2560 };
nicholas@2498 2561 this.resize();
nicholas@2498 2562 };
nicholas@2498 2563
nicholas@2498 2564 this.radioBox = function (commentQuestion) {
nicholas@2498 2565 this.specification = commentQuestion;
nicholas@2498 2566 // Create document objects to hold the comment boxes
nicholas@2498 2567 this.holder = document.createElement('div');
nicholas@2498 2568 this.holder.className = 'comment-div';
nicholas@2498 2569 // Create a string next to each comment asking for a comment
nicholas@2498 2570 this.string = document.createElement('span');
nicholas@2498 2571 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2572 var br = document.createElement('br');
nicholas@2498 2573 // Add to the holder.
nicholas@2498 2574 this.holder.appendChild(this.string);
nicholas@2498 2575 this.holder.appendChild(br);
nicholas@2498 2576 this.options = [];
nicholas@2498 2577 this.inputs = document.createElement('div');
nicholas@2498 2578 this.span = document.createElement('div');
nicholas@2498 2579 this.inputs.align = 'center';
nicholas@2498 2580 this.inputs.style.marginLeft = '12px';
nicholas@2294 2581 this.inputs.className = "comment-radio-inputs-holder";
nicholas@2498 2582 this.span.style.marginLeft = '12px';
nicholas@2498 2583 this.span.align = 'center';
nicholas@2498 2584 this.span.style.marginTop = '15px';
nicholas@2294 2585 this.span.className = "comment-radio-span-holder";
nicholas@2498 2586
nicholas@2498 2587 var optCount = commentQuestion.options.length;
nicholas@2682 2588 commentQuestion.options.forEach(function (optNode) {
nicholas@2498 2589 var div = document.createElement('div');
nicholas@2498 2590 div.style.width = '80px';
nicholas@2498 2591 div.style.float = 'left';
nicholas@2498 2592 var input = document.createElement('input');
nicholas@2498 2593 input.type = 'radio';
nicholas@2498 2594 input.name = commentQuestion.id;
nicholas@2498 2595 input.setAttribute('setvalue', optNode.name);
nicholas@2498 2596 input.className = 'comment-radio';
nicholas@2498 2597 div.appendChild(input);
nicholas@2498 2598 this.inputs.appendChild(div);
nicholas@2498 2599
nicholas@2498 2600
nicholas@2498 2601 div = document.createElement('div');
nicholas@2498 2602 div.style.width = '80px';
nicholas@2498 2603 div.style.float = 'left';
nicholas@2498 2604 div.align = 'center';
nicholas@2498 2605 var span = document.createElement('span');
nicholas@2498 2606 span.textContent = optNode.text;
nicholas@2498 2607 span.className = 'comment-radio-span';
nicholas@2498 2608 div.appendChild(span);
nicholas@2498 2609 this.span.appendChild(div);
nicholas@2498 2610 this.options.push(input);
nicholas@2682 2611 }, this);
nicholas@2498 2612 this.holder.appendChild(this.span);
nicholas@2498 2613 this.holder.appendChild(this.inputs);
nicholas@2498 2614
nicholas@2498 2615 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2616 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2617 root.id = this.specification.id;
nicholas@2498 2618 root.setAttribute('type', this.specification.type);
nicholas@2498 2619 var question = document.createElement('question');
nicholas@2498 2620 question.textContent = this.string.textContent;
nicholas@2498 2621 var response = document.createElement('response');
nicholas@2498 2622 var i = 0;
nicholas@2678 2623 while (this.options[i].checked === false) {
nicholas@2498 2624 i++;
nicholas@2498 2625 if (i >= this.options.length) {
nicholas@2498 2626 break;
nicholas@2498 2627 }
nicholas@2498 2628 }
nicholas@2498 2629 if (i >= this.options.length) {
nicholas@2498 2630 response.textContent = 'null';
nicholas@2498 2631 } else {
nicholas@2498 2632 response.textContent = this.options[i].getAttribute('setvalue');
nicholas@2498 2633 response.setAttribute('number', i);
nicholas@2498 2634 }
nicholas@2498 2635 console.log('Comment: ' + question.textContent);
nicholas@2498 2636 console.log('Response: ' + response.textContent);
nicholas@2498 2637 root.appendChild(question);
nicholas@2498 2638 root.appendChild(response);
nicholas@2224 2639 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2640 return root;
nicholas@2498 2641 };
nicholas@2498 2642 this.resize = function () {
nicholas@2498 2643 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2644 if (boxwidth >= 600) {
nicholas@2498 2645 boxwidth = 600;
nicholas@2498 2646 } else if (boxwidth < 400) {
nicholas@2498 2647 boxwidth = 400;
nicholas@2498 2648 }
nicholas@2498 2649 this.holder.style.width = boxwidth + "px";
nicholas@2498 2650 var text = this.holder.getElementsByClassName("comment-radio-span-holder")[0];
nicholas@2498 2651 var options = this.holder.getElementsByClassName("comment-radio-inputs-holder")[0];
nicholas@2498 2652 var optCount = options.childElementCount;
nicholas@2498 2653 var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px';
nicholas@2682 2654 options = options.firstChild;
nicholas@2682 2655 text = text.firstChild;
nicholas@2498 2656 options.style.marginRight = spanMargin;
nicholas@2498 2657 options.style.marginLeft = spanMargin;
nicholas@2498 2658 text.style.marginRight = spanMargin;
nicholas@2498 2659 text.style.marginLeft = spanMargin;
nicholas@2689 2660 while (options = options.nextSibling) {
nicholas@2498 2661 text = text.nextSibling;
nicholas@2498 2662 options.style.marginRight = spanMargin;
nicholas@2498 2663 options.style.marginLeft = spanMargin;
nicholas@2498 2664 text.style.marginRight = spanMargin;
nicholas@2498 2665 text.style.marginLeft = spanMargin;
nicholas@2498 2666 }
nicholas@2498 2667 };
nicholas@2498 2668 this.resize();
nicholas@2498 2669 };
nicholas@2498 2670
nicholas@2498 2671 this.checkboxBox = function (commentQuestion) {
nicholas@2498 2672 this.specification = commentQuestion;
nicholas@2498 2673 // Create document objects to hold the comment boxes
nicholas@2498 2674 this.holder = document.createElement('div');
nicholas@2498 2675 this.holder.className = 'comment-div';
nicholas@2498 2676 // Create a string next to each comment asking for a comment
nicholas@2498 2677 this.string = document.createElement('span');
nicholas@2498 2678 this.string.innerHTML = commentQuestion.statement;
nicholas@2498 2679 var br = document.createElement('br');
nicholas@2498 2680 // Add to the holder.
nicholas@2498 2681 this.holder.appendChild(this.string);
nicholas@2498 2682 this.holder.appendChild(br);
nicholas@2498 2683 this.options = [];
nicholas@2498 2684 this.inputs = document.createElement('div');
nicholas@2498 2685 this.span = document.createElement('div');
nicholas@2498 2686 this.inputs.align = 'center';
nicholas@2498 2687 this.inputs.style.marginLeft = '12px';
nicholas@2294 2688 this.inputs.className = "comment-checkbox-inputs-holder";
nicholas@2498 2689 this.span.style.marginLeft = '12px';
nicholas@2498 2690 this.span.align = 'center';
nicholas@2498 2691 this.span.style.marginTop = '15px';
nicholas@2294 2692 this.span.className = "comment-checkbox-span-holder";
nicholas@2498 2693
nicholas@2498 2694 var optCount = commentQuestion.options.length;
nicholas@2498 2695 for (var i = 0; i < optCount; i++) {
nicholas@2498 2696 var div = document.createElement('div');
nicholas@2498 2697 div.style.width = '80px';
nicholas@2498 2698 div.style.float = 'left';
nicholas@2498 2699 var input = document.createElement('input');
nicholas@2498 2700 input.type = 'checkbox';
nicholas@2498 2701 input.name = commentQuestion.id;
nicholas@2498 2702 input.setAttribute('setvalue', commentQuestion.options[i].name);
nicholas@2498 2703 input.className = 'comment-radio';
nicholas@2498 2704 div.appendChild(input);
nicholas@2498 2705 this.inputs.appendChild(div);
nicholas@2498 2706
nicholas@2498 2707
nicholas@2498 2708 div = document.createElement('div');
nicholas@2498 2709 div.style.width = '80px';
nicholas@2498 2710 div.style.float = 'left';
nicholas@2498 2711 div.align = 'center';
nicholas@2498 2712 var span = document.createElement('span');
nicholas@2498 2713 span.textContent = commentQuestion.options[i].text;
nicholas@2498 2714 span.className = 'comment-radio-span';
nicholas@2498 2715 div.appendChild(span);
nicholas@2498 2716 this.span.appendChild(div);
nicholas@2498 2717 this.options.push(input);
nicholas@2498 2718 }
nicholas@2498 2719 this.holder.appendChild(this.span);
nicholas@2498 2720 this.holder.appendChild(this.inputs);
nicholas@2498 2721
nicholas@2498 2722 this.exportXMLDOM = function (storePoint) {
nicholas@2498 2723 var root = storePoint.parent.document.createElement('comment');
nicholas@2498 2724 root.id = this.specification.id;
nicholas@2498 2725 root.setAttribute('type', this.specification.type);
nicholas@2498 2726 var question = document.createElement('question');
nicholas@2498 2727 question.textContent = this.string.textContent;
nicholas@2498 2728 root.appendChild(question);
nicholas@2498 2729 console.log('Comment: ' + question.textContent);
nicholas@2498 2730 for (var i = 0; i < this.options.length; i++) {
nicholas@2498 2731 var response = document.createElement('response');
nicholas@2498 2732 response.textContent = this.options[i].checked;
nicholas@2498 2733 response.setAttribute('name', this.options[i].getAttribute('setvalue'));
nicholas@2498 2734 root.appendChild(response);
nicholas@2498 2735 console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent);
nicholas@2498 2736 }
nicholas@2224 2737 storePoint.XMLDOM.appendChild(root);
nicholas@2498 2738 return root;
nicholas@2498 2739 };
nicholas@2498 2740 this.resize = function () {
nicholas@2498 2741 var boxwidth = (window.innerWidth - 100) / 2;
nicholas@2498 2742 if (boxwidth >= 600) {
nicholas@2498 2743 boxwidth = 600;
nicholas@2498 2744 } else if (boxwidth < 400) {
nicholas@2498 2745 boxwidth = 400;
nicholas@2498 2746 }
nicholas@2498 2747 this.holder.style.width = boxwidth + "px";
nicholas@2498 2748 var text = this.holder.getElementsByClassName("comment-checkbox-span-holder")[0];
nicholas@2498 2749 var options = this.holder.getElementsByClassName("comment-checkbox-inputs-holder")[0];
nicholas@2498 2750 var optCount = options.childElementCount;
nicholas@2498 2751 var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px';
nicholas@2682 2752 options = options.firstChild;
nicholas@2682 2753 text = text.firstChild;
nicholas@2498 2754 options.style.marginRight = spanMargin;
nicholas@2498 2755 options.style.marginLeft = spanMargin;
nicholas@2498 2756 text.style.marginRight = spanMargin;
nicholas@2498 2757 text.style.marginLeft = spanMargin;
nicholas@2689 2758 while (options = options.nextSibling) {
nicholas@2498 2759 text = text.nextSibling;
nicholas@2498 2760 options.style.marginRight = spanMargin;
nicholas@2498 2761 options.style.marginLeft = spanMargin;
nicholas@2498 2762 text.style.marginRight = spanMargin;
nicholas@2498 2763 text.style.marginLeft = spanMargin;
nicholas@2498 2764 }
nicholas@2498 2765 };
nicholas@2498 2766 this.resize();
nicholas@2498 2767 };
nicholas@2498 2768
n@2579 2769 this.sliderBox = function (commentQuestion) {
n@2579 2770 this.specification = commentQuestion;
n@2579 2771 this.holder = document.createElement("div");
n@2579 2772 this.holder.className = 'comment-div';
n@2579 2773 this.string = document.createElement("span");
n@2579 2774 this.string.innerHTML = commentQuestion.statement;
n@2579 2775 this.slider = document.createElement("input");
n@2579 2776 this.slider.type = "range";
n@2579 2777 this.slider.min = commentQuestion.min;
n@2579 2778 this.slider.max = commentQuestion.max;
n@2579 2779 this.slider.step = commentQuestion.step;
n@2579 2780 this.slider.value = commentQuestion.value;
n@2579 2781 var br = document.createElement('br');
n@2579 2782
n@2580 2783 var textHolder = document.createElement("div");
n@2580 2784 textHolder.className = "comment-slider-text-holder";
n@2580 2785
n@2580 2786 this.leftText = document.createElement("span");
n@2580 2787 this.leftText.textContent = commentQuestion.leftText;
n@2580 2788 this.rightText = document.createElement("span");
n@2580 2789 this.rightText.textContent = commentQuestion.rightText;
n@2580 2790 textHolder.appendChild(this.leftText);
n@2580 2791 textHolder.appendChild(this.rightText);
n@2580 2792
n@2579 2793 this.holder.appendChild(this.string);
n@2579 2794 this.holder.appendChild(br);
n@2579 2795 this.holder.appendChild(this.slider);
n@2580 2796 this.holder.appendChild(textHolder);
n@2579 2797
n@2579 2798 this.exportXMLDOM = function (storePoint) {
n@2579 2799 var root = storePoint.parent.document.createElement('comment');
n@2579 2800 root.id = this.specification.id;
n@2579 2801 root.setAttribute('type', this.specification.type);
n@2579 2802 console.log("Question: " + this.string.textContent);
n@2579 2803 console.log("Response: " + this.slider.value);
n@2579 2804 var question = storePoint.parent.document.createElement('question');
n@2579 2805 question.textContent = this.string.textContent;
n@2579 2806 var response = storePoint.parent.document.createElement('response');
n@2579 2807 response.textContent = this.slider.value;
n@2579 2808 root.appendChild(question);
n@2579 2809 root.appendChild(response);
n@2579 2810 storePoint.XMLDOM.appendChild(root);
n@2579 2811 return root;
n@2579 2812 };
n@2579 2813 this.resize = function () {
n@2579 2814 var boxwidth = (window.innerWidth - 100) / 2;
n@2579 2815 if (boxwidth >= 600) {
n@2579 2816 boxwidth = 600;
n@2579 2817 } else if (boxwidth < 400) {
n@2579 2818 boxwidth = 400;
n@2579 2819 }
n@2579 2820 this.holder.style.width = boxwidth + "px";
n@2579 2821 this.slider.style.width = boxwidth - 24 + "px";
n@2579 2822 };
n@2579 2823 this.resize();
n@2579 2824 };
n@2579 2825
nicholas@2498 2826 this.createCommentQuestion = function (element) {
nicholas@2498 2827 var node;
nicholas@2498 2828 if (element.type == 'question') {
nicholas@2498 2829 node = new this.commentBox(element);
nicholas@2498 2830 } else if (element.type == 'radio') {
nicholas@2498 2831 node = new this.radioBox(element);
nicholas@2498 2832 } else if (element.type == 'checkbox') {
nicholas@2498 2833 node = new this.checkboxBox(element);
n@2579 2834 } else if (element.type == 'slider') {
n@2579 2835 node = new this.sliderBox(element);
nicholas@2498 2836 }
nicholas@2498 2837 this.commentQuestions.push(node);
nicholas@2498 2838 return node;
nicholas@2498 2839 };
nicholas@2498 2840
nicholas@2498 2841 this.deleteCommentQuestions = function () {
nicholas@2498 2842 this.commentQuestions = [];
nicholas@2498 2843 };
nicholas@2498 2844
nicholas@2498 2845 this.outsideReferenceDOM = function (audioObject, index, inject) {
nicholas@2224 2846 this.parent = audioObject;
nicholas@2224 2847 this.outsideReferenceHolder = document.createElement('button');
nicholas@2224 2848 this.outsideReferenceHolder.className = 'outside-reference';
nicholas@2498 2849 this.outsideReferenceHolder.setAttribute('track-id', index);
nicholas@2409 2850 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 2851 this.outsideReferenceHolder.disabled = true;
nicholas@2701 2852 this.handleEvent = function (event) {
nicholas@2700 2853 audioEngineContext.play(this.parent.id);
nicholas@2224 2854 };
nicholas@2700 2855 this.outsideReferenceHolder.addEventListener("click", this);
nicholas@2224 2856 inject.appendChild(this.outsideReferenceHolder);
nicholas@2498 2857 this.enable = function () {
nicholas@2498 2858 if (this.parent.state == 1) {
nicholas@2224 2859 this.outsideReferenceHolder.disabled = false;
nicholas@2224 2860 }
nicholas@2224 2861 };
nicholas@2498 2862 this.updateLoading = function (progress) {
nicholas@2498 2863 if (progress != 100) {
nicholas@2224 2864 progress = String(progress);
nicholas@2224 2865 progress = progress.split('.')[0];
nicholas@2498 2866 this.outsideReferenceHolder.textContent = progress + '%';
nicholas@2224 2867 } else {
nicholas@2409 2868 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
nicholas@2224 2869 }
nicholas@2224 2870 };
nicholas@2498 2871 this.startPlayback = function () {
nicholas@2224 2872 // Called when playback has begun
nicholas@2224 2873 $('.track-slider').removeClass('track-slider-playing');
nicholas@2224 2874 $('.comment-div').removeClass('comment-box-playing');
nicholas@2224 2875 this.outsideReferenceHolder.style.backgroundColor = "#FDD";
nicholas@2224 2876 };
nicholas@2498 2877 this.stopPlayback = function () {
nicholas@2224 2878 // Called when playback has stopped. This gets called even if playback never started!
nicholas@2224 2879 this.outsideReferenceHolder.style.backgroundColor = "";
nicholas@2224 2880 };
nicholas@2498 2881 this.exportXMLDOM = function (audioObject) {
nicholas@2224 2882 return null;
nicholas@2224 2883 };
nicholas@2498 2884 this.getValue = function () {
nicholas@2224 2885 return 0;
nicholas@2224 2886 };
nicholas@2498 2887 this.getPresentedId = function () {
nicholas@2409 2888 return this.parent.specification.label || "Reference";
nicholas@2224 2889 };
nicholas@2498 2890 this.canMove = function () {
nicholas@2224 2891 return false;
nicholas@2224 2892 };
nicholas@2498 2893 this.error = function () {
nicholas@2498 2894 // audioObject has an error!!
nicholas@2224 2895 this.outsideReferenceHolder.textContent = "Error";
nicholas@2224 2896 this.outsideReferenceHolder.style.backgroundColor = "#F00";
nicholas@2678 2897 };
nicholas@2678 2898 };
nicholas@2498 2899
nicholas@2498 2900 this.playhead = new function () {
nicholas@2498 2901 this.object = document.createElement('div');
nicholas@2498 2902 this.object.className = 'playhead';
nicholas@2498 2903 this.object.align = 'left';
nicholas@2498 2904 var curTime = document.createElement('div');
nicholas@2498 2905 curTime.style.width = '50px';
nicholas@2498 2906 this.curTimeSpan = document.createElement('span');
nicholas@2498 2907 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2908 curTime.appendChild(this.curTimeSpan);
nicholas@2498 2909 this.object.appendChild(curTime);
nicholas@2498 2910 this.scrubberTrack = document.createElement('div');
nicholas@2498 2911 this.scrubberTrack.className = 'playhead-scrub-track';
nicholas@2498 2912
nicholas@2498 2913 this.scrubberHead = document.createElement('div');
nicholas@2498 2914 this.scrubberHead.id = 'playhead-scrubber';
nicholas@2498 2915 this.scrubberTrack.appendChild(this.scrubberHead);
nicholas@2498 2916 this.object.appendChild(this.scrubberTrack);
nicholas@2498 2917
nicholas@2498 2918 this.timePerPixel = 0;
nicholas@2498 2919 this.maxTime = 0;
nicholas@2498 2920
nicholas@2682 2921 this.playbackObject = undefined;
nicholas@2498 2922
nicholas@2498 2923 this.setTimePerPixel = function (audioObject) {
nicholas@2498 2924 //maxTime must be in seconds
nicholas@2498 2925 this.playbackObject = audioObject;
nicholas@2498 2926 this.maxTime = audioObject.buffer.buffer.duration;
nicholas@2498 2927 var width = 490; //500 - 10, 5 each side of the tracker head
nicholas@2498 2928 this.timePerPixel = this.maxTime / 490;
nicholas@2498 2929 if (this.maxTime < 60) {
nicholas@2498 2930 this.curTimeSpan.textContent = '0.00';
nicholas@2498 2931 } else {
nicholas@2498 2932 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2933 }
nicholas@2498 2934 };
nicholas@2498 2935
nicholas@2498 2936 this.update = function () {
nicholas@2498 2937 // Update the playhead position, startPlay must be called
nicholas@2498 2938 if (this.timePerPixel > 0) {
nicholas@2498 2939 var time = this.playbackObject.getCurrentPosition();
nicholas@2498 2940 if (time > 0 && time < this.maxTime) {
nicholas@2498 2941 var width = 490;
nicholas@2498 2942 var pix = Math.floor(time / this.timePerPixel);
nicholas@2498 2943 this.scrubberHead.style.left = pix + 'px';
nicholas@2498 2944 if (this.maxTime > 60.0) {
nicholas@2498 2945 var secs = time % 60;
nicholas@2498 2946 var mins = Math.floor((time - secs) / 60);
nicholas@2498 2947 secs = secs.toString();
nicholas@2498 2948 secs = secs.substr(0, 2);
nicholas@2498 2949 mins = mins.toString();
nicholas@2498 2950 this.curTimeSpan.textContent = mins + ':' + secs;
nicholas@2498 2951 } else {
nicholas@2498 2952 time = time.toString();
nicholas@2498 2953 this.curTimeSpan.textContent = time.substr(0, 4);
nicholas@2498 2954 }
nicholas@2498 2955 } else {
nicholas@2498 2956 this.scrubberHead.style.left = '0px';
nicholas@2498 2957 if (this.maxTime < 60) {
nicholas@2498 2958 this.curTimeSpan.textContent = '0.00';
nicholas@2498 2959 } else {
nicholas@2498 2960 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2961 }
nicholas@2498 2962 }
nicholas@2498 2963 }
nicholas@2498 2964 };
nicholas@2498 2965
nicholas@2498 2966 this.interval = undefined;
nicholas@2498 2967
nicholas@2498 2968 this.start = function () {
nicholas@2678 2969 if (this.playbackObject !== undefined && this.interval === undefined) {
nicholas@2498 2970 if (this.maxTime < 60) {
nicholas@2682 2971 this.interval = window.setInterval(function () {
nicholas@2498 2972 interfaceContext.playhead.update();
nicholas@2498 2973 }, 10);
nicholas@2498 2974 } else {
nicholas@2682 2975 this.interval = window.setInterval(function () {
nicholas@2498 2976 interfaceContext.playhead.update();
nicholas@2498 2977 }, 100);
nicholas@2498 2978 }
nicholas@2498 2979 }
nicholas@2498 2980 };
nicholas@2498 2981 this.stop = function () {
nicholas@2682 2982 window.clearInterval(this.interval);
nicholas@2498 2983 this.interval = undefined;
nicholas@2224 2984 this.scrubberHead.style.left = '0px';
nicholas@2498 2985 if (this.maxTime < 60) {
nicholas@2498 2986 this.curTimeSpan.textContent = '0.00';
nicholas@2498 2987 } else {
nicholas@2498 2988 this.curTimeSpan.textContent = '00:00';
nicholas@2498 2989 }
nicholas@2498 2990 };
nicholas@2498 2991 };
nicholas@2498 2992
nicholas@2498 2993 this.volume = new function () {
nicholas@2224 2994 // An in-built volume module which can be viewed on page
nicholas@2224 2995 // Includes trackers on page-by-page data
nicholas@2224 2996 // Volume does NOT reset to 0dB on each page load
nicholas@2224 2997 this.valueLin = 1.0;
nicholas@2224 2998 this.valueDB = 0.0;
nicholas@2352 2999 this.root = document.createElement('div');
nicholas@2352 3000 this.root.id = 'master-volume-root';
nicholas@2224 3001 this.object = document.createElement('div');
nicholas@2352 3002 this.object.className = 'master-volume-holder-float';
nicholas@2352 3003 this.object.appendChild(this.root);
nicholas@2224 3004 this.slider = document.createElement('input');
nicholas@2224 3005 this.slider.id = 'master-volume-control';
nicholas@2224 3006 this.slider.type = 'range';
nicholas@2224 3007 this.valueText = document.createElement('span');
nicholas@2224 3008 this.valueText.id = 'master-volume-feedback';
nicholas@2224 3009 this.valueText.textContent = '0dB';
nicholas@2498 3010
nicholas@2224 3011 this.slider.min = -60;
nicholas@2224 3012 this.slider.max = 12;
nicholas@2224 3013 this.slider.value = 0;
nicholas@2224 3014 this.slider.step = 1;
nicholas@2498 3015 this.slider.onmousemove = function (event) {
nicholas@2224 3016 interfaceContext.volume.valueDB = event.currentTarget.value;
nicholas@2224 3017 interfaceContext.volume.valueLin = decibelToLinear(interfaceContext.volume.valueDB);
nicholas@2498 3018 interfaceContext.volume.valueText.textContent = interfaceContext.volume.valueDB + 'dB';
nicholas@2224 3019 audioEngineContext.outputGain.gain.value = interfaceContext.volume.valueLin;
nicholas@2678 3020 };
nicholas@2498 3021 this.slider.onmouseup = function (event) {
nicholas@2224 3022 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
nicholas@2678 3023 if (storePoint.length === 0) {
nicholas@2224 3024 storePoint = storage.document.createElement('metricresult');
nicholas@2498 3025 storePoint.setAttribute('name', 'volumeTracker');
nicholas@2224 3026 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
nicholas@2498 3027 } else {
nicholas@2224 3028 storePoint = storePoint[0];
nicholas@2224 3029 }
nicholas@2224 3030 var node = storage.document.createElement('movement');
nicholas@2498 3031 node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
nicholas@2498 3032 node.setAttribute('volume', interfaceContext.volume.valueDB);
nicholas@2498 3033 node.setAttribute('format', 'dBFS');
nicholas@2224 3034 storePoint.appendChild(node);
nicholas@2678 3035 };
nicholas@2498 3036
nicholas@2224 3037 var title = document.createElement('div');
nicholas@2224 3038 title.innerHTML = '<span>Master Volume Control</span>';
nicholas@2224 3039 title.style.fontSize = '0.75em';
nicholas@2224 3040 title.style.width = "100%";
nicholas@2224 3041 title.align = 'center';
nicholas@2352 3042 this.root.appendChild(title);
nicholas@2498 3043
nicholas@2352 3044 this.root.appendChild(this.slider);
nicholas@2352 3045 this.root.appendChild(this.valueText);
nicholas@2498 3046
nicholas@2498 3047 this.resize = function (event) {
nicholas@2352 3048 if (window.innerWidth < 1000) {
nicholas@2678 3049 this.object.className = "master-volume-holder-inline";
nicholas@2352 3050 } else {
nicholas@2352 3051 this.object.className = 'master-volume-holder-float';
nicholas@2352 3052 }
nicholas@2678 3053 };
nicholas@2678 3054 };
nicholas@2498 3055
nicholas@2224 3056 this.calibrationModuleObject = null;
nicholas@2498 3057 this.calibrationModule = function () {
nicholas@2224 3058 // This creates an on-page calibration module
nicholas@2224 3059 this.storeDOM = storage.document.createElement("calibration");
nicholas@2224 3060 storage.root.appendChild(this.storeDOM);
nicholas@2224 3061 // The calibration is a fixed state module
nicholas@2224 3062 this.calibrationNodes = [];
nicholas@2224 3063 this.holder = null;
nicholas@2498 3064 this.build = function (inject) {
nicholas@2224 3065 var f0 = 62.5;
nicholas@2224 3066 this.holder = document.createElement("div");
nicholas@2224 3067 this.holder.className = "calibration-holder";
nicholas@2224 3068 this.calibrationNodes = [];
nicholas@2498 3069 while (f0 < 20000) {
nicholas@2224 3070 var obj = {
nicholas@2224 3071 root: document.createElement("div"),
nicholas@2224 3072 input: document.createElement("input"),
nicholas@2224 3073 oscillator: audioContext.createOscillator(),
nicholas@2224 3074 gain: audioContext.createGain(),
nicholas@2224 3075 f: f0,
nicholas@2224 3076 parent: this,
nicholas@2498 3077 handleEvent: function (event) {
nicholas@2498 3078 switch (event.type) {
nicholas@2224 3079 case "mouseenter":
nicholas@2224 3080 this.oscillator.start(0);
nicholas@2224 3081 break;
nicholas@2224 3082 case "mouseleave":
nicholas@2224 3083 this.oscillator.stop(0);
nicholas@2224 3084 this.oscillator = audioContext.createOscillator();
nicholas@2224 3085 this.oscillator.connect(this.gain);
nicholas@2224 3086 this.oscillator.frequency.value = this.f;
nicholas@2224 3087 break;
nicholas@2224 3088 case "mousemove":
nicholas@2498 3089 var value = Math.pow(10, this.input.value / 20);
nicholas@2224 3090 if (this.f == 1000) {
nicholas@2224 3091 audioEngineContext.outputGain.gain.value = value;
nicholas@2224 3092 interfaceContext.volume.slider.value = this.input.value;
nicholas@2224 3093 } else {
nicholas@2678 3094 this.gain.gain.value = value;
nicholas@2224 3095 }
nicholas@2224 3096 break;
nicholas@2224 3097 }
nicholas@2224 3098 },
nicholas@2498 3099 disconnect: function () {
nicholas@2224 3100 this.gain.disconnect();
nicholas@2224 3101 }
nicholas@2678 3102 };
nicholas@2224 3103 obj.root.className = "calibration-slider";
nicholas@2224 3104 obj.root.appendChild(obj.input);
nicholas@2224 3105 obj.oscillator.connect(obj.gain);
nicholas@2224 3106 obj.gain.connect(audioEngineContext.outputGain);
nicholas@2498 3107 obj.gain.gain.value = Math.random() * 2;
nicholas@2224 3108 obj.input.value = obj.gain.gain.value;
nicholas@2498 3109 obj.input.setAttribute('orient', 'vertical');
nicholas@2224 3110 obj.input.type = "range";
nicholas@2593 3111 obj.input.min = -12;
nicholas@2593 3112 obj.input.max = 0;
nicholas@2224 3113 obj.input.step = 0.25;
nicholas@2224 3114 if (f0 != 1000) {
nicholas@2498 3115 obj.input.value = (Math.random() * 12) - 6;
nicholas@2224 3116 } else {
nicholas@2224 3117 obj.input.value = 0;
nicholas@2498 3118 obj.root.style.backgroundColor = "rgb(255,125,125)";
nicholas@2224 3119 }
nicholas@2498 3120 obj.input.addEventListener("mousemove", obj);
nicholas@2498 3121 obj.input.addEventListener("mouseenter", obj);
nicholas@2498 3122 obj.input.addEventListener("mouseleave", obj);
nicholas@2498 3123 obj.gain.gain.value = Math.pow(10, obj.input.value / 20);
nicholas@2224 3124 obj.oscillator.frequency.value = f0;
nicholas@2224 3125 this.calibrationNodes.push(obj);
nicholas@2224 3126 this.holder.appendChild(obj.root);
nicholas@2224 3127 f0 *= 2;
nicholas@2224 3128 }
nicholas@2224 3129 inject.appendChild(this.holder);
nicholas@2678 3130 };
nicholas@2498 3131 this.collect = function () {
nicholas@2682 3132 this.calibrationNodes.forEach(function (obj) {
nicholas@2224 3133 var node = storage.document.createElement("calibrationresult");
nicholas@2498 3134 node.setAttribute("frequency", obj.f);
nicholas@2498 3135 node.setAttribute("range-min", obj.input.min);
nicholas@2498 3136 node.setAttribute("range-max", obj.input.max);
nicholas@2498 3137 node.setAttribute("gain-lin", obj.gain.gain.value);
nicholas@2224 3138 this.storeDOM.appendChild(node);
nicholas@2682 3139 }, this);
nicholas@2678 3140 };
nicholas@2678 3141 };
nicholas@2498 3142
nicholas@2498 3143
nicholas@2498 3144 // Global Checkers
nicholas@2498 3145 // These functions will help enforce the checkers
nicholas@2498 3146 this.checkHiddenAnchor = function () {
nicholas@2697 3147 var anchors = audioEngineContext.audioObjects.filter(function (ao) {
nicholas@2697 3148 return ao.specification.type === "anchor";
nicholas@2697 3149 });
nicholas@2697 3150 var state = anchors.some(function (ao) {
nicholas@2697 3151 return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0);
nicholas@2697 3152 });
nicholas@2697 3153 if (state) {
nicholas@2697 3154 console.log('Anchor node not below marker value');
nicholas@2697 3155 interfaceContext.lightbox.post("Message", 'Please keep listening');
nicholas@2697 3156 this.storeErrorNode('Anchor node not below marker value');
nicholas@2697 3157 return false;
nicholas@2697 3158 }
nicholas@2498 3159 return true;
nicholas@2498 3160 };
nicholas@2498 3161
nicholas@2498 3162 this.checkHiddenReference = function () {
nicholas@2697 3163 var references = audioEngineContext.audioObjects.filter(function (ao) {
nicholas@2698 3164 return ao.specification.type === "reference";
nicholas@2697 3165 });
nicholas@2697 3166 var state = references.some(function (ao) {
nicholas@2697 3167 return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0);
nicholas@2697 3168 });
nicholas@2697 3169 if (state) {
nicholas@2697 3170 console.log('Reference node not below marker value');
nicholas@2697 3171 interfaceContext.lightbox.post("Message", 'Please keep listening');
nicholas@2697 3172 this.storeErrorNode('Reference node not below marker value');
nicholas@2697 3173 return false;
nicholas@2697 3174 }
nicholas@2498 3175 return true;
nicholas@2498 3176 };
nicholas@2498 3177
nicholas@2498 3178 this.checkFragmentsFullyPlayed = function () {
nicholas@2498 3179 // Checks the entire file has been played back
nicholas@2498 3180 // NOTE ! This will return true IF playback is Looped!!!
nicholas@2498 3181 if (audioEngineContext.loopPlayback) {
nicholas@2498 3182 console.log("WARNING - Looped source: Cannot check fragments are fully played");
nicholas@2498 3183 return true;
nicholas@2498 3184 }
nicholas@2498 3185 var check_pass = true;
nicholas@2682 3186 var error_obj = [],
nicholas@2682 3187 i;
nicholas@2682 3188 for (i = 0; i < audioEngineContext.audioObjects.length; i++) {
nicholas@2498 3189 var object = audioEngineContext.audioObjects[i];
nicholas@2498 3190 var time = object.buffer.buffer.duration;
nicholas@2498 3191 var metric = object.metric;
nicholas@2498 3192 var passed = false;
nicholas@2498 3193 for (var j = 0; j < metric.listenTracker.length; j++) {
nicholas@2498 3194 var bt = metric.listenTracker[j].getElementsByTagName('testtime');
nicholas@2498 3195 var start_time = Number(bt[0].getAttribute('start'));
nicholas@2498 3196 var stop_time = Number(bt[0].getAttribute('stop'));
nicholas@2498 3197 var delta = stop_time - start_time;
nicholas@2498 3198 if (delta >= time) {
nicholas@2498 3199 passed = true;
nicholas@2498 3200 break;
nicholas@2498 3201 }
nicholas@2498 3202 }
nicholas@2678 3203 if (passed === false) {
nicholas@2498 3204 check_pass = false;
nicholas@2498 3205 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
nicholas@2498 3206 error_obj.push(object.interfaceDOM.getPresentedId());
nicholas@2498 3207 }
nicholas@2498 3208 }
nicholas@2678 3209 if (check_pass === false) {
nicholas@2498 3210 var str_start = "You have not completely listened to fragments ";
nicholas@2682 3211 for (i = 0; i < error_obj.length; i++) {
nicholas@2498 3212 str_start += error_obj[i];
nicholas@2498 3213 if (i != error_obj.length - 1) {
nicholas@2498 3214 str_start += ', ';
nicholas@2498 3215 }
nicholas@2498 3216 }
nicholas@2498 3217 str_start += ". Please keep listening";
nicholas@2498 3218 console.log("[ALERT]: " + str_start);
nicholas@2498 3219 this.storeErrorNode("[ALERT]: " + str_start);
nicholas@2498 3220 interfaceContext.lightbox.post("Error", str_start);
nicholas@2444 3221 return false;
nicholas@2498 3222 }
nicholas@2444 3223 return true;
nicholas@2498 3224 };
nicholas@2498 3225 this.checkAllMoved = function () {
nicholas@2498 3226 var str = "You have not moved ";
nicholas@2498 3227 var failed = [];
nicholas@2682 3228 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2678 3229 if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) {
nicholas@2498 3230 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3231 }
nicholas@2682 3232 }, this);
nicholas@2678 3233 if (failed.length === 0) {
nicholas@2498 3234 return true;
nicholas@2498 3235 } else if (failed.length == 1) {
nicholas@2498 3236 str += 'track ' + failed[0];
nicholas@2498 3237 } else {
nicholas@2498 3238 str += 'tracks ';
nicholas@2498 3239 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3240 str += failed[i] + ', ';
nicholas@2498 3241 }
nicholas@2498 3242 str += 'and ' + failed[i];
nicholas@2498 3243 }
nicholas@2498 3244 str += '.';
nicholas@2498 3245 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3246 console.log(str);
nicholas@2224 3247 this.storeErrorNode(str);
nicholas@2498 3248 return false;
nicholas@2498 3249 };
nicholas@2498 3250 this.checkAllPlayed = function () {
nicholas@2498 3251 var str = "You have not played ";
nicholas@2498 3252 var failed = [];
nicholas@2682 3253 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2678 3254 if (ao.metric.wasListenedTo === false) {
nicholas@2498 3255 failed.push(ao.interfaceDOM.getPresentedId());
nicholas@2498 3256 }
nicholas@2682 3257 }, this);
nicholas@2678 3258 if (failed.length === 0) {
nicholas@2498 3259 return true;
nicholas@2498 3260 } else if (failed.length == 1) {
nicholas@2498 3261 str += 'track ' + failed[0];
nicholas@2498 3262 } else {
nicholas@2498 3263 str += 'tracks ';
nicholas@2498 3264 for (var i = 0; i < failed.length - 1; i++) {
nicholas@2498 3265 str += failed[i] + ', ';
nicholas@2498 3266 }
nicholas@2498 3267 str += 'and ' + failed[i];
nicholas@2498 3268 }
nicholas@2498 3269 str += '.';
nicholas@2498 3270 interfaceContext.lightbox.post("Error", str);
nicholas@2498 3271 console.log(str);
nicholas@2224 3272 this.storeErrorNode(str);
nicholas@2498 3273 return false;
nicholas@2498 3274 };
nicholas@2540 3275 this.checkAllCommented = function () {
nicholas@2540 3276 var str = "You have not commented on all the fragments.";
nicholas@2540 3277 var cont = true,
nicholas@2540 3278 boxes = this.commentBoxes.boxes,
nicholas@2540 3279 numBoxes = boxes.length,
nicholas@2540 3280 i;
nicholas@2540 3281 for (i = 0; i < numBoxes; i++) {
nicholas@2540 3282 if (boxes[i].trackCommentBox.value === "") {
nicholas@2540 3283 interfaceContext.lightbox.post("Error", str);
nicholas@2540 3284 console.log(str);
nicholas@2540 3285 this.storeErrorNode(str);
nicholas@2540 3286 return false;
nicholas@2540 3287 }
nicholas@2540 3288 }
nicholas@2540 3289 return true;
nicholas@2678 3290 };
nicholas@2498 3291 this.checkScaleRange = function (min, max) {
nicholas@2310 3292 var page = testState.getCurrentTestPage();
nicholas@2310 3293 var audioObjects = audioEngineContext.audioObjects;
nicholas@2310 3294 var state = true;
nicholas@2310 3295 var str = "Please keep listening. ";
nicholas@2310 3296 var minRanking = Infinity;
nicholas@2310 3297 var maxRanking = -Infinity;
nicholas@2682 3298 audioEngineContext.audioObjects.forEach(function (ao) {
nicholas@2310 3299 var rank = ao.interfaceDOM.getValue();
nicholas@2498 3300 if (rank < minRanking) {
nicholas@2498 3301 minRanking = rank;
nicholas@2498 3302 }
nicholas@2498 3303 if (rank > maxRanking) {
nicholas@2498 3304 maxRanking = rank;
nicholas@2498 3305 }
nicholas@2682 3306 }, this);
nicholas@2498 3307 if (minRanking * 100 > min) {
nicholas@2498 3308 str += "At least one fragment must be below the " + min + " mark.";
nicholas@2310 3309 state = false;
nicholas@2310 3310 }
nicholas@2498 3311 if (maxRanking * 100 < max) {
nicholas@2678 3312 str += "At least one fragment must be above the " + max + " mark.";
nicholas@2310 3313 state = false;
nicholas@2310 3314 }
nicholas@2310 3315 if (!state) {
nicholas@2310 3316 console.log(str);
nicholas@2310 3317 this.storeErrorNode(str);
nicholas@2498 3318 interfaceContext.lightbox.post("Error", str);
nicholas@2310 3319 }
nicholas@2310 3320 return state;
nicholas@2678 3321 };
nicholas@2498 3322
nicholas@2498 3323 this.storeErrorNode = function (errorMessage) {
nicholas@2224 3324 var time = audioEngineContext.timer.getTestTime();
nicholas@2224 3325 var node = storage.document.createElement('error');
nicholas@2498 3326 node.setAttribute('time', time);
nicholas@2224 3327 node.textContent = errorMessage;
nicholas@2224 3328 testState.currentStore.XMLDOM.appendChild(node);
nicholas@2224 3329 };
nicholas@2595 3330
nicholas@2595 3331 this.getLabel = function (labelType, index, labelStart) {
nicholas@2595 3332 /*
nicholas@2595 3333 Get the correct label based on type, index and offset
nicholas@2595 3334 */
nicholas@2595 3335
nicholas@2595 3336 function calculateLabel(labelType, index, offset) {
nicholas@2595 3337 if (labelType == "none") {
nicholas@2595 3338 return "";
nicholas@2595 3339 }
nicholas@2595 3340 switch (labelType) {
nicholas@2595 3341 case "letter":
nicholas@2596 3342 return String.fromCharCode((index + offset) % 26 + 97);
nicholas@2595 3343 case "capital":
nicholas@2607 3344 return String.fromCharCode((index + offset) % 26 + 65);
nicholas@2625 3345 case "samediff":
nicholas@2678 3346 if (index === 0) {
nicholas@2625 3347 return "Same";
nicholas@2625 3348 } else if (index == 1) {
nicholas@2625 3349 return "Difference";
nicholas@2625 3350 }
nicholas@2678 3351 return "";
nicholas@2595 3352 case "number":
nicholas@2595 3353 return String(index + offset);
nicholas@2595 3354 default:
nicholas@2595 3355 return "";
nicholas@2595 3356 }
nicholas@2595 3357 }
nicholas@2595 3358
nicholas@2678 3359 if (typeof labelStart !== "string" || labelStart.length === 0) {
nicholas@2595 3360 labelStart = String.fromCharCode(0);
nicholas@2595 3361 }
nicholas@2595 3362
nicholas@2595 3363 switch (labelType) {
nicholas@2595 3364 case "letter":
nicholas@2595 3365 labelStart = labelStart.charCodeAt(0);
nicholas@2596 3366 if (labelStart < 97 || labelStart > 122) {
nicholas@2595 3367 labelStart = 97;
nicholas@2595 3368 }
nicholas@2595 3369 labelStart -= 97;
nicholas@2595 3370 break;
nicholas@2595 3371 case "capital":
nicholas@2595 3372 labelStart = labelStart.charCodeAt(0);
nicholas@2596 3373 if (labelStart < 65 || labelStart > 90) {
nicholas@2595 3374 labelStart = 65;
nicholas@2595 3375 }
nicholas@2595 3376 labelStart -= 65;
nicholas@2595 3377 break;
nicholas@2595 3378 case "number":
nicholas@2608 3379 labelStart = Number(labelStart);
nicholas@2608 3380 if (!isFinite(labelStart)) {
nicholas@2595 3381 labelStart = 1;
nicholas@2595 3382 }
nicholas@2595 3383 break;
nicholas@2595 3384 default:
nicholas@2596 3385 labelStart = 0;
nicholas@2595 3386 }
nicholas@2595 3387 if (typeof index == "number") {
nicholas@2595 3388 return calculateLabel(labelType, index, labelStart);
nicholas@2595 3389 } else if (index.length && index.length > 0) {
nicholas@2595 3390 var a = [],
nicholas@2595 3391 l = index.length,
nicholas@2595 3392 i;
nicholas@2595 3393 for (i = 0; i < l; i++) {
nicholas@2595 3394 a[i] = calculateLabel(labelType, index[i], labelStart);
nicholas@2595 3395 }
nicholas@2595 3396 return a;
nicholas@2595 3397 } else {
nicholas@2595 3398 throw ("Invalid arguments");
nicholas@2595 3399 }
nicholas@2678 3400 };
nicholas@2649 3401
nicholas@2649 3402 this.getCombinedInterfaces = function (page) {
nicholas@2649 3403 // Combine the interfaces with the global interface nodes
nicholas@2649 3404 var global = specification.interfaces,
nicholas@2649 3405 local = page.interfaces;
nicholas@2649 3406 local.forEach(function (locInt) {
nicholas@2649 3407 // Iterate through the options nodes
nicholas@2649 3408 var addList = [];
nicholas@2649 3409 global.options.forEach(function (gopt) {
nicholas@2649 3410 var lopt = locInt.options.find(function (lopt) {
nicholas@2649 3411 return (lopt.name == gopt.name) && (lopt.type == gopt.type);
nicholas@2649 3412 });
nicholas@2649 3413 if (!lopt) {
nicholas@2649 3414 // Global option doesn't exist locally
nicholas@2649 3415 addList.push(gopt);
nicholas@2649 3416 }
nicholas@2649 3417 });
nicholas@2649 3418 locInt.options = locInt.options.concat(addList);
nicholas@2649 3419 if (!locInt.scales && global.scales) {
nicholas@2649 3420 // Use the global default scales
nicholas@2649 3421 locInt.scales = global.scales;
nicholas@2649 3422 }
nicholas@2649 3423 });
nicholas@2649 3424 return local;
nicholas@2678 3425 };
nicholas@2224 3426 }
nicholas@2224 3427
nicholas@2498 3428 function Storage() {
nicholas@2498 3429 // Holds results in XML format until ready for collection
nicholas@2498 3430 this.globalPreTest = null;
nicholas@2498 3431 this.globalPostTest = null;
nicholas@2498 3432 this.testPages = [];
nicholas@2498 3433 this.document = null;
nicholas@2498 3434 this.root = null;
nicholas@2498 3435 this.state = 0;
nicholas@2498 3436
nicholas@2498 3437 this.initialise = function (existingStore) {
nicholas@2678 3438 if (existingStore === undefined) {
nicholas@2224 3439 // We need to get the sessionKey
nicholas@2510 3440 this.SessionKey.requestKey();
nicholas@2498 3441 this.document = document.implementation.createDocument(null, "waetresult", null);
nicholas@2224 3442 this.root = this.document.childNodes[0];
nicholas@2224 3443 var projectDocument = specification.projectXML;
nicholas@2682 3444 projectDocument.setAttribute('file-name', specification.url);
nicholas@2682 3445 projectDocument.setAttribute('url', qualifyURL(specification.url));
nicholas@2224 3446 this.root.appendChild(projectDocument);
nicholas@2224 3447 this.root.appendChild(interfaceContext.returnDateNode());
nicholas@2224 3448 this.root.appendChild(interfaceContext.returnNavigator());
nicholas@2224 3449 } else {
nicholas@2224 3450 this.document = existingStore;
nicholas@2294 3451 this.root = existingStore.firstChild;
nicholas@2224 3452 this.SessionKey.key = this.root.getAttribute("key");
nicholas@2224 3453 }
nicholas@2678 3454 if (specification.preTest !== undefined) {
nicholas@2498 3455 this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
nicholas@2498 3456 }
nicholas@2678 3457 if (specification.postTest !== undefined) {
nicholas@2498 3458 this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
nicholas@2498 3459 }
nicholas@2498 3460 };
nicholas@2498 3461
nicholas@2224 3462 this.SessionKey = {
nicholas@2224 3463 key: null,
nicholas@2224 3464 request: new XMLHttpRequest(),
nicholas@2224 3465 parent: this,
nicholas@2498 3466 handleEvent: function () {
nicholas@2224 3467 var parse = new DOMParser();
nicholas@2498 3468 var xml = parse.parseFromString(this.request.response, "text/xml");
nicholas@2678 3469 if (this.request.response.length === 0) {
nicholas@2515 3470 console.error("An unspecified error occured, no server key could be generated");
nicholas@2376 3471 return;
nicholas@2376 3472 }
nicholas@2498 3473 if (xml.getElementsByTagName("state").length > 0) {
nicholas@2498 3474 if (xml.getElementsByTagName("state")[0].textContent == "OK") {
nicholas@2498 3475 this.key = xml.getAllElementsByTagName("key")[0].textContent;
nicholas@2498 3476 this.parent.root.setAttribute("key", this.key);
nicholas@2498 3477 this.parent.root.setAttribute("state", "empty");
nicholas@2516 3478 this.update();
nicholas@2515 3479 return;
nicholas@2514 3480 } else if (xml.getElementsByTagName("state")[0].textContent == "ERROR") {
nicholas@2515 3481 this.key = null;
nicholas@2514 3482 console.error("Could not generate server key. Server responded with error message: \"" + xml.getElementsByTagName("message")[0].textContent + "\"");
nicholas@2515 3483 return;
nicholas@2498 3484 }
nicholas@2498 3485 }
nicholas@2515 3486 this.key = null;
nicholas@2515 3487 console.error("An unspecified error occured, no server key could be generated");
nicholas@2224 3488 },
nicholas@2510 3489 requestKey: function () {
nicholas@2510 3490 // For new servers, request a new key from the server
nicholas@2510 3491 var returnURL = "";
nicholas@2510 3492 if (typeof specification.projectReturn == "string") {
nicholas@2510 3493 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2510 3494 returnURL = specification.projectReturn;
nicholas@2510 3495 }
nicholas@2510 3496 }
nicholas@2510 3497 this.request.open("GET", returnURL + "php/requestKey.php", true);
nicholas@2510 3498 this.request.addEventListener("load", this);
nicholas@2510 3499 this.request.send();
nicholas@2510 3500 },
nicholas@2498 3501 update: function () {
nicholas@2678 3502 if (this.key === null) {
nicholas@2357 3503 console.log("Cannot save as key == null");
nicholas@2357 3504 return;
nicholas@2357 3505 }
nicholas@2498 3506 this.parent.root.setAttribute("state", "update");
nicholas@2224 3507 var xmlhttp = new XMLHttpRequest();
nicholas@2302 3508 var returnURL = "";
nicholas@2302 3509 if (typeof specification.projectReturn == "string") {
nicholas@2498 3510 if (specification.projectReturn.substr(0, 4) == "http") {
nicholas@2302 3511 returnURL = specification.projectReturn;
nicholas@2302 3512 }
nicholas@2302 3513 }
nicholas@2498 3514 xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key);
nicholas@2224 3515 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
nicholas@2498 3516 xmlhttp.onerror = function () {
nicholas@2224 3517 console.log('Error updating file to server!');
nicholas@2224 3518 };
nicholas@2224 3519 var hold = document.createElement("div");
nicholas@2224 3520 var clone = this.parent.root.cloneNode(true);
nicholas@2224 3521 hold.appendChild(clone);
nicholas@2498 3522 xmlhttp.onload = function () {
nicholas@2224 3523 if (this.status >= 300) {
nicholas@2224 3524 console.log("WARNING - Could not update at this time");
nicholas@2224 3525 } else {
nicholas@2224 3526 var parser = new DOMParser();
nicholas@2224 3527 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
nicholas@2224 3528 var response = xmlDoc.getElementsByTagName('response')[0];
nicholas@2224 3529 if (response.getAttribute("state") == "OK") {
nicholas@2224 3530 var file = response.getElementsByTagName("file")[0];
nicholas@2498 3531 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
nicholas@2224 3532 } else {
nicholas@2224 3533 var message = response.getElementsByTagName("message");
nicholas@2498 3534 console.log("Intermediate save: Error! " + message.textContent);
nicholas@2224 3535 }
nicholas@2224 3536 }
nicholas@2678 3537 };
nicholas@2224 3538 xmlhttp.send([hold.innerHTML]);
nicholas@2224 3539 }
nicholas@2678 3540 };
nicholas@2498 3541
nicholas@2498 3542 this.createTestPageStore = function (specification) {
nicholas@2498 3543 var store = new this.pageNode(this, specification);
nicholas@2498 3544 this.testPages.push(store);
nicholas@2498 3545 return this.testPages[this.testPages.length - 1];
nicholas@2498 3546 };
nicholas@2498 3547
nicholas@2498 3548 this.surveyNode = function (parent, root, specification) {
nicholas@2498 3549 this.specification = specification;
nicholas@2498 3550 this.parent = parent;
nicholas@2224 3551 this.state = "empty";
nicholas@2498 3552 this.XMLDOM = this.parent.document.createElement('survey');
nicholas@2498 3553 this.XMLDOM.setAttribute('location', this.specification.location);
nicholas@2498 3554 this.XMLDOM.setAttribute("state", this.state);
nicholas@2682 3555 this.specification.options.forEach(function (optNode) {
nicholas@2498 3556 if (optNode.type != 'statement') {
nicholas@2498 3557 var node = this.parent.document.createElement('surveyresult');
nicholas@2498 3558 node.setAttribute("ref", optNode.id);
nicholas@2498 3559 node.setAttribute('type', optNode.type);
nicholas@2498 3560 this.XMLDOM.appendChild(node);
nicholas@2498 3561 }
nicholas@2682 3562 }, this);
nicholas@2498 3563 root.appendChild(this.XMLDOM);
nicholas@2498 3564
nicholas@2498 3565 this.postResult = function (node) {
nicholas@2682 3566 function postNumber(doc, value) {
nicholas@2682 3567 var child = doc.createElement("response");
nicholas@2682 3568 child.textContent = value;
nicholas@2682 3569 return child;
nicholas@2682 3570 }
nicholas@2682 3571
nicholas@2682 3572 function postRadio(doc, node) {
nicholas@2682 3573 var child = doc.createElement('response');
nicholas@2682 3574 if (node.response !== null) {
nicholas@2682 3575 child.setAttribute('name', node.response.name);
nicholas@2682 3576 child.textContent = node.response.text;
nicholas@2682 3577 }
nicholas@2682 3578 return child;
nicholas@2682 3579 }
nicholas@2682 3580
nicholas@2682 3581 function postCheckbox(doc, node) {
nicholas@2682 3582 var checkNode = doc.createElement('response');
nicholas@2682 3583 checkNode.setAttribute('name', node.name);
nicholas@2682 3584 checkNode.setAttribute('checked', node.checked);
nicholas@2682 3585 return checkNode;
nicholas@2682 3586 }
nicholas@2498 3587 // From popup: node is the popupOption node containing both spec. and results
nicholas@2498 3588 // ID is the position
nicholas@2498 3589 if (node.specification.type == 'statement') {
nicholas@2498 3590 return;
nicholas@2498 3591 }
nicholas@2498 3592 var surveyresult = this.XMLDOM.firstChild;
nicholas@2678 3593 while (surveyresult !== null) {
nicholas@2498 3594 if (surveyresult.getAttribute("ref") == node.specification.id) {
nicholas@2224 3595 break;
nicholas@2224 3596 }
nicholas@2224 3597 surveyresult = surveyresult.nextElementSibling;
nicholas@2224 3598 }
nicholas@2498 3599 switch (node.specification.type) {
nicholas@2498 3600 case "number":
nicholas@2498 3601 case "question":
n@2583 3602 case "slider":
nicholas@2682 3603 surveyresult.appendChild(postNumber(this.parent.document, node.response));
nicholas@2464 3604 break;
nicholas@2498 3605 case "radio":
nicholas@2682 3606 surveyresult.appendChild(postRadio(this.parent.document, node));
nicholas@2498 3607 break;
nicholas@2498 3608 case "checkbox":
nicholas@2678 3609 if (node.response === undefined) {
nicholas@2498 3610 surveyresult.appendChild(this.parent.document.createElement('response'));
nicholas@2498 3611 break;
nicholas@2498 3612 }
nicholas@2498 3613 for (var i = 0; i < node.response.length; i++) {
nicholas@2682 3614 surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i]));
nicholas@2498 3615 }
nicholas@2498 3616 break;
nicholas@2498 3617 }
nicholas@2498 3618 };
nicholas@2498 3619 this.complete = function () {
nicholas@2498 3620 this.state = "complete";
nicholas@2498 3621 this.XMLDOM.setAttribute("state", this.state);
nicholas@2678 3622 };
nicholas@2498 3623 };
nicholas@2498 3624
nicholas@2498 3625 this.pageNode = function (parent, specification) {
nicholas@2498 3626 // Create one store per test page
nicholas@2498 3627 this.specification = specification;
nicholas@2498 3628 this.parent = parent;
nicholas@2498 3629 this.state = "empty";
nicholas@2498 3630 this.XMLDOM = this.parent.document.createElement('page');
nicholas@2498 3631 this.XMLDOM.setAttribute('ref', specification.id);
nicholas@2498 3632 this.XMLDOM.setAttribute('presentedId', specification.presentedId);
nicholas@2498 3633 this.XMLDOM.setAttribute("state", this.state);
n@2694 3634 if (specification.preTest !== undefined) {
nicholas@2498 3635 this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
nicholas@2498 3636 }
n@2694 3637 if (specification.postTest !== undefined) {
nicholas@2498 3638 this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
nicholas@2498 3639 }
nicholas@2498 3640
nicholas@2498 3641 // Add any page metrics
nicholas@2498 3642 var page_metric = this.parent.document.createElement('metric');
nicholas@2498 3643 this.XMLDOM.appendChild(page_metric);
nicholas@2498 3644
nicholas@2498 3645 // Add the audioelement
nicholas@2682 3646 this.specification.audioElements.forEach(function (element) {
nicholas@2498 3647 var aeNode = this.parent.document.createElement('audioelement');
nicholas@2498 3648 aeNode.setAttribute('ref', element.id);
nicholas@2678 3649 if (element.name !== undefined) {
nicholas@2678 3650 aeNode.setAttribute('name', element.name);
nicholas@2678 3651 }
nicholas@2498 3652 aeNode.setAttribute('type', element.type);
nicholas@2498 3653 aeNode.setAttribute('url', element.url);
nicholas@2498 3654 aeNode.setAttribute('fqurl', qualifyURL(element.url));
nicholas@2498 3655 aeNode.setAttribute('gain', element.gain);
nicholas@2498 3656 if (element.type == 'anchor' || element.type == 'reference') {
nicholas@2498 3657 if (element.marker > 0) {
nicholas@2498 3658 aeNode.setAttribute('marker', element.marker);
nicholas@2464 3659 }
nicholas@2498 3660 }
nicholas@2498 3661 var ae_metric = this.parent.document.createElement('metric');
nicholas@2498 3662 aeNode.appendChild(ae_metric);
nicholas@2498 3663 this.XMLDOM.appendChild(aeNode);
nicholas@2682 3664 }, this);
nicholas@2498 3665
nicholas@2498 3666 this.parent.root.appendChild(this.XMLDOM);
nicholas@2498 3667
nicholas@2498 3668 this.complete = function () {
nicholas@2224 3669 this.state = "complete";
nicholas@2498 3670 this.XMLDOM.setAttribute("state", "complete");
nicholas@2678 3671 };
nicholas@2498 3672 };
nicholas@2498 3673 this.update = function () {
nicholas@2224 3674 this.SessionKey.update();
nicholas@2678 3675 };
nicholas@2498 3676 this.finish = function () {
nicholas@2678 3677 if (this.state === 0) {
nicholas@2224 3678 this.update();
nicholas@2498 3679 }
nicholas@2498 3680 this.state = 1;
nicholas@2498 3681 this.root.setAttribute("state", "complete");
nicholas@2498 3682 return this.root;
nicholas@2498 3683 };
nicholas@2224 3684 }
nicholas@2384 3685
nicholas@2401 3686 var window_depedancy_callback;
nicholas@2498 3687 window_depedancy_callback = window.setInterval(function () {
nicholas@2401 3688 if (check_dependancies()) {
nicholas@2401 3689 window.clearInterval(window_depedancy_callback);
nicholas@2401 3690 onload();
nicholas@2401 3691 } else {
nicholas@2401 3692 document.getElementById("topLevelBody").innerHTML = "<h1>Loading Resources</h1>";
nicholas@2401 3693 }
nicholas@2498 3694 }, 100);