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