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