nicholas@2224: /** nicholas@2224: * core.js nicholas@3113: * nicholas@2224: * Main script to run, calls all other core functions and manages loading/store to backend. nicholas@2224: * Also contains all global variables. nicholas@2224: */ nicholas@2224: nicholas@2708: /*globals window, document, XMLDocument, Element, XMLHttpRequest, DOMParser, console, Blob, $, Promise, navigator */ nicholas@2708: /*globals AudioBuffer, AudioBufferSourceNode */ nicholas@2708: /*globals Specification, calculateLoudness, WAVE, validateXML, showdown, pageXMLSave, loadTest, resizeWindow */ nicholas@2708: nicholas@2224: /* create the web audio API context and store in audioContext*/ nicholas@2224: var audioContext; // Hold the browser web audio API nicholas@2224: var projectXML; // Hold the parsed setup XML nicholas@2224: var schemaXSD; // Hold the parsed schema XSD nicholas@2224: var specification; nicholas@2224: var interfaceContext; nicholas@2224: var storage; nicholas@2224: var popup; // Hold the interfacePopup object nicholas@2224: var testState; nicholas@2224: var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order nicholas@2224: var audioEngineContext; // The custome AudioEngine object nicholas@2329: var gReturnURL; giuliomoro@2337: var gSaveFilenamePrefix; nicholas@2224: nicholas@2224: nicholas@2224: // Add a prototype to the bufferSourceNode to reference to the audioObject holding it nicholas@2224: AudioBufferSourceNode.prototype.owner = undefined; nicholas@2224: // Add a prototype to the bufferSourceNode to hold when the object was given a play command nicholas@2224: AudioBufferSourceNode.prototype.playbackStartTime = undefined; nicholas@2224: // Add a prototype to the bufferNode to hold the desired LINEAR gain nicholas@2224: AudioBuffer.prototype.playbackGain = undefined; nicholas@2224: // Add a prototype to the bufferNode to hold the computed LUFS loudness nicholas@2224: AudioBuffer.prototype.lufs = undefined; nicholas@2224: nicholas@2224: // Convert relative URLs into absolutes nicholas@2224: function escapeHTML(s) { nicholas@2224: return s.split('&').join('&').split('<').join('<').split('"').join('"'); nicholas@2224: } nicholas@2498: nicholas@2224: function qualifyURL(url) { nicholas@2498: var el = document.createElement('div'); nicholas@2498: el.innerHTML = 'x'; nicholas@2224: return el.firstChild.href; nicholas@2224: } nicholas@2224: nicholas@3113: function insertParam(s, key, value) nicholas@3113: { nicholas@3113: key = encodeURI(key); value = encodeURI(value); nicholas@3113: if (s.split("?").length == 1) { nicholas@3113: s = s + ">"; nicholas@3113: } else { nicholas@3113: s = s + "&"; nicholas@3113: } nicholas@3113: return s+key+"="+value; nicholas@3113: } nicholas@3113: nicholas@2224: // Firefox does not have an XMLDocument.prototype.getElementsByName nicholas@2224: // and there is no searchAll style command, this custom function will nicholas@2224: // search all children recusrively for the name. Used for XSD where all nicholas@2224: // element nodes must have a name and therefore can pull the schema node nicholas@2498: XMLDocument.prototype.getAllElementsByName = function (name) { nicholas@2224: name = String(name); nicholas@2224: var selected = this.documentElement.getAllElementsByName(name); nicholas@2224: return selected; nicholas@2708: }; nicholas@2224: nicholas@2498: Element.prototype.getAllElementsByName = function (name) { nicholas@2224: name = String(name); nicholas@2224: var selected = []; nicholas@2224: var node = this.firstElementChild; nicholas@2708: while (node !== null) { nicholas@2498: if (node.getAttribute('name') == name) { nicholas@2224: selected.push(node); nicholas@2224: } nicholas@2498: if (node.childElementCount > 0) { nicholas@2224: selected = selected.concat(node.getAllElementsByName(name)); nicholas@2224: } nicholas@2224: node = node.nextElementSibling; nicholas@2224: } nicholas@2224: return selected; nicholas@2708: }; nicholas@2224: nicholas@2498: XMLDocument.prototype.getAllElementsByTagName = function (name) { nicholas@2224: name = String(name); nicholas@2224: var selected = this.documentElement.getAllElementsByTagName(name); nicholas@2224: return selected; nicholas@2708: }; nicholas@2224: nicholas@2498: Element.prototype.getAllElementsByTagName = function (name) { nicholas@2224: name = String(name); nicholas@2224: var selected = []; nicholas@2224: var node = this.firstElementChild; nicholas@2708: while (node !== null) { nicholas@2498: if (node.nodeName == name) { nicholas@2224: selected.push(node); nicholas@2224: } nicholas@2498: if (node.childElementCount > 0) { nicholas@2224: selected = selected.concat(node.getAllElementsByTagName(name)); nicholas@2224: } nicholas@2224: node = node.nextElementSibling; nicholas@2224: } nicholas@2224: return selected; nicholas@2708: }; nicholas@2224: nicholas@2224: // Firefox does not have an XMLDocument.prototype.getElementsByName nicholas@2224: if (typeof XMLDocument.prototype.getElementsByName != "function") { nicholas@2498: XMLDocument.prototype.getElementsByName = function (name) { nicholas@2224: name = String(name); nicholas@2224: var node = this.documentElement.firstElementChild; nicholas@2224: var selected = []; nicholas@2708: while (node !== null) { nicholas@2498: if (node.getAttribute('name') == name) { nicholas@2224: selected.push(node); nicholas@2224: } nicholas@2224: node = node.nextElementSibling; nicholas@2224: } nicholas@2224: return selected; nicholas@2708: }; nicholas@2224: } nicholas@2224: nicholas@2498: var check_dependancies = function () { nicholas@2401: // This will check for the data dependancies nicholas@2498: if (typeof (jQuery) != "function") { nicholas@2498: return false; nicholas@2498: } nicholas@2498: if (typeof (Specification) != "function") { nicholas@2498: return false; nicholas@2498: } nicholas@2498: if (typeof (calculateLoudness) != "function") { nicholas@2498: return false; nicholas@2498: } nicholas@2498: if (typeof (WAVE) != "function") { nicholas@2498: return false; nicholas@2498: } nicholas@2498: if (typeof (validateXML) != "function") { nicholas@2498: return false; nicholas@2498: } nicholas@2401: return true; nicholas@2708: }; nicholas@2401: nicholas@2498: var onload = function () { nicholas@2498: // Function called once the browser has loaded all files. nicholas@2498: // This should perform any initial commands such as structure / loading documents nicholas@2498: nicholas@2498: // Create a web audio API context nicholas@2498: // Fixed for cross-browser support nicholas@2498: var AudioContext = window.AudioContext || window.webkitAudioContext; nicholas@2708: audioContext = new AudioContext(); nicholas@2498: nicholas@2498: // Create test state nicholas@2498: testState = new stateMachine(); nicholas@2498: nicholas@2498: // Create the popup interface object nicholas@2498: popup = new interfacePopup(); nicholas@2498: nicholas@2224: // Create the specification object nicholas@2498: specification = new Specification(); nicholas@2498: nicholas@3101: // Create the storage object nicholas@3101: storage = new Storage(); nicholas@3101: nicholas@2498: // Create the interface object nicholas@2498: interfaceContext = new Interface(specification); nicholas@2498: nicholas@2498: // Define window callbacks for interface nicholas@2498: window.onresize = function (event) { nicholas@2498: interfaceContext.resizeWindow(event); nicholas@2498: }; nicholas@2498: nicholas@2708: if (window.location.search.length !== 0) { nicholas@2319: var search = window.location.search.split('?')[1]; nicholas@2319: // Now split the requests into pairs nicholas@2319: var searchQueries = search.split('&'); nicholas@2708: var url; nicholas@2498: for (var i in searchQueries) { giuliomoro@2331: // Split each key-value pair nicholas@2319: searchQueries[i] = searchQueries[i].split('='); giuliomoro@2331: var key = searchQueries[i][0]; giuliomoro@2331: var value = decodeURIComponent(searchQueries[i][1]); nicholas@2498: switch (key) { nicholas@2498: case "url": nicholas@2498: url = value; nicholas@2708: specification.url = url; nicholas@2498: break; nicholas@2498: case "returnURL": nicholas@2498: gReturnURL = value; nicholas@2498: break; nicholas@3113: case "testKey": nicholas@3113: storage.sessionLinked = value; nicholas@3113: break; nicholas@2498: case "saveFilenamePrefix": nicholas@2722: storage.filenamePrefix = value; nicholas@2498: break; nicholas@2319: } nicholas@2319: } nicholas@2319: loadProjectSpec(url); nicholas@2498: window.onbeforeunload = function () { nicholas@2319: return "Please only leave this page once you have completed the tests. Are you sure you have completed all testing?"; nicholas@2319: }; nicholas@2319: } nicholas@2360: interfaceContext.lightbox.resize(); nicholas@2224: }; nicholas@2224: nicholas@2224: function loadProjectSpec(url) { nicholas@2498: // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data nicholas@2498: // If url is null, request client to upload project XML document nicholas@2498: var xmlhttp = new XMLHttpRequest(); nicholas@2498: xmlhttp.open("GET", 'xml/test-schema.xsd', true); nicholas@2498: xmlhttp.onload = function () { nicholas@2708: specification.processSchema(xmlhttp.response); nicholas@2498: var r = new XMLHttpRequest(); nicholas@2498: r.open('GET', url, true); nicholas@2498: r.onload = function () { nicholas@2498: loadProjectSpecCallback(r.response); nicholas@2498: }; nicholas@2498: r.onerror = function () { nicholas@2224: document.getElementsByTagName('body')[0].innerHTML = null; nicholas@2224: var msg = document.createElement("h3"); nicholas@2224: msg.textContent = "FATAL ERROR"; nicholas@2224: var span = document.createElement("p"); nicholas@2224: 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: document.getElementsByTagName('body')[0].appendChild(msg); nicholas@2224: document.getElementsByTagName('body')[0].appendChild(span); nicholas@2708: }; nicholas@2498: r.send(); nicholas@2498: }; nicholas@2498: xmlhttp.send(); nicholas@2708: } nicholas@2224: nicholas@2224: function loadProjectSpecCallback(response) { nicholas@2498: // Function called after asynchronous download of XML project specification nicholas@2498: //var decode = $.parseXML(response); nicholas@2498: //projectXML = $(decode); nicholas@2498: nicholas@2224: // Check if XML is new or a resumption nicholas@2224: var parse = new DOMParser(); nicholas@2498: var responseDocument = parse.parseFromString(response, 'text/xml'); nicholas@2224: var errorNode = responseDocument.getElementsByTagName('parsererror'); nicholas@2708: var msg, span; nicholas@2498: if (errorNode.length >= 1) { nicholas@2708: msg = document.createElement("h3"); nicholas@2498: msg.textContent = "FATAL ERROR"; nicholas@2708: span = document.createElement("span"); nicholas@2498: span.textContent = "The XML parser returned the following errors when decoding your XML file"; nicholas@2498: document.getElementsByTagName('body')[0].innerHTML = null; nicholas@2498: document.getElementsByTagName('body')[0].appendChild(msg); nicholas@2498: document.getElementsByTagName('body')[0].appendChild(span); nicholas@2498: document.getElementsByTagName('body')[0].appendChild(errorNode[0]); nicholas@2498: return; nicholas@2498: } nicholas@2708: if (responseDocument === undefined || responseDocument.firstChild === undefined) { nicholas@2708: msg = document.createElement("h3"); nicholas@2498: msg.textContent = "FATAL ERROR"; nicholas@2708: span = document.createElement("span"); nicholas@2498: 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: document.getElementsByTagName('body')[0].innerHTML = null; nicholas@2498: document.getElementsByTagName('body')[0].appendChild(msg); nicholas@2498: document.getElementsByTagName('body')[0].appendChild(span); nicholas@2498: return; nicholas@2224: } nicholas@2247: if (responseDocument.firstChild.nodeName == "waet") { nicholas@2224: // document is a specification nicholas@2498: nicholas@2224: // Perform XML schema validation nicholas@2224: var Module = { nicholas@2224: xml: response, nicholas@2708: schema: specification.getSchemaString(), nicholas@2498: arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml'] nicholas@2224: }; nicholas@2498: projectXML = responseDocument; nicholas@2224: var xmllint = validateXML(Module); nicholas@2224: console.log(xmllint); nicholas@2498: if (xmllint != 'document.xml validates\n') { nicholas@2224: document.getElementsByTagName('body')[0].innerHTML = null; nicholas@2708: msg = document.createElement("h3"); nicholas@2224: msg.textContent = "FATAL ERROR"; nicholas@2708: span = document.createElement("h4"); nicholas@2224: span.textContent = "The XML validator returned the following errors when decoding your XML file"; nicholas@2224: document.getElementsByTagName('body')[0].appendChild(msg); nicholas@2224: document.getElementsByTagName('body')[0].appendChild(span); nicholas@2224: xmllint = xmllint.split('\n'); nicholas@2498: for (var i in xmllint) { nicholas@2224: document.getElementsByTagName('body')[0].appendChild(document.createElement('br')); nicholas@2708: span = document.createElement("span"); nicholas@2224: span.textContent = xmllint[i]; nicholas@2224: document.getElementsByTagName('body')[0].appendChild(span); nicholas@2224: } nicholas@2224: return; nicholas@2224: } nicholas@2224: // Build the specification nicholas@2498: specification.decode(projectXML); nicholas@2224: // Generate the session-key nicholas@2224: storage.initialise(); nicholas@2498: nicholas@2247: } else if (responseDocument.firstChild.nodeName == "waetresult") { nicholas@2224: // document is a result nicholas@2498: projectXML = document.implementation.createDocument(null, "waet"); nicholas@2294: projectXML.firstChild.appendChild(responseDocument.getElementsByTagName('waet')[0].getElementsByTagName("setup")[0].cloneNode(true)); nicholas@2708: var child = responseDocument.firstChild.firstChild, nicholas@2708: copy; nicholas@2708: while (child !== null) { nicholas@2224: if (child.nodeName == "survey") { nicholas@2224: // One of the global survey elements nicholas@2224: if (child.getAttribute("state") == "complete") { nicholas@2224: // We need to remove this survey from nicholas@2224: var location = child.getAttribute("location"); nicholas@2224: var globalSurveys = projectXML.getElementsByTagName("setup")[0].getElementsByTagName("survey")[0]; nicholas@2708: while (globalSurveys !== null) { nicholas@2224: if (location == "pre" || location == "before") { nicholas@2224: if (globalSurveys.getAttribute("location") == "pre" || globalSurveys.getAttribute("location") == "before") { nicholas@2224: projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys); nicholas@2224: break; nicholas@2224: } nicholas@2224: } else { nicholas@2224: if (globalSurveys.getAttribute("location") == "post" || globalSurveys.getAttribute("location") == "after") { nicholas@2224: projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys); nicholas@2224: break; nicholas@2224: } nicholas@2224: } nicholas@2224: globalSurveys = globalSurveys.nextElementSibling; nicholas@2224: } nicholas@2224: } else { nicholas@2224: // We need to complete this, so it must be regenerated by store nicholas@2708: copy = child; nicholas@2224: child = child.previousElementSibling; nicholas@2294: responseDocument.firstChild.removeChild(copy); nicholas@2224: } nicholas@2224: } else if (child.nodeName == "page") { nicholas@2224: if (child.getAttribute("state") == "empty") { nicholas@2224: // We need to complete this page nicholas@2294: projectXML.firstChild.appendChild(responseDocument.getElementById(child.getAttribute("ref")).cloneNode(true)); nicholas@2708: copy = child; nicholas@2224: child = child.previousElementSibling; nicholas@2294: responseDocument.firstChild.removeChild(copy); nicholas@2224: } nicholas@2224: } nicholas@2224: child = child.nextElementSibling; nicholas@2224: } nicholas@2224: // Build the specification nicholas@2498: specification.decode(projectXML); nicholas@2224: // Use the original nicholas@2224: storage.initialise(responseDocument); nicholas@2224: } nicholas@2498: /// CHECK FOR SAMPLE RATE COMPATIBILITY nicholas@2708: if (isFinite(specification.sampleRate)) { nicholas@2498: if (Number(specification.sampleRate) != audioContext.sampleRate) { nicholas@2498: 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: interfaceContext.lightbox.post("Error", errStr); nicholas@2498: return; nicholas@2498: } nicholas@2498: } nicholas@2498: nicholas@2624: var getInterfaces = new XMLHttpRequest(); nicholas@2624: getInterfaces.open("GET", "interfaces/interfaces.json"); nicholas@2624: getInterfaces.onerror = function (e) { nicholas@2624: throw (e); nicholas@2708: }; nicholas@2624: getInterfaces.onload = function () { nicholas@2624: if (getInterfaces.status !== 200) { nicholas@2624: throw (new Error(getInterfaces.status)); nicholas@2624: } nicholas@2624: // Get the current interface nicholas@2624: var name = specification.interface, nicholas@2624: head = document.getElementsByTagName("head")[0], nicholas@2624: data = JSON.parse(getInterfaces.responseText), nicholas@2624: interfaceObject = data.interfaces.find(function (e) { nicholas@2624: return e.name == name; nicholas@2624: }); nicholas@2624: if (!interfaceObject) { nicholas@2624: throw ("Cannot load desired interface"); nicholas@2624: } nicholas@2624: interfaceObject.scripts.forEach(function (v) { nicholas@2624: var script = document.createElement("script"); nicholas@2624: script.setAttribute("type", "text/javascript"); nicholas@2624: script.setAttribute("src", v); nicholas@2624: head.appendChild(script); nicholas@2624: }); nicholas@2624: interfaceObject.css.forEach(function (v) { nicholas@2624: var css = document.createElement("link"); nicholas@2624: css.setAttribute("rel", "stylesheet"); nicholas@2624: css.setAttribute("type", "text/css"); nicholas@2624: css.setAttribute("href", v); nicholas@2624: head.appendChild(css); nicholas@2624: }); nicholas@2708: }; nicholas@2624: getInterfaces.send(); nicholas@2498: nicholas@2708: if (gReturnURL !== undefined) { nicholas@2498: console.log("returnURL Overide from " + specification.returnURL + " to " + gReturnURL); nicholas@2329: specification.returnURL = gReturnURL; nicholas@2329: } nicholas@2708: if (gSaveFilenamePrefix !== undefined) { giuliomoro@2337: specification.saveFilenamePrefix = gSaveFilenamePrefix; giuliomoro@2337: } nicholas@2498: nicholas@2498: // Create the audio engine object nicholas@2498: audioEngineContext = new AudioEngine(specification); nicholas@2224: } nicholas@2224: nicholas@2224: function createProjectSave(destURL) { nicholas@2224: // Clear the window.onbeforeunload nicholas@2224: window.onbeforeunload = null; nicholas@2498: // Save the data from interface into XML and send to destURL nicholas@2498: // If destURL is null then download XML in client nicholas@2498: // Now time to render file locally nicholas@2733: var xmlDoc = storage.finish(); nicholas@2498: var parent = document.createElement("div"); nicholas@2498: parent.appendChild(xmlDoc); nicholas@2498: var file = [parent.innerHTML]; nicholas@2498: if (destURL == "local") { nicholas@2498: var bb = new Blob(file, { nicholas@2498: type: 'application/xml' nicholas@2498: }); nicholas@2498: var dnlk = window.URL.createObjectURL(bb); nicholas@2498: var a = document.createElement("a"); nicholas@2498: a.hidden = ''; nicholas@2498: a.href = dnlk; nicholas@2498: a.download = "save.xml"; nicholas@2498: a.textContent = "Save File"; nicholas@2498: nicholas@2498: popup.showPopup(); nicholas@2498: popup.popupContent.innerHTML = "Please save the file below to give to your test supervisor
"; nicholas@2498: popup.popupContent.appendChild(a); nicholas@2498: } else { nicholas@2498: var projectReturn = ""; nicholas@2498: if (typeof specification.projectReturn == "string") { nicholas@2498: if (specification.projectReturn.substr(0, 4) == "http") { nicholas@2498: projectReturn = specification.projectReturn; nicholas@2498: } nicholas@2498: } nicholas@2723: storage.SessionKey.finish().then(function (resolved) { nicholas@2983: var converter = new showdown.Converter(); nicholas@2723: if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) { nicholas@3113: window.location = insertParam(specification.returnURL, "testKey", storage.SessionKey.key); nicholas@2723: } else { nicholas@2983: popup.popupContent.innerHTML = converter.makeHtml(specification.exitText); nicholas@2723: } nicholas@2723: }, function (message) { nicholas@2723: console.log("Save: Error! " + message.textContent); nicholas@2498: createProjectSave("local"); nicholas@2723: }); nicholas@2498: popup.showPopup(); nicholas@2498: popup.popupContent.innerHTML = null; nicholas@2498: popup.popupContent.textContent = "Submitting. Please Wait"; nicholas@2498: if (typeof (popup.hideNextButton) === "function") { nicholas@2498: popup.hideNextButton(); nicholas@2498: } nicholas@2498: if (typeof (popup.hidePreviousButton) === "function") { nicholas@2498: popup.hidePreviousButton(); nicholas@2498: } nicholas@2498: } nicholas@2224: } nicholas@2224: nicholas@2498: function errorSessionDump(msg) { nicholas@2498: // Create the partial interface XML save nicholas@2498: // Include error node with message on why the dump occured nicholas@2498: popup.showPopup(); nicholas@2498: popup.popupContent.innerHTML = null; nicholas@2498: var err = document.createElement('error'); nicholas@2498: var parent = document.createElement("div"); nicholas@2498: if (typeof msg === "object") { nicholas@2498: err.appendChild(msg); nicholas@2498: popup.popupContent.appendChild(msg); nicholas@2498: nicholas@2498: } else { nicholas@2498: err.textContent = msg; nicholas@2498: popup.popupContent.innerHTML = "ERROR : " + msg; nicholas@2498: } nicholas@2498: var xmlDoc = interfaceXMLSave(); nicholas@2498: xmlDoc.appendChild(err); nicholas@2498: parent.appendChild(xmlDoc); nicholas@2498: var file = [parent.innerHTML]; nicholas@2498: var bb = new Blob(file, { nicholas@2498: type: 'application/xml' nicholas@2498: }); nicholas@2498: var dnlk = window.URL.createObjectURL(bb); nicholas@2498: var a = document.createElement("a"); nicholas@2498: a.hidden = ''; nicholas@2498: a.href = dnlk; nicholas@2498: a.download = "save.xml"; nicholas@2498: a.textContent = "Save File"; nicholas@2498: nicholas@2498: nicholas@2498: nicholas@2498: popup.popupContent.appendChild(a); nicholas@2224: } nicholas@2224: nicholas@2224: // Only other global function which must be defined in the interface class. Determines how to create the XML document. nicholas@2498: function interfaceXMLSave() { nicholas@2498: // Create the XML string to be exported with results nicholas@2498: return storage.finish(); nicholas@2224: } nicholas@2224: nicholas@2498: function linearToDecibel(gain) { nicholas@2498: return 20.0 * Math.log10(gain); nicholas@2224: } nicholas@2224: nicholas@2498: function decibelToLinear(gain) { nicholas@2498: return Math.pow(10, gain / 20.0); nicholas@2224: } nicholas@2224: nicholas@2498: function secondsToSamples(time, fs) { nicholas@2498: return Math.round(time * fs); nicholas@2224: } nicholas@2224: nicholas@2498: function samplesToSeconds(samples, fs) { nicholas@2224: return samples / fs; nicholas@2224: } nicholas@2224: nicholas@2224: function randomString(length) { nicholas@2708: var str = ""; nicholas@2498: for (var i = 0; i < length; i += 2) { nicholas@2498: var num = Math.floor(Math.random() * 1295); nicholas@2376: str += num.toString(36); nicholas@2376: } nicholas@2376: return str; nicholas@2376: //return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1); nicholas@2224: } nicholas@2224: nicholas@2498: function randomiseOrder(input) { nicholas@2498: // This takes an array of information and randomises the order nicholas@2498: var N = input.length; nicholas@2498: nicholas@2498: var inputSequence = []; // For safety purposes: keep track of randomisation nicholas@2498: for (var counter = 0; counter < N; ++counter) nicholas@2708: inputSequence.push(counter); // Fill array nicholas@2498: var inputSequenceClone = inputSequence.slice(0); nicholas@2498: nicholas@2498: var holdArr = []; nicholas@2498: var outputSequence = []; nicholas@2498: for (var n = 0; n < N; n++) { nicholas@2498: // First pick a random number nicholas@2498: var r = Math.random(); nicholas@2498: // Multiply and floor by the number of elements left nicholas@2498: r = Math.floor(r * input.length); nicholas@2498: // Pick out that element and delete from the array nicholas@2498: holdArr.push(input.splice(r, 1)[0]); nicholas@2498: // Do the same with sequence nicholas@2498: outputSequence.push(inputSequence.splice(r, 1)[0]); nicholas@2498: } nicholas@2498: console.log(inputSequenceClone.toString()); // print original array to console nicholas@2498: console.log(outputSequence.toString()); // print randomised array to console nicholas@2498: return holdArr; nicholas@2224: } nicholas@2224: nicholas@2498: function randomSubArray(array, num) { nicholas@2224: if (num > array.length) { nicholas@2224: num = array.length; nicholas@2224: } nicholas@2224: var ret = []; nicholas@2224: while (num > 0) { nicholas@2224: var index = Math.floor(Math.random() * array.length); nicholas@2498: ret.push(array.splice(index, 1)[0]); nicholas@2224: num--; nicholas@2224: } nicholas@2224: return ret; nicholas@2224: } nicholas@2224: nicholas@2224: function interfacePopup() { nicholas@2498: // Creates an object to manage the popup nicholas@2498: this.popup = null; nicholas@2498: this.popupContent = null; nicholas@2498: this.popupTitle = null; nicholas@2498: this.popupResponse = null; nicholas@2498: this.buttonProceed = null; nicholas@2498: this.buttonPrevious = null; nicholas@2498: this.popupOptions = null; nicholas@2498: this.currentIndex = null; nicholas@2498: this.node = null; nicholas@2498: this.store = null; nicholas@2775: var lastNodeStart; nicholas@2498: $(window).keypress(function (e) { n@2915: if (e.keyCode == 13 && popup.popup.style.visibility == 'visible' && interfaceContext.lightbox.isVisible() === false) { nicholas@2498: console.log(e); nicholas@2498: popup.buttonProceed.onclick(); nicholas@2498: e.preventDefault(); nicholas@2498: } nicholas@2498: }); nicholas@2708: // Generators & Processors // nicholas@2708: nicholas@2708: function processConditional(node, value) { nicholas@2708: function jumpToId(jumpID) { nicholas@2708: var index = this.popupOptions.findIndex(function (item, index, element) { nicholas@2708: if (item.specification.id == jumpID) { nicholas@2708: return true; nicholas@2708: } else { nicholas@2708: return false; nicholas@2708: } nicholas@2708: }, this); nicholas@2708: this.currentIndex = index - 1; nicholas@2708: } nicholas@2708: var conditionFunction; nicholas@2708: if (node.specification.type === "question") { nicholas@2708: conditionFunction = processQuestionConditional; nicholas@2708: } else if (node.specification.type === "checkbox") { nicholas@2708: conditionFunction = processCheckboxConditional; nicholas@2708: } else if (node.specification.type === "radio") { nicholas@2708: conditionFunction = processRadioConditional; nicholas@2708: } else if (node.specification.type === "number") { nicholas@2708: conditionFunction = processNumberConditional; nicholas@2708: } else if (node.specification.type === "slider") { nicholas@2708: conditionFunction = processSliderConditional; nicholas@2708: } else { nicholas@2708: return; nicholas@2708: } nicholas@2708: for (var i = 0; i < node.specification.conditions.length; i++) { nicholas@2708: var condition = node.specification.conditions[i]; nicholas@2708: var pass = conditionFunction(condition, value); nicholas@2708: var jumpID; nicholas@2708: if (pass) { nicholas@2708: jumpID = condition.jumpToOnPass; nicholas@2708: } else { nicholas@2708: jumpID = condition.jumpToOnFail; nicholas@2708: } nicholas@2735: if (jumpID !== null) { nicholas@2708: jumpToId.call(this, jumpID); nicholas@2708: break; nicholas@2708: } nicholas@2708: } nicholas@2708: } nicholas@2708: nicholas@2708: function postQuestion(node) { nicholas@2708: var textArea = document.createElement('textarea'); nicholas@2708: switch (node.specification.boxsize) { nicholas@2708: case 'small': nicholas@2708: textArea.cols = "20"; nicholas@2708: textArea.rows = "1"; nicholas@2708: break; nicholas@2708: case 'normal': nicholas@2708: textArea.cols = "30"; nicholas@2708: textArea.rows = "2"; nicholas@2708: break; nicholas@2708: case 'large': nicholas@2708: textArea.cols = "40"; nicholas@2708: textArea.rows = "5"; nicholas@2708: break; nicholas@2708: case 'huge': nicholas@2708: textArea.cols = "50"; nicholas@2708: textArea.rows = "10"; nicholas@2708: break; nicholas@2708: } nicholas@2708: if (node.response === undefined) { nicholas@2708: node.response = ""; nicholas@2708: } else { nicholas@2708: textArea.value = node.response; nicholas@2708: } nicholas@2708: this.popupResponse.appendChild(textArea); nicholas@2708: textArea.focus(); nicholas@2708: this.popupResponse.style.textAlign = "center"; nicholas@2708: this.popupResponse.style.left = "0%"; nicholas@2708: } nicholas@2708: nicholas@2708: function processQuestionConditional(condition, value) { nicholas@2708: switch (condition.check) { nicholas@2708: case "equals": nicholas@2708: // Deliberately loose check nicholas@2708: if (value == condition.value) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "greaterThan": nicholas@2708: case "lessThan": nicholas@2708: console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING"); nicholas@2708: break; nicholas@2708: case "contains": nicholas@2708: if (value.includes(condition.value)) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: } nicholas@2708: return false; nicholas@2708: } nicholas@2708: nicholas@2708: function processQuestion(node) { nicholas@2708: var textArea = this.popupResponse.getElementsByTagName("textarea")[0]; nicholas@2708: if (node.specification.mandatory === true && textArea.value.length === 0) { nicholas@2708: interfaceContext.lightbox.post("Error", "This question is mandatory"); nicholas@2708: return false; nicholas@2708: } nicholas@2708: // Save the text content nicholas@2708: console.log("Question: " + node.specification.statement); nicholas@2708: console.log("Question Response: " + textArea.value); nicholas@2708: node.response = textArea.value; nicholas@2708: processConditional.call(this, node, textArea.value); nicholas@2708: return true; nicholas@2708: } nicholas@2708: nicholas@2708: function postCheckbox(node) { n@2926: if (node.response === null) { n@2926: node.response = []; nicholas@2708: } nicholas@2708: var table = document.createElement("table"); nicholas@2708: table.className = "popup-option-list"; nicholas@2708: table.border = "0"; n@2924: var nodelist = []; nicholas@2708: node.specification.options.forEach(function (option, index) { nicholas@2708: var tr = document.createElement("tr"); n@2924: nodelist.push(tr); nicholas@2708: var td = document.createElement("td"); nicholas@2708: tr.appendChild(td); nicholas@2708: var input = document.createElement('input'); nicholas@2708: input.id = option.name; nicholas@2708: input.type = 'checkbox'; nicholas@2708: td.appendChild(input); nicholas@2708: nicholas@2708: td = document.createElement("td"); nicholas@2708: tr.appendChild(td); nicholas@2708: var span = document.createElement('span'); nicholas@2708: span.textContent = option.text; nicholas@2708: td.appendChild(span); nicholas@2708: tr = document.createElement('div'); nicholas@2708: tr.setAttribute('name', 'option'); nicholas@2708: tr.className = "popup-option-checbox"; n@2979: var resp; n@2926: if (node.response.length > 0) { n@2926: resp = node.response.find(function (a) { n@2926: return a.name == option.name; n@2926: }); n@2926: } n@2926: if (resp !== undefined) { n@2926: if (resp.checked === true) { nicholas@2708: input.checked = "true"; nicholas@2708: } n@2926: } else { n@2926: node.response.push({ n@2926: "name": option.name, n@2926: "text": option.text, n@2926: "checked": false n@2926: }); nicholas@2708: } nicholas@2708: index++; nicholas@2708: }); n@2924: if (node.specification.randomise) { n@2924: nodelist = randomiseOrder(nodelist); n@2924: } n@2924: nodelist.forEach(function (e) { n@2924: table.appendChild(e); n@2924: }); nicholas@2708: this.popupResponse.appendChild(table); nicholas@2708: } nicholas@2708: nicholas@2708: function processCheckbox(node) { nicholas@2708: console.log("Checkbox: " + node.specification.statement); nicholas@2708: var inputs = this.popupResponse.getElementsByTagName('input'); nicholas@2708: var numChecked = 0, nicholas@2708: i; nicholas@2708: for (i = 0; i < node.specification.options.length; i++) { nicholas@2708: if (inputs[i].checked) { nicholas@2708: numChecked++; nicholas@2708: } nicholas@2708: } nicholas@2708: if (node.specification.min !== undefined) { nicholas@2708: if (node.specification.max === undefined) { nicholas@2708: if (numChecked < node.specification.min) { nicholas@2708: var msg = "You must select at least " + node.specification.min + " option"; nicholas@2708: if (node.specification.min > 1) { nicholas@2708: msg += "s"; nicholas@2708: } nicholas@2708: interfaceContext.lightbox.post("Error", msg); nicholas@2708: return; nicholas@2708: } nicholas@2708: } else { nicholas@2708: if (numChecked < node.specification.min || numChecked > node.specification.max) { nicholas@2708: if (node.specification.min == node.specification.max) { nicholas@2708: interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min); nicholas@2708: } else { nicholas@2708: interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max); nicholas@2708: } nicholas@2708: return false; nicholas@2708: } nicholas@2708: } nicholas@2708: } nicholas@2708: for (i = 0; i < node.specification.options.length; i++) { n@2926: node.response.forEach(function (a) { n@2926: var input = this.popupResponse.querySelector("#" + a.name); n@2926: a.checked = input.checked; nicholas@2708: }); nicholas@2708: console.log(node.specification.options[i].name + ": " + inputs[i].checked); nicholas@2708: } nicholas@2708: processConditional.call(this, node, node.response); nicholas@2708: return true; nicholas@2708: } nicholas@2708: nicholas@2708: function processCheckboxConditional(condition, response) { nicholas@2708: switch (condition.check) { nicholas@2708: case "contains": nicholas@2708: for (var i = 0; i < response.length; i++) { nicholas@2708: var value = response[i]; nicholas@2708: if (value.name === condition.value && value.checked) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: } nicholas@2708: break; nicholas@2708: case "equals": nicholas@2708: case "greaterThan": nicholas@2708: case "lessThan": nicholas@2708: console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING"); nicholas@2708: break; nicholas@2708: default: nicholas@2708: console.log("Unknown condition. IGNORING"); nicholas@2708: break; nicholas@2708: } nicholas@2708: return false; nicholas@2708: } nicholas@2708: nicholas@2708: function postRadio(node) { nicholas@2708: if (node.response === null) { nicholas@2708: node.response = { nicholas@2708: name: "", nicholas@2708: text: "" nicholas@2708: }; nicholas@2708: } nicholas@2708: var table = document.createElement("table"); nicholas@2708: table.className = "popup-option-list"; nicholas@2708: table.border = "0"; n@2926: var nodelist = []; nicholas@2708: node.specification.options.forEach(function (option, index) { nicholas@2708: var tr = document.createElement("tr"); n@2926: nodelist.push(tr); nicholas@2708: var td = document.createElement("td"); nicholas@2708: tr.appendChild(td); nicholas@2708: var input = document.createElement('input'); nicholas@2708: input.id = option.name; nicholas@2708: input.type = 'radio'; nicholas@2708: input.name = node.specification.id; nicholas@2708: td.appendChild(input); nicholas@2708: nicholas@2708: td = document.createElement("td"); nicholas@2708: tr.appendChild(td); nicholas@2708: var span = document.createElement('span'); nicholas@2708: span.textContent = option.text; nicholas@2708: td.appendChild(span); nicholas@2708: tr = document.createElement('div'); nicholas@2708: tr.setAttribute('name', 'option'); n@2926: tr.className = "popup-option-checkbox"; n@2926: if (node.response.name === option.name) { n@2926: input.checked = true; n@2926: } n@2926: }); n@2926: if (node.specification.randomise) { n@2926: nodelist = randomiseOrder(nodelist); n@2926: } n@2926: nodelist.forEach(function (e) { n@2926: table.appendChild(e); nicholas@2708: }); nicholas@2708: this.popupResponse.appendChild(table); nicholas@2708: } nicholas@2708: nicholas@2708: function processRadio(node) { nicholas@2708: var optHold = this.popupResponse; nicholas@2708: console.log("Radio: " + node.specification.statement); nicholas@2708: node.response = null; nicholas@2708: var i = 0; nicholas@2708: var inputs = optHold.getElementsByTagName('input'); nicholas@2939: var checked; nicholas@2939: while (checked === undefined) { nicholas@2708: if (i == inputs.length) { nicholas@2708: if (node.specification.mandatory === true) { nicholas@2708: interfaceContext.lightbox.post("Error", "Please select one option"); nicholas@2708: return false; nicholas@2708: } nicholas@2708: break; nicholas@2708: } nicholas@2708: if (inputs[i].checked === true) { nicholas@2939: checked = inputs[i]; nicholas@2708: } nicholas@2708: i++; nicholas@2708: } nicholas@2939: var option = node.specification.options.find(function (a) { nicholas@2939: return checked.id == a.name; nicholas@2939: }); nicholas@2939: if (option === undefined) { nicholas@2939: interfaceContext.lightbox.post("Error", "A configuration error has occured, the test cannot be continued"); nicholas@2939: throw ("ERROR - Cannot find option"); nicholas@2939: } nicholas@2939: node.response = option; nicholas@2735: processConditional.call(this, node, node.response.name); nicholas@2708: return true; nicholas@2708: } nicholas@2708: nicholas@2708: function processRadioConditional(condition, response) { nicholas@2708: switch (condition.check) { nicholas@2708: case "equals": nicholas@2708: if (response === condition.value) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "contains": nicholas@2708: case "greaterThan": nicholas@2708: case "lessThan": nicholas@2708: console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING"); nicholas@2708: break; nicholas@2708: default: nicholas@2708: console.log("Unknown condition. IGNORING"); nicholas@2708: break; nicholas@2708: } nicholas@2708: return false; nicholas@2708: } nicholas@2708: nicholas@2708: function postNumber(node) { nicholas@2708: var input = document.createElement('input'); nicholas@2708: input.type = 'textarea'; nicholas@2708: if (node.specification.min !== null) { nicholas@2708: input.min = node.specification.min; nicholas@2708: } nicholas@2708: if (node.specification.max !== null) { nicholas@2708: input.max = node.specification.max; nicholas@2708: } nicholas@2708: if (node.specification.step !== null) { nicholas@2708: input.step = node.specification.step; nicholas@2708: } nicholas@2708: if (node.response !== undefined) { nicholas@2708: input.value = node.response; nicholas@2708: } nicholas@2708: this.popupResponse.appendChild(input); nicholas@2708: this.popupResponse.style.textAlign = "center"; nicholas@2708: this.popupResponse.style.left = "0%"; nicholas@2708: } nicholas@2708: nicholas@2708: function processNumber(node) { nicholas@2708: var input = this.popupContent.getElementsByTagName('input')[0]; nicholas@2730: if (node.specification.mandatory === true && input.value.length === 0) { nicholas@2708: interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number'); nicholas@2708: return false; nicholas@2708: } nicholas@2708: var enteredNumber = Number(input.value); nicholas@2708: if (isNaN(enteredNumber)) { nicholas@2708: interfaceContext.lightbox.post("Error", 'Please enter a valid number'); nicholas@2708: return false; nicholas@2708: } nicholas@2730: if (enteredNumber < node.specification.min && node.specification.min !== null) { nicholas@2730: interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.specification.min); nicholas@2708: return false; nicholas@2708: } nicholas@2730: if (enteredNumber > node.specification.max && node.specification.max !== null) { nicholas@2730: interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.specification.max); nicholas@2708: return false; nicholas@2708: } nicholas@2708: node.response = input.value; nicholas@2708: processConditional.call(this, node, node.response); nicholas@2708: return true; nicholas@2708: } nicholas@2708: nicholas@2708: function processNumberConditional(condtion, value) { nicholas@2708: var condition = condition; nicholas@2708: switch (condition.check) { nicholas@2708: case "greaterThan": nicholas@2708: if (value > Number(condition.value)) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "lessThan": nicholas@2708: if (value < Number(condition.value)) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "equals": nicholas@2708: if (value == condition.value) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "contains": nicholas@2708: console.log("Survey Element of type 'number' cannot interpret \"contains\" conditions. IGNORING"); nicholas@2708: break; nicholas@2708: default: nicholas@2708: console.log("Unknown condition. IGNORING"); nicholas@2708: break; nicholas@2708: } nicholas@2708: return false; nicholas@2708: } nicholas@2708: nicholas@2708: function postVideo(node) { nicholas@2708: var video = document.createElement("video"); nicholas@2708: video.src = node.specification.url; nicholas@2708: this.popupResponse.appendChild(video); nicholas@2708: } nicholas@2708: nicholas@2708: function postYoutube(node) { nicholas@2708: var iframe = document.createElement("iframe"); nicholas@2708: iframe.className = "youtube"; nicholas@2708: iframe.src = node.specification.url; nicholas@2708: this.popupResponse.appendChild(iframe); nicholas@2708: } nicholas@2708: nicholas@2708: function postSlider(node) { nicholas@2708: var hold = document.createElement('div'); nicholas@2708: var input = document.createElement('input'); nicholas@2708: input.type = 'range'; nicholas@2708: input.style.width = "90%"; nicholas@2708: if (node.specification.min !== null) { nicholas@2708: input.min = node.specification.min; nicholas@2708: } nicholas@2708: if (node.specification.max !== null) { nicholas@2708: input.max = node.specification.max; nicholas@2708: } nicholas@2708: if (node.response !== undefined) { nicholas@2708: input.value = node.response; nicholas@2708: } nicholas@2708: hold.className = "survey-slider-text-holder"; nicholas@2708: var minText = document.createElement('span'); nicholas@2708: var maxText = document.createElement('span'); nicholas@2708: minText.textContent = node.specification.leftText; nicholas@2708: maxText.textContent = node.specification.rightText; nicholas@2708: hold.appendChild(minText); nicholas@2708: hold.appendChild(maxText); nicholas@2708: this.popupResponse.appendChild(input); nicholas@2708: this.popupResponse.appendChild(hold); nicholas@2708: this.popupResponse.style.textAlign = "center"; nicholas@2708: } nicholas@2708: nicholas@2708: function processSlider(node) { nicholas@2708: var input = this.popupContent.getElementsByTagName('input')[0]; nicholas@2708: node.response = input.value; nicholas@2708: processConditional.call(this, node, node.response); nicholas@2708: return true; nicholas@2708: } nicholas@2708: nicholas@2708: function processSliderConditional(condition, value) { nicholas@2708: switch (condition.check) { nicholas@2708: case "contains": nicholas@2708: console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING"); nicholas@2708: break; nicholas@2708: case "greaterThan": nicholas@2708: if (value > Number(condition.value)) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "lessThan": nicholas@2708: if (value < Number(condition.value)) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: case "equals": nicholas@2708: if (value == condition.value) { nicholas@2708: return true; nicholas@2708: } nicholas@2708: break; nicholas@2708: default: nicholas@2708: console.log("Unknown condition. IGNORING"); nicholas@2708: break; nicholas@2708: } nicholas@2708: return false; nicholas@2708: } nicholas@2498: nicholas@2498: this.createPopup = function () { nicholas@2498: // Create popup window interface nicholas@2498: var insertPoint = document.getElementById("topLevelBody"); nicholas@2498: nicholas@2498: this.popup = document.getElementById('popupHolder'); nicholas@2498: this.popup.style.left = (window.innerWidth / 2) - 250 + 'px'; nicholas@2498: this.popup.style.top = (window.innerHeight / 2) - 125 + 'px'; nicholas@2498: nicholas@2498: this.popupContent = document.getElementById('popupContent'); nicholas@2498: nicholas@2645: this.popupTitle = document.getElementById('popupTitleHolder'); nicholas@2498: nicholas@2498: this.popupResponse = document.getElementById('popupResponse'); nicholas@2498: nicholas@2498: this.buttonProceed = document.getElementById('popup-proceed'); nicholas@2498: this.buttonProceed.onclick = function () { nicholas@2498: popup.proceedClicked(); nicholas@2498: }; nicholas@2498: nicholas@2498: this.buttonPrevious = document.getElementById('popup-previous'); nicholas@2498: this.buttonPrevious.onclick = function () { nicholas@2498: popup.previousClick(); nicholas@2498: }; nicholas@2498: nicholas@2224: this.hidePopup(); nicholas@2498: this.popup.style.visibility = 'hidden'; nicholas@2498: }; nicholas@2498: nicholas@2498: this.showPopup = function () { nicholas@2708: if (this.popup === null) { nicholas@2498: this.createPopup(); nicholas@2498: } nicholas@2498: this.popup.style.visibility = 'visible'; nicholas@2498: var blank = document.getElementsByClassName('testHalt')[0]; nicholas@2498: blank.style.visibility = 'visible'; nicholas@2498: this.popupResponse.style.left = "0%"; nicholas@2498: }; nicholas@2498: nicholas@2498: this.hidePopup = function () { nicholas@2224: if (this.popup) { nicholas@2224: this.popup.style.visibility = 'hidden'; nicholas@2224: var blank = document.getElementsByClassName('testHalt')[0]; nicholas@2224: blank.style.visibility = 'hidden'; nicholas@2224: this.buttonPrevious.style.visibility = 'inherit'; nicholas@2224: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.postNode = function () { nicholas@2498: // This will take the node from the popupOptions and display it nicholas@2645: var node = this.popupOptions[this.currentIndex], nicholas@2646: converter = new showdown.Converter(), nicholas@2646: p = new DOMParser(); nicholas@2774: lastNodeStart = new Date(); nicholas@2498: this.popupResponse.innerHTML = ""; nicholas@2648: this.popupTitle.innerHTML = ""; nicholas@2943: var strings = node.specification.statement.split("\n"); nicholas@2949: strings.forEach(function (e, i, a) { nicholas@2943: a[i] = e.trim(); nicholas@2943: }); nicholas@2943: node.specification.statement = strings.join("\n"); nicholas@2943: var statementElements = p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").querySelector("body").children; nicholas@2949: while (statementElements.length > 0) { nicholas@2943: this.popupTitle.appendChild(statementElements[0]); nicholas@2943: } nicholas@2498: if (node.specification.type == 'question') { nicholas@2708: postQuestion.call(this, node); nicholas@2498: } else if (node.specification.type == 'checkbox') { nicholas@2708: postCheckbox.call(this, node); nicholas@2498: } else if (node.specification.type == 'radio') { nicholas@2708: postRadio.call(this, node); nicholas@2498: } else if (node.specification.type == 'number') { nicholas@2708: postNumber.call(this, node); nicholas@2498: } else if (node.specification.type == "video") { nicholas@2708: postVideo.call(this, node); nicholas@2491: } else if (node.specification.type == "youtube") { nicholas@2708: postYoutube.call(this, node); n@2583: } else if (node.specification.type == "slider") { nicholas@2708: postSlider.call(this, node); nicholas@2491: } nicholas@2498: if (this.currentIndex + 1 == this.popupOptions.length) { nicholas@2498: if (this.node.location == "pre") { nicholas@2498: this.buttonProceed.textContent = 'Start'; nicholas@2498: } else { nicholas@2498: this.buttonProceed.textContent = 'Submit'; nicholas@2498: } nicholas@2498: } else { nicholas@2498: this.buttonProceed.textContent = 'Next'; nicholas@2498: } nicholas@2498: if (this.currentIndex > 0) nicholas@2498: this.buttonPrevious.style.visibility = 'visible'; nicholas@2498: else nicholas@2498: this.buttonPrevious.style.visibility = 'hidden'; nicholas@2498: }; nicholas@2498: nicholas@2498: this.initState = function (node, store) { nicholas@2498: //Call this with your preTest and postTest nodes when needed to nicholas@2498: // initialise the popup procedure. nicholas@2498: if (node.options.length > 0) { nicholas@2498: this.popupOptions = []; nicholas@2498: this.node = node; nicholas@2498: this.store = store; nicholas@2708: node.options.forEach(function (opt) { nicholas@2498: this.popupOptions.push({ nicholas@2498: specification: opt, nicholas@2498: response: null nicholas@2498: }); nicholas@2708: }, this); nicholas@2498: this.currentIndex = 0; nicholas@2498: this.showPopup(); nicholas@2498: this.postNode(); nicholas@2498: } else { nicholas@2498: advanceState(); nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.proceedClicked = function () { nicholas@2498: // Each time the popup button is clicked! nicholas@2708: var node = this.popupOptions[this.currentIndex], nicholas@2774: pass = true, nicholas@2778: timeDelta = (new Date() - lastNodeStart) / 1000.0; nicholas@3131: if (node == undefined) { nicholas@3131: advanceState(); nicholas@3131: } nicholas@2774: if (timeDelta < node.specification.minWait) { nicholas@2778: interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds"); nicholas@2774: return; nicholas@2774: } nicholas@2775: node.elapsedTime = timeDelta; nicholas@2498: if (node.specification.type == 'question') { nicholas@2498: // Must extract the question data nicholas@2708: pass = processQuestion.call(this, node); nicholas@2498: } else if (node.specification.type == 'checkbox') { nicholas@2498: // Must extract checkbox data nicholas@2708: pass = processCheckbox.call(this, node); nicholas@2708: } else if (node.specification.type == "radio") { nicholas@2464: // Perform the conditional nicholas@2708: pass = processRadio.call(this, node); nicholas@2708: } else if (node.specification.type == "number") { nicholas@2464: // Perform the conditional nicholas@2708: pass = processNumber.call(this, node); n@2583: } else if (node.specification.type == 'slider') { nicholas@2708: pass = processSlider.call(this, node); nicholas@2708: } nicholas@2708: if (pass === false) { nicholas@2708: return; nicholas@2498: } nicholas@2498: this.currentIndex++; nicholas@2498: if (this.currentIndex < this.popupOptions.length) { nicholas@2498: this.postNode(); nicholas@2498: } else { nicholas@2498: // Reached the end of the popupOptions nicholas@2645: this.popupTitle.innerHTML = ""; nicholas@2498: this.popupResponse.innerHTML = ""; nicholas@2498: this.hidePopup(); nicholas@2708: this.popupOptions.forEach(function (node) { nicholas@2498: this.store.postResult(node); nicholas@2708: }, this); nicholas@2224: this.store.complete(); nicholas@2498: advanceState(); nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.previousClick = function () { nicholas@2498: // Triggered when the 'Back' button is clicked in the survey nicholas@2498: if (this.currentIndex > 0) { nicholas@2498: this.currentIndex--; nicholas@2498: this.postNode(); nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.resize = function (event) { nicholas@2498: // Called on window resize; nicholas@2708: if (this.popup !== null) { nicholas@2498: this.popup.style.left = (window.innerWidth / 2) - 250 + 'px'; nicholas@2498: this.popup.style.top = (window.innerHeight / 2) - 125 + 'px'; nicholas@2498: var blank = document.getElementsByClassName('testHalt')[0]; nicholas@2498: blank.style.width = window.innerWidth; nicholas@2498: blank.style.height = window.innerHeight; nicholas@2498: } nicholas@2498: }; nicholas@2498: this.hideNextButton = function () { nicholas@2224: this.buttonProceed.style.visibility = "hidden"; nicholas@2708: }; nicholas@2498: this.hidePreviousButton = function () { nicholas@2224: this.buttonPrevious.style.visibility = "hidden"; nicholas@2708: }; nicholas@2498: this.showNextButton = function () { nicholas@2224: this.buttonProceed.style.visibility = "visible"; nicholas@2708: }; nicholas@2498: this.showPreviousButton = function () { nicholas@2224: this.buttonPrevious.style.visibility = "visible"; nicholas@2708: }; nicholas@2224: } nicholas@2224: nicholas@2498: function advanceState() { nicholas@2498: // Just for complete clarity nicholas@2498: testState.advanceState(); nicholas@2224: } nicholas@2224: nicholas@2498: function stateMachine() { nicholas@2498: // Object prototype for tracking and managing the test state nicholas@2722: n@2716: function pickSubPool(pool, numElements) { n@2716: // Assumes each element of pool has function "alwaysInclude" n@2716: n@2716: // First extract those excluded from picking process n@2716: var picked = []; nicholas@2833: pool.forEach(function (e, i) { n@2716: if (e.alwaysInclude) { nicholas@2833: picked.push(pool.splice(i, 1)[0]); n@2716: } n@2716: }); n@2716: n@2716: return picked.concat(randomSubArray(pool, numElements - picked.length)); n@2716: } nicholas@2722: nicholas@2498: this.stateMap = []; nicholas@2498: this.preTestSurvey = null; nicholas@2498: this.postTestSurvey = null; nicholas@2498: this.stateIndex = null; nicholas@2498: this.currentStateMap = null; nicholas@2498: this.currentStatePosition = null; nicholas@2224: this.currentStore = null; nicholas@2498: this.initialise = function () { nicholas@2498: n@2909: function randomiseElements(page) { n@2909: // Get the elements which are fixed / labelled n@2909: var fixed = [], n@2909: or = [], n@2909: remainder = []; n@2909: page.audioElements.forEach(function (a) { n@2909: if (a.label.length > 0 || a.postion !== undefined) { n@2909: fixed.push(a); n@2909: } else if (a.type === "outside-reference") { n@2909: or.push(a); n@2909: } else { n@2909: remainder.push(a); n@2909: } n@2979: }); n@2909: if (page.poolSize > 0 || page.randomiseOrder) { n@2909: page.randomiseOrder = true; n@2909: if (page.poolSize === 0) { n@2909: page.poolSize = page.audioElements.length; n@2909: } n@2909: page.poolSize -= fixed.length; n@2909: remainder = pickSubPool(remainder, page.poolSize); n@2909: } n@2909: // Randomise the remainders n@2909: if (page.randomiseOrder) { n@2909: remainder = randomiseOrder(remainder); n@2909: } n@2909: fixed = fixed.concat(remainder); n@2909: page.audioElements = fixed.concat(or); n@2909: page.audioElements.forEach(function (a, i) { n@2909: a.position = i; n@2909: }); n@2909: } n@2909: nicholas@2498: // Get the data from Specification nicholas@2498: var pagePool = []; nicholas@2722: specification.pages.forEach(function (page) { n@2716: if (page.position !== null || page.alwaysInclude) { n@2716: page.alwaysInclude = true; n@2716: } n@2716: pagePool.push(page); n@2717: }); n@2716: if (specification.numPages > 0) { n@2716: specification.randomiseOrder = true; n@2716: pagePool = pickSubPool(pagePool, specification.numPages); n@2716: } n@2716: n@2716: // Now get the order of pages n@2716: var fixed = []; nicholas@2722: pagePool.forEach(function (page) { nicholas@2748: if (page.position !== undefined) { n@2716: fixed.push(page); n@2716: var i = pagePool.indexOf(page); n@2716: pagePool.splice(i, 1); nicholas@2224: } n@2717: }); nicholas@2498: n@2716: if (specification.randomiseOrder) { n@2716: pagePool = randomiseOrder(pagePool); nicholas@2224: } nicholas@2498: n@2716: // Place in the correct order nicholas@2722: fixed.forEach(function (page) { n@2716: pagePool.splice(page.position, 0, page); n@2717: }); n@2716: n@2716: // Now process the pages n@2716: pagePool.forEach(function (page, i) { n@2716: page.presentedId = i; n@2716: this.stateMap.push(page); n@2716: var elements = page.audioElements; n@2909: randomiseElements(page); n@2716: storage.createTestPageStore(page); n@2716: audioEngineContext.loadPageData(page); n@2716: }, this); nicholas@2674: nicholas@2708: if (specification.preTest !== null) { nicholas@2498: this.preTestSurvey = specification.preTest; nicholas@2498: } nicholas@2708: if (specification.postTest !== null) { nicholas@2498: this.postTestSurvey = specification.postTest; nicholas@2498: } nicholas@2498: nicholas@2498: if (this.stateMap.length > 0) { nicholas@2708: if (this.stateIndex !== null) { nicholas@2498: console.log('NOTE - State already initialise'); nicholas@2498: } nicholas@2498: this.stateIndex = -2; nicholas@2224: console.log('Starting test...'); nicholas@2498: } else { nicholas@2498: console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP'); nicholas@2498: } nicholas@2498: }; nicholas@2498: this.advanceState = function () { nicholas@2708: if (this.stateIndex === null) { nicholas@2498: this.initialise(); nicholas@2498: } nicholas@2357: if (this.stateIndex > -2) { nicholas@2357: storage.update(); nicholas@2357: } nicholas@2498: if (this.stateIndex == -2) { nicholas@3131: this.stateIndex++; nicholas@2708: if (this.preTestSurvey !== undefined) { nicholas@2498: popup.initState(this.preTestSurvey, storage.globalPreTest); nicholas@2498: } else { nicholas@2498: this.advanceState(); nicholas@2498: } nicholas@2498: } else if (this.stateIndex == -1) { nicholas@3101: if (interfaceContext.calibrationTests.checkFrequencies) { nicholas@2224: popup.showPopup(); nicholas@3101: popup.popupTitle.textContent = "Set the levels so all tones are of equal amplitude. Move your mouse over the sliders to hear the tones. The red slider is the reference tone"; nicholas@3101: interfaceContext.calibrationTests.performFrequencyCheck(popup.popupResponse); nicholas@3101: popup.hidePreviousButton(); nicholas@3101: } else if (interfaceContext.calibrationTests.checkChannels) { nicholas@3101: popup.showPopup(); nicholas@3101: popup.popupTitle.textContent = "Click play to start the audio, the click the button corresponding to where the sound appears to be coming from."; nicholas@3101: interfaceContext.calibrationTests.performChannelCheck(popup.popupResponse); nicholas@2224: popup.hidePreviousButton(); nicholas@2224: } else { nicholas@3101: this.stateIndex++; nicholas@2224: this.advanceState(); nicholas@2224: } nicholas@2498: } else if (this.stateIndex == this.stateMap.length) { nicholas@2498: // All test pages complete, post test nicholas@2498: console.log('Ending test ...'); nicholas@2498: this.stateIndex++; nicholas@2708: if (this.postTestSurvey === undefined) { nicholas@2498: this.advanceState(); nicholas@2498: } else { nicholas@2498: popup.initState(this.postTestSurvey, storage.globalPostTest); nicholas@2498: } nicholas@2498: } else if (this.stateIndex > this.stateMap.length) { nicholas@2498: createProjectSave(specification.projectReturn); nicholas@2498: } else { nicholas@2224: popup.hidePopup(); nicholas@2708: if (this.currentStateMap === null) { nicholas@2498: this.currentStateMap = this.stateMap[this.stateIndex]; nicholas@2498: nicholas@2224: this.currentStore = storage.testPages[this.stateIndex]; nicholas@2708: if (this.currentStateMap.preTest !== undefined) { nicholas@2498: this.currentStatePosition = 'pre'; nicholas@2498: popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest); nicholas@2498: } else { nicholas@2498: this.currentStatePosition = 'test'; nicholas@2498: } nicholas@2498: interfaceContext.newPage(this.currentStateMap, storage.testPages[this.stateIndex]); nicholas@2498: return; nicholas@2498: } nicholas@2498: switch (this.currentStatePosition) { nicholas@2498: case 'pre': nicholas@2498: this.currentStatePosition = 'test'; nicholas@2498: break; nicholas@2498: case 'test': nicholas@2498: this.currentStatePosition = 'post'; nicholas@2498: // Save the data nicholas@2498: this.testPageCompleted(); nicholas@2708: if (this.currentStateMap.postTest === undefined) { nicholas@2498: this.advanceState(); nicholas@2498: return; nicholas@2498: } else { nicholas@2498: popup.initState(this.currentStateMap.postTest, storage.testPages[this.stateIndex].postTest); nicholas@2498: } nicholas@2498: break; nicholas@2498: case 'post': nicholas@2498: this.stateIndex++; nicholas@2498: this.currentStateMap = null; nicholas@2498: this.advanceState(); nicholas@2498: break; nicholas@2708: } nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.testPageCompleted = function () { nicholas@2498: // Function called each time a test page has been completed nicholas@2498: var storePoint = storage.testPages[this.stateIndex]; nicholas@2498: // First get the test metric nicholas@2498: nicholas@2498: var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0]; nicholas@2498: if (audioEngineContext.metric.enableTestTimer) { nicholas@2498: var testTime = storePoint.parent.document.createElement('metricresult'); nicholas@2498: testTime.id = 'testTime'; nicholas@2498: testTime.textContent = audioEngineContext.timer.testDuration; nicholas@2498: metric.appendChild(testTime); nicholas@2498: } nicholas@2498: nicholas@2498: var audioObjects = audioEngineContext.audioObjects; nicholas@2708: audioEngineContext.audioObjects.forEach(function (ao) { nicholas@2498: ao.exportXMLDOM(); nicholas@2708: }); nicholas@2708: interfaceContext.commentQuestions.forEach(function (element) { nicholas@2498: element.exportXMLDOM(storePoint); nicholas@2708: }); nicholas@2498: pageXMLSave(storePoint.XMLDOM, this.currentStateMap); nicholas@2224: storePoint.complete(); nicholas@2498: }; nicholas@2498: nicholas@2498: this.getCurrentTestPage = function () { nicholas@2498: if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) { nicholas@2310: return this.currentStateMap; nicholas@2310: } else { nicholas@2310: return null; nicholas@2310: } nicholas@2708: }; nicholas@2498: this.getCurrentTestPageStore = function () { nicholas@2498: if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) { nicholas@2312: return this.currentStore; nicholas@2312: } else { nicholas@2312: return null; nicholas@2312: } nicholas@2708: }; nicholas@2224: } nicholas@2224: nicholas@2224: function AudioEngine(specification) { nicholas@2498: nicholas@2498: // Create two output paths, the main outputGain and fooGain. nicholas@2498: // Output gain is default to 1 and any items for playback route here nicholas@2498: // Foo gain is used for analysis to ensure paths get processed, but are not heard nicholas@2498: // because web audio will optimise and any route which does not go to the destination gets ignored. nicholas@2498: this.outputGain = audioContext.createGain(); nicholas@2498: this.fooGain = audioContext.createGain(); nicholas@2508: this.fooGain.gain.value = 0; nicholas@2498: nicholas@2498: // Use this to detect playback state: 0 - stopped, 1 - playing nicholas@2498: this.status = 0; nicholas@2498: nicholas@2498: // Connect both gains to output nicholas@2498: this.outputGain.connect(audioContext.destination); nicholas@2498: this.fooGain.connect(audioContext.destination); nicholas@2498: nicholas@2498: // Create the timer Object nicholas@2498: this.timer = new timer(); nicholas@2498: // Create session metrics nicholas@2498: this.metric = new sessionMetrics(this, specification); nicholas@2498: nicholas@2498: this.loopPlayback = false; nicholas@2351: this.synchPlayback = false; nicholas@2351: this.pageSpecification = null; nicholas@2498: nicholas@2498: this.pageStore = null; nicholas@2498: nicholas@2508: // Chrome 53+ Error solution nicholas@2508: // Empty buffer for keep-alive nicholas@2508: var nullBuffer = audioContext.createBuffer(1, audioContext.sampleRate, audioContext.sampleRate); nicholas@2508: this.nullBufferSource = audioContext.createBufferSource(); nicholas@2508: this.nullBufferSource.buffer = nullBuffer; nicholas@2508: this.nullBufferSource.loop = true; nicholas@2508: this.nullBufferSource.start(0); nicholas@2508: nicholas@2498: // Create store for new audioObjects nicholas@2498: this.audioObjects = []; nicholas@2498: nicholas@2498: this.buffers = []; nicholas@2498: this.bufferObj = function () { nicholas@2617: var urls = []; nicholas@2498: this.buffer = null; nicholas@2498: this.users = []; nicholas@2224: this.progress = 0; nicholas@2224: this.status = 0; nicholas@2498: this.ready = function () { nicholas@2498: if (this.status >= 2) { nicholas@2224: this.status = 3; nicholas@2224: } nicholas@2498: for (var i = 0; i < this.users.length; i++) { nicholas@2498: this.users[i].state = 1; nicholas@2708: if (this.users[i].interfaceDOM !== null) { nicholas@2498: this.users[i].bufferLoaded(this); nicholas@2498: } nicholas@2498: } nicholas@2498: }; nicholas@2617: this.setUrls = function (obj) { nicholas@2617: // Obj must be an array of pairs: nicholas@2617: // [{sampleRate, url}] nicholas@2617: var localFs = audioContext.sampleRate, nicholas@2617: list = [], nicholas@2617: i; nicholas@2617: for (i = 0; i < obj.length; i++) { nicholas@2617: if (obj[i].sampleRate == localFs) { nicholas@2617: list.push(obj.splice(i, 1)[0]); nicholas@2617: } nicholas@2617: } nicholas@2617: list = list.concat(obj); nicholas@2617: urls = list; nicholas@2617: }; nicholas@2617: this.hasUrl = function (checkUrl) { nicholas@2617: var l = urls.length, nicholas@2617: i; nicholas@2617: for (i = 0; i < l; i++) { nicholas@2617: if (urls[i].url == checkUrl) { nicholas@2617: return true; nicholas@2617: } nicholas@2617: } nicholas@2617: return false; nicholas@2708: }; nicholas@2617: this.getMedia = function () { nicholas@2615: var self = this; nicholas@2616: var currentUrlIndex = 0; nicholas@2498: nicholas@2615: function get(fqurl) { nicholas@2615: return new Promise(function (resolve, reject) { nicholas@2615: var req = new XMLHttpRequest(); nicholas@2615: req.open('GET', fqurl, true); nicholas@2615: req.responseType = 'arraybuffer'; nicholas@2615: req.onload = function () { nicholas@2615: if (req.status == 200) { nicholas@2615: resolve(req.response); nicholas@2615: } nicholas@2615: }; nicholas@2615: req.onerror = function () { nicholas@2615: reject(new Error(req.statusText)); nicholas@2615: }; nicholas@2615: nicholas@2615: req.addEventListener("progress", progressCallback.bind(self)); nicholas@2615: req.send(); nicholas@2615: }); nicholas@2615: } nicholas@2615: nicholas@2615: function getNextURL() { nicholas@2615: currentUrlIndex++; nicholas@2615: var self = this; nicholas@2617: if (currentUrlIndex >= urls.length) { nicholas@2615: processError(); nicholas@2615: } else { nicholas@2617: return get(urls[currentUrlIndex].url).then(processAudio.bind(self)).catch(getNextURL.bind(self)); nicholas@2615: } nicholas@2615: } nicholas@2498: nicholas@2498: // Create callback to decode the data asynchronously nicholas@2615: function processAudio(response) { nicholas@2615: var self = this; nicholas@2615: return audioContext.decodeAudioData(response, function (decodedData) { nicholas@2615: self.buffer = decodedData; nicholas@2615: self.status = 2; nicholas@2615: calculateLoudness(self, "I"); nicholas@2615: return true; nicholas@2498: }, function (e) { nicholas@2403: var waveObj = new WAVE(); nicholas@2708: if (waveObj.open(response) === 0) { nicholas@2615: self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate); nicholas@2498: for (var c = 0; c < waveObj.num_channels; c++) { nicholas@2615: var buffer_ptr = self.buffer.getChannelData(c); nicholas@2498: for (var n = 0; n < waveObj.num_samples; n++) { nicholas@2403: buffer_ptr[n] = waveObj.decoded_data[c][n]; nicholas@2224: } nicholas@2224: } nicholas@2403: } nicholas@2708: if (self.buffer !== undefined) { nicholas@2615: self.status = 2; nicholas@2615: calculateLoudness(self, "I"); nicholas@2615: return true; nicholas@2403: } nicholas@2708: waveObj = undefined; nicholas@2615: return false; nicholas@2403: }); nicholas@2615: } nicholas@2498: nicholas@2224: // Create callback for any error in loading nicholas@2615: function processError() { nicholas@2615: this.status = -1; nicholas@2615: for (var i = 0; i < this.users.length; i++) { nicholas@2615: this.users[i].state = -1; nicholas@2708: if (this.users[i].interfaceDOM !== null) { nicholas@2615: this.users[i].bufferLoaded(this); nicholas@2224: } nicholas@2224: } nicholas@2617: interfaceContext.lightbox.post("Error", "Could not load resource " + urls[currentUrlIndex].url); nicholas@2224: } nicholas@2498: nicholas@2615: function progressCallback(event) { nicholas@2498: if (event.lengthComputable) { nicholas@2615: this.progress = event.loaded / event.total; nicholas@2615: for (var i = 0; i < this.users.length; i++) { nicholas@2708: if (this.users[i].interfaceDOM !== null) { nicholas@2615: if (typeof this.users[i].interfaceDOM.updateLoading === "function") { nicholas@2615: this.users[i].interfaceDOM.updateLoading(this.progress * 100); nicholas@2498: } nicholas@2498: } nicholas@2498: } nicholas@2498: } nicholas@2708: } nicholas@2615: nicholas@2615: this.progress = 0; nicholas@2224: this.status = 1; nicholas@2617: currentUrlIndex = 0; nicholas@2617: get(urls[0].url).then(processAudio.bind(self)).catch(getNextURL.bind(self)); nicholas@2498: }; nicholas@2498: nicholas@2498: this.registerAudioObject = function (audioObject) { nicholas@2224: // Called by an audioObject to register to the buffer for use nicholas@2224: // First check if already in the register pool nicholas@2708: this.users.forEach(function (object) { nicholas@2708: if (audioObject.id == object.id) { nicholas@2498: return 0; nicholas@2498: } nicholas@2708: }); nicholas@2224: this.users.push(audioObject); nicholas@2498: if (this.status == 3 || this.status == -1) { nicholas@2224: // The buffer is already ready, trigger bufferLoaded nicholas@2224: audioObject.bufferLoaded(this); nicholas@2224: } nicholas@2224: }; nicholas@2498: nicholas@2498: this.copyBuffer = function (preSilenceTime, postSilenceTime) { nicholas@2224: // Copies the entire bufferObj. nicholas@2708: if (preSilenceTime === undefined) { nicholas@2498: preSilenceTime = 0; nicholas@2498: } nicholas@2708: if (postSilenceTime === undefined) { nicholas@2498: postSilenceTime = 0; nicholas@2498: } nicholas@2498: var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate); nicholas@2498: var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate); nicholas@2498: var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples; nicholas@2460: var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate); nicholas@2708: var c; nicholas@2224: // Now we can use some efficient background copy schemes if we are just padding the end nicholas@2708: if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") { nicholas@2708: for (c = 0; c < this.buffer.numberOfChannels; c++) { nicholas@2498: copybuffer.copyToChannel(this.buffer.getChannelData(c), c); nicholas@2224: } nicholas@2224: } else { nicholas@2708: for (c = 0; c < this.buffer.numberOfChannels; c++) { nicholas@2224: var src = this.buffer.getChannelData(c); nicholas@2460: var dst = copybuffer.getChannelData(c); nicholas@2498: for (var n = 0; n < src.length; n++) nicholas@2498: dst[n + preSilenceSamples] = src[n]; nicholas@2224: } nicholas@2224: } nicholas@2224: // Copy in the rest of the buffer information nicholas@2460: copybuffer.lufs = this.buffer.lufs; nicholas@2460: copybuffer.playbackGain = this.buffer.playbackGain; nicholas@2460: return copybuffer; nicholas@2708: }; nicholas@2498: nicholas@2498: this.cropBuffer = function (startTime, stopTime) { nicholas@2460: // Copy and return the cropped buffer nicholas@2498: var start_sample = Math.floor(startTime * this.buffer.sampleRate); nicholas@2498: var stop_sample = Math.floor(stopTime * this.buffer.sampleRate); nicholas@2460: var newLength = stop_sample - start_sample; nicholas@2460: var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate); nicholas@2460: // Now we can use some efficient background copy schemes if we are just padding the end nicholas@2498: for (var c = 0; c < this.buffer.numberOfChannels; c++) { nicholas@2460: var buffer = this.buffer.getChannelData(c); nicholas@2498: var sub_frame = buffer.subarray(start_sample, stop_sample); nicholas@2460: if (typeof copybuffer.copyToChannel == "function") { nicholas@2498: copybuffer.copyToChannel(sub_frame, c); nicholas@2460: } else { nicholas@2460: var dst = copybuffer.getChannelData(c); nicholas@2498: for (var n = 0; n < newLength; n++) nicholas@2505: dst[n] = buffer[n + start_sample]; nicholas@2460: } nicholas@2460: } nicholas@2460: return copybuffer; nicholas@2708: }; nicholas@2498: }; nicholas@2498: nicholas@2498: this.loadPageData = function (page) { nicholas@2224: // Load the URL from pages nicholas@2708: function loadAudioElementData(element) { nicholas@2224: var URL = page.hostURL + element.url; nicholas@2708: var buffer = this.buffers.find(function (buffObj) { nicholas@2708: return buffObj.hasUrl(URL); nicholas@2708: }); nicholas@2708: if (buffer === undefined) { nicholas@2224: buffer = new this.bufferObj(); nicholas@2617: var urls = [{ nicholas@2617: url: URL, nicholas@2617: sampleRate: element.sampleRate nicholas@2617: }]; nicholas@2615: element.alternatives.forEach(function (e) { nicholas@2617: urls.push({ nicholas@2617: url: e.url, nicholas@2617: sampleRate: e.sampleRate nicholas@2617: }); nicholas@2615: }); nicholas@2617: buffer.setUrls(urls); nicholas@2617: buffer.getMedia(); nicholas@2224: this.buffers.push(buffer); nicholas@2224: } nicholas@2224: } nicholas@2708: page.audioElements.forEach(loadAudioElementData, this); nicholas@2224: }; nicholas@2498: nicholas@2708: function playNormal(id) { nicholas@2708: var playTime = audioContext.currentTime + 0.1; nicholas@2708: var stopTime = playTime + specification.crossFade; nicholas@2708: this.audioObjects.forEach(function (ao) { nicholas@2708: if (ao.id === id) { nicholas@2942: ao.setupPlayback(); nicholas@2942: ao.bufferStart(playTime); nicholas@2942: ao.listenStart(playTime); nicholas@2708: } else { nicholas@2942: ao.listenStop(playTime); nicholas@2942: ao.bufferStop(stopTime); nicholas@2708: } nicholas@2708: }); nicholas@2708: } nicholas@2708: nicholas@2942: function playSync(id) { nicholas@2708: var playTime = audioContext.currentTime + 0.1; nicholas@2708: var stopTime = playTime + specification.crossFade; nicholas@2708: this.audioObjects.forEach(function (ao) { nicholas@2942: ao.setupPlayback(); nicholas@2942: ao.bufferStart(playTime); nicholas@2708: if (ao.id === id) { nicholas@2942: ao.listenStart(playTime); nicholas@2708: } else { nicholas@2942: ao.listenStop(playTime); nicholas@2708: } nicholas@2708: }); nicholas@2708: } nicholas@2708: nicholas@2498: this.play = function (id) { nicholas@2498: // Start the timer and set the audioEngine state to playing (1) nicholas@2708: if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) { nicholas@2708: throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)'); nicholas@2498: } nicholas@2823: var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays; nicholas@2823: if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) { nicholas@2823: interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times"); nicholas@2823: return; nicholas@2823: } nicholas@2708: if (this.status === 1) { nicholas@2498: this.timer.startTest(); nicholas@2708: interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]); nicholas@2942: if (this.synchPlayback) { nicholas@2351: // Traditional looped playback nicholas@2942: playSync.call(this, id); nicholas@2708: } else { nicholas@2708: if (this.bufferReady(id) === false) { nicholas@2708: console.log("Cannot play. Buffer not ready"); nicholas@2708: return; nicholas@2498: } nicholas@2708: playNormal.call(this, id); nicholas@2498: } nicholas@2498: interfaceContext.playhead.start(); nicholas@2498: } nicholas@2498: }; nicholas@2224: nicholas@2498: this.stop = function () { nicholas@2498: // Send stop and reset command to all playback buffers nicholas@2498: if (this.status == 1) { nicholas@2498: var setTime = audioContext.currentTime + 0.1; nicholas@2708: this.audioObjects.forEach(function (a) { nicholas@2942: a.listenStop(setTime); nicholas@2942: a.bufferStop(setTime); nicholas@2708: }); nicholas@2498: interfaceContext.playhead.stop(); nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.newTrack = function (element) { nicholas@2498: // Pull data from given URL into new audio buffer nicholas@2498: // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin' nicholas@2498: nicholas@2498: // Create the audioObject with ID of the new track length; nicholas@2708: var audioObjectId = this.audioObjects.length; nicholas@2498: this.audioObjects[audioObjectId] = new audioObject(audioObjectId); nicholas@2498: nicholas@2498: // Check if audioObject buffer is currently stored by full URL nicholas@2498: var URL = testState.currentStateMap.hostURL + element.url; nicholas@2708: var buffer = this.buffers.find(function (buffObj) { nicholas@2708: return buffObj.hasUrl(URL); nicholas@2708: }); nicholas@2708: if (buffer === undefined) { nicholas@2498: console.log("[WARN]: Buffer was not loaded in pre-test! " + URL); nicholas@2498: buffer = new this.bufferObj(); nicholas@2224: this.buffers.push(buffer); nicholas@2498: buffer.getMedia(URL); nicholas@2498: } nicholas@2498: this.audioObjects[audioObjectId].specification = element; nicholas@2498: this.audioObjects[audioObjectId].url = URL; nicholas@2498: // Obtain store node nicholas@2498: var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement'); nicholas@2498: for (var i = 0; i < aeNodes.length; i++) { nicholas@2498: if (aeNodes[i].getAttribute("ref") == element.id) { nicholas@2498: this.audioObjects[audioObjectId].storeDOM = aeNodes[i]; nicholas@2498: break; nicholas@2498: } nicholas@2498: } nicholas@2224: buffer.registerAudioObject(this.audioObjects[audioObjectId]); nicholas@2498: return this.audioObjects[audioObjectId]; nicholas@2498: }; nicholas@2498: nicholas@2498: this.newTestPage = function (audioHolderObject, store) { nicholas@2498: this.pageStore = store; nicholas@2351: this.pageSpecification = audioHolderObject; nicholas@2498: this.status = 0; nicholas@2498: this.audioObjectsReady = false; nicholas@2498: this.metric.reset(); nicholas@2708: this.buffers.forEach(function (buffer) { nicholas@2708: buffer.users = []; nicholas@2708: }); nicholas@2498: this.audioObjects = []; nicholas@2224: this.timer = new timer(); nicholas@2224: this.loopPlayback = audioHolderObject.loop; nicholas@2351: this.synchPlayback = audioHolderObject.synchronous; nicholas@2955: interfaceContext.keyboardInterface.resetKeyBindings(); nicholas@2498: }; nicholas@2498: nicholas@2498: this.checkAllPlayed = function () { nicholas@2708: var arr = []; nicholas@2498: for (var id = 0; id < this.audioObjects.length; id++) { nicholas@2708: if (this.audioObjects[id].metric.wasListenedTo === false) { nicholas@2498: arr.push(this.audioObjects[id].id); nicholas@2498: } nicholas@2498: } nicholas@2498: return arr; nicholas@2498: }; nicholas@2498: nicholas@2498: this.checkAllReady = function () { nicholas@2498: var ready = true; nicholas@2498: for (var i = 0; i < this.audioObjects.length; i++) { nicholas@2708: if (this.audioObjects[i].state === 0) { nicholas@2498: // Track not ready nicholas@2498: console.log('WAIT -- audioObject ' + i + ' not ready yet!'); nicholas@2498: ready = false; nicholas@2708: } nicholas@2498: } nicholas@2498: return ready; nicholas@2498: }; nicholas@2498: nicholas@2498: this.setSynchronousLoop = function () { nicholas@2570: // Pads the signals so they are all exactly the same duration nicholas@2570: // Get the duration of the longest signal. nicholas@2570: var duration = 0; nicholas@2498: var maxId; nicholas@2498: for (var i = 0; i < this.audioObjects.length; i++) { nicholas@2570: if (duration < this.audioObjects[i].buffer.buffer.duration) { nicholas@2570: duration = this.audioObjects[i].buffer.buffer.duration; nicholas@2498: maxId = i; nicholas@2498: } nicholas@2498: } nicholas@2498: // Extract the audio and zero-pad nicholas@2708: this.audioObjects.forEach(function (ao) { nicholas@2570: if (ao.buffer.buffer.duration !== duration) { nicholas@2570: ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration); nicholas@2500: } nicholas@2708: }); nicholas@2498: }; nicholas@2498: nicholas@2498: this.bufferReady = function (id) { nicholas@2498: if (this.checkAllReady()) { nicholas@2498: if (this.synchPlayback) { nicholas@2498: this.setSynchronousLoop(); nicholas@2498: } nicholas@2460: this.status = 1; nicholas@2460: return true; nicholas@2460: } nicholas@2460: return false; nicholas@2224: }; nicholas@2498: nicholas@2224: } nicholas@2224: nicholas@2224: function audioObject(id) { nicholas@2498: // The main buffer object with common control nodes to the AudioEngine nicholas@2498: nicholas@2823: var playCounter = 0; nicholas@2823: nicholas@2708: this.specification = undefined; nicholas@2498: this.id = id; nicholas@2498: this.state = 0; // 0 - no data, 1 - ready nicholas@2498: this.url = null; // Hold the URL given for the output back to the results. nicholas@2498: this.metric = new metricTracker(this); nicholas@2498: this.storeDOM = null; nicholas@2949: this.playing = false; nicholas@2498: nicholas@2498: // Bindings for GUI nicholas@2498: this.interfaceDOM = null; nicholas@2498: this.commentDOM = null; nicholas@2498: nicholas@2498: // Create a buffer and external gain control to allow internal patching of effects and volume leveling. nicholas@2498: this.bufferNode = undefined; nicholas@2498: this.outputGain = audioContext.createGain(); nicholas@2942: this.outputGain.gain.value = 0.0; nicholas@2498: nicholas@2498: this.onplayGain = 1.0; nicholas@2498: nicholas@2498: // Connect buffer to the audio graph nicholas@2498: this.outputGain.connect(audioEngineContext.outputGain); nicholas@2508: audioEngineContext.nullBufferSource.connect(this.outputGain); nicholas@2498: nicholas@2498: // the audiobuffer is not designed for multi-start playback nicholas@2498: // When stopeed, the buffer node is deleted and recreated with the stored buffer. nicholas@2708: this.buffer = undefined; nicholas@2498: nicholas@2498: this.bufferLoaded = function (callee) { nicholas@2498: // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the nicholas@2498: // audioObject and trigger the interfaceDOM.enable() function for user feedback nicholas@2224: if (callee.status == -1) { nicholas@2224: // ERROR nicholas@2224: this.state = -1; nicholas@2708: if (this.interfaceDOM !== null) { nicholas@2498: this.interfaceDOM.error(); nicholas@2498: } nicholas@2224: this.buffer = callee; nicholas@2224: return; nicholas@2224: } nicholas@2224: this.buffer = callee; nicholas@2224: var preSilenceTime = this.specification.preSilence || this.specification.parent.preSilence || specification.preSilence || 0.0; nicholas@2224: var postSilenceTime = this.specification.postSilence || this.specification.parent.postSilence || specification.postSilence || 0.0; nicholas@2460: var startTime = this.specification.startTime; nicholas@2460: var stopTime = this.specification.stopTime; nicholas@2460: var copybuffer = new callee.constructor(); nicholas@2500: nicholas@2500: copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration); nicholas@2708: if (preSilenceTime !== 0 || postSilenceTime !== 0) { nicholas@2500: copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime); nicholas@2460: } nicholas@2500: nicholas@2660: copybuffer.buffer.lufs = callee.buffer.lufs; nicholas@2500: this.buffer = copybuffer; nicholas@2498: nicholas@2661: var targetLUFS = this.specification.loudness || this.specification.parent.loudness || specification.loudness; nicholas@2498: if (typeof targetLUFS === "number" && isFinite(targetLUFS)) { nicholas@2498: this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs); nicholas@2498: } else { nicholas@2498: this.buffer.buffer.playbackGain = 1.0; nicholas@2498: } nicholas@2708: if (this.interfaceDOM !== null) { nicholas@2498: this.interfaceDOM.enable(); nicholas@2498: } nicholas@2498: this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0); nicholas@2498: this.storeDOM.setAttribute('playGain', linearToDecibel(this.onplayGain)); nicholas@2460: this.state = 1; nicholas@2460: audioEngineContext.bufferReady(id); nicholas@2498: }; nicholas@2498: nicholas@2498: this.bindInterface = function (interfaceObject) { nicholas@2498: this.interfaceDOM = interfaceObject; nicholas@2498: this.metric.initialise(interfaceObject.getValue()); nicholas@2498: if (this.state == 1) { nicholas@2498: this.interfaceDOM.enable(); nicholas@2498: } else if (this.state == -1) { nicholas@2224: // ERROR nicholas@2224: this.interfaceDOM.error(); nicholas@2224: return; nicholas@2224: } nicholas@2955: var presentedId = interfaceObject.getPresentedId(); nicholas@2955: this.storeDOM.setAttribute('presentedId', presentedId); nicholas@2955: nicholas@2955: // Key-bindings nicholas@2955: if (presentedId.length == 1) { nicholas@2955: interfaceContext.keyboardInterface.registerKeyBinding(presentedId, this); nicholas@2955: } nicholas@2498: }; nicholas@2498: nicholas@2942: this.listenStart = function (setTime) { nicholas@2949: if (this.playing === false) { nicholas@2942: playCounter++; nicholas@2942: this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, setTime); nicholas@2942: this.metric.startListening(audioEngineContext.timer.getTestTime()); nicholas@2942: this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime(); nicholas@2942: this.interfaceDOM.startPlayback(); nicholas@2949: this.playing = true; nicholas@2942: } nicholas@2498: }; nicholas@2498: nicholas@2942: this.listenStop = function (setTime) { nicholas@2949: if (this.playing === true) { nicholas@2498: this.outputGain.gain.linearRampToValueAtTime(0.0, setTime); nicholas@2942: this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition()); nicholas@2498: } nicholas@2224: this.interfaceDOM.stopPlayback(); nicholas@2949: this.playing = false; nicholas@2498: }; nicholas@2498: nicholas@2942: this.setupPlayback = function () { nicholas@2708: if (this.bufferNode === undefined && this.buffer.buffer !== undefined) { nicholas@2498: this.bufferNode = audioContext.createBufferSource(); nicholas@2498: this.bufferNode.owner = this; nicholas@2498: this.bufferNode.connect(this.outputGain); nicholas@2498: this.bufferNode.buffer = this.buffer.buffer; nicholas@2942: if (audioEngineContext.loopPlayback) { nicholas@2942: this.bufferNode.loopStart = this.specification.startTime || 0; nicholas@2942: this.bufferNode.loopEnd = this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration; nicholas@2942: this.bufferNode.loop = true; nicholas@2942: } nicholas@2498: this.bufferNode.onended = function (event) { nicholas@2498: // Safari does not like using 'this' to reference the calling object! nicholas@2498: //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition()); nicholas@2708: if (event.currentTarget !== null) { nicholas@2942: event.currentTarget.owner.bufferStop(audioContext.currentTime + 0.1); nicholas@2950: event.currentTarget.owner.listenStop(audioContext.currentTime + 0.1); nicholas@2224: } nicholas@2498: }; nicholas@2942: this.bufferNode.state = 0; nicholas@2942: } nicholas@2942: }; nicholas@2942: nicholas@2942: this.bufferStart = function (startTime) { nicholas@2942: this.outputGain.gain.cancelScheduledValues(audioContext.currentTime); n@2979: if (this.bufferNode && this.bufferNode.state === 0) { nicholas@2942: this.bufferNode.state = 1; n@2979: if (this.bufferNode.loop === true) { nicholas@2499: this.bufferNode.start(startTime); nicholas@2499: } else { nicholas@2499: this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration); nicholas@2499: } nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2942: this.bufferStop = function (stopTime) { nicholas@2224: this.outputGain.gain.cancelScheduledValues(audioContext.currentTime); nicholas@2942: if (this.bufferNode !== undefined && this.bufferNode.state > 0) { nicholas@2498: this.bufferNode.stop(stopTime); nicholas@2498: this.bufferNode = undefined; nicholas@2498: } nicholas@2529: this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime); nicholas@2224: this.interfaceDOM.stopPlayback(); nicholas@2498: }; nicholas@2498: nicholas@2498: this.getCurrentPosition = function () { nicholas@2498: var time = audioEngineContext.timer.getTestTime(); nicholas@2708: if (this.bufferNode !== undefined) { nicholas@2498: var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration; nicholas@2498: if (isNaN(position)) { nicholas@2498: return 0; nicholas@2498: } nicholas@2224: return position; nicholas@2498: } else { nicholas@2498: return 0; nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.exportXMLDOM = function () { nicholas@2498: var file = storage.document.createElement('file'); nicholas@2498: file.setAttribute('sampleRate', this.buffer.buffer.sampleRate); nicholas@2498: file.setAttribute('channels', this.buffer.buffer.numberOfChannels); nicholas@2498: file.setAttribute('sampleCount', this.buffer.buffer.length); nicholas@2498: file.setAttribute('duration', this.buffer.buffer.duration); nicholas@2498: this.storeDOM.appendChild(file); nicholas@2498: if (this.specification.type != 'outside-reference') { nicholas@2498: var interfaceXML = this.interfaceDOM.exportXMLDOM(this); nicholas@2708: if (interfaceXML !== null) { nicholas@2708: if (interfaceXML.length === undefined) { nicholas@2498: this.storeDOM.appendChild(interfaceXML); nicholas@2498: } else { nicholas@2498: for (var i = 0; i < interfaceXML.length; i++) { nicholas@2498: this.storeDOM.appendChild(interfaceXML[i]); nicholas@2498: } nicholas@2498: } nicholas@2498: } nicholas@2708: if (this.commentDOM !== null) { nicholas@2498: this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this)); nicholas@2498: } nicholas@2498: } nicholas@2708: this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]); nicholas@2498: }; nicholas@2823: nicholas@2823: Object.defineProperties(this, { nicholas@2823: "numberOfPlays": { nicholas@2823: 'get': function () { nicholas@2823: return playCounter; nicholas@2823: }, nicholas@2823: 'set': function () { nicholas@2823: return playCounter; nicholas@2823: } nicholas@2823: } nicholas@2823: }); nicholas@2224: } nicholas@2224: nicholas@2498: function timer() { nicholas@2498: /* Timer object used in audioEngine to keep track of session timings nicholas@2498: * Uses the timer of the web audio API, so sample resolution nicholas@2498: */ nicholas@2498: this.testStarted = false; nicholas@2498: this.testStartTime = 0; nicholas@2498: this.testDuration = 0; nicholas@2498: this.minimumTestTime = 0; // No minimum test time nicholas@2498: this.startTest = function () { nicholas@2708: if (this.testStarted === false) { nicholas@2498: this.testStartTime = audioContext.currentTime; nicholas@2498: this.testStarted = true; nicholas@2498: this.updateTestTime(); nicholas@2498: audioEngineContext.metric.initialiseTest(); nicholas@2498: } nicholas@2498: }; nicholas@2498: this.stopTest = function () { nicholas@2498: if (this.testStarted) { nicholas@2498: this.testDuration = this.getTestTime(); nicholas@2498: this.testStarted = false; nicholas@2498: } else { nicholas@2498: console.log('ERR: Test tried to end before beginning'); nicholas@2498: } nicholas@2498: }; nicholas@2498: this.updateTestTime = function () { nicholas@2498: if (this.testStarted) { nicholas@2498: this.testDuration = audioContext.currentTime - this.testStartTime; nicholas@2498: } nicholas@2498: }; nicholas@2498: this.getTestTime = function () { nicholas@2498: this.updateTestTime(); nicholas@2498: return this.testDuration; nicholas@2498: }; nicholas@2224: } nicholas@2224: nicholas@2498: function sessionMetrics(engine, specification) { nicholas@2498: /* Used by audioEngine to link to audioObjects to minimise the timer call timers; nicholas@2498: */ nicholas@2498: this.engine = engine; nicholas@2498: this.lastClicked = -1; nicholas@2498: this.data = -1; nicholas@2498: this.reset = function () { nicholas@2498: this.lastClicked = -1; nicholas@2498: this.data = -1; nicholas@2498: }; nicholas@2498: nicholas@2498: this.enableElementInitialPosition = false; nicholas@2498: this.enableElementListenTracker = false; nicholas@2498: this.enableElementTimer = false; nicholas@2498: this.enableElementTracker = false; nicholas@2498: this.enableFlagListenedTo = false; nicholas@2498: this.enableFlagMoved = false; nicholas@2498: this.enableTestTimer = false; nicholas@2498: // Obtain the metrics enabled nicholas@2498: for (var i = 0; i < specification.metrics.enabled.length; i++) { nicholas@2498: var node = specification.metrics.enabled[i]; nicholas@2498: switch (node) { nicholas@2498: case 'testTimer': nicholas@2498: this.enableTestTimer = true; nicholas@2498: break; nicholas@2498: case 'elementTimer': nicholas@2498: this.enableElementTimer = true; nicholas@2498: break; nicholas@2498: case 'elementTracker': nicholas@2498: this.enableElementTracker = true; nicholas@2498: break; nicholas@2498: case 'elementListenTracker': nicholas@2498: this.enableElementListenTracker = true; nicholas@2498: break; nicholas@2498: case 'elementInitialPosition': nicholas@2498: this.enableElementInitialPosition = true; nicholas@2498: break; nicholas@2498: case 'elementFlagListenedTo': nicholas@2498: this.enableFlagListenedTo = true; nicholas@2498: break; nicholas@2498: case 'elementFlagMoved': nicholas@2498: this.enableFlagMoved = true; nicholas@2498: break; nicholas@2498: case 'elementFlagComments': nicholas@2498: this.enableFlagComments = true; nicholas@2498: break; nicholas@2498: } nicholas@2498: } nicholas@2498: this.initialiseTest = function () {}; nicholas@2224: } nicholas@2224: nicholas@2498: function metricTracker(caller) { nicholas@2498: /* Custom object to track and collect metric data nicholas@2498: * Used only inside the audioObjects object. nicholas@2498: */ nicholas@2498: nicholas@2498: this.listenedTimer = 0; nicholas@2498: this.listenStart = 0; nicholas@2498: this.listenHold = false; nicholas@2498: this.initialPosition = -1; nicholas@2498: this.movementTracker = []; nicholas@2498: this.listenTracker = []; nicholas@2498: this.wasListenedTo = false; nicholas@2498: this.wasMoved = false; nicholas@2498: this.hasComments = false; nicholas@2498: this.parent = caller; nicholas@2498: nicholas@2498: this.initialise = function (position) { nicholas@2498: if (this.initialPosition == -1) { nicholas@2498: this.initialPosition = position; nicholas@2498: this.moved(0, position); nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.moved = function (time, position) { nicholas@3064: var last; nicholas@2498: if (time > 0) { nicholas@2498: this.wasMoved = true; nicholas@2498: } nicholas@3064: // Get the last entry nicholas@3064: if (this.movementTracker.length > 0) { nicholas@3064: last = this.movementTracker[this.movementTracker.length - 1]; nicholas@3064: } else { nicholas@3064: last = -1; nicholas@3064: } nicholas@3064: if (position != last[1]) { nicholas@3064: this.movementTracker[this.movementTracker.length] = [time, position]; nicholas@3064: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.startListening = function (time) { nicholas@2708: if (this.listenHold === false) { nicholas@2498: this.wasListenedTo = true; nicholas@2498: this.listenStart = time; nicholas@2498: this.listenHold = true; nicholas@2498: nicholas@2498: var evnt = document.createElement('event'); nicholas@2498: var testTime = document.createElement('testTime'); nicholas@2498: testTime.setAttribute('start', time); nicholas@2498: var bufferTime = document.createElement('bufferTime'); nicholas@2498: bufferTime.setAttribute('start', this.parent.getCurrentPosition()); nicholas@2498: evnt.appendChild(testTime); nicholas@2498: evnt.appendChild(bufferTime); nicholas@2498: this.listenTracker.push(evnt); nicholas@2498: nicholas@2498: console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.stopListening = function (time, bufferStopTime) { nicholas@2708: if (this.listenHold === true) { nicholas@2498: var diff = time - this.listenStart; nicholas@2498: this.listenedTimer += (diff); nicholas@2498: this.listenStart = 0; nicholas@2498: this.listenHold = false; nicholas@2498: nicholas@2498: var evnt = this.listenTracker[this.listenTracker.length - 1]; nicholas@2498: var testTime = evnt.getElementsByTagName('testTime')[0]; nicholas@2498: var bufferTime = evnt.getElementsByTagName('bufferTime')[0]; nicholas@2498: testTime.setAttribute('stop', time); nicholas@2708: if (bufferStopTime === undefined) { nicholas@2498: bufferTime.setAttribute('stop', this.parent.getCurrentPosition()); nicholas@2498: } else { nicholas@2498: bufferTime.setAttribute('stop', bufferStopTime); nicholas@2498: } nicholas@2498: console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2708: function exportElementTimer(parentElement) { nicholas@2708: var mElementTimer = storage.document.createElement('metricresult'); nicholas@2708: mElementTimer.setAttribute('name', 'enableElementTimer'); nicholas@2708: mElementTimer.textContent = this.listenedTimer; nicholas@2708: parentElement.appendChild(mElementTimer); nicholas@2708: return mElementTimer; nicholas@2708: } nicholas@2708: nicholas@2708: function exportElementTrack(parentElement) { nicholas@2708: var elementTrackerFull = storage.document.createElement('metricresult'); nicholas@2708: elementTrackerFull.setAttribute('name', 'elementTrackerFull'); nicholas@2708: for (var k = 0; k < this.movementTracker.length; k++) { nicholas@2708: var timePos = storage.document.createElement('movement'); nicholas@2708: timePos.setAttribute("time", this.movementTracker[k][0]); nicholas@2708: timePos.setAttribute("value", this.movementTracker[k][1]); nicholas@2708: elementTrackerFull.appendChild(timePos); nicholas@2708: } nicholas@2708: parentElement.appendChild(elementTrackerFull); nicholas@2708: return elementTrackerFull; nicholas@2708: } nicholas@2708: nicholas@2708: function exportElementListenTracker(parentElement) { nicholas@2708: var elementListenTracker = storage.document.createElement('metricresult'); nicholas@2708: elementListenTracker.setAttribute('name', 'elementListenTracker'); nicholas@2708: for (var k = 0; k < this.listenTracker.length; k++) { nicholas@2708: elementListenTracker.appendChild(this.listenTracker[k]); nicholas@2708: } nicholas@2708: parentElement.appendChild(elementListenTracker); nicholas@2708: return elementListenTracker; nicholas@2708: } nicholas@2708: nicholas@2708: function exportElementInitialPosition(parentElement) { nicholas@2708: var elementInitial = storage.document.createElement('metricresult'); nicholas@2708: elementInitial.setAttribute('name', 'elementInitialPosition'); nicholas@2708: elementInitial.textContent = this.initialPosition; nicholas@2708: parentElement.appendChild(elementInitial); nicholas@2708: return elementInitial; nicholas@2708: } nicholas@2708: nicholas@2708: function exportFlagListenedTo(parentElement) { nicholas@2708: var flagListenedTo = storage.document.createElement('metricresult'); nicholas@2708: flagListenedTo.setAttribute('name', 'elementFlagListenedTo'); nicholas@2708: flagListenedTo.textContent = this.wasListenedTo; nicholas@2708: parentElement.appendChild(flagListenedTo); nicholas@2708: return flagListenedTo; nicholas@2708: } nicholas@2708: nicholas@2708: function exportFlagMoved(parentElement) { nicholas@2708: var flagMoved = storage.document.createElement('metricresult'); nicholas@2708: flagMoved.setAttribute('name', 'elementFlagMoved'); nicholas@2708: flagMoved.textContent = this.wasMoved; nicholas@2708: parentElement.appendChild(flagMoved); nicholas@2708: return flagMoved; nicholas@2708: } nicholas@2708: nicholas@2708: function exportFlagComments(parentElement) { nicholas@2708: var flagComments = storage.document.createElement('metricresult'); nicholas@2708: flagComments.setAttribute('name', 'elementFlagComments'); nicholas@2708: if (this.parent.commentDOM === null) { nicholas@2708: flagComments.textContent = 'false'; nicholas@2708: } else if (this.parent.commentDOM.textContent.length === 0) { nicholas@2708: flagComments.textContent = 'false'; nicholas@2708: } else { nicholas@2708: flagComments.textContet = 'true'; nicholas@2708: } nicholas@2708: parentElement.appendChild(flagComments); nicholas@2708: return flagComments; nicholas@2708: } nicholas@2708: nicholas@2708: this.exportXMLDOM = function (parentElement) { nicholas@2708: var elems = []; nicholas@2498: if (audioEngineContext.metric.enableElementTimer) { nicholas@2708: elems.push(exportElementTimer.call(this, parentElement)); nicholas@2498: } nicholas@2498: if (audioEngineContext.metric.enableElementTracker) { nicholas@2708: elems.push(exportElementTrack.call(this, parentElement)); nicholas@2498: } nicholas@2498: if (audioEngineContext.metric.enableElementListenTracker) { nicholas@2708: elems.push(exportElementListenTracker.call(this, parentElement)); nicholas@2498: } nicholas@2498: if (audioEngineContext.metric.enableElementInitialPosition) { nicholas@2708: elems.push(exportElementInitialPosition.call(this, parentElement)); nicholas@2498: } nicholas@2498: if (audioEngineContext.metric.enableFlagListenedTo) { nicholas@2708: elems.push(exportFlagListenedTo.call(this, parentElement)); nicholas@2498: } nicholas@2498: if (audioEngineContext.metric.enableFlagMoved) { nicholas@2708: elems.push(exportFlagMoved.call(this, parentElement)); nicholas@2498: } nicholas@2498: if (audioEngineContext.metric.enableFlagComments) { nicholas@2708: elems.push(exportFlagComments.call(this, parentElement)); nicholas@2498: } nicholas@2708: return elems; nicholas@2498: }; nicholas@2224: } nicholas@2498: nicholas@2224: function Interface(specificationObject) { nicholas@2498: // This handles the bindings between the interface and the audioEngineContext; nicholas@2498: this.specification = specificationObject; nicholas@2498: this.insertPoint = document.getElementById("topLevelBody"); nicholas@2498: nicholas@2498: this.newPage = function (audioHolderObject, store) { nicholas@2498: audioEngineContext.newTestPage(audioHolderObject, store); nicholas@2498: interfaceContext.commentBoxes.deleteCommentBoxes(); nicholas@2498: interfaceContext.deleteCommentQuestions(); nicholas@2498: loadTest(audioHolderObject, store); nicholas@2498: }; nicholas@2498: nicholas@2955: this.keyboardInterface = (function () { nicholas@2955: var keyboardInterfaceController = { nicholas@2955: keys: [], nicholas@2955: registerKeyBinding: function (key, audioObject) { nicholas@2955: if (typeof key != "string" || key.length != 1) { nicholas@2955: throw ("Key must be a singular character"); nicholas@2955: } nicholas@2955: var included = this.keys.findIndex(function (k) { n@2979: return k.key == key; nicholas@2955: }) >= 0; nicholas@2955: if (included) { nicholas@2955: throw ("Key " + key + " already bounded!"); nicholas@2955: } nicholas@2955: this.keys.push({ nicholas@2955: key: key, nicholas@2955: audioObject: audioObject nicholas@2955: }); nicholas@2955: return true; nicholas@2955: }, nicholas@2955: deregisterKeyBinding: function (key) { nicholas@2955: var index = this.keys.findIndex(function (k) { nicholas@2955: return k.key == key; nicholas@2955: }); nicholas@2955: if (index == -1) { nicholas@2955: throw ("Key " + key + " not bounded!"); nicholas@2955: } nicholas@2955: this.keys.splice(index, 1); nicholas@2955: return true; nicholas@2955: }, nicholas@2955: resetKeyBindings: function () { nicholas@2955: this.keys = []; nicholas@2955: }, nicholas@2955: handleEvent: function (e) { nicholas@2955: function isPlaying() { nicholas@2955: return audioEngineContext.audioObjects.some(function (a) { nicholas@2955: return a.playing; nicholas@2955: }); nicholas@2955: } nicholas@2955: nicholas@2955: function keypress(key) { nicholas@2955: var index = this.keys.findIndex(function (k) { n@2979: return k.key == key; nicholas@2955: }); nicholas@2955: if (index >= 0) { nicholas@2955: audioEngineContext.play(this.keys[index].audioObject.id); nicholas@2955: } nicholas@2955: } n@2965: n@2965: function trackCommentFocus() { n@2965: return document.activeElement.className.indexOf("trackComment") >= 0; n@2965: } nicholas@3119: if (testState.currentStatePosition != "test") { nicholas@3119: return; nicholas@3119: } nicholas@2982: if (trackCommentFocus()) { nicholas@2982: return; nicholas@2982: } nicholas@2955: if (e.key === " ") { nicholas@2982: if (isPlaying()) { n@2965: e.preventDefault(); nicholas@2955: audioEngineContext.stop(); nicholas@2955: } nicholas@2955: } else { nicholas@2955: keypress.call(this, e.key); nicholas@2955: } nicholas@2955: } nicholas@2955: }; nicholas@2955: document.addEventListener("keydown", keyboardInterfaceController, false); nicholas@2955: return keyboardInterfaceController; nicholas@2955: })(); nicholas@2955: nicholas@2498: // Bounded by interface!! nicholas@2498: // Interface object MUST have an exportXMLDOM method which returns the various DOM levels nicholas@2498: // For example, APE returns the slider position normalised in a tag. nicholas@2498: this.interfaceObjects = []; nicholas@2498: this.interfaceObject = function () {}; nicholas@2498: nicholas@2498: this.resizeWindow = function (event) { nicholas@2498: popup.resize(event); nicholas@2352: this.volume.resize(); nicholas@2360: this.lightbox.resize(); n@2718: this.commentBoxes.boxes.forEach(function (elem) { nicholas@2708: elem.resize(); nicholas@2708: }); nicholas@2708: this.commentQuestions.forEach(function (elem) { nicholas@2708: elem.resize(); nicholas@2708: }); nicholas@2498: try { nicholas@2498: resizeWindow(event); nicholas@2498: } catch (err) { nicholas@2498: console.log("Warning - Interface does not have Resize option"); nicholas@2498: console.log(err); nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2498: this.returnNavigator = function () { nicholas@2498: var node = storage.document.createElement("navigator"); nicholas@2498: var platform = storage.document.createElement("platform"); nicholas@2498: platform.textContent = navigator.platform; nicholas@2498: var vendor = storage.document.createElement("vendor"); nicholas@2498: vendor.textContent = navigator.vendor; nicholas@2498: var userAgent = storage.document.createElement("uagent"); nicholas@2498: userAgent.textContent = navigator.userAgent; nicholas@2224: var screen = storage.document.createElement("window"); nicholas@2498: screen.setAttribute('innerWidth', window.innerWidth); nicholas@2498: screen.setAttribute('innerHeight', window.innerHeight); nicholas@2498: node.appendChild(platform); nicholas@2498: node.appendChild(vendor); nicholas@2498: node.appendChild(userAgent); nicholas@2224: node.appendChild(screen); nicholas@2498: return node; nicholas@2498: }; nicholas@2498: nicholas@2498: this.returnDateNode = function () { nicholas@2224: // Create an XML Node for the Date and Time a test was conducted nicholas@2224: // Structure is nicholas@3113: // nicholas@2224: // DD/MM/YY nicholas@2224: // nicholas@2224: // nicholas@2224: var dateTime = new Date(); nicholas@2224: var hold = storage.document.createElement("datetime"); nicholas@2224: var date = storage.document.createElement("date"); nicholas@2224: var time = storage.document.createElement("time"); nicholas@2498: date.setAttribute('year', dateTime.getFullYear()); nicholas@2498: date.setAttribute('month', dateTime.getMonth() + 1); nicholas@2498: date.setAttribute('day', dateTime.getDate()); nicholas@2498: time.setAttribute('hour', dateTime.getHours()); nicholas@2498: time.setAttribute('minute', dateTime.getMinutes()); nicholas@2498: time.setAttribute('secs', dateTime.getSeconds()); nicholas@2498: nicholas@2224: hold.appendChild(date); nicholas@2224: hold.appendChild(time); nicholas@2224: return hold; nicholas@2224: nicholas@2708: }; nicholas@2498: nicholas@2360: this.lightbox = { nicholas@2360: parent: this, nicholas@2360: root: document.createElement("div"), nicholas@2360: content: document.createElement("div"), nicholas@2360: accept: document.createElement("button"), nicholas@2360: blanker: document.createElement("div"), nicholas@2498: post: function (type, message) { nicholas@2498: switch (type) { nicholas@2360: case "Error": nicholas@2360: this.content.className = "lightbox-error"; nicholas@2360: break; nicholas@2360: case "Warning": nicholas@2360: this.content.className = "lightbox-warning"; nicholas@2360: break; nicholas@2360: default: nicholas@2360: this.content.className = "lightbox-message"; nicholas@2360: break; nicholas@2360: } nicholas@2360: var msg = document.createElement("p"); nicholas@2360: msg.textContent = message; nicholas@2360: this.content.appendChild(msg); nicholas@2360: this.show(); nicholas@2360: }, nicholas@2498: show: function () { nicholas@2360: this.root.style.visibility = "visible"; nicholas@2360: this.blanker.style.visibility = "visible"; n@2914: this.accept.focus(); nicholas@2360: }, nicholas@2498: clear: function () { nicholas@2360: this.root.style.visibility = ""; nicholas@2360: this.blanker.style.visibility = ""; nicholas@2360: this.content.textContent = ""; nicholas@2360: }, nicholas@2498: handleEvent: function (event) { nicholas@2360: if (event.currentTarget == this.accept) { nicholas@2360: this.clear(); nicholas@2360: } nicholas@2360: }, nicholas@2498: resize: function (event) { nicholas@2498: this.root.style.left = (window.innerWidth / 2) - 250 + 'px'; n@2915: }, n@2915: isVisible: function () { n@2915: return this.root.style.visibility == "visible"; nicholas@2360: } nicholas@2708: }; nicholas@2498: nicholas@2360: this.lightbox.root.appendChild(this.lightbox.content); nicholas@2360: this.lightbox.root.appendChild(this.lightbox.accept); nicholas@2360: this.lightbox.root.className = "popupHolder"; nicholas@2360: this.lightbox.root.id = "lightbox-root"; nicholas@2360: this.lightbox.accept.className = "popupButton"; nicholas@2360: this.lightbox.accept.style.bottom = "10px"; nicholas@2360: this.lightbox.accept.textContent = "OK"; nicholas@2360: this.lightbox.accept.style.left = "237.5px"; nicholas@2498: this.lightbox.accept.addEventListener("click", this.lightbox); nicholas@2360: this.lightbox.blanker.className = "testHalt"; nicholas@2360: this.lightbox.blanker.id = "lightbox-blanker"; nicholas@2360: document.getElementsByTagName("body")[0].appendChild(this.lightbox.root); nicholas@2360: document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker); nicholas@2498: nicholas@2712: this.commentBoxes = (function () { nicholas@2712: var commentBoxes = {}; nicholas@2712: commentBoxes.boxes = []; nicholas@2712: commentBoxes.injectPoint = null; nicholas@2712: commentBoxes.elementCommentBox = function (audioObject) { nicholas@2224: var element = audioObject.specification; nicholas@2224: this.audioObject = audioObject; nicholas@2224: this.id = audioObject.id; nicholas@2224: var audioHolderObject = audioObject.specification.parent; nicholas@2224: // Create document objects to hold the comment boxes nicholas@2224: this.trackComment = document.createElement('div'); nicholas@2224: this.trackComment.className = 'comment-div'; nicholas@2498: this.trackComment.id = 'comment-div-' + audioObject.id; nicholas@2224: // Create a string next to each comment asking for a comment nicholas@2224: this.trackString = document.createElement('span'); nicholas@2498: this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId(); nicholas@2224: // Create the HTML5 comment box 'textarea' nicholas@2224: this.trackCommentBox = document.createElement('textarea'); nicholas@2224: this.trackCommentBox.rows = '4'; nicholas@2224: this.trackCommentBox.cols = '100'; nicholas@2498: this.trackCommentBox.name = 'trackComment' + audioObject.id; nicholas@2224: this.trackCommentBox.className = 'trackComment'; nicholas@2224: var br = document.createElement('br'); nicholas@2224: // Add to the holder. nicholas@2224: this.trackComment.appendChild(this.trackString); nicholas@2224: this.trackComment.appendChild(br); nicholas@2224: this.trackComment.appendChild(this.trackCommentBox); nicholas@2224: nicholas@2498: this.exportXMLDOM = function () { nicholas@2224: var root = document.createElement('comment'); nicholas@2224: var question = document.createElement('question'); nicholas@2224: question.textContent = this.trackString.textContent; nicholas@2224: var response = document.createElement('response'); nicholas@2224: response.textContent = this.trackCommentBox.value; nicholas@2498: console.log("Comment frag-" + this.id + ": " + response.textContent); nicholas@2224: root.appendChild(question); nicholas@2224: root.appendChild(response); nicholas@2224: return root; nicholas@2224: }; nicholas@2498: this.resize = function () { nicholas@2498: var boxwidth = (window.innerWidth - 100) / 2; nicholas@2498: if (boxwidth >= 600) { nicholas@2224: boxwidth = 600; nicholas@2498: } else if (boxwidth < 400) { nicholas@2224: boxwidth = 400; nicholas@2224: } nicholas@2498: this.trackComment.style.width = boxwidth + "px"; nicholas@2498: this.trackCommentBox.style.width = boxwidth - 6 + "px"; nicholas@2224: }; nicholas@2224: this.resize(); nicholas@2725: this.highlight = function (state) { nicholas@2725: if (state === true) { nicholas@2725: $(this.trackComment).addClass("comment-box-playing"); nicholas@2725: } else { nicholas@2725: $(this.trackComment).removeClass("comment-box-playing"); nicholas@2725: } nicholas@2725: }; nicholas@2224: }; nicholas@2712: commentBoxes.createCommentBox = function (audioObject) { nicholas@2224: var node = new this.elementCommentBox(audioObject); nicholas@2224: this.boxes.push(node); nicholas@2224: audioObject.commentDOM = node; nicholas@2224: return node; nicholas@2224: }; nicholas@2712: commentBoxes.sortCommentBoxes = function () { nicholas@2498: this.boxes.sort(function (a, b) { nicholas@2498: return a.id - b.id; nicholas@2498: }); nicholas@2224: }; nicholas@2224: nicholas@2712: commentBoxes.showCommentBoxes = function (inject, sort) { nicholas@2224: this.injectPoint = inject; nicholas@2498: if (sort) { nicholas@2498: this.sortCommentBoxes(); nicholas@2498: } nicholas@2708: this.boxes.forEach(function (box) { nicholas@2224: inject.appendChild(box.trackComment); nicholas@2708: }); nicholas@2224: }; nicholas@2224: nicholas@2712: commentBoxes.deleteCommentBoxes = function () { nicholas@2708: if (this.injectPoint !== null) { nicholas@2708: this.boxes.forEach(function (box) { nicholas@2224: this.injectPoint.removeChild(box.trackComment); nicholas@2708: }, this); nicholas@2224: this.injectPoint = null; nicholas@2224: } nicholas@2224: this.boxes = []; nicholas@2224: }; nicholas@2725: commentBoxes.highlightById = function (id) { nicholas@2725: if (id === undefined || typeof id !== "number" || id >= this.boxes.length) { nicholas@2725: console.log("Error - Invalid id"); nicholas@2725: id = -1; nicholas@2725: } nicholas@2725: this.boxes.forEach(function (a) { nicholas@2725: if (a.id === id) { nicholas@2725: a.highlight(true); nicholas@2725: } else { nicholas@2725: a.highlight(false); nicholas@2725: } nicholas@2725: }); nicholas@2725: }; nicholas@2712: return commentBoxes; nicholas@2712: })(); nicholas@2498: nicholas@2498: this.commentQuestions = []; nicholas@2498: nicholas@2498: this.commentBox = function (commentQuestion) { nicholas@2498: this.specification = commentQuestion; nicholas@2498: // Create document objects to hold the comment boxes nicholas@2498: this.holder = document.createElement('div'); nicholas@2498: this.holder.className = 'comment-div'; nicholas@2498: // Create a string next to each comment asking for a comment nicholas@2498: this.string = document.createElement('span'); nicholas@2498: this.string.innerHTML = commentQuestion.statement; nicholas@2498: // Create the HTML5 comment box 'textarea' nicholas@2498: this.textArea = document.createElement('textarea'); nicholas@2498: this.textArea.rows = '4'; nicholas@2498: this.textArea.cols = '100'; nicholas@2498: this.textArea.className = 'trackComment'; nicholas@2498: var br = document.createElement('br'); nicholas@2498: // Add to the holder. nicholas@2498: this.holder.appendChild(this.string); nicholas@2498: this.holder.appendChild(br); nicholas@2498: this.holder.appendChild(this.textArea); nicholas@2498: nicholas@2498: this.exportXMLDOM = function (storePoint) { nicholas@2498: var root = storePoint.parent.document.createElement('comment'); nicholas@2498: root.id = this.specification.id; nicholas@2498: root.setAttribute('type', this.specification.type); nicholas@2498: console.log("Question: " + this.string.textContent); nicholas@2498: console.log("Response: " + root.textContent); nicholas@2224: var question = storePoint.parent.document.createElement('question'); nicholas@2224: question.textContent = this.string.textContent; nicholas@2224: var response = storePoint.parent.document.createElement('response'); nicholas@2224: response.textContent = this.textArea.value; nicholas@2224: root.appendChild(question); nicholas@2224: root.appendChild(response); nicholas@2224: storePoint.XMLDOM.appendChild(root); nicholas@2498: return root; nicholas@2498: }; nicholas@2498: this.resize = function () { nicholas@2498: var boxwidth = (window.innerWidth - 100) / 2; nicholas@2498: if (boxwidth >= 600) { nicholas@2498: boxwidth = 600; nicholas@2498: } else if (boxwidth < 400) { nicholas@2498: boxwidth = 400; nicholas@2498: } nicholas@2498: this.holder.style.width = boxwidth + "px"; nicholas@2498: this.textArea.style.width = boxwidth - 6 + "px"; nicholas@2498: }; nicholas@2498: this.resize(); nicholas@3063: this.check = function () { n@3095: if (this.specification.mandatory && this.textArea.value.length === 0) { nicholas@3063: return false; nicholas@3063: } nicholas@3063: return true; n@3095: }; nicholas@2498: }; nicholas@2498: nicholas@2498: this.radioBox = function (commentQuestion) { nicholas@2498: this.specification = commentQuestion; nicholas@2498: // Create document objects to hold the comment boxes nicholas@2498: this.holder = document.createElement('div'); nicholas@2498: this.holder.className = 'comment-div'; nicholas@2498: // Create a string next to each comment asking for a comment nicholas@2498: this.string = document.createElement('span'); nicholas@2498: this.string.innerHTML = commentQuestion.statement; nicholas@2498: // Add to the holder. nicholas@2498: this.holder.appendChild(this.string); nicholas@2498: this.options = []; nicholas@2498: this.inputs = document.createElement('div'); nicholas@2711: this.inputs.className = "comment-checkbox-inputs-holder"; nicholas@2498: nicholas@2498: var optCount = commentQuestion.options.length; nicholas@2711: for (var i = 0; i < optCount; i++) { nicholas@2498: var div = document.createElement('div'); nicholas@2711: div.className = "comment-checkbox-inputs-flex"; nicholas@2722: nicholas@2711: var span = document.createElement('span'); nicholas@2711: span.textContent = commentQuestion.options[i].text; nicholas@2711: span.className = 'comment-radio-span'; nicholas@2711: div.appendChild(span); nicholas@2722: nicholas@2498: var input = document.createElement('input'); nicholas@2498: input.type = 'radio'; nicholas@2498: input.name = commentQuestion.id; nicholas@2711: input.setAttribute('setvalue', commentQuestion.options[i].name); nicholas@2498: input.className = 'comment-radio'; nicholas@2498: div.appendChild(input); nicholas@2722: nicholas@2498: this.inputs.appendChild(div); nicholas@2498: this.options.push(input); nicholas@2498: } nicholas@2498: this.holder.appendChild(this.inputs); nicholas@2498: nicholas@2498: this.exportXMLDOM = function (storePoint) { nicholas@2498: var root = storePoint.parent.document.createElement('comment'); nicholas@2498: root.id = this.specification.id; nicholas@2498: root.setAttribute('type', this.specification.type); nicholas@2498: var question = document.createElement('question'); nicholas@2498: question.textContent = this.string.textContent; nicholas@2498: var response = document.createElement('response'); nicholas@2498: var i = 0; nicholas@2708: while (this.options[i].checked === false) { nicholas@2498: i++; nicholas@2498: if (i >= this.options.length) { nicholas@2498: break; nicholas@2498: } nicholas@2498: } nicholas@2498: if (i >= this.options.length) { nicholas@2498: response.textContent = 'null'; nicholas@2498: } else { nicholas@2498: response.textContent = this.options[i].getAttribute('setvalue'); nicholas@2498: response.setAttribute('number', i); nicholas@2498: } nicholas@2498: console.log('Comment: ' + question.textContent); nicholas@2498: console.log('Response: ' + response.textContent); nicholas@2498: root.appendChild(question); nicholas@2498: root.appendChild(response); nicholas@2224: storePoint.XMLDOM.appendChild(root); nicholas@2498: return root; nicholas@2498: }; nicholas@2498: this.resize = function () { nicholas@2498: var boxwidth = (window.innerWidth - 100) / 2; nicholas@2498: if (boxwidth >= 600) { nicholas@2498: boxwidth = 600; nicholas@2498: } else if (boxwidth < 400) { nicholas@2498: boxwidth = 400; nicholas@2498: } nicholas@2498: this.holder.style.width = boxwidth + "px"; nicholas@2498: }; nicholas@3063: this.check = function () { nicholas@3063: var anyChecked = this.options.some(function (a) { nicholas@3063: return a.checked; nicholas@3063: }); n@3095: if (this.specification.mandatory && anyChecked === false) { nicholas@3063: return false; nicholas@3063: } nicholas@3063: return true; n@3095: }; nicholas@2498: this.resize(); nicholas@2498: }; nicholas@2498: nicholas@2498: this.checkboxBox = function (commentQuestion) { nicholas@2498: this.specification = commentQuestion; nicholas@2498: // Create document objects to hold the comment boxes nicholas@2498: this.holder = document.createElement('div'); nicholas@2498: this.holder.className = 'comment-div'; nicholas@2498: // Create a string next to each comment asking for a comment nicholas@2498: this.string = document.createElement('span'); nicholas@2498: this.string.innerHTML = commentQuestion.statement; nicholas@2498: // Add to the holder. nicholas@2498: this.holder.appendChild(this.string); nicholas@2498: this.options = []; nicholas@2498: this.inputs = document.createElement('div'); nicholas@2294: this.inputs.className = "comment-checkbox-inputs-holder"; nicholas@2498: nicholas@2498: var optCount = commentQuestion.options.length; nicholas@2498: for (var i = 0; i < optCount; i++) { nicholas@2498: var div = document.createElement('div'); nicholas@2711: div.className = "comment-checkbox-inputs-flex"; nicholas@2722: nicholas@2711: var span = document.createElement('span'); nicholas@2711: span.textContent = commentQuestion.options[i].text; nicholas@2711: span.className = 'comment-radio-span'; nicholas@2711: div.appendChild(span); nicholas@2722: nicholas@2498: var input = document.createElement('input'); nicholas@2498: input.type = 'checkbox'; nicholas@2498: input.name = commentQuestion.id; nicholas@2498: input.setAttribute('setvalue', commentQuestion.options[i].name); nicholas@2498: input.className = 'comment-radio'; nicholas@2498: div.appendChild(input); nicholas@2722: nicholas@2498: this.inputs.appendChild(div); nicholas@2498: this.options.push(input); nicholas@2498: } nicholas@2498: this.holder.appendChild(this.inputs); nicholas@2498: nicholas@2498: this.exportXMLDOM = function (storePoint) { nicholas@2498: var root = storePoint.parent.document.createElement('comment'); nicholas@2498: root.id = this.specification.id; nicholas@2498: root.setAttribute('type', this.specification.type); nicholas@2498: var question = document.createElement('question'); nicholas@2498: question.textContent = this.string.textContent; nicholas@2498: root.appendChild(question); nicholas@2498: console.log('Comment: ' + question.textContent); nicholas@2498: for (var i = 0; i < this.options.length; i++) { nicholas@2498: var response = document.createElement('response'); nicholas@2498: response.textContent = this.options[i].checked; nicholas@2498: response.setAttribute('name', this.options[i].getAttribute('setvalue')); nicholas@2498: root.appendChild(response); nicholas@2498: console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent); nicholas@2498: } nicholas@2224: storePoint.XMLDOM.appendChild(root); nicholas@2498: return root; nicholas@2498: }; nicholas@2498: this.resize = function () { nicholas@2498: var boxwidth = (window.innerWidth - 100) / 2; nicholas@2498: if (boxwidth >= 600) { nicholas@2498: boxwidth = 600; nicholas@2498: } else if (boxwidth < 400) { nicholas@2498: boxwidth = 400; nicholas@2498: } nicholas@2498: this.holder.style.width = boxwidth + "px"; nicholas@2498: }; nicholas@3063: this.check = function () { nicholas@3063: var anyChecked = this.options.some(function (a) { nicholas@3063: return a.checked; nicholas@3063: }); n@3095: if (this.specification.mandatory && anyChecked === false) { nicholas@3063: return false; nicholas@3063: } nicholas@3063: return true; nicholas@3063: }; nicholas@2498: this.resize(); nicholas@2498: }; nicholas@2498: n@2579: this.sliderBox = function (commentQuestion) { n@2579: this.specification = commentQuestion; n@2579: this.holder = document.createElement("div"); n@2579: this.holder.className = 'comment-div'; n@2579: this.string = document.createElement("span"); n@2579: this.string.innerHTML = commentQuestion.statement; n@2579: this.slider = document.createElement("input"); n@2579: this.slider.type = "range"; n@2579: this.slider.min = commentQuestion.min; n@2579: this.slider.max = commentQuestion.max; n@2579: this.slider.step = commentQuestion.step; n@2579: this.slider.value = commentQuestion.value; n@2579: var br = document.createElement('br'); n@2579: n@2580: var textHolder = document.createElement("div"); n@2580: textHolder.className = "comment-slider-text-holder"; n@2580: n@2580: this.leftText = document.createElement("span"); n@2580: this.leftText.textContent = commentQuestion.leftText; n@2580: this.rightText = document.createElement("span"); n@2580: this.rightText.textContent = commentQuestion.rightText; n@2580: textHolder.appendChild(this.leftText); n@2580: textHolder.appendChild(this.rightText); n@2580: n@2579: this.holder.appendChild(this.string); n@2579: this.holder.appendChild(br); n@2579: this.holder.appendChild(this.slider); n@2580: this.holder.appendChild(textHolder); n@2579: n@2579: this.exportXMLDOM = function (storePoint) { n@2579: var root = storePoint.parent.document.createElement('comment'); n@2579: root.id = this.specification.id; n@2579: root.setAttribute('type', this.specification.type); n@2579: console.log("Question: " + this.string.textContent); n@2579: console.log("Response: " + this.slider.value); n@2579: var question = storePoint.parent.document.createElement('question'); n@2579: question.textContent = this.string.textContent; n@2579: var response = storePoint.parent.document.createElement('response'); n@2579: response.textContent = this.slider.value; n@2579: root.appendChild(question); n@2579: root.appendChild(response); n@2579: storePoint.XMLDOM.appendChild(root); n@2579: return root; n@2579: }; n@2579: this.resize = function () { n@2579: var boxwidth = (window.innerWidth - 100) / 2; n@2579: if (boxwidth >= 600) { n@2579: boxwidth = 600; n@2579: } else if (boxwidth < 400) { n@2579: boxwidth = 400; n@2579: } n@2579: this.holder.style.width = boxwidth + "px"; n@2579: this.slider.style.width = boxwidth - 24 + "px"; n@2579: }; nicholas@3063: this.check = function () { nicholas@3063: return true; n@3095: }; n@2579: this.resize(); n@2579: }; n@2579: nicholas@2498: this.createCommentQuestion = function (element) { nicholas@2498: var node; nicholas@2498: if (element.type == 'question') { nicholas@2498: node = new this.commentBox(element); nicholas@2498: } else if (element.type == 'radio') { nicholas@2498: node = new this.radioBox(element); nicholas@2498: } else if (element.type == 'checkbox') { nicholas@2498: node = new this.checkboxBox(element); n@2579: } else if (element.type == 'slider') { n@2579: node = new this.sliderBox(element); nicholas@2498: } nicholas@2498: this.commentQuestions.push(node); nicholas@2498: return node; nicholas@2498: }; nicholas@2498: nicholas@2498: this.deleteCommentQuestions = function () { nicholas@2498: this.commentQuestions = []; nicholas@2498: }; nicholas@2498: nicholas@3063: this.checkCommentQuestions = function () { nicholas@3063: var errored = this.commentQuestions.reduce(function (a, cq) { n@3095: if (cq.check() === false) { nicholas@3063: a.push(cq); nicholas@3063: } nicholas@3063: return a; nicholas@3063: }, []); n@3095: if (errored.length === 0) { nicholas@3063: return true; nicholas@3063: } nicholas@3063: interfaceContext.lightbox.post("Message", "Not all the mandatory comment boxes below have been filled."); n@3095: }; nicholas@3063: nicholas@2498: this.outsideReferenceDOM = function (audioObject, index, inject) { nicholas@2224: this.parent = audioObject; nicholas@2224: this.outsideReferenceHolder = document.createElement('button'); nicholas@2224: this.outsideReferenceHolder.className = 'outside-reference'; nicholas@2498: this.outsideReferenceHolder.setAttribute('track-id', index); nicholas@2409: this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference"; nicholas@2224: this.outsideReferenceHolder.disabled = true; nicholas@2708: this.handleEvent = function (event) { nicholas@2708: audioEngineContext.play(this.parent.id); nicholas@2224: }; nicholas@2708: this.outsideReferenceHolder.addEventListener("click", this); nicholas@2224: inject.appendChild(this.outsideReferenceHolder); nicholas@2498: this.enable = function () { nicholas@2498: if (this.parent.state == 1) { nicholas@2224: this.outsideReferenceHolder.disabled = false; nicholas@2224: } nicholas@2224: }; nicholas@2498: this.updateLoading = function (progress) { nicholas@2498: if (progress != 100) { nicholas@2224: progress = String(progress); nicholas@2224: progress = progress.split('.')[0]; nicholas@2498: this.outsideReferenceHolder.textContent = progress + '%'; nicholas@2224: } else { nicholas@2409: this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference"; nicholas@2224: } nicholas@2224: }; nicholas@2498: this.startPlayback = function () { nicholas@2224: // Called when playback has begun nicholas@2224: $('.track-slider').removeClass('track-slider-playing'); nicholas@2224: $('.comment-div').removeClass('comment-box-playing'); nicholas@2224: this.outsideReferenceHolder.style.backgroundColor = "#FDD"; nicholas@2224: }; nicholas@2498: this.stopPlayback = function () { nicholas@2224: // Called when playback has stopped. This gets called even if playback never started! nicholas@2224: this.outsideReferenceHolder.style.backgroundColor = ""; nicholas@2224: }; nicholas@2498: this.exportXMLDOM = function (audioObject) { nicholas@2224: return null; nicholas@2224: }; nicholas@2498: this.getValue = function () { nicholas@2224: return 0; nicholas@2224: }; nicholas@2498: this.getPresentedId = function () { nicholas@2409: return this.parent.specification.label || "Reference"; nicholas@2224: }; nicholas@2498: this.canMove = function () { nicholas@2224: return false; nicholas@2224: }; nicholas@2498: this.error = function () { nicholas@2498: // audioObject has an error!! nicholas@2224: this.outsideReferenceHolder.textContent = "Error"; nicholas@2224: this.outsideReferenceHolder.style.backgroundColor = "#F00"; nicholas@2708: }; nicholas@2708: }; nicholas@2498: nicholas@2712: this.playhead = (function () { nicholas@2722: var playhead = {}; nicholas@2712: playhead.object = document.createElement('div'); nicholas@2712: playhead.object.className = 'playhead'; nicholas@2712: playhead.object.align = 'left'; nicholas@2498: var curTime = document.createElement('div'); nicholas@2498: curTime.style.width = '50px'; nicholas@2712: playhead.curTimeSpan = document.createElement('span'); nicholas@2712: playhead.curTimeSpan.textContent = '00:00'; nicholas@2712: curTime.appendChild(playhead.curTimeSpan); nicholas@2712: playhead.object.appendChild(curTime); nicholas@2712: playhead.scrubberTrack = document.createElement('div'); nicholas@2712: playhead.scrubberTrack.className = 'playhead-scrub-track'; nicholas@2498: nicholas@2712: playhead.scrubberHead = document.createElement('div'); nicholas@2712: playhead.scrubberHead.id = 'playhead-scrubber'; nicholas@2712: playhead.scrubberTrack.appendChild(playhead.scrubberHead); nicholas@2712: playhead.object.appendChild(playhead.scrubberTrack); nicholas@2498: nicholas@2712: playhead.timePerPixel = 0; nicholas@2712: playhead.maxTime = 0; nicholas@2498: nicholas@2712: playhead.playbackObject = undefined; nicholas@2498: nicholas@2712: playhead.setTimePerPixel = function (audioObject) { nicholas@2498: //maxTime must be in seconds nicholas@2498: this.playbackObject = audioObject; nicholas@2498: this.maxTime = audioObject.buffer.buffer.duration; nicholas@2498: var width = 490; //500 - 10, 5 each side of the tracker head nicholas@2498: this.timePerPixel = this.maxTime / 490; nicholas@2498: if (this.maxTime < 60) { nicholas@2498: this.curTimeSpan.textContent = '0.00'; nicholas@2498: } else { nicholas@2498: this.curTimeSpan.textContent = '00:00'; nicholas@2498: } nicholas@2498: }; nicholas@2498: nicholas@2712: playhead.update = function () { nicholas@2498: // Update the playhead position, startPlay must be called nicholas@2498: if (this.timePerPixel > 0) { nicholas@2498: var time = this.playbackObject.getCurrentPosition(); nicholas@2498: if (time > 0 && time < this.maxTime) { nicholas@2498: var width = 490; nicholas@2498: var pix = Math.floor(time / this.timePerPixel); nicholas@2498: this.scrubberHead.style.left = pix + 'px'; nicholas@2498: if (this.maxTime > 60.0) { nicholas@2498: var secs = time % 60; nicholas@2498: var mins = Math.floor((time - secs) / 60); nicholas@2498: secs = secs.toString(); nicholas@2498: secs = secs.substr(0, 2); nicholas@2498: mins = mins.toString(); nicholas@2498: this.curTimeSpan.textContent = mins + ':' + secs; nicholas@2498: } else { nicholas@2498: time = time.toString(); nicholas@2498: this.curTimeSpan.textContent = time.substr(0, 4); nicholas@2498: } nicholas@2498: } else { nicholas@2498: this.scrubberHead.style.left = '0px'; nicholas@2498: if (this.maxTime < 60) { nicholas@2498: this.curTimeSpan.textContent = '0.00'; nicholas@2498: } else { nicholas@2498: this.curTimeSpan.textContent = '00:00'; nicholas@2498: } nicholas@2498: } nicholas@2498: } nicholas@2817: if (this.playbackObject !== undefined && this.interval === undefined) { nicholas@2817: window.requestAnimationFrame(this.update.bind(this)); nicholas@2817: } nicholas@2498: }; nicholas@2498: nicholas@2712: playhead.interval = undefined; nicholas@2498: nicholas@2712: playhead.start = function () { nicholas@2708: if (this.playbackObject !== undefined && this.interval === undefined) { nicholas@2817: window.requestAnimationFrame(this.update.bind(this)); nicholas@2498: } nicholas@2498: }; nicholas@2712: playhead.stop = function () { nicholas@2817: this.timePerPixel = 0; nicholas@2498: }; nicholas@2712: return playhead; nicholas@2712: })(); nicholas@2498: nicholas@2712: this.volume = (function () { nicholas@2224: // An in-built volume module which can be viewed on page nicholas@2224: // Includes trackers on page-by-page data nicholas@2224: // Volume does NOT reset to 0dB on each page load nicholas@2712: var volume = {}; nicholas@2712: volume.valueLin = 1.0; nicholas@2712: volume.valueDB = 0.0; nicholas@2712: volume.root = document.createElement('div'); nicholas@2712: volume.root.id = 'master-volume-root'; nicholas@2712: volume.object = document.createElement('div'); nicholas@2712: volume.object.className = 'master-volume-holder-float'; nicholas@2712: volume.object.appendChild(volume.root); nicholas@2712: volume.slider = document.createElement('input'); nicholas@2712: volume.slider.id = 'master-volume-control'; nicholas@2712: volume.slider.type = 'range'; nicholas@2712: volume.valueText = document.createElement('span'); nicholas@2712: volume.valueText.id = 'master-volume-feedback'; nicholas@2712: volume.valueText.textContent = '0dB'; nicholas@2498: nicholas@2712: volume.slider.min = -60; nicholas@2712: volume.slider.max = 12; nicholas@2712: volume.slider.value = 0; nicholas@2712: volume.slider.step = 1; nicholas@2712: volume.handleEvent = function (event) { nicholas@2951: if (event.type == "mousemove" || event.type == "mouseup") { nicholas@2669: this.valueDB = Number(this.slider.value); nicholas@2669: this.valueLin = decibelToLinear(this.valueDB); nicholas@2669: this.valueText.textContent = this.valueDB + 'dB'; nicholas@2669: audioEngineContext.outputGain.gain.value = this.valueLin; nicholas@2951: } nicholas@2951: if (event.type == "mouseup") { nicholas@2669: this.onmouseup(); nicholas@2669: } nicholas@2669: this.slider.value = this.valueDB; nicholas@2669: nicholas@2669: if (event.stopPropagation) { nicholas@2669: event.stopPropagation(); nicholas@2669: } nicholas@2711: }; nicholas@2712: volume.onmouseup = function () { nicholas@2224: var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker'); nicholas@2708: if (storePoint.length === 0) { nicholas@2224: storePoint = storage.document.createElement('metricresult'); nicholas@2498: storePoint.setAttribute('name', 'volumeTracker'); nicholas@2224: testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint); nicholas@2498: } else { nicholas@2224: storePoint = storePoint[0]; nicholas@2224: } nicholas@2224: var node = storage.document.createElement('movement'); nicholas@2498: node.setAttribute('test-time', audioEngineContext.timer.getTestTime()); nicholas@2669: node.setAttribute('volume', this.valueDB); nicholas@2498: node.setAttribute('format', 'dBFS'); nicholas@2224: storePoint.appendChild(node); nicholas@2711: }; nicholas@2712: volume.slider.addEventListener("mousemove", volume); nicholas@2712: volume.root.addEventListener("mouseup", volume); nicholas@2498: nicholas@2224: var title = document.createElement('div'); nicholas@2224: title.innerHTML = 'Master Volume Control'; nicholas@2224: title.style.fontSize = '0.75em'; nicholas@2224: title.style.width = "100%"; nicholas@2224: title.align = 'center'; nicholas@2712: volume.root.appendChild(title); nicholas@2498: nicholas@2712: volume.root.appendChild(volume.slider); nicholas@2712: volume.root.appendChild(volume.valueText); nicholas@2498: nicholas@2712: volume.resize = function (event) { nicholas@2352: if (window.innerWidth < 1000) { nicholas@2708: this.object.className = "master-volume-holder-inline"; nicholas@2352: } else { nicholas@2352: this.object.className = 'master-volume-holder-float'; nicholas@2352: } nicholas@2708: }; nicholas@2712: return volume; nicholas@2712: })(); nicholas@2498: nicholas@2778: this.imageHolder = (function () { nicholas@2778: var imageController = {}; nicholas@2778: imageController.root = document.createElement("div"); nicholas@2778: imageController.root.id = "imageController"; nicholas@2778: imageController.img = document.createElement("img"); nicholas@2778: imageController.root.appendChild(imageController.img); nicholas@2778: imageController.setImage = function (src) { nicholas@2778: imageController.img.src = ""; n@2785: if (typeof src !== "string" || src.length === undefined) { nicholas@2778: return; nicholas@2778: } nicholas@2778: imageController.img.src = src; n@2785: }; nicholas@2778: return imageController; nicholas@2778: })(); nicholas@2778: nicholas@3101: this.calibrationTests = (function () { nicholas@3101: function readonly(t) { nicholas@3101: throw ("Cannot set read-only variable"); nicholas@3101: } nicholas@3101: nicholas@3101: function getStorageRoot() { nicholas@3101: var storageRoot = storage.root.querySelector("calibration"); nicholas@3101: if (storageRoot === undefined) { nicholas@3101: storageRoot = storage.document.createElement("calibration"); nicholas@3101: storage.root.appendChild(storageRoot); nicholas@3101: } nicholas@3101: return storageRoot; nicholas@3101: } n@3102: var calibrationObject, nicholas@3101: _checkedFrequency = false, nicholas@3101: _checkedChannels = false; nicholas@3101: nicholas@3101: // Define the checkFrequencies test! nicholas@3101: var checkFrequencyUnit = function (htmlRoot, storageRoot) { nicholas@3101: nicholas@3101: function createFrequencyElement(frequency) { nicholas@3101: return (function (frequency) { n@3102: var hold = document.createElement("div"); n@3102: hold.className = "calibration-slider"; nicholas@3101: var range = document.createElement("input"); nicholas@3101: range.type = "range"; nicholas@3101: range.min = "-24"; nicholas@3101: range.max = "24"; nicholas@3101: range.step = "0.5"; nicholas@3101: range.setAttribute("orient", "vertical"); nicholas@3101: range.value = (Math.random() - 0.5) * 24; nicholas@3101: range.setAttribute("frequency", frequency); n@3102: hold.appendChild(range); n@3102: htmlRoot.appendChild(hold); nicholas@3101: nicholas@3101: var gain = audioContext.createGain(); nicholas@3101: gain.connect(outputGain); nicholas@3101: gain.gain.value = Math.pow(10, Number(range.value) / 20.0); n@3102: var osc; nicholas@3101: nicholas@3101: var store = storage.document.createElement("response"); nicholas@3101: store.setAttribute("frequency", frequency); nicholas@3101: storageHook.appendChild(store); nicholas@3101: var interface = {}; nicholas@3101: Object.defineProperties(interface, { nicholas@3101: "handleEvent": { nicholas@3101: "value": function (e) { nicholas@3101: if (e.type == "mouseenter") { nicholas@3101: osc = audioContext.createOscillator(); nicholas@3101: osc.frequency.value = frequency; nicholas@3101: osc.connect(gain); nicholas@3101: osc.start(); nicholas@3101: console.log("start " + frequency); nicholas@3101: } else if (e.type == "mouseleave") { nicholas@3101: console.log("stop " + frequency); nicholas@3101: osc.stop(); nicholas@3101: osc = undefined; nicholas@2224: } nicholas@3101: store.textContent = e.currentTarget.value; nicholas@3101: gain.gain.value = Math.pow(10, Number(e.currentTarget.value) / 20.0); nicholas@3101: } nicholas@2224: } nicholas@3101: }); nicholas@3101: range.addEventListener("mousemove", interface); nicholas@3101: range.addEventListener("mouseenter", interface); nicholas@3101: range.addEventListener("mouseleave", interface); nicholas@3101: return interface; nicholas@3101: })(frequency); nicholas@3101: } nicholas@3101: var htmlHook = document.createElement("div"); nicholas@3101: htmlRoot.appendChild(htmlHook); nicholas@3101: var storageHook = storage.document.createElement("frequency"); nicholas@3101: storageRoot.appendChild(storageHook); nicholas@3101: var frequencies = [100, 200, 400, 800, 1200, 1600, 2000, 4000, 8000, 12000]; nicholas@3101: var outputGain = audioContext.createGain(); nicholas@3101: outputGain.gain.value = 0.25; nicholas@3101: outputGain.connect(audioContext.destination); nicholas@3101: this.sliders = frequencies.map(createFrequencyElement); n@3102: }; nicholas@3101: nicholas@3101: var checkChannelsUnit = function (htmlRoot, storageRoot) { nicholas@3101: nicholas@3101: function onclick(ev) { nicholas@3101: var storageHook = storage.document.querySelector("calibration").querySelector("channels"); nicholas@3101: storageHook.setAttribute("selected", ev.currentTarget.value); nicholas@3101: storageHook.setAttribute("selectedText", ev.currentTarget.textContent); nicholas@3101: osc.stop(); nicholas@3101: gainL = undefined; nicholas@3101: gainR = undefined; nicholas@3101: cmerge = undefined; nicholas@3101: popup.proceedClicked(); nicholas@3101: } nicholas@3101: var osc = audioContext.createOscillator(); nicholas@3101: var gainL = audioContext.createGain(); nicholas@3101: var gainR = audioContext.createGain(); nicholas@3101: gainL.channelCount = 1; nicholas@3101: gainR.channelCount = 1; nicholas@3101: var cmerge = audioContext.createChannelMerger(2); nicholas@3101: osc.connect(gainL, 0, 0); nicholas@3101: osc.connect(gainR, 0, 0); nicholas@3101: gainL.connect(cmerge, 0, 0); nicholas@3101: gainR.connect(cmerge, 0, 1); nicholas@3101: cmerge.connect(audioContext.destination); nicholas@3101: var play = document.createElement("button"); nicholas@3101: play.textContent = "Play Audio"; nicholas@3101: play.onclick = function () { nicholas@3101: osc.start(); nicholas@3101: play.disabled = true; n@3102: }; n@3102: play.className = "calibration-button"; nicholas@3101: htmlRoot.appendChild(play); nicholas@3101: var choiceHolder = document.createElement("div"); nicholas@3101: var leftButton = document.createElement("button"); nicholas@3101: leftButton.textContent = "Left"; nicholas@3101: leftButton.value = "-1"; n@3102: leftButton.className = "calibration-button"; nicholas@3101: var centerButton = document.createElement("button"); nicholas@3101: centerButton.textContent = "Middle"; nicholas@3101: centerButton.value = "0"; n@3102: centerButton.className = "calibration-button"; nicholas@3101: var rightButton = document.createElement("button"); nicholas@3101: rightButton.textContent = "Right"; nicholas@3101: rightButton.value = "1"; n@3102: rightButton.className = "calibration-button"; nicholas@3101: choiceHolder.appendChild(leftButton); nicholas@3101: choiceHolder.appendChild(centerButton); nicholas@3101: choiceHolder.appendChild(rightButton); nicholas@3101: htmlRoot.appendChild(choiceHolder); nicholas@3101: leftButton.addEventListener("click", onclick); nicholas@3101: centerButton.addEventListener("click", onclick); nicholas@3101: rightButton.addEventListener("click", onclick); nicholas@3101: nicholas@3101: var storageHook = storage.document.createElement("channels"); nicholas@3101: storageRoot.appendChild(storageHook); nicholas@3101: nicholas@3101: var pan; nicholas@3101: if (Math.random() > 0.5) { nicholas@3101: pan = 1; nicholas@3101: gainL.gain.value = 0.0; nicholas@3101: gainR.gain.value = 0.25; nicholas@3101: storageHook.setAttribute("presented", pan); nicholas@3101: storageHook.setAttribute("presentedText", "Right"); nicholas@3101: } else { nicholas@3101: pan = -1; nicholas@3101: gainL.gain.value = 0.25; nicholas@3101: gainR.gain.value = 0.0; nicholas@3101: storageHook.setAttribute("presented", pan); nicholas@3101: storageHook.setAttribute("presentedText", "Left"); nicholas@3101: } n@3102: }; nicholas@3101: nicholas@3101: var interface = {}; nicholas@3101: Object.defineProperties(interface, { nicholas@3101: "calibrationObject": { nicholas@3101: "get": function () { n@3102: return calibrationObject; nicholas@3101: }, nicholas@3101: "set": readonly nicholas@3101: }, nicholas@3101: "checkFrequencies": { nicholas@3101: "get": function () { nicholas@3101: if (specification.calibration.checkFrequencies && _checkedFrequency === false) { nicholas@3101: return true; nicholas@2224: } nicholas@3101: return false; nicholas@3101: }, nicholas@3101: "set": readonly nicholas@3101: }, nicholas@3101: "checkChannels": { nicholas@3101: "get": function () { nicholas@3101: if (specification.calibration.checkChannels && _checkedChannels === false) { nicholas@3101: return true; nicholas@3101: } nicholas@3101: return false; nicholas@3101: }, nicholas@3101: "set": readonly nicholas@3101: }, nicholas@3101: "performFrequencyCheck": { nicholas@3101: "value": function (htmlRoot) { nicholas@3101: htmlRoot.innerHTML = ""; nicholas@3101: calibrationObject = new checkFrequencyUnit(htmlRoot, getStorageRoot()); nicholas@3101: _checkedFrequency = true; nicholas@2224: } nicholas@3101: }, nicholas@3101: "performChannelCheck": { nicholas@3101: "value": function (htmlRoot) { nicholas@3101: htmlRoot.innerHTML = ""; nicholas@3101: calibrationObject = new checkChannelsUnit(htmlRoot, getStorageRoot()); nicholas@3101: _checkedChannels = true; nicholas@3101: } nicholas@2224: } n@3102: }); nicholas@3101: return interface; nicholas@3101: })(); nicholas@2498: nicholas@2498: nicholas@2498: // Global Checkers nicholas@2498: // These functions will help enforce the checkers n@2789: this.checkHiddenAnchor = function (message) { nicholas@2708: var anchors = audioEngineContext.audioObjects.filter(function (ao) { nicholas@2708: return ao.specification.type === "anchor"; nicholas@2708: }); nicholas@2708: var state = anchors.some(function (ao) { nicholas@2708: return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0); nicholas@2708: }); nicholas@2708: if (state) { nicholas@2708: console.log('Anchor node not below marker value'); n@2789: if (message) { n@2789: interfaceContext.lightbox.post("Message", message); n@2789: } else { n@2789: interfaceContext.lightbox.post("Message", 'Please keep listening'); n@2789: } nicholas@2708: this.storeErrorNode('Anchor node not below marker value'); nicholas@2708: return false; nicholas@2498: } nicholas@2498: return true; nicholas@2498: }; nicholas@2498: n@2789: this.checkHiddenReference = function (message) { nicholas@2708: var references = audioEngineContext.audioObjects.filter(function (ao) { nicholas@2708: return ao.specification.type === "reference"; nicholas@2708: }); nicholas@2708: var state = references.some(function (ao) { nicholas@2708: return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0); nicholas@2708: }); nicholas@2708: if (state) { nicholas@2708: console.log('Reference node not below marker value'); n@2789: if (message) { n@2789: interfaceContext.lightbox.post("Message", message); n@2789: } else { n@2789: interfaceContext.lightbox.post("Message", 'Please keep listening'); n@2789: } nicholas@2708: this.storeErrorNode('Reference node not below marker value'); nicholas@2708: return false; nicholas@2498: } nicholas@2498: return true; nicholas@2498: }; nicholas@2498: n@2789: this.checkFragmentsFullyPlayed = function (message) { nicholas@2498: // Checks the entire file has been played back nicholas@2498: // NOTE ! This will return true IF playback is Looped!!! nicholas@2498: if (audioEngineContext.loopPlayback) { nicholas@2498: console.log("WARNING - Looped source: Cannot check fragments are fully played"); nicholas@2498: return true; nicholas@2498: } nicholas@2498: var check_pass = true; nicholas@2708: var error_obj = [], nicholas@2708: i; nicholas@2708: for (i = 0; i < audioEngineContext.audioObjects.length; i++) { nicholas@2498: var object = audioEngineContext.audioObjects[i]; nicholas@2498: var time = object.buffer.buffer.duration; nicholas@2498: var metric = object.metric; nicholas@2498: var passed = false; nicholas@2498: for (var j = 0; j < metric.listenTracker.length; j++) { nicholas@2498: var bt = metric.listenTracker[j].getElementsByTagName('testtime'); nicholas@2498: var start_time = Number(bt[0].getAttribute('start')); nicholas@2498: var stop_time = Number(bt[0].getAttribute('stop')); nicholas@2498: var delta = stop_time - start_time; nicholas@2498: if (delta >= time) { nicholas@2498: passed = true; nicholas@2498: break; nicholas@2498: } nicholas@2498: } nicholas@2708: if (passed === false) { nicholas@2498: check_pass = false; nicholas@2498: console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId()); nicholas@2498: error_obj.push(object.interfaceDOM.getPresentedId()); nicholas@2498: } nicholas@2498: } nicholas@2708: if (check_pass === false) { nicholas@2498: var str_start = "You have not completely listened to fragments "; nicholas@2708: for (i = 0; i < error_obj.length; i++) { nicholas@2498: str_start += error_obj[i]; nicholas@2498: if (i != error_obj.length - 1) { nicholas@2498: str_start += ', '; nicholas@2498: } nicholas@2498: } nicholas@2498: str_start += ". Please keep listening"; n@2789: console.log(str_start); n@2789: this.storeErrorNode(str_start); n@2789: if (message) { n@2789: str_start = message; n@2789: } nicholas@2498: interfaceContext.lightbox.post("Error", str_start); nicholas@2444: return false; nicholas@2498: } nicholas@2444: return true; nicholas@2498: }; n@2789: this.checkAllMoved = function (message) { nicholas@2498: var str = "You have not moved "; nicholas@2498: var failed = []; nicholas@2708: audioEngineContext.audioObjects.forEach(function (ao) { nicholas@2708: if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) { nicholas@2498: failed.push(ao.interfaceDOM.getPresentedId()); nicholas@2498: } nicholas@2708: }, this); nicholas@2708: if (failed.length === 0) { nicholas@2498: return true; nicholas@2498: } else if (failed.length == 1) { nicholas@2498: str += 'track ' + failed[0]; nicholas@2498: } else { nicholas@2498: str += 'tracks '; nicholas@2498: for (var i = 0; i < failed.length - 1; i++) { nicholas@2498: str += failed[i] + ', '; nicholas@2498: } nicholas@2498: str += 'and ' + failed[i]; nicholas@2498: } nicholas@2498: str += '.'; nicholas@2498: console.log(str); nicholas@2224: this.storeErrorNode(str); n@2789: if (message) { n@2789: str = message; n@2789: } n@2789: interfaceContext.lightbox.post("Error", str); nicholas@2498: return false; nicholas@2498: }; n@2789: this.checkAllPlayed = function (message) { nicholas@2498: var str = "You have not played "; nicholas@2498: var failed = []; nicholas@2708: audioEngineContext.audioObjects.forEach(function (ao) { nicholas@2708: if (ao.metric.wasListenedTo === false) { nicholas@2498: failed.push(ao.interfaceDOM.getPresentedId()); nicholas@2498: } nicholas@2708: }, this); nicholas@2708: if (failed.length === 0) { nicholas@2498: return true; nicholas@2498: } else if (failed.length == 1) { nicholas@2498: str += 'track ' + failed[0]; nicholas@2498: } else { nicholas@2498: str += 'tracks '; nicholas@2498: for (var i = 0; i < failed.length - 1; i++) { nicholas@2498: str += failed[i] + ', '; nicholas@2498: } nicholas@2498: str += 'and ' + failed[i]; nicholas@2498: } nicholas@2498: str += '.'; nicholas@2498: console.log(str); nicholas@2224: this.storeErrorNode(str); n@2789: if (message) { n@2789: str = message; n@2789: } n@2789: interfaceContext.lightbox.post("Error", str); nicholas@2498: return false; nicholas@2498: }; n@2789: this.checkAllCommented = function (message) { nicholas@2540: var str = "You have not commented on all the fragments."; nicholas@2540: var cont = true, nicholas@2540: boxes = this.commentBoxes.boxes, nicholas@2540: numBoxes = boxes.length, nicholas@2540: i; nicholas@2540: for (i = 0; i < numBoxes; i++) { nicholas@2540: if (boxes[i].trackCommentBox.value === "") { nicholas@2540: console.log(str); nicholas@2540: this.storeErrorNode(str); n@2789: if (message) { n@2789: str = message; n@2789: } n@2789: interfaceContext.lightbox.post("Error", str); nicholas@2540: return false; nicholas@2540: } nicholas@2540: } nicholas@2540: return true; nicholas@2708: }; n@2789: this.checkScaleRange = function (message) { nicholas@2310: var page = testState.getCurrentTestPage(); nicholas@2708: var interfaceObject = page.interfaces; nicholas@2310: var state = true; nicholas@2310: var str = "Please keep listening. "; nicholas@2708: if (interfaceObject === undefined) { nicholas@2708: return true; nicholas@2310: } nicholas@2708: interfaceObject = interfaceObject[0]; nicholas@2708: var scales = (function () { nicholas@2708: var scaleRange = interfaceObject.options.find(function (a) { nicholas@2708: return a.name == "scalerange"; nicholas@2708: }); nicholas@2708: return { nicholas@2708: min: scaleRange.min, nicholas@2708: max: scaleRange.max nicholas@2708: }; nicholas@2708: })(); nicholas@2708: var range = audioEngineContext.audioObjects.reduce(function (a, b) { nicholas@2742: var v = b.interfaceDOM.getValue() * 100.0; nicholas@2708: return { nicholas@2708: min: Math.min(a.min, v), nicholas@2708: max: Math.max(a.max, v) nicholas@2712: }; nicholas@2708: }, { nicholas@2708: min: 100, nicholas@2708: max: 0 nicholas@2708: }); nicholas@2708: if (range.min > scales.min) { nicholas@2742: str += "At least one fragment must be below the " + scales.min + " mark."; nicholas@2708: state = false; nicholas@2712: } else if (range.max < scales.max) { nicholas@2742: str += "At least one fragment must be above the " + scales.max + " mark."; nicholas@2310: state = false; nicholas@2310: } nicholas@2708: if (state === false) { nicholas@2310: console.log(str); nicholas@2310: this.storeErrorNode(str); n@2789: if (message) { n@2789: str = message; n@2789: } nicholas@2498: interfaceContext.lightbox.post("Error", str); nicholas@2310: } nicholas@2310: return state; nicholas@2708: }; nicholas@2826: this.checkFragmentMinPlays = function () { nicholas@2826: var failedObjects = audioEngineContext.audioObjects.filter(function (a) { nicholas@2826: var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays; nicholas@2826: if (minPlays === undefined || a.numberOfPlays >= minPlays) { nicholas@2826: return false; nicholas@2826: } nicholas@2826: return true; nicholas@2826: }); nicholas@2826: if (failedObjects.length === 0) { nicholas@2827: return true; nicholas@2826: } nicholas@2826: var failedString = []; nicholas@2826: failedObjects.forEach(function (a) { nicholas@2826: failedString.push(a.interfaceDOM.getPresentedId()); nicholas@2826: }); nicholas@2826: var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening"; nicholas@2826: interfaceContext.lightbox.post("Message", str); nicholas@2826: this.storeErrorNode(str); nicholas@2827: return false; nicholas@2826: }; nicholas@2826: nicholas@2498: nicholas@2849: this.sortFragmentsByScore = function () { nicholas@2849: var elements = audioEngineContext.audioObjects.filter(function (elem) { nicholas@2849: return elem.specification.type !== "outside-reference"; nicholas@2849: }); nicholas@2849: var indexes = []; nicholas@2849: var i = 0; nicholas@2849: while (indexes.push(i++) < elements.length); nicholas@2849: return indexes.sort(function (x, y) { nicholas@2849: var a = elements[x].interfaceDOM.getValue(); nicholas@2849: var b = elements[y].interfaceDOM.getValue(); nicholas@2849: if (a > b) { nicholas@2849: return 1; nicholas@2849: } else if (a < b) { nicholas@2849: return -1; nicholas@2849: } nicholas@2849: return 0; nicholas@2849: }, elements[0].interfaceDOM.getValue()); nicholas@2849: }; nicholas@2849: nicholas@2498: this.storeErrorNode = function (errorMessage) { nicholas@2224: var time = audioEngineContext.timer.getTestTime(); nicholas@2224: var node = storage.document.createElement('error'); nicholas@2498: node.setAttribute('time', time); nicholas@2224: node.textContent = errorMessage; nicholas@2224: testState.currentStore.XMLDOM.appendChild(node); nicholas@2224: }; nicholas@2595: nicholas@2595: this.getLabel = function (labelType, index, labelStart) { nicholas@2595: /* nicholas@2595: Get the correct label based on type, index and offset nicholas@2595: */ nicholas@2595: nicholas@2595: function calculateLabel(labelType, index, offset) { nicholas@2595: if (labelType == "none") { nicholas@2595: return ""; nicholas@2595: } nicholas@2595: switch (labelType) { nicholas@2595: case "letter": nicholas@2596: return String.fromCharCode((index + offset) % 26 + 97); nicholas@2595: case "capital": nicholas@2607: return String.fromCharCode((index + offset) % 26 + 65); nicholas@2625: case "samediff": nicholas@2708: if (index === 0) { nicholas@2625: return "Same"; nicholas@2625: } else if (index == 1) { nicholas@2625: return "Difference"; nicholas@2625: } nicholas@2708: return ""; nicholas@2595: case "number": nicholas@2595: return String(index + offset); nicholas@2595: default: nicholas@2595: return ""; nicholas@2595: } nicholas@2595: } nicholas@2595: nicholas@2708: if (typeof labelStart !== "string" || labelStart.length === 0) { nicholas@2595: labelStart = String.fromCharCode(0); nicholas@2595: } nicholas@2595: nicholas@2595: switch (labelType) { nicholas@2595: case "letter": nicholas@2595: labelStart = labelStart.charCodeAt(0); nicholas@2596: if (labelStart < 97 || labelStart > 122) { nicholas@2595: labelStart = 97; nicholas@2595: } nicholas@2595: labelStart -= 97; nicholas@2595: break; nicholas@2595: case "capital": nicholas@2595: labelStart = labelStart.charCodeAt(0); nicholas@2596: if (labelStart < 65 || labelStart > 90) { nicholas@2595: labelStart = 65; nicholas@2595: } nicholas@2595: labelStart -= 65; nicholas@2595: break; nicholas@2595: case "number": nicholas@2608: labelStart = Number(labelStart); nicholas@2608: if (!isFinite(labelStart)) { nicholas@2595: labelStart = 1; nicholas@2595: } nicholas@2595: break; nicholas@2595: default: nicholas@2596: labelStart = 0; nicholas@2595: } nicholas@2595: if (typeof index == "number") { nicholas@2595: return calculateLabel(labelType, index, labelStart); nicholas@2595: } else if (index.length && index.length > 0) { nicholas@2595: var a = [], nicholas@2595: l = index.length, nicholas@2595: i; nicholas@2595: for (i = 0; i < l; i++) { nicholas@2595: a[i] = calculateLabel(labelType, index[i], labelStart); nicholas@2595: } nicholas@2595: return a; nicholas@2595: } else { nicholas@2595: throw ("Invalid arguments"); nicholas@2595: } nicholas@2708: }; nicholas@2649: nicholas@2649: this.getCombinedInterfaces = function (page) { nicholas@2649: // Combine the interfaces with the global interface nodes nicholas@2649: var global = specification.interfaces, nicholas@2649: local = page.interfaces; nicholas@2649: local.forEach(function (locInt) { nicholas@2649: // Iterate through the options nodes nicholas@2649: var addList = []; nicholas@2649: global.options.forEach(function (gopt) { nicholas@2649: var lopt = locInt.options.find(function (lopt) { nicholas@2649: return (lopt.name == gopt.name) && (lopt.type == gopt.type); nicholas@2649: }); nicholas@2649: if (!lopt) { nicholas@2649: // Global option doesn't exist locally nicholas@2649: addList.push(gopt); nicholas@2649: } nicholas@2649: }); nicholas@2649: locInt.options = locInt.options.concat(addList); nicholas@2649: if (!locInt.scales && global.scales) { nicholas@2649: // Use the global default scales nicholas@2649: locInt.scales = global.scales; nicholas@2649: } nicholas@2649: }); nicholas@2649: return local; nicholas@2708: }; nicholas@2224: } nicholas@2224: nicholas@2498: function Storage() { nicholas@2498: // Holds results in XML format until ready for collection nicholas@2498: this.globalPreTest = null; nicholas@2498: this.globalPostTest = null; nicholas@2498: this.testPages = []; nicholas@2498: this.document = null; nicholas@2498: this.root = null; nicholas@2498: this.state = 0; nicholas@3113: var linkedID = undefined; nicholas@2733: var pFilenamePrefix = "save"; nicholas@2498: nicholas@2498: this.initialise = function (existingStore) { nicholas@2708: if (existingStore === undefined) { nicholas@2224: // We need to get the sessionKey nicholas@2510: this.SessionKey.requestKey(); nicholas@2498: this.document = document.implementation.createDocument(null, "waetresult", null); nicholas@2224: this.root = this.document.childNodes[0]; nicholas@2224: var projectDocument = specification.projectXML; nicholas@2708: projectDocument.setAttribute('file-name', specification.url); nicholas@2708: projectDocument.setAttribute('url', qualifyURL(specification.url)); nicholas@2224: this.root.appendChild(projectDocument); nicholas@2224: this.root.appendChild(interfaceContext.returnDateNode()); nicholas@2224: this.root.appendChild(interfaceContext.returnNavigator()); nicholas@2224: } else { nicholas@2224: this.document = existingStore; nicholas@2294: this.root = existingStore.firstChild; nicholas@2224: this.SessionKey.key = this.root.getAttribute("key"); nicholas@2224: } nicholas@2708: if (specification.preTest !== undefined) { nicholas@2498: this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest); nicholas@2498: } nicholas@2708: if (specification.postTest !== undefined) { nicholas@2498: this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest); nicholas@2498: } nicholas@3113: if (linkedID) { nicholas@3113: this.root.setAttribute("linked", linkedID); nicholas@3113: } nicholas@2498: }; nicholas@2498: n@2967: this.SessionKey = (function (parent) { n@2970: var returnURL = ""; n@2970: if (window.returnURL !== undefined) { n@2970: returnURL = String(window.returnURL); n@2970: } nicholas@3129: nicholas@3118: var chainCount = 0; nicholas@3118: var chainPosition = chainCount; n@2970: n@2967: function postUpdate() { n@2967: return new Promise(function (resolve, reject) { nicholas@3118: // Return a new promise. nicholas@3118: chainPosition+=1; nicholas@3118: var hold = document.createElement("div"); nicholas@3118: var clone = parent.root.cloneNode(true); nicholas@3118: hold.appendChild(clone); n@2967: // Do the usual XHR stuff n@2977: console.log("Requested save..."); n@2967: var req = new XMLHttpRequest(); n@2973: req.open("POST", returnURL + "php/save.php?key=" + sessionKey + "&saveFilenamePrefix=" + parent.filenamePrefix); n@2967: req.setRequestHeader('Content-Type', 'text/xml'); n@2967: n@2967: req.onload = function () { n@2967: // This is called even on 404 etc n@2967: // so check the status n@2967: if (this.status >= 300) { n@2967: console.log("WARNING - Could not update at this time"); n@2967: } else { n@2967: var parser = new DOMParser(); n@2974: var xmlDoc = parser.parseFromString(req.responseText, "application/xml"); n@2967: var response = xmlDoc.getElementsByTagName('response')[0]; n@2967: if (response.getAttribute("state") == "OK") { n@2967: var file = response.getElementsByTagName("file")[0]; n@2967: console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B"); n@2967: resolve(true); n@2967: } else { n@2967: var message = response.getElementsByTagName("message"); n@2967: console.log("Intermediate save: Error! " + message.textContent); n@2967: reject("Intermediate save: Error! " + message.textContent); n@2967: } n@2967: } n@2967: }; n@2967: n@2967: // Handle network errors n@2967: req.onerror = function () { n@2967: reject(Error("Network Error")); n@2967: }; n@2967: n@2967: // Make the request nicholas@3118: if (chainCount > chainPosition) { nicholas@3118: // We have items downstream that will upload for us nicholas@3118: resolve(true); nicholas@3118: } else { nicholas@3118: req.send([hold.innerHTML]); nicholas@3118: } n@2967: }); n@2979: } n@2967: n@2967: function keyPromise() { n@2967: return new Promise(function (resolve, reject) { n@2967: var req = new XMLHttpRequest(); nicholas@3139: req.open("POST", returnURL + "php/requestKey.php?saveFilenamePrefix=" + parent.filenamePrefix, true); n@2967: req.onload = function () { n@2967: // This is called even on 404 etc n@2967: // so check the status n@2967: if (req.status == 200) { n@2967: // Resolve the promise with the response text n@2967: resolve(req.response); n@2967: } else { n@2967: // Otherwise reject with the status text n@2967: // which will hopefully be a meaningful error n@2967: reject(Error(req.statusText)); n@2967: } n@2967: }; n@2967: n@2967: // Handle network errors n@2967: req.onerror = function () { n@2967: reject(Error("Network Error")); n@2967: }; n@2967: n@2967: req.send(); n@2979: }); n@2967: } n@2967: n@2967: var requestChains = null; n@2969: var sessionKey = null; n@2967: var object = {}; n@2967: n@2967: Object.defineProperties(object, { n@2967: "key": { n@2967: "get": function () { n@2969: return sessionKey; n@2967: }, n@2967: "set": function (a) { n@2979: throw ("Cannot set read-only property"); n@2967: } n@2967: }, n@2967: "request": { n@2967: "value": new XMLHttpRequest() n@2967: }, n@2967: "parent": { n@2967: "value": parent n@2967: }, n@2967: "requestKey": { n@2967: "value": function () { n@2967: requestChains = keyPromise().then(function (response) { n@2967: function throwerror() { n@2969: sessionKey = null; n@2967: throw ("An unspecified error occured, no server key could be generated"); n@2967: } n@2967: var parse = new DOMParser(); n@2967: var xml = parse.parseFromString(response, "text/xml"); n@2971: if (response.length === 0) { n@2967: throwerror(); n@2967: } n@2967: if (xml.getElementsByTagName("state").length > 0) { n@2967: if (xml.getElementsByTagName("state")[0].textContent == "OK") { n@2969: sessionKey = xml.getAllElementsByTagName("key")[0].textContent; n@2972: parent.root.setAttribute("key", sessionKey); n@2972: parent.root.setAttribute("state", "empty"); n@2967: return (true); n@2967: } else if (xml.getElementsByTagName("state")[0].textContent == "ERROR") { n@2969: sessionKey = null; n@2967: console.error("Could not generate server key. Server responded with error message: \"" + xml.getElementsByTagName("message")[0].textContent + "\""); n@2969: return (false); n@2967: } n@2967: } else { n@2967: throwerror(); n@2967: } n@2967: return (true); n@2967: }); n@2967: } n@2967: }, n@2968: "update": { n@2968: "value": function () { n@3104: if (requestChains === undefined) { n@3104: throw ("Initiate key exchange first"); n@2968: } nicholas@3118: chainCount += 1; n@2968: this.parent.root.setAttribute("state", "update"); n@2978: requestChains = requestChains.then(postUpdate); n@2967: } n@2967: }, n@2968: "finish": { n@2968: "value": function () { n@2979: if (this.key === null || requestChains === undefined) { n@2968: throw ("Cannot save as key == null"); n@2968: } n@2968: this.parent.finish(); n@2975: return requestChains.then(postUpdate()).then(function () { n@2968: console.log("OK"); nicholas@3113: return true; n@2968: }, function () { n@2968: createProjectSave("local"); n@2979: }); n@2967: } n@2967: } n@2968: }); n@2967: return object; n@2967: })(this); n@2967: /* nicholas@2224: this.SessionKey = { nicholas@2224: key: null, nicholas@2224: request: new XMLHttpRequest(), nicholas@2224: parent: this, nicholas@2498: handleEvent: function () { n@2967: nicholas@2224: }, nicholas@2510: requestKey: function () { n@2967: nicholas@2510: }, nicholas@2498: update: function () { nicholas@2708: if (this.key === null) { nicholas@2357: console.log("Cannot save as key == null"); nicholas@2357: return; nicholas@2357: } nicholas@2498: this.parent.root.setAttribute("state", "update"); nicholas@2224: var xmlhttp = new XMLHttpRequest(); nicholas@2302: var returnURL = ""; nicholas@2302: if (typeof specification.projectReturn == "string") { nicholas@2498: if (specification.projectReturn.substr(0, 4) == "http") { nicholas@2302: returnURL = specification.projectReturn; nicholas@2302: } nicholas@2302: } nicholas@2722: xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix); nicholas@2224: xmlhttp.setRequestHeader('Content-Type', 'text/xml'); nicholas@2498: xmlhttp.onerror = function () { nicholas@2224: console.log('Error updating file to server!'); nicholas@2224: }; nicholas@2224: var hold = document.createElement("div"); nicholas@2224: var clone = this.parent.root.cloneNode(true); nicholas@2224: hold.appendChild(clone); nicholas@2498: xmlhttp.onload = function () { nicholas@2224: if (this.status >= 300) { nicholas@2224: console.log("WARNING - Could not update at this time"); nicholas@2224: } else { nicholas@2224: var parser = new DOMParser(); nicholas@2224: var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml"); nicholas@2224: var response = xmlDoc.getElementsByTagName('response')[0]; nicholas@2224: if (response.getAttribute("state") == "OK") { nicholas@2224: var file = response.getElementsByTagName("file")[0]; nicholas@2498: console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B"); nicholas@2224: } else { nicholas@2224: var message = response.getElementsByTagName("message"); nicholas@2498: console.log("Intermediate save: Error! " + message.textContent); nicholas@2224: } nicholas@2224: } nicholas@2708: }; nicholas@2224: xmlhttp.send([hold.innerHTML]); nicholas@2723: }, nicholas@2723: finish: function () { nicholas@2723: // Final upload to complete the test nicholas@2723: this.parent.finish(); nicholas@2723: var hold = document.createElement("div"); nicholas@2723: var clone = this.parent.root.cloneNode(true); nicholas@2723: hold.appendChild(clone); nicholas@2733: var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix="; nicholas@2742: if (this.parent.filenamePrefix.length === 0) { nicholas@2733: saveURL += "save"; nicholas@2733: } else { nicholas@2733: saveURL += this.parent.filenamePrefix; nicholas@2733: } nicholas@2723: return new Promise(function (resolve, reject) { nicholas@2723: var xmlhttp = new XMLHttpRequest(); nicholas@2723: xmlhttp.open("POST", saveURL); nicholas@2723: xmlhttp.setRequestHeader('Content-Type', 'text/xml'); nicholas@2723: xmlhttp.onerror = function () { nicholas@2723: console.log('Error updating file to server!'); nicholas@2723: createProjectSave("local"); nicholas@2723: }; nicholas@2723: xmlhttp.onload = function () { nicholas@2723: if (this.status >= 300) { nicholas@2723: console.log("WARNING - Could not update at this time"); nicholas@2723: createProjectSave("local"); nicholas@2723: } else { nicholas@2723: var parser = new DOMParser(); nicholas@2723: var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml"); nicholas@2723: var response = xmlDoc.getElementsByTagName('response')[0]; nicholas@2723: if (response.getAttribute("state") == "OK") { nicholas@2723: var file = response.getElementsByTagName("file")[0]; nicholas@2723: console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B"); nicholas@2723: resolve(response); nicholas@2723: } else { nicholas@2723: var message = response.getElementsByTagName("message"); nicholas@2723: reject(message); nicholas@2723: } nicholas@2723: } nicholas@2723: }; nicholas@2723: xmlhttp.send([hold.innerHTML]); nicholas@2723: }); nicholas@2224: } nicholas@2708: }; n@2967: */ nicholas@2498: this.createTestPageStore = function (specification) { nicholas@2498: var store = new this.pageNode(this, specification); nicholas@2498: this.testPages.push(store); nicholas@2498: return this.testPages[this.testPages.length - 1]; nicholas@2498: }; nicholas@2498: nicholas@2498: this.surveyNode = function (parent, root, specification) { nicholas@2498: this.specification = specification; nicholas@2498: this.parent = parent; nicholas@2224: this.state = "empty"; nicholas@2498: this.XMLDOM = this.parent.document.createElement('survey'); nicholas@2498: this.XMLDOM.setAttribute('location', this.specification.location); nicholas@2498: this.XMLDOM.setAttribute("state", this.state); nicholas@2708: this.specification.options.forEach(function (optNode) { nicholas@2498: if (optNode.type != 'statement') { nicholas@2498: var node = this.parent.document.createElement('surveyresult'); nicholas@2498: node.setAttribute("ref", optNode.id); nicholas@2498: node.setAttribute('type', optNode.type); nicholas@2498: this.XMLDOM.appendChild(node); nicholas@2498: } nicholas@2708: }, this); nicholas@2498: root.appendChild(this.XMLDOM); nicholas@2498: nicholas@2498: this.postResult = function (node) { nicholas@2708: function postNumber(doc, value) { nicholas@2708: var child = doc.createElement("response"); nicholas@2708: child.textContent = value; nicholas@2708: return child; nicholas@2708: } nicholas@2708: nicholas@2708: function postRadio(doc, node) { nicholas@2708: var child = doc.createElement('response'); nicholas@2708: if (node.response !== null) { nicholas@2708: child.setAttribute('name', node.response.name); nicholas@2708: child.textContent = node.response.text; nicholas@2708: } nicholas@2708: return child; nicholas@2708: } nicholas@2708: nicholas@2708: function postCheckbox(doc, node) { nicholas@2708: var checkNode = doc.createElement('response'); nicholas@2708: checkNode.setAttribute('name', node.name); nicholas@2708: checkNode.setAttribute('checked', node.checked); nicholas@2708: return checkNode; nicholas@2708: } nicholas@2498: // From popup: node is the popupOption node containing both spec. and results nicholas@2498: // ID is the position nicholas@2498: if (node.specification.type == 'statement') { nicholas@2498: return; nicholas@2498: } nicholas@2498: var surveyresult = this.XMLDOM.firstChild; nicholas@2708: while (surveyresult !== null) { nicholas@2498: if (surveyresult.getAttribute("ref") == node.specification.id) { nicholas@2224: break; nicholas@2224: } nicholas@2224: surveyresult = surveyresult.nextElementSibling; nicholas@2224: } nicholas@2775: surveyresult.setAttribute("duration", node.elapsedTime); nicholas@2498: switch (node.specification.type) { nicholas@2498: case "number": nicholas@2498: case "question": n@2583: case "slider": nicholas@2708: surveyresult.appendChild(postNumber(this.parent.document, node.response)); nicholas@2464: break; nicholas@2498: case "radio": nicholas@2708: surveyresult.appendChild(postRadio(this.parent.document, node)); nicholas@2498: break; nicholas@2498: case "checkbox": nicholas@2708: if (node.response === undefined) { nicholas@2498: surveyresult.appendChild(this.parent.document.createElement('response')); nicholas@2498: break; nicholas@2498: } nicholas@2498: for (var i = 0; i < node.response.length; i++) { nicholas@2708: surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i])); nicholas@2498: } nicholas@2498: break; nicholas@2498: } nicholas@2498: }; nicholas@2498: this.complete = function () { nicholas@2498: this.state = "complete"; nicholas@2498: this.XMLDOM.setAttribute("state", this.state); nicholas@2708: }; nicholas@2498: }; nicholas@2498: nicholas@2498: this.pageNode = function (parent, specification) { nicholas@2498: // Create one store per test page nicholas@2498: this.specification = specification; nicholas@2498: this.parent = parent; nicholas@2498: this.state = "empty"; nicholas@2498: this.XMLDOM = this.parent.document.createElement('page'); nicholas@2498: this.XMLDOM.setAttribute('ref', specification.id); nicholas@2498: this.XMLDOM.setAttribute('presentedId', specification.presentedId); nicholas@2498: this.XMLDOM.setAttribute("state", this.state); nicholas@2708: if (specification.preTest !== undefined) { nicholas@2498: this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest); nicholas@2498: } nicholas@2708: if (specification.postTest !== undefined) { nicholas@2498: this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest); nicholas@2498: } nicholas@2498: nicholas@2498: // Add any page metrics nicholas@2498: var page_metric = this.parent.document.createElement('metric'); nicholas@2498: this.XMLDOM.appendChild(page_metric); nicholas@2498: nicholas@2498: // Add the audioelement nicholas@2708: this.specification.audioElements.forEach(function (element) { nicholas@2498: var aeNode = this.parent.document.createElement('audioelement'); nicholas@2498: aeNode.setAttribute('ref', element.id); nicholas@2708: if (element.name !== undefined) { nicholas@2708: aeNode.setAttribute('name', element.name); nicholas@2708: } nicholas@2498: aeNode.setAttribute('type', element.type); nicholas@2498: aeNode.setAttribute('url', element.url); nicholas@2498: aeNode.setAttribute('fqurl', qualifyURL(element.url)); nicholas@2498: aeNode.setAttribute('gain', element.gain); nicholas@2498: if (element.type == 'anchor' || element.type == 'reference') { nicholas@2498: if (element.marker > 0) { nicholas@2498: aeNode.setAttribute('marker', element.marker); nicholas@2464: } nicholas@2498: } nicholas@2498: var ae_metric = this.parent.document.createElement('metric'); nicholas@2498: aeNode.appendChild(ae_metric); nicholas@2498: this.XMLDOM.appendChild(aeNode); nicholas@2708: }, this); nicholas@2498: nicholas@2498: this.parent.root.appendChild(this.XMLDOM); nicholas@2498: nicholas@2498: this.complete = function () { nicholas@2224: this.state = "complete"; nicholas@2498: this.XMLDOM.setAttribute("state", "complete"); nicholas@2708: }; nicholas@2498: }; nicholas@2498: this.update = function () { nicholas@2224: this.SessionKey.update(); nicholas@2708: }; nicholas@2498: this.finish = function () { nicholas@2498: this.state = 1; nicholas@2498: this.root.setAttribute("state", "complete"); nicholas@2498: return this.root; nicholas@2498: }; nicholas@2722: nicholas@2722: Object.defineProperties(this, { nicholas@2722: 'filenamePrefix': { nicholas@2722: 'get': function () { nicholas@2722: return pFilenamePrefix; nicholas@2722: }, nicholas@2722: 'set': function (value) { nicholas@2722: if (typeof value !== "string") { nicholas@2722: value = String(value); nicholas@2722: } nicholas@2722: pFilenamePrefix = value; nicholas@2722: return value; nicholas@2722: } nicholas@3113: }, nicholas@3113: "sessionLinked": { nicholas@3113: 'get': function () { nicholas@3113: return linkedID; nicholas@3113: }, nicholas@3113: 'set': function(s) { nicholas@3113: if (typeof s == "string") { nicholas@3113: linkedID = s; nicholas@3113: if (this.root) { nicholas@3113: this.root.setAttribute("linked", s); nicholas@3113: } nicholas@3113: } nicholas@3113: return linkedID; nicholas@3113: } nicholas@2722: } nicholas@2725: }); nicholas@2224: } nicholas@2384: nicholas@2401: var window_depedancy_callback; nicholas@2498: window_depedancy_callback = window.setInterval(function () { nicholas@2401: if (check_dependancies()) { nicholas@2401: window.clearInterval(window_depedancy_callback); nicholas@2401: onload(); nicholas@2401: } else { nicholas@2401: document.getElementById("topLevelBody").innerHTML = "

Loading Resources

"; nicholas@2401: } nicholas@2498: }, 100);