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@3020
|
1110 if (this.currentIndex > 0 && this.node.showBackButton)
|
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@2942
|
1731 ao.setupPlayback();
|
nicholas@2942
|
1732 ao.bufferStart(playTime);
|
nicholas@2942
|
1733 ao.listenStart(playTime);
|
nicholas@2708
|
1734 } else {
|
nicholas@2942
|
1735 ao.listenStop(playTime);
|
nicholas@2942
|
1736 ao.bufferStop(stopTime);
|
nicholas@2708
|
1737 }
|
nicholas@2708
|
1738 });
|
nicholas@2708
|
1739 }
|
nicholas@2708
|
1740
|
nicholas@2942
|
1741 function playSync(id) {
|
nicholas@2708
|
1742 var playTime = audioContext.currentTime + 0.1;
|
nicholas@2708
|
1743 var stopTime = playTime + specification.crossFade;
|
nicholas@2708
|
1744 this.audioObjects.forEach(function (ao) {
|
nicholas@2942
|
1745 ao.setupPlayback();
|
nicholas@2942
|
1746 ao.bufferStart(playTime);
|
nicholas@2708
|
1747 if (ao.id === id) {
|
nicholas@2942
|
1748 ao.listenStart(playTime);
|
nicholas@2708
|
1749 } else {
|
nicholas@2942
|
1750 ao.listenStop(playTime);
|
nicholas@2708
|
1751 }
|
nicholas@2708
|
1752 });
|
nicholas@2708
|
1753 }
|
nicholas@2708
|
1754
|
nicholas@2498
|
1755 this.play = function (id) {
|
nicholas@2498
|
1756 // Start the timer and set the audioEngine state to playing (1)
|
nicholas@2708
|
1757 if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) {
|
nicholas@2708
|
1758 throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
|
nicholas@2498
|
1759 }
|
nicholas@2823
|
1760 var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays;
|
nicholas@2823
|
1761 if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) {
|
nicholas@2823
|
1762 interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times");
|
nicholas@2823
|
1763 return;
|
nicholas@2823
|
1764 }
|
nicholas@2708
|
1765 if (this.status === 1) {
|
nicholas@2498
|
1766 this.timer.startTest();
|
nicholas@2708
|
1767 interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
|
nicholas@2942
|
1768 if (this.synchPlayback) {
|
nicholas@2351
|
1769 // Traditional looped playback
|
nicholas@2942
|
1770 playSync.call(this, id);
|
nicholas@2708
|
1771 } else {
|
nicholas@2708
|
1772 if (this.bufferReady(id) === false) {
|
nicholas@2708
|
1773 console.log("Cannot play. Buffer not ready");
|
nicholas@2708
|
1774 return;
|
nicholas@2498
|
1775 }
|
nicholas@2708
|
1776 playNormal.call(this, id);
|
nicholas@2498
|
1777 }
|
nicholas@2498
|
1778 interfaceContext.playhead.start();
|
nicholas@2498
|
1779 }
|
nicholas@2498
|
1780 };
|
nicholas@2224
|
1781
|
nicholas@2498
|
1782 this.stop = function () {
|
nicholas@2498
|
1783 // Send stop and reset command to all playback buffers
|
nicholas@2498
|
1784 if (this.status == 1) {
|
nicholas@2498
|
1785 var setTime = audioContext.currentTime + 0.1;
|
nicholas@2708
|
1786 this.audioObjects.forEach(function (a) {
|
nicholas@2942
|
1787 a.listenStop(setTime);
|
nicholas@2942
|
1788 a.bufferStop(setTime);
|
nicholas@2708
|
1789 });
|
nicholas@2498
|
1790 interfaceContext.playhead.stop();
|
nicholas@2498
|
1791 }
|
nicholas@2498
|
1792 };
|
nicholas@2498
|
1793
|
nicholas@2498
|
1794 this.newTrack = function (element) {
|
nicholas@2498
|
1795 // Pull data from given URL into new audio buffer
|
nicholas@2498
|
1796 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
|
nicholas@2498
|
1797
|
nicholas@2498
|
1798 // Create the audioObject with ID of the new track length;
|
nicholas@2708
|
1799 var audioObjectId = this.audioObjects.length;
|
nicholas@2498
|
1800 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
|
nicholas@2498
|
1801
|
nicholas@2498
|
1802 // Check if audioObject buffer is currently stored by full URL
|
nicholas@2498
|
1803 var URL = testState.currentStateMap.hostURL + element.url;
|
nicholas@2708
|
1804 var buffer = this.buffers.find(function (buffObj) {
|
nicholas@2708
|
1805 return buffObj.hasUrl(URL);
|
nicholas@2708
|
1806 });
|
nicholas@2708
|
1807 if (buffer === undefined) {
|
nicholas@2498
|
1808 console.log("[WARN]: Buffer was not loaded in pre-test! " + URL);
|
nicholas@2498
|
1809 buffer = new this.bufferObj();
|
nicholas@2224
|
1810 this.buffers.push(buffer);
|
nicholas@2498
|
1811 buffer.getMedia(URL);
|
nicholas@2498
|
1812 }
|
nicholas@2498
|
1813 this.audioObjects[audioObjectId].specification = element;
|
nicholas@2498
|
1814 this.audioObjects[audioObjectId].url = URL;
|
nicholas@2498
|
1815 // Obtain store node
|
nicholas@2498
|
1816 var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
|
nicholas@2498
|
1817 for (var i = 0; i < aeNodes.length; i++) {
|
nicholas@2498
|
1818 if (aeNodes[i].getAttribute("ref") == element.id) {
|
nicholas@2498
|
1819 this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
|
nicholas@2498
|
1820 break;
|
nicholas@2498
|
1821 }
|
nicholas@2498
|
1822 }
|
nicholas@2224
|
1823 buffer.registerAudioObject(this.audioObjects[audioObjectId]);
|
nicholas@2498
|
1824 return this.audioObjects[audioObjectId];
|
nicholas@2498
|
1825 };
|
nicholas@2498
|
1826
|
nicholas@2498
|
1827 this.newTestPage = function (audioHolderObject, store) {
|
nicholas@2498
|
1828 this.pageStore = store;
|
nicholas@2351
|
1829 this.pageSpecification = audioHolderObject;
|
nicholas@2498
|
1830 this.status = 0;
|
nicholas@2498
|
1831 this.audioObjectsReady = false;
|
nicholas@2498
|
1832 this.metric.reset();
|
nicholas@2708
|
1833 this.buffers.forEach(function (buffer) {
|
nicholas@2708
|
1834 buffer.users = [];
|
nicholas@2708
|
1835 });
|
nicholas@2498
|
1836 this.audioObjects = [];
|
nicholas@2224
|
1837 this.timer = new timer();
|
nicholas@2224
|
1838 this.loopPlayback = audioHolderObject.loop;
|
nicholas@2351
|
1839 this.synchPlayback = audioHolderObject.synchronous;
|
nicholas@2498
|
1840 };
|
nicholas@2498
|
1841
|
nicholas@2498
|
1842 this.checkAllPlayed = function () {
|
nicholas@2708
|
1843 var arr = [];
|
nicholas@2498
|
1844 for (var id = 0; id < this.audioObjects.length; id++) {
|
nicholas@2708
|
1845 if (this.audioObjects[id].metric.wasListenedTo === false) {
|
nicholas@2498
|
1846 arr.push(this.audioObjects[id].id);
|
nicholas@2498
|
1847 }
|
nicholas@2498
|
1848 }
|
nicholas@2498
|
1849 return arr;
|
nicholas@2498
|
1850 };
|
nicholas@2498
|
1851
|
nicholas@2498
|
1852 this.checkAllReady = function () {
|
nicholas@2498
|
1853 var ready = true;
|
nicholas@2498
|
1854 for (var i = 0; i < this.audioObjects.length; i++) {
|
nicholas@2708
|
1855 if (this.audioObjects[i].state === 0) {
|
nicholas@2498
|
1856 // Track not ready
|
nicholas@2498
|
1857 console.log('WAIT -- audioObject ' + i + ' not ready yet!');
|
nicholas@2498
|
1858 ready = false;
|
nicholas@2708
|
1859 }
|
nicholas@2498
|
1860 }
|
nicholas@2498
|
1861 return ready;
|
nicholas@2498
|
1862 };
|
nicholas@2498
|
1863
|
nicholas@2498
|
1864 this.setSynchronousLoop = function () {
|
nicholas@2570
|
1865 // Pads the signals so they are all exactly the same duration
|
nicholas@2570
|
1866 // Get the duration of the longest signal.
|
nicholas@2570
|
1867 var duration = 0;
|
nicholas@2498
|
1868 var maxId;
|
nicholas@2498
|
1869 for (var i = 0; i < this.audioObjects.length; i++) {
|
nicholas@2570
|
1870 if (duration < this.audioObjects[i].buffer.buffer.duration) {
|
nicholas@2570
|
1871 duration = this.audioObjects[i].buffer.buffer.duration;
|
nicholas@2498
|
1872 maxId = i;
|
nicholas@2498
|
1873 }
|
nicholas@2498
|
1874 }
|
nicholas@2498
|
1875 // Extract the audio and zero-pad
|
nicholas@2708
|
1876 this.audioObjects.forEach(function (ao) {
|
nicholas@2570
|
1877 if (ao.buffer.buffer.duration !== duration) {
|
nicholas@2570
|
1878 ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration);
|
nicholas@2500
|
1879 }
|
nicholas@2708
|
1880 });
|
nicholas@2498
|
1881 };
|
nicholas@2498
|
1882
|
nicholas@2498
|
1883 this.bufferReady = function (id) {
|
nicholas@2498
|
1884 if (this.checkAllReady()) {
|
nicholas@2498
|
1885 if (this.synchPlayback) {
|
nicholas@2498
|
1886 this.setSynchronousLoop();
|
nicholas@2498
|
1887 }
|
nicholas@2460
|
1888 this.status = 1;
|
nicholas@2460
|
1889 return true;
|
nicholas@2460
|
1890 }
|
nicholas@2460
|
1891 return false;
|
nicholas@2224
|
1892 };
|
nicholas@2498
|
1893
|
nicholas@2224
|
1894 }
|
nicholas@2224
|
1895
|
nicholas@2224
|
1896 function audioObject(id) {
|
nicholas@2498
|
1897 // The main buffer object with common control nodes to the AudioEngine
|
nicholas@2498
|
1898
|
nicholas@2823
|
1899 var playCounter = 0;
|
nicholas@2823
|
1900
|
nicholas@2708
|
1901 this.specification = undefined;
|
nicholas@2498
|
1902 this.id = id;
|
nicholas@2498
|
1903 this.state = 0; // 0 - no data, 1 - ready
|
nicholas@2498
|
1904 this.url = null; // Hold the URL given for the output back to the results.
|
nicholas@2498
|
1905 this.metric = new metricTracker(this);
|
nicholas@2498
|
1906 this.storeDOM = null;
|
nicholas@2498
|
1907
|
nicholas@2498
|
1908 // Bindings for GUI
|
nicholas@2498
|
1909 this.interfaceDOM = null;
|
nicholas@2498
|
1910 this.commentDOM = null;
|
nicholas@2498
|
1911
|
nicholas@2498
|
1912 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
|
nicholas@2498
|
1913 this.bufferNode = undefined;
|
nicholas@2498
|
1914 this.outputGain = audioContext.createGain();
|
nicholas@2942
|
1915 this.outputGain.gain.value = 0.0;
|
nicholas@2498
|
1916
|
nicholas@2498
|
1917 this.onplayGain = 1.0;
|
nicholas@2498
|
1918
|
nicholas@2498
|
1919 // Connect buffer to the audio graph
|
nicholas@2498
|
1920 this.outputGain.connect(audioEngineContext.outputGain);
|
nicholas@2508
|
1921 audioEngineContext.nullBufferSource.connect(this.outputGain);
|
nicholas@2498
|
1922
|
nicholas@2498
|
1923 // the audiobuffer is not designed for multi-start playback
|
nicholas@2498
|
1924 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
|
nicholas@2708
|
1925 this.buffer = undefined;
|
nicholas@2498
|
1926
|
nicholas@2498
|
1927 this.bufferLoaded = function (callee) {
|
nicholas@2498
|
1928 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
|
nicholas@2498
|
1929 // audioObject and trigger the interfaceDOM.enable() function for user feedback
|
nicholas@2224
|
1930 if (callee.status == -1) {
|
nicholas@2224
|
1931 // ERROR
|
nicholas@2224
|
1932 this.state = -1;
|
nicholas@2708
|
1933 if (this.interfaceDOM !== null) {
|
nicholas@2498
|
1934 this.interfaceDOM.error();
|
nicholas@2498
|
1935 }
|
nicholas@2224
|
1936 this.buffer = callee;
|
nicholas@2224
|
1937 return;
|
nicholas@2224
|
1938 }
|
nicholas@2224
|
1939 this.buffer = callee;
|
nicholas@2224
|
1940 var preSilenceTime = this.specification.preSilence || this.specification.parent.preSilence || specification.preSilence || 0.0;
|
nicholas@2224
|
1941 var postSilenceTime = this.specification.postSilence || this.specification.parent.postSilence || specification.postSilence || 0.0;
|
nicholas@2460
|
1942 var startTime = this.specification.startTime;
|
nicholas@2460
|
1943 var stopTime = this.specification.stopTime;
|
nicholas@2460
|
1944 var copybuffer = new callee.constructor();
|
nicholas@2500
|
1945
|
nicholas@2500
|
1946 copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration);
|
nicholas@2708
|
1947 if (preSilenceTime !== 0 || postSilenceTime !== 0) {
|
nicholas@2500
|
1948 copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime);
|
nicholas@2460
|
1949 }
|
nicholas@2500
|
1950
|
nicholas@2660
|
1951 copybuffer.buffer.lufs = callee.buffer.lufs;
|
nicholas@2500
|
1952 this.buffer = copybuffer;
|
nicholas@2498
|
1953
|
nicholas@2661
|
1954 var targetLUFS = this.specification.loudness || this.specification.parent.loudness || specification.loudness;
|
nicholas@2498
|
1955 if (typeof targetLUFS === "number" && isFinite(targetLUFS)) {
|
nicholas@2498
|
1956 this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
|
nicholas@2498
|
1957 } else {
|
nicholas@2498
|
1958 this.buffer.buffer.playbackGain = 1.0;
|
nicholas@2498
|
1959 }
|
nicholas@2708
|
1960 if (this.interfaceDOM !== null) {
|
nicholas@2498
|
1961 this.interfaceDOM.enable();
|
nicholas@2498
|
1962 }
|
nicholas@2498
|
1963 this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0);
|
nicholas@2498
|
1964 this.storeDOM.setAttribute('playGain', linearToDecibel(this.onplayGain));
|
nicholas@2460
|
1965 this.state = 1;
|
nicholas@2460
|
1966 audioEngineContext.bufferReady(id);
|
nicholas@2498
|
1967 };
|
nicholas@2498
|
1968
|
nicholas@2498
|
1969 this.bindInterface = function (interfaceObject) {
|
nicholas@2498
|
1970 this.interfaceDOM = interfaceObject;
|
nicholas@2498
|
1971 this.metric.initialise(interfaceObject.getValue());
|
nicholas@2498
|
1972 if (this.state == 1) {
|
nicholas@2498
|
1973 this.interfaceDOM.enable();
|
nicholas@2498
|
1974 } else if (this.state == -1) {
|
nicholas@2224
|
1975 // ERROR
|
nicholas@2224
|
1976 this.interfaceDOM.error();
|
nicholas@2224
|
1977 return;
|
nicholas@2224
|
1978 }
|
nicholas@2498
|
1979 this.storeDOM.setAttribute('presentedId', interfaceObject.getPresentedId());
|
nicholas@2498
|
1980 };
|
nicholas@2498
|
1981
|
nicholas@2942
|
1982 this.listenStart = function (setTime) {
|
nicholas@2942
|
1983 if (this.outputGain.gain.value !== this.onplayGain) {
|
nicholas@2942
|
1984 playCounter++;
|
nicholas@2942
|
1985 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, setTime);
|
nicholas@2942
|
1986 this.metric.startListening(audioEngineContext.timer.getTestTime());
|
nicholas@2942
|
1987 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
|
nicholas@2942
|
1988 this.interfaceDOM.startPlayback();
|
nicholas@2942
|
1989 }
|
nicholas@2498
|
1990 };
|
nicholas@2498
|
1991
|
nicholas@2942
|
1992 this.listenStop = function (setTime) {
|
nicholas@2708
|
1993 if (this.outputGain.gain.value !== 0.0) {
|
nicholas@2498
|
1994 this.outputGain.gain.linearRampToValueAtTime(0.0, setTime);
|
nicholas@2942
|
1995 this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
|
nicholas@2498
|
1996 }
|
nicholas@2224
|
1997 this.interfaceDOM.stopPlayback();
|
nicholas@2498
|
1998 };
|
nicholas@2498
|
1999
|
nicholas@2942
|
2000 this.setupPlayback = function () {
|
nicholas@2708
|
2001 if (this.bufferNode === undefined && this.buffer.buffer !== undefined) {
|
nicholas@2498
|
2002 this.bufferNode = audioContext.createBufferSource();
|
nicholas@2498
|
2003 this.bufferNode.owner = this;
|
nicholas@2498
|
2004 this.bufferNode.connect(this.outputGain);
|
nicholas@2498
|
2005 this.bufferNode.buffer = this.buffer.buffer;
|
nicholas@2942
|
2006 if (audioEngineContext.loopPlayback) {
|
nicholas@2942
|
2007 this.bufferNode.loopStart = this.specification.startTime || 0;
|
nicholas@2942
|
2008 this.bufferNode.loopEnd = this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration;
|
nicholas@2942
|
2009 this.bufferNode.loop = true;
|
nicholas@2942
|
2010 }
|
nicholas@2498
|
2011 this.bufferNode.onended = function (event) {
|
nicholas@2498
|
2012 // Safari does not like using 'this' to reference the calling object!
|
nicholas@2498
|
2013 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
|
nicholas@2708
|
2014 if (event.currentTarget !== null) {
|
nicholas@2942
|
2015 event.currentTarget.owner.bufferStop(audioContext.currentTime + 0.1);
|
nicholas@2224
|
2016 }
|
nicholas@2498
|
2017 };
|
nicholas@2942
|
2018 this.bufferNode.state = 0;
|
nicholas@2942
|
2019 }
|
nicholas@2942
|
2020 };
|
nicholas@2942
|
2021
|
nicholas@2942
|
2022 this.bufferStart = function (startTime) {
|
nicholas@2942
|
2023 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
|
nicholas@2942
|
2024 if (this.bufferNode && this.bufferNode.state == 0) {
|
nicholas@2942
|
2025 this.bufferNode.state = 1;
|
nicholas@2942
|
2026 if (this.bufferNode.loop == true) {
|
nicholas@2499
|
2027 this.bufferNode.start(startTime);
|
nicholas@2499
|
2028 } else {
|
nicholas@2499
|
2029 this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration);
|
nicholas@2499
|
2030 }
|
nicholas@2498
|
2031 }
|
nicholas@2498
|
2032 };
|
nicholas@2498
|
2033
|
nicholas@2942
|
2034 this.bufferStop = function (stopTime) {
|
nicholas@2224
|
2035 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
|
nicholas@2942
|
2036 if (this.bufferNode !== undefined && this.bufferNode.state > 0) {
|
nicholas@2498
|
2037 this.bufferNode.stop(stopTime);
|
nicholas@2498
|
2038 this.bufferNode = undefined;
|
nicholas@2498
|
2039 }
|
nicholas@2529
|
2040 this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime);
|
nicholas@2224
|
2041 this.interfaceDOM.stopPlayback();
|
nicholas@2498
|
2042 };
|
nicholas@2498
|
2043
|
nicholas@2498
|
2044 this.getCurrentPosition = function () {
|
nicholas@2498
|
2045 var time = audioEngineContext.timer.getTestTime();
|
nicholas@2708
|
2046 if (this.bufferNode !== undefined) {
|
nicholas@2498
|
2047 var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
|
nicholas@2498
|
2048 if (isNaN(position)) {
|
nicholas@2498
|
2049 return 0;
|
nicholas@2498
|
2050 }
|
nicholas@2224
|
2051 return position;
|
nicholas@2498
|
2052 } else {
|
nicholas@2498
|
2053 return 0;
|
nicholas@2498
|
2054 }
|
nicholas@2498
|
2055 };
|
nicholas@2498
|
2056
|
nicholas@2498
|
2057 this.exportXMLDOM = function () {
|
nicholas@2498
|
2058 var file = storage.document.createElement('file');
|
nicholas@2498
|
2059 file.setAttribute('sampleRate', this.buffer.buffer.sampleRate);
|
nicholas@2498
|
2060 file.setAttribute('channels', this.buffer.buffer.numberOfChannels);
|
nicholas@2498
|
2061 file.setAttribute('sampleCount', this.buffer.buffer.length);
|
nicholas@2498
|
2062 file.setAttribute('duration', this.buffer.buffer.duration);
|
nicholas@2498
|
2063 this.storeDOM.appendChild(file);
|
nicholas@2498
|
2064 if (this.specification.type != 'outside-reference') {
|
nicholas@2498
|
2065 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
|
nicholas@2708
|
2066 if (interfaceXML !== null) {
|
nicholas@2708
|
2067 if (interfaceXML.length === undefined) {
|
nicholas@2498
|
2068 this.storeDOM.appendChild(interfaceXML);
|
nicholas@2498
|
2069 } else {
|
nicholas@2498
|
2070 for (var i = 0; i < interfaceXML.length; i++) {
|
nicholas@2498
|
2071 this.storeDOM.appendChild(interfaceXML[i]);
|
nicholas@2498
|
2072 }
|
nicholas@2498
|
2073 }
|
nicholas@2498
|
2074 }
|
nicholas@2708
|
2075 if (this.commentDOM !== null) {
|
nicholas@2498
|
2076 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
|
nicholas@2498
|
2077 }
|
nicholas@2498
|
2078 }
|
nicholas@2708
|
2079 this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]);
|
nicholas@2498
|
2080 };
|
nicholas@2823
|
2081
|
nicholas@2823
|
2082 Object.defineProperties(this, {
|
nicholas@2823
|
2083 "numberOfPlays": {
|
nicholas@2823
|
2084 'get': function () {
|
nicholas@2823
|
2085 return playCounter;
|
nicholas@2823
|
2086 },
|
nicholas@2823
|
2087 'set': function () {
|
nicholas@2823
|
2088 return playCounter;
|
nicholas@2823
|
2089 }
|
nicholas@2823
|
2090 }
|
nicholas@2823
|
2091 });
|
nicholas@2224
|
2092 }
|
nicholas@2224
|
2093
|
nicholas@2498
|
2094 function timer() {
|
nicholas@2498
|
2095 /* Timer object used in audioEngine to keep track of session timings
|
nicholas@2498
|
2096 * Uses the timer of the web audio API, so sample resolution
|
nicholas@2498
|
2097 */
|
nicholas@2498
|
2098 this.testStarted = false;
|
nicholas@2498
|
2099 this.testStartTime = 0;
|
nicholas@2498
|
2100 this.testDuration = 0;
|
nicholas@2498
|
2101 this.minimumTestTime = 0; // No minimum test time
|
nicholas@2498
|
2102 this.startTest = function () {
|
nicholas@2708
|
2103 if (this.testStarted === false) {
|
nicholas@2498
|
2104 this.testStartTime = audioContext.currentTime;
|
nicholas@2498
|
2105 this.testStarted = true;
|
nicholas@2498
|
2106 this.updateTestTime();
|
nicholas@2498
|
2107 audioEngineContext.metric.initialiseTest();
|
nicholas@2498
|
2108 }
|
nicholas@2498
|
2109 };
|
nicholas@2498
|
2110 this.stopTest = function () {
|
nicholas@2498
|
2111 if (this.testStarted) {
|
nicholas@2498
|
2112 this.testDuration = this.getTestTime();
|
nicholas@2498
|
2113 this.testStarted = false;
|
nicholas@2498
|
2114 } else {
|
nicholas@2498
|
2115 console.log('ERR: Test tried to end before beginning');
|
nicholas@2498
|
2116 }
|
nicholas@2498
|
2117 };
|
nicholas@2498
|
2118 this.updateTestTime = function () {
|
nicholas@2498
|
2119 if (this.testStarted) {
|
nicholas@2498
|
2120 this.testDuration = audioContext.currentTime - this.testStartTime;
|
nicholas@2498
|
2121 }
|
nicholas@2498
|
2122 };
|
nicholas@2498
|
2123 this.getTestTime = function () {
|
nicholas@2498
|
2124 this.updateTestTime();
|
nicholas@2498
|
2125 return this.testDuration;
|
nicholas@2498
|
2126 };
|
nicholas@2224
|
2127 }
|
nicholas@2224
|
2128
|
nicholas@2498
|
2129 function sessionMetrics(engine, specification) {
|
nicholas@2498
|
2130 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
|
nicholas@2498
|
2131 */
|
nicholas@2498
|
2132 this.engine = engine;
|
nicholas@2498
|
2133 this.lastClicked = -1;
|
nicholas@2498
|
2134 this.data = -1;
|
nicholas@2498
|
2135 this.reset = function () {
|
nicholas@2498
|
2136 this.lastClicked = -1;
|
nicholas@2498
|
2137 this.data = -1;
|
nicholas@2498
|
2138 };
|
nicholas@2498
|
2139
|
nicholas@2498
|
2140 this.enableElementInitialPosition = false;
|
nicholas@2498
|
2141 this.enableElementListenTracker = false;
|
nicholas@2498
|
2142 this.enableElementTimer = false;
|
nicholas@2498
|
2143 this.enableElementTracker = false;
|
nicholas@2498
|
2144 this.enableFlagListenedTo = false;
|
nicholas@2498
|
2145 this.enableFlagMoved = false;
|
nicholas@2498
|
2146 this.enableTestTimer = false;
|
nicholas@2498
|
2147 // Obtain the metrics enabled
|
nicholas@2498
|
2148 for (var i = 0; i < specification.metrics.enabled.length; i++) {
|
nicholas@2498
|
2149 var node = specification.metrics.enabled[i];
|
nicholas@2498
|
2150 switch (node) {
|
nicholas@2498
|
2151 case 'testTimer':
|
nicholas@2498
|
2152 this.enableTestTimer = true;
|
nicholas@2498
|
2153 break;
|
nicholas@2498
|
2154 case 'elementTimer':
|
nicholas@2498
|
2155 this.enableElementTimer = true;
|
nicholas@2498
|
2156 break;
|
nicholas@2498
|
2157 case 'elementTracker':
|
nicholas@2498
|
2158 this.enableElementTracker = true;
|
nicholas@2498
|
2159 break;
|
nicholas@2498
|
2160 case 'elementListenTracker':
|
nicholas@2498
|
2161 this.enableElementListenTracker = true;
|
nicholas@2498
|
2162 break;
|
nicholas@2498
|
2163 case 'elementInitialPosition':
|
nicholas@2498
|
2164 this.enableElementInitialPosition = true;
|
nicholas@2498
|
2165 break;
|
nicholas@2498
|
2166 case 'elementFlagListenedTo':
|
nicholas@2498
|
2167 this.enableFlagListenedTo = true;
|
nicholas@2498
|
2168 break;
|
nicholas@2498
|
2169 case 'elementFlagMoved':
|
nicholas@2498
|
2170 this.enableFlagMoved = true;
|
nicholas@2498
|
2171 break;
|
nicholas@2498
|
2172 case 'elementFlagComments':
|
nicholas@2498
|
2173 this.enableFlagComments = true;
|
nicholas@2498
|
2174 break;
|
nicholas@2498
|
2175 }
|
nicholas@2498
|
2176 }
|
nicholas@2498
|
2177 this.initialiseTest = function () {};
|
nicholas@2224
|
2178 }
|
nicholas@2224
|
2179
|
nicholas@2498
|
2180 function metricTracker(caller) {
|
nicholas@2498
|
2181 /* Custom object to track and collect metric data
|
nicholas@2498
|
2182 * Used only inside the audioObjects object.
|
nicholas@2498
|
2183 */
|
nicholas@2498
|
2184
|
nicholas@2498
|
2185 this.listenedTimer = 0;
|
nicholas@2498
|
2186 this.listenStart = 0;
|
nicholas@2498
|
2187 this.listenHold = false;
|
nicholas@2498
|
2188 this.initialPosition = -1;
|
nicholas@2498
|
2189 this.movementTracker = [];
|
nicholas@2498
|
2190 this.listenTracker = [];
|
nicholas@2498
|
2191 this.wasListenedTo = false;
|
nicholas@2498
|
2192 this.wasMoved = false;
|
nicholas@2498
|
2193 this.hasComments = false;
|
nicholas@2498
|
2194 this.parent = caller;
|
nicholas@2498
|
2195
|
nicholas@2498
|
2196 this.initialise = function (position) {
|
nicholas@2498
|
2197 if (this.initialPosition == -1) {
|
nicholas@2498
|
2198 this.initialPosition = position;
|
nicholas@2498
|
2199 this.moved(0, position);
|
nicholas@2498
|
2200 }
|
nicholas@2498
|
2201 };
|
nicholas@2498
|
2202
|
nicholas@2498
|
2203 this.moved = function (time, position) {
|
nicholas@2498
|
2204 if (time > 0) {
|
nicholas@2498
|
2205 this.wasMoved = true;
|
nicholas@2498
|
2206 }
|
nicholas@2498
|
2207 this.movementTracker[this.movementTracker.length] = [time, position];
|
nicholas@2498
|
2208 };
|
nicholas@2498
|
2209
|
nicholas@2498
|
2210 this.startListening = function (time) {
|
nicholas@2708
|
2211 if (this.listenHold === false) {
|
nicholas@2498
|
2212 this.wasListenedTo = true;
|
nicholas@2498
|
2213 this.listenStart = time;
|
nicholas@2498
|
2214 this.listenHold = true;
|
nicholas@2498
|
2215
|
nicholas@2498
|
2216 var evnt = document.createElement('event');
|
nicholas@2498
|
2217 var testTime = document.createElement('testTime');
|
nicholas@2498
|
2218 testTime.setAttribute('start', time);
|
nicholas@2498
|
2219 var bufferTime = document.createElement('bufferTime');
|
nicholas@2498
|
2220 bufferTime.setAttribute('start', this.parent.getCurrentPosition());
|
nicholas@2498
|
2221 evnt.appendChild(testTime);
|
nicholas@2498
|
2222 evnt.appendChild(bufferTime);
|
nicholas@2498
|
2223 this.listenTracker.push(evnt);
|
nicholas@2498
|
2224
|
nicholas@2498
|
2225 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
|
nicholas@2498
|
2226 }
|
nicholas@2498
|
2227 };
|
nicholas@2498
|
2228
|
nicholas@2498
|
2229 this.stopListening = function (time, bufferStopTime) {
|
nicholas@2708
|
2230 if (this.listenHold === true) {
|
nicholas@2498
|
2231 var diff = time - this.listenStart;
|
nicholas@2498
|
2232 this.listenedTimer += (diff);
|
nicholas@2498
|
2233 this.listenStart = 0;
|
nicholas@2498
|
2234 this.listenHold = false;
|
nicholas@2498
|
2235
|
nicholas@2498
|
2236 var evnt = this.listenTracker[this.listenTracker.length - 1];
|
nicholas@2498
|
2237 var testTime = evnt.getElementsByTagName('testTime')[0];
|
nicholas@2498
|
2238 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
|
nicholas@2498
|
2239 testTime.setAttribute('stop', time);
|
nicholas@2708
|
2240 if (bufferStopTime === undefined) {
|
nicholas@2498
|
2241 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
|
nicholas@2498
|
2242 } else {
|
nicholas@2498
|
2243 bufferTime.setAttribute('stop', bufferStopTime);
|
nicholas@2498
|
2244 }
|
nicholas@2498
|
2245 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
|
nicholas@2498
|
2246 }
|
nicholas@2498
|
2247 };
|
nicholas@2498
|
2248
|
nicholas@2708
|
2249 function exportElementTimer(parentElement) {
|
nicholas@2708
|
2250 var mElementTimer = storage.document.createElement('metricresult');
|
nicholas@2708
|
2251 mElementTimer.setAttribute('name', 'enableElementTimer');
|
nicholas@2708
|
2252 mElementTimer.textContent = this.listenedTimer;
|
nicholas@2708
|
2253 parentElement.appendChild(mElementTimer);
|
nicholas@2708
|
2254 return mElementTimer;
|
nicholas@2708
|
2255 }
|
nicholas@2708
|
2256
|
nicholas@2708
|
2257 function exportElementTrack(parentElement) {
|
nicholas@2708
|
2258 var elementTrackerFull = storage.document.createElement('metricresult');
|
nicholas@2708
|
2259 elementTrackerFull.setAttribute('name', 'elementTrackerFull');
|
nicholas@2708
|
2260 for (var k = 0; k < this.movementTracker.length; k++) {
|
nicholas@2708
|
2261 var timePos = storage.document.createElement('movement');
|
nicholas@2708
|
2262 timePos.setAttribute("time", this.movementTracker[k][0]);
|
nicholas@2708
|
2263 timePos.setAttribute("value", this.movementTracker[k][1]);
|
nicholas@2708
|
2264 elementTrackerFull.appendChild(timePos);
|
nicholas@2708
|
2265 }
|
nicholas@2708
|
2266 parentElement.appendChild(elementTrackerFull);
|
nicholas@2708
|
2267 return elementTrackerFull;
|
nicholas@2708
|
2268 }
|
nicholas@2708
|
2269
|
nicholas@2708
|
2270 function exportElementListenTracker(parentElement) {
|
nicholas@2708
|
2271 var elementListenTracker = storage.document.createElement('metricresult');
|
nicholas@2708
|
2272 elementListenTracker.setAttribute('name', 'elementListenTracker');
|
nicholas@2708
|
2273 for (var k = 0; k < this.listenTracker.length; k++) {
|
nicholas@2708
|
2274 elementListenTracker.appendChild(this.listenTracker[k]);
|
nicholas@2708
|
2275 }
|
nicholas@2708
|
2276 parentElement.appendChild(elementListenTracker);
|
nicholas@2708
|
2277 return elementListenTracker;
|
nicholas@2708
|
2278 }
|
nicholas@2708
|
2279
|
nicholas@2708
|
2280 function exportElementInitialPosition(parentElement) {
|
nicholas@2708
|
2281 var elementInitial = storage.document.createElement('metricresult');
|
nicholas@2708
|
2282 elementInitial.setAttribute('name', 'elementInitialPosition');
|
nicholas@2708
|
2283 elementInitial.textContent = this.initialPosition;
|
nicholas@2708
|
2284 parentElement.appendChild(elementInitial);
|
nicholas@2708
|
2285 return elementInitial;
|
nicholas@2708
|
2286 }
|
nicholas@2708
|
2287
|
nicholas@2708
|
2288 function exportFlagListenedTo(parentElement) {
|
nicholas@2708
|
2289 var flagListenedTo = storage.document.createElement('metricresult');
|
nicholas@2708
|
2290 flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
|
nicholas@2708
|
2291 flagListenedTo.textContent = this.wasListenedTo;
|
nicholas@2708
|
2292 parentElement.appendChild(flagListenedTo);
|
nicholas@2708
|
2293 return flagListenedTo;
|
nicholas@2708
|
2294 }
|
nicholas@2708
|
2295
|
nicholas@2708
|
2296 function exportFlagMoved(parentElement) {
|
nicholas@2708
|
2297 var flagMoved = storage.document.createElement('metricresult');
|
nicholas@2708
|
2298 flagMoved.setAttribute('name', 'elementFlagMoved');
|
nicholas@2708
|
2299 flagMoved.textContent = this.wasMoved;
|
nicholas@2708
|
2300 parentElement.appendChild(flagMoved);
|
nicholas@2708
|
2301 return flagMoved;
|
nicholas@2708
|
2302 }
|
nicholas@2708
|
2303
|
nicholas@2708
|
2304 function exportFlagComments(parentElement) {
|
nicholas@2708
|
2305 var flagComments = storage.document.createElement('metricresult');
|
nicholas@2708
|
2306 flagComments.setAttribute('name', 'elementFlagComments');
|
nicholas@2708
|
2307 if (this.parent.commentDOM === null) {
|
nicholas@2708
|
2308 flagComments.textContent = 'false';
|
nicholas@2708
|
2309 } else if (this.parent.commentDOM.textContent.length === 0) {
|
nicholas@2708
|
2310 flagComments.textContent = 'false';
|
nicholas@2708
|
2311 } else {
|
nicholas@2708
|
2312 flagComments.textContet = 'true';
|
nicholas@2708
|
2313 }
|
nicholas@2708
|
2314 parentElement.appendChild(flagComments);
|
nicholas@2708
|
2315 return flagComments;
|
nicholas@2708
|
2316 }
|
nicholas@2708
|
2317
|
nicholas@2708
|
2318 this.exportXMLDOM = function (parentElement) {
|
nicholas@2708
|
2319 var elems = [];
|
nicholas@2498
|
2320 if (audioEngineContext.metric.enableElementTimer) {
|
nicholas@2708
|
2321 elems.push(exportElementTimer.call(this, parentElement));
|
nicholas@2498
|
2322 }
|
nicholas@2498
|
2323 if (audioEngineContext.metric.enableElementTracker) {
|
nicholas@2708
|
2324 elems.push(exportElementTrack.call(this, parentElement));
|
nicholas@2498
|
2325 }
|
nicholas@2498
|
2326 if (audioEngineContext.metric.enableElementListenTracker) {
|
nicholas@2708
|
2327 elems.push(exportElementListenTracker.call(this, parentElement));
|
nicholas@2498
|
2328 }
|
nicholas@2498
|
2329 if (audioEngineContext.metric.enableElementInitialPosition) {
|
nicholas@2708
|
2330 elems.push(exportElementInitialPosition.call(this, parentElement));
|
nicholas@2498
|
2331 }
|
nicholas@2498
|
2332 if (audioEngineContext.metric.enableFlagListenedTo) {
|
nicholas@2708
|
2333 elems.push(exportFlagListenedTo.call(this, parentElement));
|
nicholas@2498
|
2334 }
|
nicholas@2498
|
2335 if (audioEngineContext.metric.enableFlagMoved) {
|
nicholas@2708
|
2336 elems.push(exportFlagMoved.call(this, parentElement));
|
nicholas@2498
|
2337 }
|
nicholas@2498
|
2338 if (audioEngineContext.metric.enableFlagComments) {
|
nicholas@2708
|
2339 elems.push(exportFlagComments.call(this, parentElement));
|
nicholas@2498
|
2340 }
|
nicholas@2708
|
2341 return elems;
|
nicholas@2498
|
2342 };
|
nicholas@2224
|
2343 }
|
nicholas@2498
|
2344
|
nicholas@2224
|
2345 function Interface(specificationObject) {
|
nicholas@2498
|
2346 // This handles the bindings between the interface and the audioEngineContext;
|
nicholas@2498
|
2347 this.specification = specificationObject;
|
nicholas@2498
|
2348 this.insertPoint = document.getElementById("topLevelBody");
|
nicholas@2498
|
2349
|
nicholas@2498
|
2350 this.newPage = function (audioHolderObject, store) {
|
nicholas@2498
|
2351 audioEngineContext.newTestPage(audioHolderObject, store);
|
nicholas@2498
|
2352 interfaceContext.commentBoxes.deleteCommentBoxes();
|
nicholas@2498
|
2353 interfaceContext.deleteCommentQuestions();
|
nicholas@2498
|
2354 loadTest(audioHolderObject, store);
|
nicholas@2498
|
2355 };
|
nicholas@2498
|
2356
|
nicholas@2498
|
2357 // Bounded by interface!!
|
nicholas@2498
|
2358 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
|
nicholas@2498
|
2359 // For example, APE returns the slider position normalised in a <value> tag.
|
nicholas@2498
|
2360 this.interfaceObjects = [];
|
nicholas@2498
|
2361 this.interfaceObject = function () {};
|
nicholas@2498
|
2362
|
nicholas@2498
|
2363 this.resizeWindow = function (event) {
|
nicholas@2498
|
2364 popup.resize(event);
|
nicholas@2352
|
2365 this.volume.resize();
|
nicholas@2360
|
2366 this.lightbox.resize();
|
n@2718
|
2367 this.commentBoxes.boxes.forEach(function (elem) {
|
nicholas@2708
|
2368 elem.resize();
|
nicholas@2708
|
2369 });
|
nicholas@2708
|
2370 this.commentQuestions.forEach(function (elem) {
|
nicholas@2708
|
2371 elem.resize();
|
nicholas@2708
|
2372 });
|
nicholas@2498
|
2373 try {
|
nicholas@2498
|
2374 resizeWindow(event);
|
nicholas@2498
|
2375 } catch (err) {
|
nicholas@2498
|
2376 console.log("Warning - Interface does not have Resize option");
|
nicholas@2498
|
2377 console.log(err);
|
nicholas@2498
|
2378 }
|
nicholas@2498
|
2379 };
|
nicholas@2498
|
2380
|
nicholas@2498
|
2381 this.returnNavigator = function () {
|
nicholas@2498
|
2382 var node = storage.document.createElement("navigator");
|
nicholas@2498
|
2383 var platform = storage.document.createElement("platform");
|
nicholas@2498
|
2384 platform.textContent = navigator.platform;
|
nicholas@2498
|
2385 var vendor = storage.document.createElement("vendor");
|
nicholas@2498
|
2386 vendor.textContent = navigator.vendor;
|
nicholas@2498
|
2387 var userAgent = storage.document.createElement("uagent");
|
nicholas@2498
|
2388 userAgent.textContent = navigator.userAgent;
|
nicholas@2224
|
2389 var screen = storage.document.createElement("window");
|
nicholas@2498
|
2390 screen.setAttribute('innerWidth', window.innerWidth);
|
nicholas@2498
|
2391 screen.setAttribute('innerHeight', window.innerHeight);
|
nicholas@2498
|
2392 node.appendChild(platform);
|
nicholas@2498
|
2393 node.appendChild(vendor);
|
nicholas@2498
|
2394 node.appendChild(userAgent);
|
nicholas@2224
|
2395 node.appendChild(screen);
|
nicholas@2498
|
2396 return node;
|
nicholas@2498
|
2397 };
|
nicholas@2498
|
2398
|
nicholas@2498
|
2399 this.returnDateNode = function () {
|
nicholas@2224
|
2400 // Create an XML Node for the Date and Time a test was conducted
|
nicholas@2224
|
2401 // Structure is
|
nicholas@2224
|
2402 // <datetime>
|
nicholas@2224
|
2403 // <date year="##" month="##" day="##">DD/MM/YY</date>
|
nicholas@2224
|
2404 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
|
nicholas@2224
|
2405 // </datetime>
|
nicholas@2224
|
2406 var dateTime = new Date();
|
nicholas@2224
|
2407 var hold = storage.document.createElement("datetime");
|
nicholas@2224
|
2408 var date = storage.document.createElement("date");
|
nicholas@2224
|
2409 var time = storage.document.createElement("time");
|
nicholas@2498
|
2410 date.setAttribute('year', dateTime.getFullYear());
|
nicholas@2498
|
2411 date.setAttribute('month', dateTime.getMonth() + 1);
|
nicholas@2498
|
2412 date.setAttribute('day', dateTime.getDate());
|
nicholas@2498
|
2413 time.setAttribute('hour', dateTime.getHours());
|
nicholas@2498
|
2414 time.setAttribute('minute', dateTime.getMinutes());
|
nicholas@2498
|
2415 time.setAttribute('secs', dateTime.getSeconds());
|
nicholas@2498
|
2416
|
nicholas@2224
|
2417 hold.appendChild(date);
|
nicholas@2224
|
2418 hold.appendChild(time);
|
nicholas@2224
|
2419 return hold;
|
nicholas@2224
|
2420
|
nicholas@2708
|
2421 };
|
nicholas@2498
|
2422
|
nicholas@2360
|
2423 this.lightbox = {
|
nicholas@2360
|
2424 parent: this,
|
nicholas@2360
|
2425 root: document.createElement("div"),
|
nicholas@2360
|
2426 content: document.createElement("div"),
|
nicholas@2360
|
2427 accept: document.createElement("button"),
|
nicholas@2360
|
2428 blanker: document.createElement("div"),
|
nicholas@2498
|
2429 post: function (type, message) {
|
nicholas@2498
|
2430 switch (type) {
|
nicholas@2360
|
2431 case "Error":
|
nicholas@2360
|
2432 this.content.className = "lightbox-error";
|
nicholas@2360
|
2433 break;
|
nicholas@2360
|
2434 case "Warning":
|
nicholas@2360
|
2435 this.content.className = "lightbox-warning";
|
nicholas@2360
|
2436 break;
|
nicholas@2360
|
2437 default:
|
nicholas@2360
|
2438 this.content.className = "lightbox-message";
|
nicholas@2360
|
2439 break;
|
nicholas@2360
|
2440 }
|
nicholas@2360
|
2441 var msg = document.createElement("p");
|
nicholas@2360
|
2442 msg.textContent = message;
|
nicholas@2360
|
2443 this.content.appendChild(msg);
|
nicholas@2360
|
2444 this.show();
|
nicholas@2360
|
2445 },
|
nicholas@2498
|
2446 show: function () {
|
nicholas@2360
|
2447 this.root.style.visibility = "visible";
|
nicholas@2360
|
2448 this.blanker.style.visibility = "visible";
|
n@2914
|
2449 this.accept.focus();
|
nicholas@2360
|
2450 },
|
nicholas@2498
|
2451 clear: function () {
|
nicholas@2360
|
2452 this.root.style.visibility = "";
|
nicholas@2360
|
2453 this.blanker.style.visibility = "";
|
nicholas@2360
|
2454 this.content.textContent = "";
|
nicholas@2360
|
2455 },
|
nicholas@2498
|
2456 handleEvent: function (event) {
|
nicholas@2360
|
2457 if (event.currentTarget == this.accept) {
|
nicholas@2360
|
2458 this.clear();
|
nicholas@2360
|
2459 }
|
nicholas@2360
|
2460 },
|
nicholas@2498
|
2461 resize: function (event) {
|
nicholas@2498
|
2462 this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
|
n@2915
|
2463 },
|
n@2915
|
2464 isVisible: function () {
|
n@2915
|
2465 return this.root.style.visibility == "visible";
|
nicholas@2360
|
2466 }
|
nicholas@2708
|
2467 };
|
nicholas@2498
|
2468
|
nicholas@2360
|
2469 this.lightbox.root.appendChild(this.lightbox.content);
|
nicholas@2360
|
2470 this.lightbox.root.appendChild(this.lightbox.accept);
|
nicholas@2360
|
2471 this.lightbox.root.className = "popupHolder";
|
nicholas@2360
|
2472 this.lightbox.root.id = "lightbox-root";
|
nicholas@2360
|
2473 this.lightbox.accept.className = "popupButton";
|
nicholas@2360
|
2474 this.lightbox.accept.style.bottom = "10px";
|
nicholas@2360
|
2475 this.lightbox.accept.textContent = "OK";
|
nicholas@2360
|
2476 this.lightbox.accept.style.left = "237.5px";
|
nicholas@2498
|
2477 this.lightbox.accept.addEventListener("click", this.lightbox);
|
nicholas@2360
|
2478 this.lightbox.blanker.className = "testHalt";
|
nicholas@2360
|
2479 this.lightbox.blanker.id = "lightbox-blanker";
|
nicholas@2360
|
2480 document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
|
nicholas@2360
|
2481 document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
|
nicholas@2498
|
2482
|
nicholas@2712
|
2483 this.commentBoxes = (function () {
|
nicholas@2712
|
2484 var commentBoxes = {};
|
nicholas@2712
|
2485 commentBoxes.boxes = [];
|
nicholas@2712
|
2486 commentBoxes.injectPoint = null;
|
nicholas@2712
|
2487 commentBoxes.elementCommentBox = function (audioObject) {
|
nicholas@2224
|
2488 var element = audioObject.specification;
|
nicholas@2224
|
2489 this.audioObject = audioObject;
|
nicholas@2224
|
2490 this.id = audioObject.id;
|
nicholas@2224
|
2491 var audioHolderObject = audioObject.specification.parent;
|
nicholas@2224
|
2492 // Create document objects to hold the comment boxes
|
nicholas@2224
|
2493 this.trackComment = document.createElement('div');
|
nicholas@2224
|
2494 this.trackComment.className = 'comment-div';
|
nicholas@2498
|
2495 this.trackComment.id = 'comment-div-' + audioObject.id;
|
nicholas@2224
|
2496 // Create a string next to each comment asking for a comment
|
nicholas@2224
|
2497 this.trackString = document.createElement('span');
|
nicholas@2498
|
2498 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId();
|
nicholas@2224
|
2499 // Create the HTML5 comment box 'textarea'
|
nicholas@2224
|
2500 this.trackCommentBox = document.createElement('textarea');
|
nicholas@2224
|
2501 this.trackCommentBox.rows = '4';
|
nicholas@2224
|
2502 this.trackCommentBox.cols = '100';
|
nicholas@2498
|
2503 this.trackCommentBox.name = 'trackComment' + audioObject.id;
|
nicholas@2224
|
2504 this.trackCommentBox.className = 'trackComment';
|
nicholas@2224
|
2505 var br = document.createElement('br');
|
nicholas@2224
|
2506 // Add to the holder.
|
nicholas@2224
|
2507 this.trackComment.appendChild(this.trackString);
|
nicholas@2224
|
2508 this.trackComment.appendChild(br);
|
nicholas@2224
|
2509 this.trackComment.appendChild(this.trackCommentBox);
|
nicholas@2224
|
2510
|
nicholas@2498
|
2511 this.exportXMLDOM = function () {
|
nicholas@2224
|
2512 var root = document.createElement('comment');
|
nicholas@2224
|
2513 var question = document.createElement('question');
|
nicholas@2224
|
2514 question.textContent = this.trackString.textContent;
|
nicholas@2224
|
2515 var response = document.createElement('response');
|
nicholas@2224
|
2516 response.textContent = this.trackCommentBox.value;
|
nicholas@2498
|
2517 console.log("Comment frag-" + this.id + ": " + response.textContent);
|
nicholas@2224
|
2518 root.appendChild(question);
|
nicholas@2224
|
2519 root.appendChild(response);
|
nicholas@2224
|
2520 return root;
|
nicholas@2224
|
2521 };
|
nicholas@2498
|
2522 this.resize = function () {
|
nicholas@2498
|
2523 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2524 if (boxwidth >= 600) {
|
nicholas@2224
|
2525 boxwidth = 600;
|
nicholas@2498
|
2526 } else if (boxwidth < 400) {
|
nicholas@2224
|
2527 boxwidth = 400;
|
nicholas@2224
|
2528 }
|
nicholas@2498
|
2529 this.trackComment.style.width = boxwidth + "px";
|
nicholas@2498
|
2530 this.trackCommentBox.style.width = boxwidth - 6 + "px";
|
nicholas@2224
|
2531 };
|
nicholas@2224
|
2532 this.resize();
|
nicholas@2725
|
2533 this.highlight = function (state) {
|
nicholas@2725
|
2534 if (state === true) {
|
nicholas@2725
|
2535 $(this.trackComment).addClass("comment-box-playing");
|
nicholas@2725
|
2536 } else {
|
nicholas@2725
|
2537 $(this.trackComment).removeClass("comment-box-playing");
|
nicholas@2725
|
2538 }
|
nicholas@2725
|
2539 };
|
nicholas@2224
|
2540 };
|
nicholas@2712
|
2541 commentBoxes.createCommentBox = function (audioObject) {
|
nicholas@2224
|
2542 var node = new this.elementCommentBox(audioObject);
|
nicholas@2224
|
2543 this.boxes.push(node);
|
nicholas@2224
|
2544 audioObject.commentDOM = node;
|
nicholas@2224
|
2545 return node;
|
nicholas@2224
|
2546 };
|
nicholas@2712
|
2547 commentBoxes.sortCommentBoxes = function () {
|
nicholas@2498
|
2548 this.boxes.sort(function (a, b) {
|
nicholas@2498
|
2549 return a.id - b.id;
|
nicholas@2498
|
2550 });
|
nicholas@2224
|
2551 };
|
nicholas@2224
|
2552
|
nicholas@2712
|
2553 commentBoxes.showCommentBoxes = function (inject, sort) {
|
nicholas@2224
|
2554 this.injectPoint = inject;
|
nicholas@2498
|
2555 if (sort) {
|
nicholas@2498
|
2556 this.sortCommentBoxes();
|
nicholas@2498
|
2557 }
|
nicholas@2708
|
2558 this.boxes.forEach(function (box) {
|
nicholas@2224
|
2559 inject.appendChild(box.trackComment);
|
nicholas@2708
|
2560 });
|
nicholas@2224
|
2561 };
|
nicholas@2224
|
2562
|
nicholas@2712
|
2563 commentBoxes.deleteCommentBoxes = function () {
|
nicholas@2708
|
2564 if (this.injectPoint !== null) {
|
nicholas@2708
|
2565 this.boxes.forEach(function (box) {
|
nicholas@2224
|
2566 this.injectPoint.removeChild(box.trackComment);
|
nicholas@2708
|
2567 }, this);
|
nicholas@2224
|
2568 this.injectPoint = null;
|
nicholas@2224
|
2569 }
|
nicholas@2224
|
2570 this.boxes = [];
|
nicholas@2224
|
2571 };
|
nicholas@2725
|
2572 commentBoxes.highlightById = function (id) {
|
nicholas@2725
|
2573 if (id === undefined || typeof id !== "number" || id >= this.boxes.length) {
|
nicholas@2725
|
2574 console.log("Error - Invalid id");
|
nicholas@2725
|
2575 id = -1;
|
nicholas@2725
|
2576 }
|
nicholas@2725
|
2577 this.boxes.forEach(function (a) {
|
nicholas@2725
|
2578 if (a.id === id) {
|
nicholas@2725
|
2579 a.highlight(true);
|
nicholas@2725
|
2580 } else {
|
nicholas@2725
|
2581 a.highlight(false);
|
nicholas@2725
|
2582 }
|
nicholas@2725
|
2583 });
|
nicholas@2725
|
2584 };
|
nicholas@2712
|
2585 return commentBoxes;
|
nicholas@2712
|
2586 })();
|
nicholas@2498
|
2587
|
nicholas@2498
|
2588 this.commentQuestions = [];
|
nicholas@2498
|
2589
|
nicholas@2498
|
2590 this.commentBox = function (commentQuestion) {
|
nicholas@2498
|
2591 this.specification = commentQuestion;
|
nicholas@2498
|
2592 // Create document objects to hold the comment boxes
|
nicholas@2498
|
2593 this.holder = document.createElement('div');
|
nicholas@2498
|
2594 this.holder.className = 'comment-div';
|
nicholas@2498
|
2595 // Create a string next to each comment asking for a comment
|
nicholas@2498
|
2596 this.string = document.createElement('span');
|
nicholas@2498
|
2597 this.string.innerHTML = commentQuestion.statement;
|
nicholas@2498
|
2598 // Create the HTML5 comment box 'textarea'
|
nicholas@2498
|
2599 this.textArea = document.createElement('textarea');
|
nicholas@2498
|
2600 this.textArea.rows = '4';
|
nicholas@2498
|
2601 this.textArea.cols = '100';
|
nicholas@2498
|
2602 this.textArea.className = 'trackComment';
|
nicholas@2498
|
2603 var br = document.createElement('br');
|
nicholas@2498
|
2604 // Add to the holder.
|
nicholas@2498
|
2605 this.holder.appendChild(this.string);
|
nicholas@2498
|
2606 this.holder.appendChild(br);
|
nicholas@2498
|
2607 this.holder.appendChild(this.textArea);
|
nicholas@2498
|
2608
|
nicholas@2498
|
2609 this.exportXMLDOM = function (storePoint) {
|
nicholas@2498
|
2610 var root = storePoint.parent.document.createElement('comment');
|
nicholas@2498
|
2611 root.id = this.specification.id;
|
nicholas@2498
|
2612 root.setAttribute('type', this.specification.type);
|
nicholas@2498
|
2613 console.log("Question: " + this.string.textContent);
|
nicholas@2498
|
2614 console.log("Response: " + root.textContent);
|
nicholas@2224
|
2615 var question = storePoint.parent.document.createElement('question');
|
nicholas@2224
|
2616 question.textContent = this.string.textContent;
|
nicholas@2224
|
2617 var response = storePoint.parent.document.createElement('response');
|
nicholas@2224
|
2618 response.textContent = this.textArea.value;
|
nicholas@2224
|
2619 root.appendChild(question);
|
nicholas@2224
|
2620 root.appendChild(response);
|
nicholas@2224
|
2621 storePoint.XMLDOM.appendChild(root);
|
nicholas@2498
|
2622 return root;
|
nicholas@2498
|
2623 };
|
nicholas@2498
|
2624 this.resize = function () {
|
nicholas@2498
|
2625 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2626 if (boxwidth >= 600) {
|
nicholas@2498
|
2627 boxwidth = 600;
|
nicholas@2498
|
2628 } else if (boxwidth < 400) {
|
nicholas@2498
|
2629 boxwidth = 400;
|
nicholas@2498
|
2630 }
|
nicholas@2498
|
2631 this.holder.style.width = boxwidth + "px";
|
nicholas@2498
|
2632 this.textArea.style.width = boxwidth - 6 + "px";
|
nicholas@2498
|
2633 };
|
nicholas@2498
|
2634 this.resize();
|
nicholas@2498
|
2635 };
|
nicholas@2498
|
2636
|
nicholas@2498
|
2637 this.radioBox = function (commentQuestion) {
|
nicholas@2498
|
2638 this.specification = commentQuestion;
|
nicholas@2498
|
2639 // Create document objects to hold the comment boxes
|
nicholas@2498
|
2640 this.holder = document.createElement('div');
|
nicholas@2498
|
2641 this.holder.className = 'comment-div';
|
nicholas@2498
|
2642 // Create a string next to each comment asking for a comment
|
nicholas@2498
|
2643 this.string = document.createElement('span');
|
nicholas@2498
|
2644 this.string.innerHTML = commentQuestion.statement;
|
nicholas@2498
|
2645 // Add to the holder.
|
nicholas@2498
|
2646 this.holder.appendChild(this.string);
|
nicholas@2498
|
2647 this.options = [];
|
nicholas@2498
|
2648 this.inputs = document.createElement('div');
|
nicholas@2711
|
2649 this.inputs.className = "comment-checkbox-inputs-holder";
|
nicholas@2498
|
2650
|
nicholas@2498
|
2651 var optCount = commentQuestion.options.length;
|
nicholas@2711
|
2652 for (var i = 0; i < optCount; i++) {
|
nicholas@2498
|
2653 var div = document.createElement('div');
|
nicholas@2711
|
2654 div.className = "comment-checkbox-inputs-flex";
|
nicholas@2722
|
2655
|
nicholas@2711
|
2656 var span = document.createElement('span');
|
nicholas@2711
|
2657 span.textContent = commentQuestion.options[i].text;
|
nicholas@2711
|
2658 span.className = 'comment-radio-span';
|
nicholas@2711
|
2659 div.appendChild(span);
|
nicholas@2722
|
2660
|
nicholas@2498
|
2661 var input = document.createElement('input');
|
nicholas@2498
|
2662 input.type = 'radio';
|
nicholas@2498
|
2663 input.name = commentQuestion.id;
|
nicholas@2711
|
2664 input.setAttribute('setvalue', commentQuestion.options[i].name);
|
nicholas@2498
|
2665 input.className = 'comment-radio';
|
nicholas@2498
|
2666 div.appendChild(input);
|
nicholas@2722
|
2667
|
nicholas@2498
|
2668 this.inputs.appendChild(div);
|
nicholas@2498
|
2669 this.options.push(input);
|
nicholas@2498
|
2670 }
|
nicholas@2498
|
2671 this.holder.appendChild(this.inputs);
|
nicholas@2498
|
2672
|
nicholas@2498
|
2673 this.exportXMLDOM = function (storePoint) {
|
nicholas@2498
|
2674 var root = storePoint.parent.document.createElement('comment');
|
nicholas@2498
|
2675 root.id = this.specification.id;
|
nicholas@2498
|
2676 root.setAttribute('type', this.specification.type);
|
nicholas@2498
|
2677 var question = document.createElement('question');
|
nicholas@2498
|
2678 question.textContent = this.string.textContent;
|
nicholas@2498
|
2679 var response = document.createElement('response');
|
nicholas@2498
|
2680 var i = 0;
|
nicholas@2708
|
2681 while (this.options[i].checked === false) {
|
nicholas@2498
|
2682 i++;
|
nicholas@2498
|
2683 if (i >= this.options.length) {
|
nicholas@2498
|
2684 break;
|
nicholas@2498
|
2685 }
|
nicholas@2498
|
2686 }
|
nicholas@2498
|
2687 if (i >= this.options.length) {
|
nicholas@2498
|
2688 response.textContent = 'null';
|
nicholas@2498
|
2689 } else {
|
nicholas@2498
|
2690 response.textContent = this.options[i].getAttribute('setvalue');
|
nicholas@2498
|
2691 response.setAttribute('number', i);
|
nicholas@2498
|
2692 }
|
nicholas@2498
|
2693 console.log('Comment: ' + question.textContent);
|
nicholas@2498
|
2694 console.log('Response: ' + response.textContent);
|
nicholas@2498
|
2695 root.appendChild(question);
|
nicholas@2498
|
2696 root.appendChild(response);
|
nicholas@2224
|
2697 storePoint.XMLDOM.appendChild(root);
|
nicholas@2498
|
2698 return root;
|
nicholas@2498
|
2699 };
|
nicholas@2498
|
2700 this.resize = function () {
|
nicholas@2498
|
2701 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2702 if (boxwidth >= 600) {
|
nicholas@2498
|
2703 boxwidth = 600;
|
nicholas@2498
|
2704 } else if (boxwidth < 400) {
|
nicholas@2498
|
2705 boxwidth = 400;
|
nicholas@2498
|
2706 }
|
nicholas@2498
|
2707 this.holder.style.width = boxwidth + "px";
|
nicholas@2498
|
2708 };
|
nicholas@2498
|
2709 this.resize();
|
nicholas@2498
|
2710 };
|
nicholas@2498
|
2711
|
nicholas@2498
|
2712 this.checkboxBox = function (commentQuestion) {
|
nicholas@2498
|
2713 this.specification = commentQuestion;
|
nicholas@2498
|
2714 // Create document objects to hold the comment boxes
|
nicholas@2498
|
2715 this.holder = document.createElement('div');
|
nicholas@2498
|
2716 this.holder.className = 'comment-div';
|
nicholas@2498
|
2717 // Create a string next to each comment asking for a comment
|
nicholas@2498
|
2718 this.string = document.createElement('span');
|
nicholas@2498
|
2719 this.string.innerHTML = commentQuestion.statement;
|
nicholas@2498
|
2720 // Add to the holder.
|
nicholas@2498
|
2721 this.holder.appendChild(this.string);
|
nicholas@2498
|
2722 this.options = [];
|
nicholas@2498
|
2723 this.inputs = document.createElement('div');
|
nicholas@2294
|
2724 this.inputs.className = "comment-checkbox-inputs-holder";
|
nicholas@2498
|
2725
|
nicholas@2498
|
2726 var optCount = commentQuestion.options.length;
|
nicholas@2498
|
2727 for (var i = 0; i < optCount; i++) {
|
nicholas@2498
|
2728 var div = document.createElement('div');
|
nicholas@2711
|
2729 div.className = "comment-checkbox-inputs-flex";
|
nicholas@2722
|
2730
|
nicholas@2711
|
2731 var span = document.createElement('span');
|
nicholas@2711
|
2732 span.textContent = commentQuestion.options[i].text;
|
nicholas@2711
|
2733 span.className = 'comment-radio-span';
|
nicholas@2711
|
2734 div.appendChild(span);
|
nicholas@2722
|
2735
|
nicholas@2498
|
2736 var input = document.createElement('input');
|
nicholas@2498
|
2737 input.type = 'checkbox';
|
nicholas@2498
|
2738 input.name = commentQuestion.id;
|
nicholas@2498
|
2739 input.setAttribute('setvalue', commentQuestion.options[i].name);
|
nicholas@2498
|
2740 input.className = 'comment-radio';
|
nicholas@2498
|
2741 div.appendChild(input);
|
nicholas@2722
|
2742
|
nicholas@2498
|
2743 this.inputs.appendChild(div);
|
nicholas@2498
|
2744 this.options.push(input);
|
nicholas@2498
|
2745 }
|
nicholas@2498
|
2746 this.holder.appendChild(this.inputs);
|
nicholas@2498
|
2747
|
nicholas@2498
|
2748 this.exportXMLDOM = function (storePoint) {
|
nicholas@2498
|
2749 var root = storePoint.parent.document.createElement('comment');
|
nicholas@2498
|
2750 root.id = this.specification.id;
|
nicholas@2498
|
2751 root.setAttribute('type', this.specification.type);
|
nicholas@2498
|
2752 var question = document.createElement('question');
|
nicholas@2498
|
2753 question.textContent = this.string.textContent;
|
nicholas@2498
|
2754 root.appendChild(question);
|
nicholas@2498
|
2755 console.log('Comment: ' + question.textContent);
|
nicholas@2498
|
2756 for (var i = 0; i < this.options.length; i++) {
|
nicholas@2498
|
2757 var response = document.createElement('response');
|
nicholas@2498
|
2758 response.textContent = this.options[i].checked;
|
nicholas@2498
|
2759 response.setAttribute('name', this.options[i].getAttribute('setvalue'));
|
nicholas@2498
|
2760 root.appendChild(response);
|
nicholas@2498
|
2761 console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent);
|
nicholas@2498
|
2762 }
|
nicholas@2224
|
2763 storePoint.XMLDOM.appendChild(root);
|
nicholas@2498
|
2764 return root;
|
nicholas@2498
|
2765 };
|
nicholas@2498
|
2766 this.resize = function () {
|
nicholas@2498
|
2767 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2768 if (boxwidth >= 600) {
|
nicholas@2498
|
2769 boxwidth = 600;
|
nicholas@2498
|
2770 } else if (boxwidth < 400) {
|
nicholas@2498
|
2771 boxwidth = 400;
|
nicholas@2498
|
2772 }
|
nicholas@2498
|
2773 this.holder.style.width = boxwidth + "px";
|
nicholas@2498
|
2774 };
|
nicholas@2498
|
2775 this.resize();
|
nicholas@2498
|
2776 };
|
nicholas@2498
|
2777
|
n@2579
|
2778 this.sliderBox = function (commentQuestion) {
|
n@2579
|
2779 this.specification = commentQuestion;
|
n@2579
|
2780 this.holder = document.createElement("div");
|
n@2579
|
2781 this.holder.className = 'comment-div';
|
n@2579
|
2782 this.string = document.createElement("span");
|
n@2579
|
2783 this.string.innerHTML = commentQuestion.statement;
|
n@2579
|
2784 this.slider = document.createElement("input");
|
n@2579
|
2785 this.slider.type = "range";
|
n@2579
|
2786 this.slider.min = commentQuestion.min;
|
n@2579
|
2787 this.slider.max = commentQuestion.max;
|
n@2579
|
2788 this.slider.step = commentQuestion.step;
|
n@2579
|
2789 this.slider.value = commentQuestion.value;
|
n@2579
|
2790 var br = document.createElement('br');
|
n@2579
|
2791
|
n@2580
|
2792 var textHolder = document.createElement("div");
|
n@2580
|
2793 textHolder.className = "comment-slider-text-holder";
|
n@2580
|
2794
|
n@2580
|
2795 this.leftText = document.createElement("span");
|
n@2580
|
2796 this.leftText.textContent = commentQuestion.leftText;
|
n@2580
|
2797 this.rightText = document.createElement("span");
|
n@2580
|
2798 this.rightText.textContent = commentQuestion.rightText;
|
n@2580
|
2799 textHolder.appendChild(this.leftText);
|
n@2580
|
2800 textHolder.appendChild(this.rightText);
|
n@2580
|
2801
|
n@2579
|
2802 this.holder.appendChild(this.string);
|
n@2579
|
2803 this.holder.appendChild(br);
|
n@2579
|
2804 this.holder.appendChild(this.slider);
|
n@2580
|
2805 this.holder.appendChild(textHolder);
|
n@2579
|
2806
|
n@2579
|
2807 this.exportXMLDOM = function (storePoint) {
|
n@2579
|
2808 var root = storePoint.parent.document.createElement('comment');
|
n@2579
|
2809 root.id = this.specification.id;
|
n@2579
|
2810 root.setAttribute('type', this.specification.type);
|
n@2579
|
2811 console.log("Question: " + this.string.textContent);
|
n@2579
|
2812 console.log("Response: " + this.slider.value);
|
n@2579
|
2813 var question = storePoint.parent.document.createElement('question');
|
n@2579
|
2814 question.textContent = this.string.textContent;
|
n@2579
|
2815 var response = storePoint.parent.document.createElement('response');
|
n@2579
|
2816 response.textContent = this.slider.value;
|
n@2579
|
2817 root.appendChild(question);
|
n@2579
|
2818 root.appendChild(response);
|
n@2579
|
2819 storePoint.XMLDOM.appendChild(root);
|
n@2579
|
2820 return root;
|
n@2579
|
2821 };
|
n@2579
|
2822 this.resize = function () {
|
n@2579
|
2823 var boxwidth = (window.innerWidth - 100) / 2;
|
n@2579
|
2824 if (boxwidth >= 600) {
|
n@2579
|
2825 boxwidth = 600;
|
n@2579
|
2826 } else if (boxwidth < 400) {
|
n@2579
|
2827 boxwidth = 400;
|
n@2579
|
2828 }
|
n@2579
|
2829 this.holder.style.width = boxwidth + "px";
|
n@2579
|
2830 this.slider.style.width = boxwidth - 24 + "px";
|
n@2579
|
2831 };
|
n@2579
|
2832 this.resize();
|
n@2579
|
2833 };
|
n@2579
|
2834
|
nicholas@2498
|
2835 this.createCommentQuestion = function (element) {
|
nicholas@2498
|
2836 var node;
|
nicholas@2498
|
2837 if (element.type == 'question') {
|
nicholas@2498
|
2838 node = new this.commentBox(element);
|
nicholas@2498
|
2839 } else if (element.type == 'radio') {
|
nicholas@2498
|
2840 node = new this.radioBox(element);
|
nicholas@2498
|
2841 } else if (element.type == 'checkbox') {
|
nicholas@2498
|
2842 node = new this.checkboxBox(element);
|
n@2579
|
2843 } else if (element.type == 'slider') {
|
n@2579
|
2844 node = new this.sliderBox(element);
|
nicholas@2498
|
2845 }
|
nicholas@2498
|
2846 this.commentQuestions.push(node);
|
nicholas@2498
|
2847 return node;
|
nicholas@2498
|
2848 };
|
nicholas@2498
|
2849
|
nicholas@2498
|
2850 this.deleteCommentQuestions = function () {
|
nicholas@2498
|
2851 this.commentQuestions = [];
|
nicholas@2498
|
2852 };
|
nicholas@2498
|
2853
|
nicholas@2498
|
2854 this.outsideReferenceDOM = function (audioObject, index, inject) {
|
nicholas@2224
|
2855 this.parent = audioObject;
|
nicholas@2224
|
2856 this.outsideReferenceHolder = document.createElement('button');
|
nicholas@2224
|
2857 this.outsideReferenceHolder.className = 'outside-reference';
|
nicholas@2498
|
2858 this.outsideReferenceHolder.setAttribute('track-id', index);
|
nicholas@2409
|
2859 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
|
nicholas@2224
|
2860 this.outsideReferenceHolder.disabled = true;
|
nicholas@2708
|
2861 this.handleEvent = function (event) {
|
nicholas@2708
|
2862 audioEngineContext.play(this.parent.id);
|
nicholas@2224
|
2863 };
|
nicholas@2708
|
2864 this.outsideReferenceHolder.addEventListener("click", this);
|
nicholas@2224
|
2865 inject.appendChild(this.outsideReferenceHolder);
|
nicholas@2498
|
2866 this.enable = function () {
|
nicholas@2498
|
2867 if (this.parent.state == 1) {
|
nicholas@2224
|
2868 this.outsideReferenceHolder.disabled = false;
|
nicholas@2224
|
2869 }
|
nicholas@2224
|
2870 };
|
nicholas@2498
|
2871 this.updateLoading = function (progress) {
|
nicholas@2498
|
2872 if (progress != 100) {
|
nicholas@2224
|
2873 progress = String(progress);
|
nicholas@2224
|
2874 progress = progress.split('.')[0];
|
nicholas@2498
|
2875 this.outsideReferenceHolder.textContent = progress + '%';
|
nicholas@2224
|
2876 } else {
|
nicholas@2409
|
2877 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
|
nicholas@2224
|
2878 }
|
nicholas@2224
|
2879 };
|
nicholas@2498
|
2880 this.startPlayback = function () {
|
nicholas@2224
|
2881 // Called when playback has begun
|
nicholas@2224
|
2882 $('.track-slider').removeClass('track-slider-playing');
|
nicholas@2224
|
2883 $('.comment-div').removeClass('comment-box-playing');
|
nicholas@2224
|
2884 this.outsideReferenceHolder.style.backgroundColor = "#FDD";
|
nicholas@2224
|
2885 };
|
nicholas@2498
|
2886 this.stopPlayback = function () {
|
nicholas@2224
|
2887 // Called when playback has stopped. This gets called even if playback never started!
|
nicholas@2224
|
2888 this.outsideReferenceHolder.style.backgroundColor = "";
|
nicholas@2224
|
2889 };
|
nicholas@2498
|
2890 this.exportXMLDOM = function (audioObject) {
|
nicholas@2224
|
2891 return null;
|
nicholas@2224
|
2892 };
|
nicholas@2498
|
2893 this.getValue = function () {
|
nicholas@2224
|
2894 return 0;
|
nicholas@2224
|
2895 };
|
nicholas@2498
|
2896 this.getPresentedId = function () {
|
nicholas@2409
|
2897 return this.parent.specification.label || "Reference";
|
nicholas@2224
|
2898 };
|
nicholas@2498
|
2899 this.canMove = function () {
|
nicholas@2224
|
2900 return false;
|
nicholas@2224
|
2901 };
|
nicholas@2498
|
2902 this.error = function () {
|
nicholas@2498
|
2903 // audioObject has an error!!
|
nicholas@2224
|
2904 this.outsideReferenceHolder.textContent = "Error";
|
nicholas@2224
|
2905 this.outsideReferenceHolder.style.backgroundColor = "#F00";
|
nicholas@2708
|
2906 };
|
nicholas@2708
|
2907 };
|
nicholas@2498
|
2908
|
nicholas@2712
|
2909 this.playhead = (function () {
|
nicholas@2722
|
2910 var playhead = {};
|
nicholas@2712
|
2911 playhead.object = document.createElement('div');
|
nicholas@2712
|
2912 playhead.object.className = 'playhead';
|
nicholas@2712
|
2913 playhead.object.align = 'left';
|
nicholas@2498
|
2914 var curTime = document.createElement('div');
|
nicholas@2498
|
2915 curTime.style.width = '50px';
|
nicholas@2712
|
2916 playhead.curTimeSpan = document.createElement('span');
|
nicholas@2712
|
2917 playhead.curTimeSpan.textContent = '00:00';
|
nicholas@2712
|
2918 curTime.appendChild(playhead.curTimeSpan);
|
nicholas@2712
|
2919 playhead.object.appendChild(curTime);
|
nicholas@2712
|
2920 playhead.scrubberTrack = document.createElement('div');
|
nicholas@2712
|
2921 playhead.scrubberTrack.className = 'playhead-scrub-track';
|
nicholas@2498
|
2922
|
nicholas@2712
|
2923 playhead.scrubberHead = document.createElement('div');
|
nicholas@2712
|
2924 playhead.scrubberHead.id = 'playhead-scrubber';
|
nicholas@2712
|
2925 playhead.scrubberTrack.appendChild(playhead.scrubberHead);
|
nicholas@2712
|
2926 playhead.object.appendChild(playhead.scrubberTrack);
|
nicholas@2498
|
2927
|
nicholas@2712
|
2928 playhead.timePerPixel = 0;
|
nicholas@2712
|
2929 playhead.maxTime = 0;
|
nicholas@2498
|
2930
|
nicholas@2712
|
2931 playhead.playbackObject = undefined;
|
nicholas@2498
|
2932
|
nicholas@2712
|
2933 playhead.setTimePerPixel = function (audioObject) {
|
nicholas@2498
|
2934 //maxTime must be in seconds
|
nicholas@2498
|
2935 this.playbackObject = audioObject;
|
nicholas@2498
|
2936 this.maxTime = audioObject.buffer.buffer.duration;
|
nicholas@2498
|
2937 var width = 490; //500 - 10, 5 each side of the tracker head
|
nicholas@2498
|
2938 this.timePerPixel = this.maxTime / 490;
|
nicholas@2498
|
2939 if (this.maxTime < 60) {
|
nicholas@2498
|
2940 this.curTimeSpan.textContent = '0.00';
|
nicholas@2498
|
2941 } else {
|
nicholas@2498
|
2942 this.curTimeSpan.textContent = '00:00';
|
nicholas@2498
|
2943 }
|
nicholas@2498
|
2944 };
|
nicholas@2498
|
2945
|
nicholas@2712
|
2946 playhead.update = function () {
|
nicholas@2498
|
2947 // Update the playhead position, startPlay must be called
|
nicholas@2498
|
2948 if (this.timePerPixel > 0) {
|
nicholas@2498
|
2949 var time = this.playbackObject.getCurrentPosition();
|
nicholas@2498
|
2950 if (time > 0 && time < this.maxTime) {
|
nicholas@2498
|
2951 var width = 490;
|
nicholas@2498
|
2952 var pix = Math.floor(time / this.timePerPixel);
|
nicholas@2498
|
2953 this.scrubberHead.style.left = pix + 'px';
|
nicholas@2498
|
2954 if (this.maxTime > 60.0) {
|
nicholas@2498
|
2955 var secs = time % 60;
|
nicholas@2498
|
2956 var mins = Math.floor((time - secs) / 60);
|
nicholas@2498
|
2957 secs = secs.toString();
|
nicholas@2498
|
2958 secs = secs.substr(0, 2);
|
nicholas@2498
|
2959 mins = mins.toString();
|
nicholas@2498
|
2960 this.curTimeSpan.textContent = mins + ':' + secs;
|
nicholas@2498
|
2961 } else {
|
nicholas@2498
|
2962 time = time.toString();
|
nicholas@2498
|
2963 this.curTimeSpan.textContent = time.substr(0, 4);
|
nicholas@2498
|
2964 }
|
nicholas@2498
|
2965 } else {
|
nicholas@2498
|
2966 this.scrubberHead.style.left = '0px';
|
nicholas@2498
|
2967 if (this.maxTime < 60) {
|
nicholas@2498
|
2968 this.curTimeSpan.textContent = '0.00';
|
nicholas@2498
|
2969 } else {
|
nicholas@2498
|
2970 this.curTimeSpan.textContent = '00:00';
|
nicholas@2498
|
2971 }
|
nicholas@2498
|
2972 }
|
nicholas@2498
|
2973 }
|
nicholas@2817
|
2974 if (this.playbackObject !== undefined && this.interval === undefined) {
|
nicholas@2817
|
2975 window.requestAnimationFrame(this.update.bind(this));
|
nicholas@2817
|
2976 }
|
nicholas@2498
|
2977 };
|
nicholas@2498
|
2978
|
nicholas@2712
|
2979 playhead.interval = undefined;
|
nicholas@2498
|
2980
|
nicholas@2712
|
2981 playhead.start = function () {
|
nicholas@2708
|
2982 if (this.playbackObject !== undefined && this.interval === undefined) {
|
nicholas@2817
|
2983 window.requestAnimationFrame(this.update.bind(this));
|
nicholas@2498
|
2984 }
|
nicholas@2498
|
2985 };
|
nicholas@2712
|
2986 playhead.stop = function () {
|
nicholas@2817
|
2987 this.timePerPixel = 0;
|
nicholas@2498
|
2988 };
|
nicholas@2712
|
2989 return playhead;
|
nicholas@2712
|
2990 })();
|
nicholas@2498
|
2991
|
nicholas@2712
|
2992 this.volume = (function () {
|
nicholas@2224
|
2993 // An in-built volume module which can be viewed on page
|
nicholas@2224
|
2994 // Includes trackers on page-by-page data
|
nicholas@2224
|
2995 // Volume does NOT reset to 0dB on each page load
|
nicholas@2712
|
2996 var volume = {};
|
nicholas@2712
|
2997 volume.valueLin = 1.0;
|
nicholas@2712
|
2998 volume.valueDB = 0.0;
|
nicholas@2712
|
2999 volume.root = document.createElement('div');
|
nicholas@2712
|
3000 volume.root.id = 'master-volume-root';
|
nicholas@2712
|
3001 volume.object = document.createElement('div');
|
nicholas@2712
|
3002 volume.object.className = 'master-volume-holder-float';
|
nicholas@2712
|
3003 volume.object.appendChild(volume.root);
|
nicholas@2712
|
3004 volume.slider = document.createElement('input');
|
nicholas@2712
|
3005 volume.slider.id = 'master-volume-control';
|
nicholas@2712
|
3006 volume.slider.type = 'range';
|
nicholas@2712
|
3007 volume.valueText = document.createElement('span');
|
nicholas@2712
|
3008 volume.valueText.id = 'master-volume-feedback';
|
nicholas@2712
|
3009 volume.valueText.textContent = '0dB';
|
nicholas@2498
|
3010
|
nicholas@2712
|
3011 volume.slider.min = -60;
|
nicholas@2712
|
3012 volume.slider.max = 12;
|
nicholas@2712
|
3013 volume.slider.value = 0;
|
nicholas@2712
|
3014 volume.slider.step = 1;
|
nicholas@2712
|
3015 volume.handleEvent = function (event) {
|
nicholas@2669
|
3016 if (event.type == "mousemove") {
|
nicholas@2669
|
3017 this.valueDB = Number(this.slider.value);
|
nicholas@2669
|
3018 this.valueLin = decibelToLinear(this.valueDB);
|
nicholas@2669
|
3019 this.valueText.textContent = this.valueDB + 'dB';
|
nicholas@2669
|
3020 audioEngineContext.outputGain.gain.value = this.valueLin;
|
nicholas@2669
|
3021 } else if (event.type == "mouseup") {
|
nicholas@2669
|
3022 this.onmouseup();
|
nicholas@2669
|
3023 }
|
nicholas@2669
|
3024 this.slider.value = this.valueDB;
|
nicholas@2669
|
3025
|
nicholas@2669
|
3026 if (event.stopPropagation) {
|
nicholas@2669
|
3027 event.stopPropagation();
|
nicholas@2669
|
3028 }
|
nicholas@2711
|
3029 };
|
nicholas@2712
|
3030 volume.onmouseup = function () {
|
nicholas@2224
|
3031 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
|
nicholas@2708
|
3032 if (storePoint.length === 0) {
|
nicholas@2224
|
3033 storePoint = storage.document.createElement('metricresult');
|
nicholas@2498
|
3034 storePoint.setAttribute('name', 'volumeTracker');
|
nicholas@2224
|
3035 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
|
nicholas@2498
|
3036 } else {
|
nicholas@2224
|
3037 storePoint = storePoint[0];
|
nicholas@2224
|
3038 }
|
nicholas@2224
|
3039 var node = storage.document.createElement('movement');
|
nicholas@2498
|
3040 node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
|
nicholas@2669
|
3041 node.setAttribute('volume', this.valueDB);
|
nicholas@2498
|
3042 node.setAttribute('format', 'dBFS');
|
nicholas@2224
|
3043 storePoint.appendChild(node);
|
nicholas@2711
|
3044 };
|
nicholas@2712
|
3045 volume.slider.addEventListener("mousemove", volume);
|
nicholas@2712
|
3046 volume.root.addEventListener("mouseup", volume);
|
nicholas@2498
|
3047
|
nicholas@2224
|
3048 var title = document.createElement('div');
|
nicholas@2224
|
3049 title.innerHTML = '<span>Master Volume Control</span>';
|
nicholas@2224
|
3050 title.style.fontSize = '0.75em';
|
nicholas@2224
|
3051 title.style.width = "100%";
|
nicholas@2224
|
3052 title.align = 'center';
|
nicholas@2712
|
3053 volume.root.appendChild(title);
|
nicholas@2498
|
3054
|
nicholas@2712
|
3055 volume.root.appendChild(volume.slider);
|
nicholas@2712
|
3056 volume.root.appendChild(volume.valueText);
|
nicholas@2498
|
3057
|
nicholas@2712
|
3058 volume.resize = function (event) {
|
nicholas@2352
|
3059 if (window.innerWidth < 1000) {
|
nicholas@2708
|
3060 this.object.className = "master-volume-holder-inline";
|
nicholas@2352
|
3061 } else {
|
nicholas@2352
|
3062 this.object.className = 'master-volume-holder-float';
|
nicholas@2352
|
3063 }
|
nicholas@2708
|
3064 };
|
nicholas@2712
|
3065 return volume;
|
nicholas@2712
|
3066 })();
|
nicholas@2498
|
3067
|
nicholas@2778
|
3068 this.imageHolder = (function () {
|
nicholas@2778
|
3069 var imageController = {};
|
nicholas@2778
|
3070 imageController.root = document.createElement("div");
|
nicholas@2778
|
3071 imageController.root.id = "imageController";
|
nicholas@2778
|
3072 imageController.img = document.createElement("img");
|
nicholas@2778
|
3073 imageController.root.appendChild(imageController.img);
|
nicholas@2778
|
3074 imageController.setImage = function (src) {
|
nicholas@2778
|
3075 imageController.img.src = "";
|
n@2785
|
3076 if (typeof src !== "string" || src.length === undefined) {
|
nicholas@2778
|
3077 return;
|
nicholas@2778
|
3078 }
|
nicholas@2778
|
3079 imageController.img.src = src;
|
n@2785
|
3080 };
|
nicholas@2778
|
3081 return imageController;
|
nicholas@2778
|
3082 })();
|
nicholas@2778
|
3083
|
nicholas@2224
|
3084 this.calibrationModuleObject = null;
|
nicholas@2498
|
3085 this.calibrationModule = function () {
|
nicholas@2224
|
3086 // This creates an on-page calibration module
|
nicholas@2224
|
3087 this.storeDOM = storage.document.createElement("calibration");
|
nicholas@2224
|
3088 storage.root.appendChild(this.storeDOM);
|
nicholas@2224
|
3089 // The calibration is a fixed state module
|
nicholas@2224
|
3090 this.calibrationNodes = [];
|
nicholas@2224
|
3091 this.holder = null;
|
nicholas@2498
|
3092 this.build = function (inject) {
|
nicholas@2224
|
3093 var f0 = 62.5;
|
nicholas@2224
|
3094 this.holder = document.createElement("div");
|
nicholas@2224
|
3095 this.holder.className = "calibration-holder";
|
nicholas@2224
|
3096 this.calibrationNodes = [];
|
nicholas@2498
|
3097 while (f0 < 20000) {
|
nicholas@2712
|
3098 /* jshint loopfunc: true */
|
nicholas@2224
|
3099 var obj = {
|
nicholas@2224
|
3100 root: document.createElement("div"),
|
nicholas@2224
|
3101 input: document.createElement("input"),
|
nicholas@2224
|
3102 oscillator: audioContext.createOscillator(),
|
nicholas@2224
|
3103 gain: audioContext.createGain(),
|
nicholas@2224
|
3104 f: f0,
|
nicholas@2224
|
3105 parent: this,
|
nicholas@2498
|
3106 handleEvent: function (event) {
|
nicholas@2498
|
3107 switch (event.type) {
|
nicholas@2224
|
3108 case "mouseenter":
|
nicholas@2224
|
3109 this.oscillator.start(0);
|
nicholas@2224
|
3110 break;
|
nicholas@2224
|
3111 case "mouseleave":
|
nicholas@2224
|
3112 this.oscillator.stop(0);
|
nicholas@2224
|
3113 this.oscillator = audioContext.createOscillator();
|
nicholas@2224
|
3114 this.oscillator.connect(this.gain);
|
nicholas@2224
|
3115 this.oscillator.frequency.value = this.f;
|
nicholas@2224
|
3116 break;
|
nicholas@2224
|
3117 case "mousemove":
|
nicholas@2498
|
3118 var value = Math.pow(10, this.input.value / 20);
|
nicholas@2224
|
3119 if (this.f == 1000) {
|
nicholas@2224
|
3120 audioEngineContext.outputGain.gain.value = value;
|
nicholas@2224
|
3121 interfaceContext.volume.slider.value = this.input.value;
|
nicholas@2224
|
3122 } else {
|
nicholas@2708
|
3123 this.gain.gain.value = value;
|
nicholas@2224
|
3124 }
|
nicholas@2224
|
3125 break;
|
nicholas@2224
|
3126 }
|
nicholas@2224
|
3127 },
|
nicholas@2498
|
3128 disconnect: function () {
|
nicholas@2224
|
3129 this.gain.disconnect();
|
nicholas@2224
|
3130 }
|
nicholas@2708
|
3131 };
|
nicholas@2224
|
3132 obj.root.className = "calibration-slider";
|
nicholas@2224
|
3133 obj.root.appendChild(obj.input);
|
nicholas@2224
|
3134 obj.oscillator.connect(obj.gain);
|
nicholas@2224
|
3135 obj.gain.connect(audioEngineContext.outputGain);
|
nicholas@2498
|
3136 obj.gain.gain.value = Math.random() * 2;
|
nicholas@2224
|
3137 obj.input.value = obj.gain.gain.value;
|
nicholas@2498
|
3138 obj.input.setAttribute('orient', 'vertical');
|
nicholas@2224
|
3139 obj.input.type = "range";
|
nicholas@2593
|
3140 obj.input.min = -12;
|
nicholas@2593
|
3141 obj.input.max = 0;
|
nicholas@2224
|
3142 obj.input.step = 0.25;
|
nicholas@2224
|
3143 if (f0 != 1000) {
|
nicholas@2498
|
3144 obj.input.value = (Math.random() * 12) - 6;
|
nicholas@2224
|
3145 } else {
|
nicholas@2224
|
3146 obj.input.value = 0;
|
nicholas@2498
|
3147 obj.root.style.backgroundColor = "rgb(255,125,125)";
|
nicholas@2224
|
3148 }
|
nicholas@2498
|
3149 obj.input.addEventListener("mousemove", obj);
|
nicholas@2498
|
3150 obj.input.addEventListener("mouseenter", obj);
|
nicholas@2498
|
3151 obj.input.addEventListener("mouseleave", obj);
|
nicholas@2498
|
3152 obj.gain.gain.value = Math.pow(10, obj.input.value / 20);
|
nicholas@2224
|
3153 obj.oscillator.frequency.value = f0;
|
nicholas@2224
|
3154 this.calibrationNodes.push(obj);
|
nicholas@2224
|
3155 this.holder.appendChild(obj.root);
|
nicholas@2224
|
3156 f0 *= 2;
|
nicholas@2224
|
3157 }
|
nicholas@2224
|
3158 inject.appendChild(this.holder);
|
nicholas@2708
|
3159 };
|
nicholas@2498
|
3160 this.collect = function () {
|
nicholas@2708
|
3161 this.calibrationNodes.forEach(function (obj) {
|
nicholas@2224
|
3162 var node = storage.document.createElement("calibrationresult");
|
nicholas@2498
|
3163 node.setAttribute("frequency", obj.f);
|
nicholas@2498
|
3164 node.setAttribute("range-min", obj.input.min);
|
nicholas@2498
|
3165 node.setAttribute("range-max", obj.input.max);
|
nicholas@2498
|
3166 node.setAttribute("gain-lin", obj.gain.gain.value);
|
nicholas@2224
|
3167 this.storeDOM.appendChild(node);
|
nicholas@2708
|
3168 }, this);
|
nicholas@2708
|
3169 };
|
nicholas@2708
|
3170 };
|
nicholas@2498
|
3171
|
nicholas@2498
|
3172
|
nicholas@2498
|
3173 // Global Checkers
|
nicholas@2498
|
3174 // These functions will help enforce the checkers
|
n@2789
|
3175 this.checkHiddenAnchor = function (message) {
|
nicholas@2708
|
3176 var anchors = audioEngineContext.audioObjects.filter(function (ao) {
|
nicholas@2708
|
3177 return ao.specification.type === "anchor";
|
nicholas@2708
|
3178 });
|
nicholas@2708
|
3179 var state = anchors.some(function (ao) {
|
nicholas@2708
|
3180 return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0);
|
nicholas@2708
|
3181 });
|
nicholas@2708
|
3182 if (state) {
|
nicholas@2708
|
3183 console.log('Anchor node not below marker value');
|
n@2789
|
3184 if (message) {
|
n@2789
|
3185 interfaceContext.lightbox.post("Message", message);
|
n@2789
|
3186 } else {
|
n@2789
|
3187 interfaceContext.lightbox.post("Message", 'Please keep listening');
|
n@2789
|
3188 }
|
nicholas@2708
|
3189 this.storeErrorNode('Anchor node not below marker value');
|
nicholas@2708
|
3190 return false;
|
nicholas@2498
|
3191 }
|
nicholas@2498
|
3192 return true;
|
nicholas@2498
|
3193 };
|
nicholas@2498
|
3194
|
n@2789
|
3195 this.checkHiddenReference = function (message) {
|
nicholas@2708
|
3196 var references = audioEngineContext.audioObjects.filter(function (ao) {
|
nicholas@2708
|
3197 return ao.specification.type === "reference";
|
nicholas@2708
|
3198 });
|
nicholas@2708
|
3199 var state = references.some(function (ao) {
|
nicholas@2708
|
3200 return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0);
|
nicholas@2708
|
3201 });
|
nicholas@2708
|
3202 if (state) {
|
nicholas@2708
|
3203 console.log('Reference node not below marker value');
|
n@2789
|
3204 if (message) {
|
n@2789
|
3205 interfaceContext.lightbox.post("Message", message);
|
n@2789
|
3206 } else {
|
n@2789
|
3207 interfaceContext.lightbox.post("Message", 'Please keep listening');
|
n@2789
|
3208 }
|
nicholas@2708
|
3209 this.storeErrorNode('Reference node not below marker value');
|
nicholas@2708
|
3210 return false;
|
nicholas@2498
|
3211 }
|
nicholas@2498
|
3212 return true;
|
nicholas@2498
|
3213 };
|
nicholas@2498
|
3214
|
n@2789
|
3215 this.checkFragmentsFullyPlayed = function (message) {
|
nicholas@2498
|
3216 // Checks the entire file has been played back
|
nicholas@2498
|
3217 // NOTE ! This will return true IF playback is Looped!!!
|
nicholas@2498
|
3218 if (audioEngineContext.loopPlayback) {
|
nicholas@2498
|
3219 console.log("WARNING - Looped source: Cannot check fragments are fully played");
|
nicholas@2498
|
3220 return true;
|
nicholas@2498
|
3221 }
|
nicholas@2498
|
3222 var check_pass = true;
|
nicholas@2708
|
3223 var error_obj = [],
|
nicholas@2708
|
3224 i;
|
nicholas@2708
|
3225 for (i = 0; i < audioEngineContext.audioObjects.length; i++) {
|
nicholas@2498
|
3226 var object = audioEngineContext.audioObjects[i];
|
nicholas@2498
|
3227 var time = object.buffer.buffer.duration;
|
nicholas@2498
|
3228 var metric = object.metric;
|
nicholas@2498
|
3229 var passed = false;
|
nicholas@2498
|
3230 for (var j = 0; j < metric.listenTracker.length; j++) {
|
nicholas@2498
|
3231 var bt = metric.listenTracker[j].getElementsByTagName('testtime');
|
nicholas@2498
|
3232 var start_time = Number(bt[0].getAttribute('start'));
|
nicholas@2498
|
3233 var stop_time = Number(bt[0].getAttribute('stop'));
|
nicholas@2498
|
3234 var delta = stop_time - start_time;
|
nicholas@2498
|
3235 if (delta >= time) {
|
nicholas@2498
|
3236 passed = true;
|
nicholas@2498
|
3237 break;
|
nicholas@2498
|
3238 }
|
nicholas@2498
|
3239 }
|
nicholas@2708
|
3240 if (passed === false) {
|
nicholas@2498
|
3241 check_pass = false;
|
nicholas@2498
|
3242 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3243 error_obj.push(object.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3244 }
|
nicholas@2498
|
3245 }
|
nicholas@2708
|
3246 if (check_pass === false) {
|
nicholas@2498
|
3247 var str_start = "You have not completely listened to fragments ";
|
nicholas@2708
|
3248 for (i = 0; i < error_obj.length; i++) {
|
nicholas@2498
|
3249 str_start += error_obj[i];
|
nicholas@2498
|
3250 if (i != error_obj.length - 1) {
|
nicholas@2498
|
3251 str_start += ', ';
|
nicholas@2498
|
3252 }
|
nicholas@2498
|
3253 }
|
nicholas@2498
|
3254 str_start += ". Please keep listening";
|
n@2789
|
3255 console.log(str_start);
|
n@2789
|
3256 this.storeErrorNode(str_start);
|
n@2789
|
3257 if (message) {
|
n@2789
|
3258 str_start = message;
|
n@2789
|
3259 }
|
nicholas@2498
|
3260 interfaceContext.lightbox.post("Error", str_start);
|
nicholas@2444
|
3261 return false;
|
nicholas@2498
|
3262 }
|
nicholas@2444
|
3263 return true;
|
nicholas@2498
|
3264 };
|
n@2789
|
3265 this.checkAllMoved = function (message) {
|
nicholas@2498
|
3266 var str = "You have not moved ";
|
nicholas@2498
|
3267 var failed = [];
|
nicholas@2708
|
3268 audioEngineContext.audioObjects.forEach(function (ao) {
|
nicholas@2708
|
3269 if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) {
|
nicholas@2498
|
3270 failed.push(ao.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3271 }
|
nicholas@2708
|
3272 }, this);
|
nicholas@2708
|
3273 if (failed.length === 0) {
|
nicholas@2498
|
3274 return true;
|
nicholas@2498
|
3275 } else if (failed.length == 1) {
|
nicholas@2498
|
3276 str += 'track ' + failed[0];
|
nicholas@2498
|
3277 } else {
|
nicholas@2498
|
3278 str += 'tracks ';
|
nicholas@2498
|
3279 for (var i = 0; i < failed.length - 1; i++) {
|
nicholas@2498
|
3280 str += failed[i] + ', ';
|
nicholas@2498
|
3281 }
|
nicholas@2498
|
3282 str += 'and ' + failed[i];
|
nicholas@2498
|
3283 }
|
nicholas@2498
|
3284 str += '.';
|
nicholas@2498
|
3285 console.log(str);
|
nicholas@2224
|
3286 this.storeErrorNode(str);
|
n@2789
|
3287 if (message) {
|
n@2789
|
3288 str = message;
|
n@2789
|
3289 }
|
n@2789
|
3290 interfaceContext.lightbox.post("Error", str);
|
nicholas@2498
|
3291 return false;
|
nicholas@2498
|
3292 };
|
n@2789
|
3293 this.checkAllPlayed = function (message) {
|
nicholas@2498
|
3294 var str = "You have not played ";
|
nicholas@2498
|
3295 var failed = [];
|
nicholas@2708
|
3296 audioEngineContext.audioObjects.forEach(function (ao) {
|
nicholas@2708
|
3297 if (ao.metric.wasListenedTo === false) {
|
nicholas@2498
|
3298 failed.push(ao.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3299 }
|
nicholas@2708
|
3300 }, this);
|
nicholas@2708
|
3301 if (failed.length === 0) {
|
nicholas@2498
|
3302 return true;
|
nicholas@2498
|
3303 } else if (failed.length == 1) {
|
nicholas@2498
|
3304 str += 'track ' + failed[0];
|
nicholas@2498
|
3305 } else {
|
nicholas@2498
|
3306 str += 'tracks ';
|
nicholas@2498
|
3307 for (var i = 0; i < failed.length - 1; i++) {
|
nicholas@2498
|
3308 str += failed[i] + ', ';
|
nicholas@2498
|
3309 }
|
nicholas@2498
|
3310 str += 'and ' + failed[i];
|
nicholas@2498
|
3311 }
|
nicholas@2498
|
3312 str += '.';
|
nicholas@2498
|
3313 console.log(str);
|
nicholas@2224
|
3314 this.storeErrorNode(str);
|
n@2789
|
3315 if (message) {
|
n@2789
|
3316 str = message;
|
n@2789
|
3317 }
|
n@2789
|
3318 interfaceContext.lightbox.post("Error", str);
|
nicholas@2498
|
3319 return false;
|
nicholas@2498
|
3320 };
|
n@2789
|
3321 this.checkAllCommented = function (message) {
|
nicholas@2540
|
3322 var str = "You have not commented on all the fragments.";
|
nicholas@2540
|
3323 var cont = true,
|
nicholas@2540
|
3324 boxes = this.commentBoxes.boxes,
|
nicholas@2540
|
3325 numBoxes = boxes.length,
|
nicholas@2540
|
3326 i;
|
nicholas@2540
|
3327 for (i = 0; i < numBoxes; i++) {
|
nicholas@2540
|
3328 if (boxes[i].trackCommentBox.value === "") {
|
nicholas@2540
|
3329 console.log(str);
|
nicholas@2540
|
3330 this.storeErrorNode(str);
|
n@2789
|
3331 if (message) {
|
n@2789
|
3332 str = message;
|
n@2789
|
3333 }
|
n@2789
|
3334 interfaceContext.lightbox.post("Error", str);
|
nicholas@2540
|
3335 return false;
|
nicholas@2540
|
3336 }
|
nicholas@2540
|
3337 }
|
nicholas@2540
|
3338 return true;
|
nicholas@2708
|
3339 };
|
n@2789
|
3340 this.checkScaleRange = function (message) {
|
nicholas@2310
|
3341 var page = testState.getCurrentTestPage();
|
nicholas@2708
|
3342 var interfaceObject = page.interfaces;
|
nicholas@2310
|
3343 var state = true;
|
nicholas@2310
|
3344 var str = "Please keep listening. ";
|
nicholas@2708
|
3345 if (interfaceObject === undefined) {
|
nicholas@2708
|
3346 return true;
|
nicholas@2310
|
3347 }
|
nicholas@2708
|
3348 interfaceObject = interfaceObject[0];
|
nicholas@2708
|
3349 var scales = (function () {
|
nicholas@2708
|
3350 var scaleRange = interfaceObject.options.find(function (a) {
|
nicholas@2708
|
3351 return a.name == "scalerange";
|
nicholas@2708
|
3352 });
|
nicholas@2708
|
3353 return {
|
nicholas@2708
|
3354 min: scaleRange.min,
|
nicholas@2708
|
3355 max: scaleRange.max
|
nicholas@2708
|
3356 };
|
nicholas@2708
|
3357 })();
|
nicholas@2708
|
3358 var range = audioEngineContext.audioObjects.reduce(function (a, b) {
|
nicholas@2742
|
3359 var v = b.interfaceDOM.getValue() * 100.0;
|
nicholas@2708
|
3360 return {
|
nicholas@2708
|
3361 min: Math.min(a.min, v),
|
nicholas@2708
|
3362 max: Math.max(a.max, v)
|
nicholas@2712
|
3363 };
|
nicholas@2708
|
3364 }, {
|
nicholas@2708
|
3365 min: 100,
|
nicholas@2708
|
3366 max: 0
|
nicholas@2708
|
3367 });
|
nicholas@2708
|
3368 if (range.min > scales.min) {
|
nicholas@2742
|
3369 str += "At least one fragment must be below the " + scales.min + " mark.";
|
nicholas@2708
|
3370 state = false;
|
nicholas@2712
|
3371 } else if (range.max < scales.max) {
|
nicholas@2742
|
3372 str += "At least one fragment must be above the " + scales.max + " mark.";
|
nicholas@2310
|
3373 state = false;
|
nicholas@2310
|
3374 }
|
nicholas@2708
|
3375 if (state === false) {
|
nicholas@2310
|
3376 console.log(str);
|
nicholas@2310
|
3377 this.storeErrorNode(str);
|
n@2789
|
3378 if (message) {
|
n@2789
|
3379 str = message;
|
n@2789
|
3380 }
|
nicholas@2498
|
3381 interfaceContext.lightbox.post("Error", str);
|
nicholas@2310
|
3382 }
|
nicholas@2310
|
3383 return state;
|
nicholas@2708
|
3384 };
|
nicholas@2826
|
3385 this.checkFragmentMinPlays = function () {
|
nicholas@2826
|
3386 var failedObjects = audioEngineContext.audioObjects.filter(function (a) {
|
nicholas@2826
|
3387 var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays;
|
nicholas@2826
|
3388 if (minPlays === undefined || a.numberOfPlays >= minPlays) {
|
nicholas@2826
|
3389 return false;
|
nicholas@2826
|
3390 }
|
nicholas@2826
|
3391 return true;
|
nicholas@2826
|
3392 });
|
nicholas@2826
|
3393 if (failedObjects.length === 0) {
|
nicholas@2827
|
3394 return true;
|
nicholas@2826
|
3395 }
|
nicholas@2826
|
3396 var failedString = [];
|
nicholas@2826
|
3397 failedObjects.forEach(function (a) {
|
nicholas@2826
|
3398 failedString.push(a.interfaceDOM.getPresentedId());
|
nicholas@2826
|
3399 });
|
nicholas@2826
|
3400 var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening";
|
nicholas@2826
|
3401 interfaceContext.lightbox.post("Message", str);
|
nicholas@2826
|
3402 this.storeErrorNode(str);
|
nicholas@2827
|
3403 return false;
|
nicholas@2826
|
3404 };
|
nicholas@2826
|
3405
|
nicholas@2498
|
3406
|
nicholas@2849
|
3407 this.sortFragmentsByScore = function () {
|
nicholas@2849
|
3408 var elements = audioEngineContext.audioObjects.filter(function (elem) {
|
nicholas@2849
|
3409 return elem.specification.type !== "outside-reference";
|
nicholas@2849
|
3410 });
|
nicholas@2849
|
3411 var indexes = [];
|
nicholas@2849
|
3412 var i = 0;
|
nicholas@2849
|
3413 while (indexes.push(i++) < elements.length);
|
nicholas@2849
|
3414 return indexes.sort(function (x, y) {
|
nicholas@2849
|
3415 var a = elements[x].interfaceDOM.getValue();
|
nicholas@2849
|
3416 var b = elements[y].interfaceDOM.getValue();
|
nicholas@2849
|
3417 if (a > b) {
|
nicholas@2849
|
3418 return 1;
|
nicholas@2849
|
3419 } else if (a < b) {
|
nicholas@2849
|
3420 return -1;
|
nicholas@2849
|
3421 }
|
nicholas@2849
|
3422 return 0;
|
nicholas@2849
|
3423 }, elements[0].interfaceDOM.getValue());
|
nicholas@2849
|
3424 };
|
nicholas@2849
|
3425
|
nicholas@2498
|
3426 this.storeErrorNode = function (errorMessage) {
|
nicholas@2224
|
3427 var time = audioEngineContext.timer.getTestTime();
|
nicholas@2224
|
3428 var node = storage.document.createElement('error');
|
nicholas@2498
|
3429 node.setAttribute('time', time);
|
nicholas@2224
|
3430 node.textContent = errorMessage;
|
nicholas@2224
|
3431 testState.currentStore.XMLDOM.appendChild(node);
|
nicholas@2224
|
3432 };
|
nicholas@2595
|
3433
|
nicholas@2595
|
3434 this.getLabel = function (labelType, index, labelStart) {
|
nicholas@2595
|
3435 /*
|
nicholas@2595
|
3436 Get the correct label based on type, index and offset
|
nicholas@2595
|
3437 */
|
nicholas@2595
|
3438
|
nicholas@2595
|
3439 function calculateLabel(labelType, index, offset) {
|
nicholas@2595
|
3440 if (labelType == "none") {
|
nicholas@2595
|
3441 return "";
|
nicholas@2595
|
3442 }
|
nicholas@2595
|
3443 switch (labelType) {
|
nicholas@2595
|
3444 case "letter":
|
nicholas@2596
|
3445 return String.fromCharCode((index + offset) % 26 + 97);
|
nicholas@2595
|
3446 case "capital":
|
nicholas@2607
|
3447 return String.fromCharCode((index + offset) % 26 + 65);
|
nicholas@2625
|
3448 case "samediff":
|
nicholas@2708
|
3449 if (index === 0) {
|
nicholas@2625
|
3450 return "Same";
|
nicholas@2625
|
3451 } else if (index == 1) {
|
nicholas@2625
|
3452 return "Difference";
|
nicholas@2625
|
3453 }
|
nicholas@2708
|
3454 return "";
|
nicholas@2595
|
3455 case "number":
|
nicholas@2595
|
3456 return String(index + offset);
|
nicholas@2595
|
3457 default:
|
nicholas@2595
|
3458 return "";
|
nicholas@2595
|
3459 }
|
nicholas@2595
|
3460 }
|
nicholas@2595
|
3461
|
nicholas@2708
|
3462 if (typeof labelStart !== "string" || labelStart.length === 0) {
|
nicholas@2595
|
3463 labelStart = String.fromCharCode(0);
|
nicholas@2595
|
3464 }
|
nicholas@2595
|
3465
|
nicholas@2595
|
3466 switch (labelType) {
|
nicholas@2595
|
3467 case "letter":
|
nicholas@2595
|
3468 labelStart = labelStart.charCodeAt(0);
|
nicholas@2596
|
3469 if (labelStart < 97 || labelStart > 122) {
|
nicholas@2595
|
3470 labelStart = 97;
|
nicholas@2595
|
3471 }
|
nicholas@2595
|
3472 labelStart -= 97;
|
nicholas@2595
|
3473 break;
|
nicholas@2595
|
3474 case "capital":
|
nicholas@2595
|
3475 labelStart = labelStart.charCodeAt(0);
|
nicholas@2596
|
3476 if (labelStart < 65 || labelStart > 90) {
|
nicholas@2595
|
3477 labelStart = 65;
|
nicholas@2595
|
3478 }
|
nicholas@2595
|
3479 labelStart -= 65;
|
nicholas@2595
|
3480 break;
|
nicholas@2595
|
3481 case "number":
|
nicholas@2608
|
3482 labelStart = Number(labelStart);
|
nicholas@2608
|
3483 if (!isFinite(labelStart)) {
|
nicholas@2595
|
3484 labelStart = 1;
|
nicholas@2595
|
3485 }
|
nicholas@2595
|
3486 break;
|
nicholas@2595
|
3487 default:
|
nicholas@2596
|
3488 labelStart = 0;
|
nicholas@2595
|
3489 }
|
nicholas@2595
|
3490 if (typeof index == "number") {
|
nicholas@2595
|
3491 return calculateLabel(labelType, index, labelStart);
|
nicholas@2595
|
3492 } else if (index.length && index.length > 0) {
|
nicholas@2595
|
3493 var a = [],
|
nicholas@2595
|
3494 l = index.length,
|
nicholas@2595
|
3495 i;
|
nicholas@2595
|
3496 for (i = 0; i < l; i++) {
|
nicholas@2595
|
3497 a[i] = calculateLabel(labelType, index[i], labelStart);
|
nicholas@2595
|
3498 }
|
nicholas@2595
|
3499 return a;
|
nicholas@2595
|
3500 } else {
|
nicholas@2595
|
3501 throw ("Invalid arguments");
|
nicholas@2595
|
3502 }
|
nicholas@2708
|
3503 };
|
nicholas@2649
|
3504
|
nicholas@2649
|
3505 this.getCombinedInterfaces = function (page) {
|
nicholas@2649
|
3506 // Combine the interfaces with the global interface nodes
|
nicholas@2649
|
3507 var global = specification.interfaces,
|
nicholas@2649
|
3508 local = page.interfaces;
|
nicholas@2649
|
3509 local.forEach(function (locInt) {
|
nicholas@2649
|
3510 // Iterate through the options nodes
|
nicholas@2649
|
3511 var addList = [];
|
nicholas@2649
|
3512 global.options.forEach(function (gopt) {
|
nicholas@2649
|
3513 var lopt = locInt.options.find(function (lopt) {
|
nicholas@2649
|
3514 return (lopt.name == gopt.name) && (lopt.type == gopt.type);
|
nicholas@2649
|
3515 });
|
nicholas@2649
|
3516 if (!lopt) {
|
nicholas@2649
|
3517 // Global option doesn't exist locally
|
nicholas@2649
|
3518 addList.push(gopt);
|
nicholas@2649
|
3519 }
|
nicholas@2649
|
3520 });
|
nicholas@2649
|
3521 locInt.options = locInt.options.concat(addList);
|
nicholas@2649
|
3522 if (!locInt.scales && global.scales) {
|
nicholas@2649
|
3523 // Use the global default scales
|
nicholas@2649
|
3524 locInt.scales = global.scales;
|
nicholas@2649
|
3525 }
|
nicholas@2649
|
3526 });
|
nicholas@2649
|
3527 return local;
|
nicholas@2708
|
3528 };
|
nicholas@2224
|
3529 }
|
nicholas@2224
|
3530
|
nicholas@2498
|
3531 function Storage() {
|
nicholas@2498
|
3532 // Holds results in XML format until ready for collection
|
nicholas@2498
|
3533 this.globalPreTest = null;
|
nicholas@2498
|
3534 this.globalPostTest = null;
|
nicholas@2498
|
3535 this.testPages = [];
|
nicholas@2498
|
3536 this.document = null;
|
nicholas@2498
|
3537 this.root = null;
|
nicholas@2498
|
3538 this.state = 0;
|
nicholas@2733
|
3539 var pFilenamePrefix = "save";
|
nicholas@2498
|
3540
|
nicholas@2498
|
3541 this.initialise = function (existingStore) {
|
nicholas@2708
|
3542 if (existingStore === undefined) {
|
nicholas@2224
|
3543 // We need to get the sessionKey
|
nicholas@2510
|
3544 this.SessionKey.requestKey();
|
nicholas@2498
|
3545 this.document = document.implementation.createDocument(null, "waetresult", null);
|
nicholas@2224
|
3546 this.root = this.document.childNodes[0];
|
nicholas@2224
|
3547 var projectDocument = specification.projectXML;
|
nicholas@2708
|
3548 projectDocument.setAttribute('file-name', specification.url);
|
nicholas@2708
|
3549 projectDocument.setAttribute('url', qualifyURL(specification.url));
|
nicholas@2224
|
3550 this.root.appendChild(projectDocument);
|
nicholas@2224
|
3551 this.root.appendChild(interfaceContext.returnDateNode());
|
nicholas@2224
|
3552 this.root.appendChild(interfaceContext.returnNavigator());
|
nicholas@2224
|
3553 } else {
|
nicholas@2224
|
3554 this.document = existingStore;
|
nicholas@2294
|
3555 this.root = existingStore.firstChild;
|
nicholas@2224
|
3556 this.SessionKey.key = this.root.getAttribute("key");
|
nicholas@2224
|
3557 }
|
nicholas@2708
|
3558 if (specification.preTest !== undefined) {
|
nicholas@2498
|
3559 this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
|
nicholas@2498
|
3560 }
|
nicholas@2708
|
3561 if (specification.postTest !== undefined) {
|
nicholas@2498
|
3562 this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
|
nicholas@2498
|
3563 }
|
nicholas@2498
|
3564 };
|
nicholas@2498
|
3565
|
nicholas@2224
|
3566 this.SessionKey = {
|
nicholas@2224
|
3567 key: null,
|
nicholas@2224
|
3568 request: new XMLHttpRequest(),
|
nicholas@2224
|
3569 parent: this,
|
nicholas@2498
|
3570 handleEvent: function () {
|
nicholas@2224
|
3571 var parse = new DOMParser();
|
nicholas@2498
|
3572 var xml = parse.parseFromString(this.request.response, "text/xml");
|
nicholas@2708
|
3573 if (this.request.response.length === 0) {
|
nicholas@2515
|
3574 console.error("An unspecified error occured, no server key could be generated");
|
nicholas@2376
|
3575 return;
|
nicholas@2376
|
3576 }
|
nicholas@2498
|
3577 if (xml.getElementsByTagName("state").length > 0) {
|
nicholas@2498
|
3578 if (xml.getElementsByTagName("state")[0].textContent == "OK") {
|
nicholas@2498
|
3579 this.key = xml.getAllElementsByTagName("key")[0].textContent;
|
nicholas@2498
|
3580 this.parent.root.setAttribute("key", this.key);
|
nicholas@2498
|
3581 this.parent.root.setAttribute("state", "empty");
|
nicholas@2516
|
3582 this.update();
|
nicholas@2515
|
3583 return;
|
nicholas@2514
|
3584 } else if (xml.getElementsByTagName("state")[0].textContent == "ERROR") {
|
nicholas@2515
|
3585 this.key = null;
|
nicholas@2514
|
3586 console.error("Could not generate server key. Server responded with error message: \"" + xml.getElementsByTagName("message")[0].textContent + "\"");
|
nicholas@2515
|
3587 return;
|
nicholas@2498
|
3588 }
|
nicholas@2498
|
3589 }
|
nicholas@2515
|
3590 this.key = null;
|
nicholas@2515
|
3591 console.error("An unspecified error occured, no server key could be generated");
|
nicholas@2224
|
3592 },
|
nicholas@2510
|
3593 requestKey: function () {
|
nicholas@2510
|
3594 // For new servers, request a new key from the server
|
nicholas@2510
|
3595 var returnURL = "";
|
nicholas@2510
|
3596 if (typeof specification.projectReturn == "string") {
|
nicholas@2510
|
3597 if (specification.projectReturn.substr(0, 4) == "http") {
|
nicholas@2510
|
3598 returnURL = specification.projectReturn;
|
nicholas@2510
|
3599 }
|
nicholas@2510
|
3600 }
|
nicholas@2940
|
3601 this.request.open("GET", returnURL + "php/requestKey.php?saveFilenamePrefix=" + this.parent.filenamePrefix, true);
|
nicholas@2510
|
3602 this.request.addEventListener("load", this);
|
nicholas@2510
|
3603 this.request.send();
|
nicholas@2510
|
3604 },
|
nicholas@2498
|
3605 update: function () {
|
nicholas@2708
|
3606 if (this.key === null) {
|
nicholas@2357
|
3607 console.log("Cannot save as key == null");
|
nicholas@2357
|
3608 return;
|
nicholas@2357
|
3609 }
|
nicholas@2498
|
3610 this.parent.root.setAttribute("state", "update");
|
nicholas@2224
|
3611 var xmlhttp = new XMLHttpRequest();
|
nicholas@2302
|
3612 var returnURL = "";
|
nicholas@2302
|
3613 if (typeof specification.projectReturn == "string") {
|
nicholas@2498
|
3614 if (specification.projectReturn.substr(0, 4) == "http") {
|
nicholas@2302
|
3615 returnURL = specification.projectReturn;
|
nicholas@2302
|
3616 }
|
nicholas@2302
|
3617 }
|
n@2997
|
3618 xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix + "&state=update");
|
nicholas@2224
|
3619 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
|
nicholas@2498
|
3620 xmlhttp.onerror = function () {
|
nicholas@2224
|
3621 console.log('Error updating file to server!');
|
nicholas@2224
|
3622 };
|
nicholas@2224
|
3623 var hold = document.createElement("div");
|
nicholas@2224
|
3624 var clone = this.parent.root.cloneNode(true);
|
nicholas@2224
|
3625 hold.appendChild(clone);
|
nicholas@2498
|
3626 xmlhttp.onload = function () {
|
nicholas@2224
|
3627 if (this.status >= 300) {
|
nicholas@2224
|
3628 console.log("WARNING - Could not update at this time");
|
nicholas@2224
|
3629 } else {
|
nicholas@2224
|
3630 var parser = new DOMParser();
|
nicholas@2224
|
3631 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
|
nicholas@2224
|
3632 var response = xmlDoc.getElementsByTagName('response')[0];
|
nicholas@2224
|
3633 if (response.getAttribute("state") == "OK") {
|
nicholas@2224
|
3634 var file = response.getElementsByTagName("file")[0];
|
nicholas@2498
|
3635 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
|
nicholas@2224
|
3636 } else {
|
nicholas@2224
|
3637 var message = response.getElementsByTagName("message");
|
nicholas@2498
|
3638 console.log("Intermediate save: Error! " + message.textContent);
|
nicholas@2224
|
3639 }
|
nicholas@2224
|
3640 }
|
nicholas@2708
|
3641 };
|
nicholas@2224
|
3642 xmlhttp.send([hold.innerHTML]);
|
nicholas@2723
|
3643 },
|
nicholas@2723
|
3644 finish: function () {
|
nicholas@2723
|
3645 // Final upload to complete the test
|
nicholas@2723
|
3646 this.parent.finish();
|
nicholas@2723
|
3647 var hold = document.createElement("div");
|
nicholas@2723
|
3648 var clone = this.parent.root.cloneNode(true);
|
nicholas@2723
|
3649 hold.appendChild(clone);
|
nicholas@2733
|
3650 var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=";
|
nicholas@2742
|
3651 if (this.parent.filenamePrefix.length === 0) {
|
nicholas@2733
|
3652 saveURL += "save";
|
nicholas@2733
|
3653 } else {
|
nicholas@2733
|
3654 saveURL += this.parent.filenamePrefix;
|
nicholas@2733
|
3655 }
|
n@2997
|
3656 saveURL += "&state=finish";
|
nicholas@2723
|
3657 return new Promise(function (resolve, reject) {
|
nicholas@2723
|
3658 var xmlhttp = new XMLHttpRequest();
|
nicholas@2723
|
3659 xmlhttp.open("POST", saveURL);
|
nicholas@2723
|
3660 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
|
nicholas@2723
|
3661 xmlhttp.onerror = function () {
|
nicholas@2723
|
3662 console.log('Error updating file to server!');
|
nicholas@2723
|
3663 createProjectSave("local");
|
nicholas@2723
|
3664 };
|
nicholas@2723
|
3665 xmlhttp.onload = function () {
|
nicholas@2723
|
3666 if (this.status >= 300) {
|
nicholas@2723
|
3667 console.log("WARNING - Could not update at this time");
|
nicholas@2723
|
3668 createProjectSave("local");
|
nicholas@2723
|
3669 } else {
|
nicholas@2723
|
3670 var parser = new DOMParser();
|
nicholas@2723
|
3671 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
|
nicholas@2723
|
3672 var response = xmlDoc.getElementsByTagName('response')[0];
|
nicholas@2723
|
3673 if (response.getAttribute("state") == "OK") {
|
nicholas@2723
|
3674 var file = response.getElementsByTagName("file")[0];
|
nicholas@2723
|
3675 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
|
nicholas@2723
|
3676 resolve(response);
|
nicholas@2723
|
3677 } else {
|
nicholas@2723
|
3678 var message = response.getElementsByTagName("message");
|
nicholas@2723
|
3679 reject(message);
|
nicholas@2723
|
3680 }
|
nicholas@2723
|
3681 }
|
nicholas@2723
|
3682 };
|
nicholas@2723
|
3683 xmlhttp.send([hold.innerHTML]);
|
nicholas@2723
|
3684 });
|
nicholas@2224
|
3685 }
|
nicholas@2708
|
3686 };
|
nicholas@2498
|
3687
|
nicholas@2498
|
3688 this.createTestPageStore = function (specification) {
|
nicholas@2498
|
3689 var store = new this.pageNode(this, specification);
|
nicholas@2498
|
3690 this.testPages.push(store);
|
nicholas@2498
|
3691 return this.testPages[this.testPages.length - 1];
|
nicholas@2498
|
3692 };
|
nicholas@2498
|
3693
|
nicholas@2498
|
3694 this.surveyNode = function (parent, root, specification) {
|
nicholas@2498
|
3695 this.specification = specification;
|
nicholas@2498
|
3696 this.parent = parent;
|
nicholas@2224
|
3697 this.state = "empty";
|
nicholas@2498
|
3698 this.XMLDOM = this.parent.document.createElement('survey');
|
nicholas@2498
|
3699 this.XMLDOM.setAttribute('location', this.specification.location);
|
nicholas@2498
|
3700 this.XMLDOM.setAttribute("state", this.state);
|
nicholas@2708
|
3701 this.specification.options.forEach(function (optNode) {
|
nicholas@2498
|
3702 if (optNode.type != 'statement') {
|
nicholas@2498
|
3703 var node = this.parent.document.createElement('surveyresult');
|
nicholas@2498
|
3704 node.setAttribute("ref", optNode.id);
|
nicholas@2498
|
3705 node.setAttribute('type', optNode.type);
|
nicholas@2498
|
3706 this.XMLDOM.appendChild(node);
|
nicholas@2498
|
3707 }
|
nicholas@2708
|
3708 }, this);
|
nicholas@2498
|
3709 root.appendChild(this.XMLDOM);
|
nicholas@2498
|
3710
|
nicholas@2498
|
3711 this.postResult = function (node) {
|
nicholas@2708
|
3712 function postNumber(doc, value) {
|
nicholas@2708
|
3713 var child = doc.createElement("response");
|
nicholas@2708
|
3714 child.textContent = value;
|
nicholas@2708
|
3715 return child;
|
nicholas@2708
|
3716 }
|
nicholas@2708
|
3717
|
nicholas@2708
|
3718 function postRadio(doc, node) {
|
nicholas@2708
|
3719 var child = doc.createElement('response');
|
nicholas@2708
|
3720 if (node.response !== null) {
|
nicholas@2708
|
3721 child.setAttribute('name', node.response.name);
|
nicholas@2708
|
3722 child.textContent = node.response.text;
|
nicholas@2708
|
3723 }
|
nicholas@2708
|
3724 return child;
|
nicholas@2708
|
3725 }
|
nicholas@2708
|
3726
|
nicholas@2708
|
3727 function postCheckbox(doc, node) {
|
nicholas@2708
|
3728 var checkNode = doc.createElement('response');
|
nicholas@2708
|
3729 checkNode.setAttribute('name', node.name);
|
nicholas@2708
|
3730 checkNode.setAttribute('checked', node.checked);
|
nicholas@2708
|
3731 return checkNode;
|
nicholas@2708
|
3732 }
|
nicholas@2498
|
3733 // From popup: node is the popupOption node containing both spec. and results
|
nicholas@2498
|
3734 // ID is the position
|
nicholas@2498
|
3735 if (node.specification.type == 'statement') {
|
nicholas@2498
|
3736 return;
|
nicholas@2498
|
3737 }
|
nicholas@2498
|
3738 var surveyresult = this.XMLDOM.firstChild;
|
nicholas@2708
|
3739 while (surveyresult !== null) {
|
nicholas@2498
|
3740 if (surveyresult.getAttribute("ref") == node.specification.id) {
|
nicholas@2224
|
3741 break;
|
nicholas@2224
|
3742 }
|
nicholas@2224
|
3743 surveyresult = surveyresult.nextElementSibling;
|
nicholas@2224
|
3744 }
|
nicholas@2775
|
3745 surveyresult.setAttribute("duration", node.elapsedTime);
|
nicholas@2498
|
3746 switch (node.specification.type) {
|
nicholas@2498
|
3747 case "number":
|
nicholas@2498
|
3748 case "question":
|
n@2583
|
3749 case "slider":
|
nicholas@2708
|
3750 surveyresult.appendChild(postNumber(this.parent.document, node.response));
|
nicholas@2464
|
3751 break;
|
nicholas@2498
|
3752 case "radio":
|
nicholas@2708
|
3753 surveyresult.appendChild(postRadio(this.parent.document, node));
|
nicholas@2498
|
3754 break;
|
nicholas@2498
|
3755 case "checkbox":
|
nicholas@2708
|
3756 if (node.response === undefined) {
|
nicholas@2498
|
3757 surveyresult.appendChild(this.parent.document.createElement('response'));
|
nicholas@2498
|
3758 break;
|
nicholas@2498
|
3759 }
|
nicholas@2498
|
3760 for (var i = 0; i < node.response.length; i++) {
|
nicholas@2708
|
3761 surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i]));
|
nicholas@2498
|
3762 }
|
nicholas@2498
|
3763 break;
|
nicholas@2498
|
3764 }
|
nicholas@2498
|
3765 };
|
nicholas@2498
|
3766 this.complete = function () {
|
nicholas@2498
|
3767 this.state = "complete";
|
nicholas@2498
|
3768 this.XMLDOM.setAttribute("state", this.state);
|
nicholas@2708
|
3769 };
|
nicholas@2498
|
3770 };
|
nicholas@2498
|
3771
|
nicholas@2498
|
3772 this.pageNode = function (parent, specification) {
|
nicholas@2498
|
3773 // Create one store per test page
|
nicholas@2498
|
3774 this.specification = specification;
|
nicholas@2498
|
3775 this.parent = parent;
|
nicholas@2498
|
3776 this.state = "empty";
|
nicholas@2498
|
3777 this.XMLDOM = this.parent.document.createElement('page');
|
nicholas@2498
|
3778 this.XMLDOM.setAttribute('ref', specification.id);
|
nicholas@2498
|
3779 this.XMLDOM.setAttribute('presentedId', specification.presentedId);
|
nicholas@2498
|
3780 this.XMLDOM.setAttribute("state", this.state);
|
nicholas@2708
|
3781 if (specification.preTest !== undefined) {
|
nicholas@2498
|
3782 this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
|
nicholas@2498
|
3783 }
|
nicholas@2708
|
3784 if (specification.postTest !== undefined) {
|
nicholas@2498
|
3785 this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
|
nicholas@2498
|
3786 }
|
nicholas@2498
|
3787
|
nicholas@2498
|
3788 // Add any page metrics
|
nicholas@2498
|
3789 var page_metric = this.parent.document.createElement('metric');
|
nicholas@2498
|
3790 this.XMLDOM.appendChild(page_metric);
|
nicholas@2498
|
3791
|
nicholas@2498
|
3792 // Add the audioelement
|
nicholas@2708
|
3793 this.specification.audioElements.forEach(function (element) {
|
nicholas@2498
|
3794 var aeNode = this.parent.document.createElement('audioelement');
|
nicholas@2498
|
3795 aeNode.setAttribute('ref', element.id);
|
nicholas@2708
|
3796 if (element.name !== undefined) {
|
nicholas@2708
|
3797 aeNode.setAttribute('name', element.name);
|
nicholas@2708
|
3798 }
|
nicholas@2498
|
3799 aeNode.setAttribute('type', element.type);
|
nicholas@2498
|
3800 aeNode.setAttribute('url', element.url);
|
nicholas@2498
|
3801 aeNode.setAttribute('fqurl', qualifyURL(element.url));
|
nicholas@2498
|
3802 aeNode.setAttribute('gain', element.gain);
|
nicholas@2498
|
3803 if (element.type == 'anchor' || element.type == 'reference') {
|
nicholas@2498
|
3804 if (element.marker > 0) {
|
nicholas@2498
|
3805 aeNode.setAttribute('marker', element.marker);
|
nicholas@2464
|
3806 }
|
nicholas@2498
|
3807 }
|
nicholas@2498
|
3808 var ae_metric = this.parent.document.createElement('metric');
|
nicholas@2498
|
3809 aeNode.appendChild(ae_metric);
|
nicholas@2498
|
3810 this.XMLDOM.appendChild(aeNode);
|
nicholas@2708
|
3811 }, this);
|
nicholas@2498
|
3812
|
nicholas@2498
|
3813 this.parent.root.appendChild(this.XMLDOM);
|
nicholas@2498
|
3814
|
nicholas@2498
|
3815 this.complete = function () {
|
nicholas@2224
|
3816 this.state = "complete";
|
nicholas@2498
|
3817 this.XMLDOM.setAttribute("state", "complete");
|
nicholas@2708
|
3818 };
|
nicholas@2498
|
3819 };
|
nicholas@2498
|
3820 this.update = function () {
|
nicholas@2224
|
3821 this.SessionKey.update();
|
nicholas@2708
|
3822 };
|
nicholas@2498
|
3823 this.finish = function () {
|
nicholas@2498
|
3824 this.state = 1;
|
nicholas@2498
|
3825 this.root.setAttribute("state", "complete");
|
nicholas@2498
|
3826 return this.root;
|
nicholas@2498
|
3827 };
|
nicholas@2722
|
3828
|
nicholas@2722
|
3829 Object.defineProperties(this, {
|
nicholas@2722
|
3830 'filenamePrefix': {
|
nicholas@2722
|
3831 'get': function () {
|
nicholas@2722
|
3832 return pFilenamePrefix;
|
nicholas@2722
|
3833 },
|
nicholas@2722
|
3834 'set': function (value) {
|
nicholas@2722
|
3835 if (typeof value !== "string") {
|
nicholas@2722
|
3836 value = String(value);
|
nicholas@2722
|
3837 }
|
nicholas@2722
|
3838 pFilenamePrefix = value;
|
nicholas@2722
|
3839 return value;
|
nicholas@2722
|
3840 }
|
nicholas@2722
|
3841 }
|
nicholas@2725
|
3842 });
|
nicholas@2224
|
3843 }
|
nicholas@2384
|
3844
|
nicholas@2401
|
3845 var window_depedancy_callback;
|
nicholas@2498
|
3846 window_depedancy_callback = window.setInterval(function () {
|
nicholas@2401
|
3847 if (check_dependancies()) {
|
nicholas@2401
|
3848 window.clearInterval(window_depedancy_callback);
|
nicholas@2401
|
3849 onload();
|
nicholas@2401
|
3850 } else {
|
nicholas@2401
|
3851 document.getElementById("topLevelBody").innerHTML = "<h1>Loading Resources</h1>";
|
nicholas@2401
|
3852 }
|
nicholas@2498
|
3853 }, 100);
|