To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Revision:

root / core.js

History | View | Annotate | Download (111 KB)

1
/**
2
 * core.js
3
 * 
4
 * Main script to run, calls all other core functions and manages loading/store to backend.
5
 * Also contains all global variables.
6
 */
7

    
8
/* create the web audio API context and store in audioContext*/
9
var audioContext; // Hold the browser web audio API
10
var projectXML; // Hold the parsed setup XML
11
var schemaXSD; // Hold the parsed schema XSD
12
var specification;
13
var interfaceContext;
14
var storage;
15
var popup; // Hold the interfacePopup object
16
var testState;
17
var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
18
var audioEngineContext; // The custom AudioEngine object
19
var projectReturn; 
20
var returnUrl; // Holds the url to be redirected to at the end of the test
21

    
22
// Add a prototype to the bufferSourceNode to reference to the audioObject holding it
23
AudioBufferSourceNode.prototype.owner = undefined;
24
// Add a prototype to the bufferSourceNode to hold when the object was given a play command
25
AudioBufferSourceNode.prototype.playbackStartTime = undefined;
26
// Add a prototype to the bufferNode to hold the desired LINEAR gain
27
AudioBuffer.prototype.playbackGain = undefined;
28
// Add a prototype to the bufferNode to hold the computed LUFS loudness
29
AudioBuffer.prototype.lufs = undefined;
30

    
31
// Firefox does not have an XMLDocument.prototype.getElementsByName
32
// and there is no searchAll style command, this custom function will
33
// search all children recusrively for the name. Used for XSD where all
34
// element nodes must have a name and therefore can pull the schema node
35
XMLDocument.prototype.getAllElementsByName = function(name)
36
{
37
    name = String(name);
38
    var selected = this.documentElement.getAllElementsByName(name);
39
    return selected;
40
}
41

    
42
Element.prototype.getAllElementsByName = function(name)
43
{
44
    name = String(name);
45
    var selected = [];
46
    var node = this.firstElementChild;
47
    while(node != null)
48
    {
49
        if (node.getAttribute('name') == name)
50
        {
51
            selected.push(node);
52
        }
53
        if (node.childElementCount > 0)
54
        {
55
            selected = selected.concat(node.getAllElementsByName(name));
56
        }
57
        node = node.nextElementSibling;
58
    }
59
    return selected;
60
}
61

    
62
XMLDocument.prototype.getAllElementsByTagName = function(name)
63
{
64
    name = String(name);
65
    var selected = this.documentElement.getAllElementsByTagName(name);
66
    return selected;
67
}
68

    
69
Element.prototype.getAllElementsByTagName = function(name)
70
{
71
    name = String(name);
72
    var selected = [];
73
    var node = this.firstElementChild;
74
    while(node != null)
75
    {
76
        if (node.nodeName == name)
77
        {
78
            selected.push(node);
79
        }
80
        if (node.childElementCount > 0)
81
        {
82
            selected = selected.concat(node.getAllElementsByTagName(name));
83
        }
84
        node = node.nextElementSibling;
85
    }
86
    return selected;
87
}
88

    
89
// Firefox does not have an XMLDocument.prototype.getElementsByName
90
if (typeof XMLDocument.prototype.getElementsByName != "function") {
91
    XMLDocument.prototype.getElementsByName = function(name)
92
    {
93
        name = String(name);
94
        var node = this.documentElement.firstElementChild;
95
        var selected = [];
96
        while(node != null)
97
        {
98
            if (node.getAttribute('name') == name)
99
            {
100
                selected.push(node);
101
            }
102
            node = node.nextElementSibling;
103
        }
104
        return selected;
105
    }
106
}
107

    
108
window.onload = function() {
109
        // Function called once the browser has loaded all files.
110
        // This should perform any initial commands such as structure / loading documents
111
        
112
        // Create a web audio API context
113
        // Fixed for cross-browser support
114
        var AudioContext = window.AudioContext || window.webkitAudioContext;
115
        audioContext = new AudioContext;
116
        
117
        // Create test state
118
        testState = new stateMachine();
119
        
120
        // Create the popup interface object
121
        popup = new interfacePopup();
122
    
123
    // Create the specification object
124
        specification = new Specification();
125
        
126
        // Create the interface object
127
        interfaceContext = new Interface(specification);
128
        
129
        // Create the storage object
130
        storage = new Storage();
131
        // Define window callbacks for interface
132
        window.onresize = function(event){interfaceContext.resizeWindow(event);};
133
};
134

    
135
function loadProjectSpec(url) {
136
        // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
137
        // If url is null, request client to upload project XML document
138
        var xmlhttp = new XMLHttpRequest();
139
        xmlhttp.open("GET",'test-schema.xsd',true);
140
        xmlhttp.onload = function()
141
        {
142
                schemaXSD = xmlhttp.response;
143
                var parse = new DOMParser();
144
                specification.schema = parse.parseFromString(xmlhttp.response,'text/xml');
145
                var r = new XMLHttpRequest();
146
                r.open('GET',url,true);
147
                r.onload = function() {
148
                        loadProjectSpecCallback(r.response);
149
                };
150
        r.onerror = function() {
151
            document.getElementsByTagName('body')[0].innerHTML = null;
152
            var msg = document.createElement("h3");
153
            msg.textContent = "FATAL ERROR";
154
            var span = document.createElement("p");
155
            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.";
156
            document.getElementsByTagName('body')[0].appendChild(msg);
157
            document.getElementsByTagName('body')[0].appendChild(span);
158
        }
159
                r.send();
160
        };
161
        xmlhttp.send();
162
};
163

    
164
function loadProjectSpecCallback(response) {
165
        // Function called after asynchronous download of XML project specification
166
        //var decode = $.parseXML(response);
167
        //projectXML = $(decode);
168
        
169
    // Check if XML is new or a resumption
170
    var parse = new DOMParser();
171
        var responseDocument = parse.parseFromString(response,'text/xml');
172
    var errorNode = responseDocument.getElementsByTagName('parsererror');
173
        if (errorNode.length >= 1)
174
        {
175
                var msg = document.createElement("h3");
176
                msg.textContent = "FATAL ERROR";
177
                var span = document.createElement("span");
178
                span.textContent = "The XML parser returned the following errors when decoding your XML file";
179
                document.getElementsByTagName('body')[0].innerHTML = null;
180
                document.getElementsByTagName('body')[0].appendChild(msg);
181
                document.getElementsByTagName('body')[0].appendChild(span);
182
                document.getElementsByTagName('body')[0].appendChild(errorNode[0]);
183
                return;
184
        }
185
    if (responseDocument.children[0].nodeName == "waet") {
186
        // document is a specification
187
        
188
        // Perform XML schema validation
189
        var Module = {
190
            xml: response,
191
            schema: schemaXSD,
192
            arguments:["--noout", "--schema", 'test-schema.xsd','document.xml']
193
        };
194
            projectXML = responseDocument;
195
        var xmllint = validateXML(Module);
196
        console.log(xmllint);
197
        if(xmllint != 'document.xml validates\n')
198
        {
199
            document.getElementsByTagName('body')[0].innerHTML = null;
200
            var msg = document.createElement("h3");
201
            msg.textContent = "FATAL ERROR";
202
            var span = document.createElement("h4");
203
            span.textContent = "The XML validator returned the following errors when decoding your XML file";
204
            document.getElementsByTagName('body')[0].appendChild(msg);
205
            document.getElementsByTagName('body')[0].appendChild(span);
206
            xmllint = xmllint.split('\n');
207
            for (var i in xmllint)
208
            {
209
                document.getElementsByTagName('body')[0].appendChild(document.createElement('br'));
210
                var span = document.createElement("span");
211
                span.textContent = xmllint[i];
212
                document.getElementsByTagName('body')[0].appendChild(span);
213
            }
214
            return;
215
        }
216
        // Build the specification
217
           specification.decode(projectXML);
218
        // Generate the session-key
219
        storage.initialise();
220
        
221
    } else if (responseDocument.children[0].nodeName == "waetresult") {
222
        // document is a result
223
        projectXML = document.implementation.createDocument(null,"waet");
224
        projectXML.children[0].appendChild(responseDocument.getElementsByTagName('waet')[0].getElementsByTagName("setup")[0].cloneNode(true));
225
        var child = responseDocument.children[0].children[0];
226
        while (child != null) {
227
            if (child.nodeName == "survey") {
228
                // One of the global survey elements
229
                if (child.getAttribute("state") == "complete") {
230
                    // We need to remove this survey from <setup>
231
                    var location = child.getAttribute("location");
232
                    var globalSurveys = projectXML.getElementsByTagName("setup")[0].getElementsByTagName("survey")[0];
233
                    while(globalSurveys != null) {
234
                        if (location == "pre" || location == "before") {
235
                            if (globalSurveys.getAttribute("location") == "pre" || globalSurveys.getAttribute("location") == "before") {
236
                                projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
237
                                break;
238
                            }
239
                        } else {
240
                            if (globalSurveys.getAttribute("location") == "post" || globalSurveys.getAttribute("location") == "after") {
241
                                projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
242
                                break;
243
                            }
244
                        }
245
                        globalSurveys = globalSurveys.nextElementSibling;
246
                    }
247
                } else {
248
                    // We need to complete this, so it must be regenerated by store
249
                    var copy = child;
250
                    child = child.previousElementSibling;
251
                    responseDocument.children[0].removeChild(copy);
252
                }
253
            } else if (child.nodeName == "page") {
254
                if (child.getAttribute("state") == "empty") {
255
                    // We need to complete this page
256
                    projectXML.children[0].appendChild(responseDocument.getElementById(child.getAttribute("ref")).cloneNode(true));
257
                    var copy = child;
258
                    child = child.previousElementSibling;
259
                    responseDocument.children[0].removeChild(copy);
260
                }
261
            }
262
            child = child.nextElementSibling;
263
        }
264
        // Build the specification
265
            specification.decode(projectXML);
266
        // Use the original
267
        storage.initialise(responseDocument);
268
    }
269
        /// CHECK FOR SAMPLE RATE COMPATIBILITY
270
        if (specification.sampleRate != undefined) {
271
                if (Number(specification.sampleRate) != audioContext.sampleRate) {
272
                        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.';
273
                        alert(errStr);
274
                        return;
275
                }
276
        }
277
        
278
        // Detect the interface to use and load the relevant javascripts.
279
        var interfaceJS = document.createElement('script');
280
        interfaceJS.setAttribute("type","text/javascript");
281
        switch(specification.interface)
282
        {
283
                case "APE":
284
                interfaceJS.setAttribute("src","interfaces/ape.js");
285
                
286
                // APE comes with a css file
287
                var css = document.createElement('link');
288
                css.rel = 'stylesheet';
289
                css.type = 'text/css';
290
                css.href = 'interfaces/ape.css';
291
                
292
                document.getElementsByTagName("head")[0].appendChild(css);
293
                break;
294
                
295
                case "MUSHRA":
296
                interfaceJS.setAttribute("src","interfaces/mushra.js");
297
                
298
                // MUSHRA comes with a css file
299
                var css = document.createElement('link');
300
                css.rel = 'stylesheet';
301
                css.type = 'text/css';
302
                css.href = 'interfaces/mushra.css';
303
                
304
                document.getElementsByTagName("head")[0].appendChild(css);
305
                break;
306
                
307
                case "AB":
308
                interfaceJS.setAttribute("src","interfaces/AB.js?"+Math.random());
309
                
310
                // AB comes with a css file
311
                var css = document.createElement('link');
312
                css.rel = 'stylesheet';
313
                css.type = 'text/css';
314
                css.href = 'interfaces/AB.css?'+Math.random();
315
                
316
                document.getElementsByTagName("head")[0].appendChild(css);
317
                break;
318
                case "Bipolar":
319
                case "ACR":
320
                case "DCR":
321
                case "CCR":
322
                case "ABC":
323
                // Above enumerate to horizontal sliders
324
                interfaceJS.setAttribute("src","interfaces/horizontal-sliders.js");
325
                
326
                // horizontal-sliders comes with a css file
327
                var css = document.createElement('link');
328
                css.rel = 'stylesheet';
329
                css.type = 'text/css';
330
                css.href = 'interfaces/horizontal-sliders.css';
331
                
332
                document.getElementsByTagName("head")[0].appendChild(css);
333
                break;
334
                case "discrete":
335
                case "likert":
336
                // Above enumerate to horizontal discrete radios
337
                interfaceJS.setAttribute("src","interfaces/discrete.js");
338
                
339
                // horizontal-sliders comes with a css file
340
                var css = document.createElement('link');
341
                css.rel = 'stylesheet';
342
                css.type = 'text/css';
343
                css.href = 'interfaces/discrete.css';
344
                
345
                document.getElementsByTagName("head")[0].appendChild(css);
346
                break;
347
        }
348
        document.getElementsByTagName("head")[0].appendChild(interfaceJS);
349
        
350
        // Create the audio engine object
351
        audioEngineContext = new AudioEngine(specification);
352
}
353

    
354
function createProjectSave(destURL) {
355
        // Save the data from interface into XML and send to destURL
356
        // If destURL is null then download XML in client
357
        // Now time to render file locally
358
        var xmlDoc = interfaceXMLSave();
359
        var parent = document.createElement("div");
360
        parent.appendChild(xmlDoc);
361
        var file = [parent.innerHTML];
362
        if (destURL == "local") {
363
                var bb = new Blob(file,{type : 'application/xml'});
364
                var dnlk = window.URL.createObjectURL(bb);
365
                var a = document.createElement("a");
366
                a.hidden = '';
367
                a.href = dnlk;
368
                a.download = "save.xml";
369
                a.textContent = "Save File";
370
                
371
                popup.showPopup();
372
                popup.popupContent.innerHTML = "<span>Please save the file below to give to your test supervisor</span><br>";
373
                popup.popupContent.appendChild(a);
374
        } else {
375
                destUrlFull = destURL+"?key="+storage.SessionKey.key;
376
                var saveFilenamePrefix;
377
                // parse the querystring of destUrl, get the "id" (if any) and append it to destUrl
378
                if(typeof(returnUrl) !== "undefined"){
379
                        var qs = returnUrl.split("?");
380
                        if(qs.length == 2){
381
                                qs = qs[1];
382
                                qs = qs.split("&");
383
                                for(var n = 0; n < qs.length; n++){
384
                                        var pair = qs[n].split("=");
385
                                        if (pair[0] == "id") {
386
                                                saveFilenamePrefix = pair[1];
387
                                        }
388
                                }
389
                        }
390
                }
391
                if(typeof(saveFilenamePrefix) !== "undefined"){
392
                        destUrlFull+="&saveFilenamePrefix="+saveFilenamePrefix;
393
                }
394
                var xmlhttp = new XMLHttpRequest;
395
                xmlhttp.open("POST",destUrlFull,true);
396
                xmlhttp.setRequestHeader('Content-Type', 'text/xml');
397
                xmlhttp.onerror = function(){
398
                        console.log('Error saving file to server! Presenting download locally');
399
                        createProjectSave("local");
400
                };
401
                xmlhttp.onload = function() {
402
            console.log(xmlhttp);
403
            if (this.status >= 300) {
404
                console.log("WARNING - Could not update at this time");
405
                createProjectSave("local");
406
            } else {
407
                var parser = new DOMParser();
408
                var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
409
                var response = xmlDoc.getElementsByTagName('response')[0];
410
                                              window.onbeforeunload=null;
411
                if (response.getAttribute("state") == "OK") {
412
                    var file = response.getElementsByTagName("file")[0];
413
                                                              if(typeof(returnUrl) !== "undefined"){        
414
                                                                                                window.location = returnUrl;
415
                                                                                }
416
                    console.log("Save: OK, written "+file.getAttribute("bytes")+"B");
417
                    popup.popupContent.textContent = "Thank you. Your session has been saved.";
418
                      } else {
419
                    var message = response.getElementsByTagName("message");
420
                    console.log("Save: Error! "+message.textContent);
421
                    createProjectSave("local");
422
                }
423
            }
424
        };
425
                xmlhttp.send(file);
426
                popup.showPopup();
427
                popup.popupContent.innerHTML = null;
428
                popup.popupContent.textContent = "Submitting. Please Wait";
429
        popup.hideNextButton();
430
        popup.hidePreviousButton();
431
        }
432
}
433

    
434
function errorSessionDump(msg){
435
        // Create the partial interface XML save
436
        // Include error node with message on why the dump occured
437
        popup.showPopup();
438
        popup.popupContent.innerHTML = null;
439
        var err = document.createElement('error');
440
        var parent = document.createElement("div");
441
        if (typeof msg === "object")
442
        {
443
                err.appendChild(msg);
444
                popup.popupContent.appendChild(msg);
445
                
446
        } else {
447
                err.textContent = msg;
448
                popup.popupContent.innerHTML = "ERROR : "+msg;
449
        }
450
        var xmlDoc = interfaceXMLSave();
451
        xmlDoc.appendChild(err);
452
        parent.appendChild(xmlDoc);
453
        var file = [parent.innerHTML];
454
        var bb = new Blob(file,{type : 'application/xml'});
455
        var dnlk = window.URL.createObjectURL(bb);
456
        var a = document.createElement("a");
457
        a.hidden = '';
458
        a.href = dnlk;
459
        a.download = "save.xml";
460
        a.textContent = "Save File";
461
        
462
        
463
        
464
        popup.popupContent.appendChild(a);
465
}
466

    
467
// Only other global function which must be defined in the interface class. Determines how to create the XML document.
468
function interfaceXMLSave(){
469
        // Create the XML string to be exported with results
470
        return storage.finish();
471
}
472

    
473
function linearToDecibel(gain)
474
{
475
        return 20.0*Math.log10(gain);
476
}
477

    
478
function decibelToLinear(gain)
479
{
480
        return Math.pow(10,gain/20.0);
481
}
482

    
483
function randomString(length) {
484
    return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
485
}
486

    
487
function interfacePopup() {
488
        // Creates an object to manage the popup
489
        this.popup = null;
490
        this.popupContent = null;
491
        this.popupTitle = null;
492
        this.popupResponse = null;
493
        this.buttonProceed = null;
494
        this.buttonPrevious = null;
495
        this.popupOptions = null;
496
        this.currentIndex = null;
497
        this.node = null;
498
        this.store = null;
499
        $(window).keypress(function(e){
500
                        if (e.keyCode == 13 && popup.popup.style.visibility == 'visible')
501
                        {
502
                                console.log(e);
503
                                popup.buttonProceed.onclick();
504
                                e.preventDefault();
505
                        }
506
                });
507
        
508
        this.createPopup = function(){
509
                // Create popup window interface
510
                var insertPoint = document.getElementById("topLevelBody");
511
                
512
                this.popup = document.getElementById('popupHolder');
513
                this.popup.style.left = (window.innerWidth/2)-250 + 'px';
514
                this.popup.style.top = (window.innerHeight/2)-125 + 'px';
515
                
516
                this.popupContent = document.getElementById('popupContent');
517
                
518
                this.popupTitle = document.getElementById('popupTitle');
519
                
520
                this.popupResponse = document.getElementById('popupResponse');
521
                
522
                this.buttonProceed = document.getElementById('popup-proceed');
523
                this.buttonProceed.onclick = function(){popup.proceedClicked();};
524
                
525
                this.buttonPrevious = document.getElementById('popup-previous');
526
                this.buttonPrevious.onclick = function(){popup.previousClick();};
527
                
528
        this.hidePopup();
529
        
530
                this.popup.style.zIndex = -1;
531
                this.popup.style.visibility = 'hidden';
532
        };
533
        
534
        this.showPopup = function(){
535
                if (this.popup == null) {
536
                        this.createPopup();
537
                }
538
                this.popup.style.zIndex = 3;
539
                this.popup.style.visibility = 'visible';
540
                var blank = document.getElementsByClassName('testHalt')[0];
541
                blank.style.zIndex = 2;
542
                blank.style.visibility = 'visible';
543
        };
544
        
545
        this.hidePopup = function(){
546
                this.popup.style.zIndex = -1;
547
                this.popup.style.visibility = 'hidden';
548
                var blank = document.getElementsByClassName('testHalt')[0];
549
                blank.style.zIndex = -2;
550
                blank.style.visibility = 'hidden';
551
                this.buttonPrevious.style.visibility = 'inherit';
552
        };
553
        
554
        this.postNode = function() {
555
                // This will take the node from the popupOptions and display it
556
                var node = this.popupOptions[this.currentIndex];
557
                this.popupResponse.innerHTML = null;
558
                this.popupTitle.textContent = node.specification.statement;
559
                if (node.specification.type == 'question') {
560
                        var textArea = document.createElement('textarea');
561
                        switch (node.specification.boxsize) {
562
                        case 'small':
563
                                textArea.cols = "20";
564
                                textArea.rows = "1";
565
                                break;
566
                        case 'normal':
567
                                textArea.cols = "30";
568
                                textArea.rows = "2";
569
                                break;
570
                        case 'large':
571
                                textArea.cols = "40";
572
                                textArea.rows = "5";
573
                                break;
574
                        case 'huge':
575
                                textArea.cols = "50";
576
                                textArea.rows = "10";
577
                                break;
578
                        }
579
            if (node.response == undefined) {
580
                node.response = "";
581
            } else {
582
                textArea.value = node.response;
583
            }
584
                        this.popupResponse.appendChild(textArea);
585
                        textArea.focus();
586
            this.popupResponse.style.textAlign="center";
587
            this.popupResponse.style.left="0%";
588
                } else if (node.specification.type == 'checkbox') {
589
            if (node.response == undefined) {
590
                node.response = Array(node.specification.options.length);
591
            }
592
            var index = 0;
593
            var max_w = 0;
594
                        for (var option of node.specification.options) {
595
                                var input = document.createElement('input');
596
                                input.id = option.name;
597
                                input.type = 'checkbox';
598
                                var span = document.createElement('span');
599
                                span.textContent = option.text;
600
                                var hold = document.createElement('div');
601
                                hold.setAttribute('name','option');
602
                                hold.style.padding = '4px';
603
                                hold.appendChild(input);
604
                                hold.appendChild(span);
605
                                this.popupResponse.appendChild(hold);
606
                if (node.response[index] != undefined){
607
                    if (node.response[index].checked == true) {
608
                        input.checked = "true";
609
                    }
610
                }
611
                var w = $(span).width();
612
                if (w > max_w)
613
                    max_w = w;
614
                index++;
615
                        }
616
            max_w += 12;
617
            this.popupResponse.style.textAlign="";
618
            var leftP = ((max_w/500)/2)*100;
619
            this.popupResponse.style.left=leftP+"%";
620
                } else if (node.specification.type == 'radio') {
621
            if (node.response == undefined) {
622
                node.response = {name: "", text: ""};
623
            }
624
            var index = 0;
625
            var max_w = 0;
626
                        for (var option of node.specification.options) {
627
                                var input = document.createElement('input');
628
                                input.id = option.name;
629
                                input.type = 'radio';
630
                                input.name = node.specification.id;
631
                                var span = document.createElement('span');
632
                                span.textContent = option.text;
633
                                var hold = document.createElement('div');
634
                                hold.setAttribute('name','option');
635
                                hold.style.padding = '4px';
636
                                hold.appendChild(input);
637
                                hold.appendChild(span);
638
                                this.popupResponse.appendChild(hold);
639
                if (input.id == node.response.name) {
640
                    input.checked = "true";
641
                }
642
                var w = $(span).width();
643
                if (w > max_w)
644
                    max_w = w;
645
                        }
646
            max_w += 12;
647
            this.popupResponse.style.textAlign="";
648
            var leftP = ((max_w/500)/2)*100;
649
            this.popupResponse.style.left=leftP+"%";
650
                } else if (node.specification.type == 'number') {
651
                        var input = document.createElement('input');
652
                        input.type = 'textarea';
653
                        if (node.min != null) {input.min = node.specification.min;}
654
                        if (node.max != null) {input.max = node.specification.max;}
655
                        if (node.step != null) {input.step = node.specification.step;}
656
            if (node.response != undefined) {
657
                input.value = node.response;
658
            }
659
                        this.popupResponse.appendChild(input);
660
            this.popupResponse.style.textAlign="center";
661
            this.popupResponse.style.left="0%";
662
                }
663
                if(this.currentIndex+1 == this.popupOptions.length) {
664
                        if (this.node.location == "pre") {
665
                                this.buttonProceed.textContent = 'Start';
666
                        } else {
667
                                this.buttonProceed.textContent = 'Submit';
668
                        }
669
                } else {
670
                        this.buttonProceed.textContent = 'Next';
671
                }
672
                if(this.currentIndex > 0)
673
                        this.buttonPrevious.style.visibility = 'visible';
674
                else
675
                        this.buttonPrevious.style.visibility = 'hidden';
676
        };
677
        
678
        this.initState = function(node,store) {
679
                //Call this with your preTest and postTest nodes when needed to
680
                // initialise the popup procedure.
681
                if (node.options.length > 0) {
682
                        this.popupOptions = [];
683
                        this.node = node;
684
                        this.store = store;
685
                        for (var opt of node.options)
686
                        {
687
                                this.popupOptions.push({
688
                                        specification: opt,
689
                                        response: null
690
                                });
691
                        }                        
692
                        this.currentIndex = 0;
693
                        this.showPopup();
694
                        this.postNode();
695
                } else {
696
                        advanceState();
697
                }
698
        };
699
        
700
        this.proceedClicked = function() {
701
                // Each time the popup button is clicked!
702
                var node = this.popupOptions[this.currentIndex];
703
                if (node.specification.type == 'question') {
704
                        // Must extract the question data
705
                        var textArea = $(popup.popupContent).find('textarea')[0];
706
                        if (node.specification.mandatory == true && textArea.value.length == 0) {
707
                                alert('This question is mandatory');
708
                                return;
709
                        } else {
710
                                // Save the text content
711
                                console.log("Question: "+ node.specification.statement);
712
                                console.log("Question Response: "+ textArea.value);
713
                                node.response = textArea.value;
714
                        }
715
                } else if (node.specification.type == 'checkbox') {
716
                        // Must extract checkbox data
717
                        console.log("Checkbox: "+ node.specification.statement);
718
                        var inputs = this.popupResponse.getElementsByTagName('input');
719
                        node.response = [];
720
                        for (var i=0; i<node.specification.options.length; i++) {
721
                                node.response.push({
722
                                        name: node.specification.options[i].name,
723
                                        text: node.specification.options[i].text,
724
                                        checked: inputs[i].checked
725
                                });
726
                                console.log(node.specification.options[i].name+": "+ inputs[i].checked);
727
                        }
728
                } else if (node.specification.type == "radio") {
729
                        var optHold = this.popupResponse;
730
                        console.log("Radio: "+ node.specification.statement);
731
                        node.response = null;
732
                        var i=0;
733
                        var inputs = optHold.getElementsByTagName('input');
734
                        while(node.response == null) {
735
                                if (i == inputs.length)
736
                                {
737
                                        if (node.specification.mandatory == true)
738
                                        {
739
                                                alert("This radio is mandatory");
740
                                        } else {
741
                                                node.response = -1;
742
                                        }
743
                                        return;
744
                                }
745
                                if (inputs[i].checked == true) {
746
                                        node.response = node.specification.options[i];
747
                                        console.log("Selected: "+ node.specification.options[i].name);
748
                                }
749
                                i++;
750
                        }
751
                } else if (node.specification.type == "number") {
752
                        var input = this.popupContent.getElementsByTagName('input')[0];
753
                        if (node.mandatory == true && input.value.length == 0) {
754
                                alert('This question is mandatory. Please enter a number');
755
                                return;
756
                        }
757
                        var enteredNumber = Number(input.value);
758
                        if (isNaN(enteredNumber)) {
759
                                alert('Please enter a valid number');
760
                                return;
761
                        }
762
                        if (enteredNumber < node.min && node.min != null) {
763
                                alert('Number is below the minimum value of '+node.min);
764
                                return;
765
                        }
766
                        if (enteredNumber > node.max && node.max != null) {
767
                                alert('Number is above the maximum value of '+node.max);
768
                                return;
769
                        }
770
                        node.response = input.value;
771
                }
772
                this.currentIndex++;
773
                if (this.currentIndex < this.popupOptions.length) {
774
                        this.postNode();
775
                } else {
776
                        // Reached the end of the popupOptions
777
                        this.hidePopup();
778
                        for (var node of this.popupOptions)
779
                        {
780
                                this.store.postResult(node);
781
                        }
782
            this.store.complete();
783
                        advanceState();
784
                }
785
        };
786
        
787
        this.previousClick = function() {
788
                // Triggered when the 'Back' button is clicked in the survey
789
                if (this.currentIndex > 0) {
790
                        this.currentIndex--;
791
                        this.postNode();
792
                }
793
        };
794
        
795
        this.resize = function(event)
796
        {
797
                // Called on window resize;
798
                if (this.popup != null) {
799
                        this.popup.style.left = (window.innerWidth/2)-250 + 'px';
800
                        this.popup.style.top = (window.innerHeight/2)-125 + 'px';
801
                        var blank = document.getElementsByClassName('testHalt')[0];
802
                        blank.style.width = window.innerWidth;
803
                        blank.style.height = window.innerHeight;
804
                }
805
        };
806
    this.hideNextButton = function() {
807
        this.buttonProceed.style.visibility = "hidden";
808
    }
809
    this.hidePreviousButton = function() {
810
        this.buttonPrevious.style.visibility = "hidden";
811
    }
812
    this.showNextButton = function() {
813
        this.buttonProceed.style.visibility = "visible";
814
    }
815
    this.showPreviousButton = function() {
816
        this.buttonPrevious.style.visibility = "visible";
817
    }
818
}
819

    
820
function advanceState()
821
{
822
        // Just for complete clarity
823
        testState.advanceState();
824
}
825

    
826
function stateMachine()
827
{
828
        // Object prototype for tracking and managing the test state
829
        this.stateMap = [];
830
        this.preTestSurvey = null;
831
        this.postTestSurvey = null;
832
        this.stateIndex = null;
833
        this.currentStateMap = null;
834
        this.currentStatePosition = null;
835
    this.currentStore = null;
836
        this.initialise = function(){
837
                
838
                // Get the data from Specification
839
                var pageHolder = [];
840
                for (var page of specification.pages)
841
                {
842
            var repeat = page.repeatCount;
843
            while(repeat >= 0)
844
            {
845
                pageHolder.push(page);
846
                repeat--;
847
            }
848
                }
849
                if (specification.randomiseOrder)
850
                {
851
                        pageHolder = randomiseOrder(pageHolder);
852
                }
853
                for (var i=0; i<pageHolder.length; i++)
854
                {
855
                        pageHolder[i].presentedId = i;
856
                }
857
                for (var i=0; i<specification.pages.length; i++)
858
                {
859
                        if (specification.testPages <= i && specification.testPages != 0) {break;}
860
                        this.stateMap.push(pageHolder[i]);
861
            storage.createTestPageStore(pageHolder[i]);
862
            for (var element of pageHolder[i].audioElements) {
863
                var URL = pageHolder[i].hostURL + element.url;
864
                var buffer = null;
865
                for (var buffObj of audioEngineContext.buffers) {
866
                    if (URL == buffObj.url) {
867
                        buffer = buffObj;
868
                        break;
869
                    }
870
                }
871
                if (buffer == null) {
872
                    buffer = new audioEngineContext.bufferObj();
873
                    buffer.getMedia(URL);
874
                    audioEngineContext.buffers.push(buffer);
875
                }
876
            }
877
                }
878
        
879
                if (specification.preTest != null) {this.preTestSurvey = specification.preTest;}
880
                if (specification.postTest != null) {this.postTestSurvey = specification.postTest;}
881
                
882
                if (this.stateMap.length > 0) {
883
                        if(this.stateIndex != null) {
884
                                console.log('NOTE - State already initialise');
885
                        }
886
                        this.stateIndex = -1;
887
                } else {
888
                        console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
889
                }
890
        };
891
        this.advanceState = function(){
892
                if (this.stateIndex == null) {
893
                        this.initialise();
894
                }
895
                 $('#box-holders').fadeTo(200, 0.1, function(){
896
                         $('#box-holders').fadeTo(200, 1);
897
                });
898

    
899

    
900
                storage.update();
901
                if (this.stateIndex == -1) {
902
                        this.stateIndex++;
903
                        console.log('Starting test...');
904
                        if (this.preTestSurvey != null)
905
                        {
906
                                popup.initState(this.preTestSurvey,storage.globalPreTest);
907
                        } else {
908
                                this.advanceState();
909
                        }
910
                } else if (this.stateIndex == this.stateMap.length)
911
                {
912
                        // All test pages complete, post test
913
                        console.log('Ending test ...');
914
                        this.stateIndex++;
915
                        if (this.postTestSurvey == null) {
916
                                this.advanceState();
917
                        } else {
918
                                popup.initState(this.postTestSurvey,storage.globalPostTest);
919
                        }
920
                } else if (this.stateIndex > this.stateMap.length)
921
                {
922
                        createProjectSave(specification.projectReturn);
923
                }
924
                else
925
                {
926
                        if (this.currentStateMap == null)
927
                        {
928
                                this.currentStateMap = this.stateMap[this.stateIndex];
929
                                if (this.currentStateMap.randomiseOrder)
930
                                {
931
                                        this.currentStateMap.audioElements = randomiseOrder(this.currentStateMap.audioElements);
932
                                }
933
                this.currentStore = storage.testPages[this.stateIndex];
934
                                if (this.currentStateMap.preTest != null)
935
                                {
936
                                        this.currentStatePosition = 'pre';
937
                                        popup.initState(this.currentStateMap.preTest,storage.testPages[this.stateIndex].preTest);
938
                                } else {
939
                                        this.currentStatePosition = 'test';
940
                                }
941
                                interfaceContext.newPage(this.currentStateMap,storage.testPages[this.stateIndex]);
942
                                return;
943
                        }
944
                        switch(this.currentStatePosition)
945
                        {
946
                        case 'pre':
947
                                this.currentStatePosition = 'test';
948
                                break;
949
                        case 'test':
950
                                this.currentStatePosition = 'post';
951
                                // Save the data
952
                                this.testPageCompleted();
953
                                if (this.currentStateMap.postTest == null)
954
                                {
955
                                        this.advanceState();
956
                                        return;
957
                                } else {
958
                                        popup.initState(this.currentStateMap.postTest,storage.testPages[this.stateIndex].postTest);
959
                                }
960
                                break;
961
                        case 'post':
962
                                this.stateIndex++;
963
                                this.currentStateMap = null;
964
                                this.advanceState();
965
                                break;
966
                        };
967
                }
968
        };
969
        
970
        this.testPageCompleted = function() {
971
                // Function called each time a test page has been completed
972
                var storePoint = storage.testPages[this.stateIndex];
973
                // First get the test metric
974
                
975
                var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0];
976
                if (audioEngineContext.metric.enableTestTimer)
977
                {
978
                        var testTime = storePoint.parent.document.createElement('metricresult');
979
                        testTime.id = 'testTime';
980
                        testTime.textContent = audioEngineContext.timer.testDuration;
981
                        metric.appendChild(testTime);
982
                }
983
                
984
                var audioObjects = audioEngineContext.audioObjects;
985
                for (var ao of audioEngineContext.audioObjects) 
986
                {
987
                        ao.exportXMLDOM();
988
                }
989
                for (var element of interfaceContext.commentQuestions)
990
                {
991
                        element.exportXMLDOM(storePoint);
992
                }
993
                pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
994
        storePoint.complete();
995
        };
996
}
997

    
998
function AudioEngine(specification) {
999
        
1000
        // Create two output paths, the main outputGain and fooGain.
1001
        // Output gain is default to 1 and any items for playback route here
1002
        // Foo gain is used for analysis to ensure paths get processed, but are not heard
1003
        // because web audio will optimise and any route which does not go to the destination gets ignored.
1004
        this.outputGain = audioContext.createGain();
1005
        this.fooGain = audioContext.createGain();
1006
        this.fooGain.gain = 0;
1007
        
1008
        // Use this to detect playback state: 0 - stopped, 1 - playing
1009
        this.status = 0;
1010
        
1011
        // Connect both gains to output
1012
        this.outputGain.connect(audioContext.destination);
1013
        this.fooGain.connect(audioContext.destination);
1014
        
1015
        // Create the timer Object
1016
        this.timer = new timer();
1017
        // Create session metrics
1018
        this.metric = new sessionMetrics(this,specification);
1019
        
1020
        this.loopPlayback = false;
1021
        
1022
        this.pageStore = null;
1023
        
1024
        // Create store for new audioObjects
1025
        this.audioObjects = [];
1026
        
1027
        this.buffers = [];
1028
        this.bufferObj = function()
1029
        {
1030
                this.url = null;
1031
                this.buffer = null;
1032
                this.xmlRequest = new XMLHttpRequest();
1033
                this.xmlRequest.parent = this;
1034
                this.users = [];
1035
        this.progress = 0;
1036
        this.status = 0;
1037
                this.ready = function()
1038
                {
1039
            if (this.status >= 2)
1040
            {
1041
                this.status = 3;
1042
            }
1043
                        for (var i=0; i<this.users.length; i++)
1044
                        {
1045
                                this.users[i].state = 1;
1046
                                if (this.users[i].interfaceDOM != null)
1047
                                {
1048
                                        this.users[i].bufferLoaded(this);
1049
                                }
1050
                        }
1051
                };
1052
                this.getMedia = function(url) {
1053
                        this.url = url;
1054
                        this.xmlRequest.open('GET',this.url,true);
1055
                        this.xmlRequest.responseType = 'arraybuffer';
1056
                        
1057
                        var bufferObj = this;
1058
                        
1059
                        // Create callback to decode the data asynchronously
1060
                        this.xmlRequest.onloadend = function() {
1061
                // Use inbuilt WAVE decoder first
1062
                if (this.status == -1) {return;}
1063
                var waveObj = new WAVE();
1064
                if (waveObj.open(bufferObj.xmlRequest.response) == 0)
1065
                {
1066
                    bufferObj.buffer = audioContext.createBuffer(waveObj.num_channels,waveObj.num_samples,waveObj.sample_rate);
1067
                    for (var c=0; c<waveObj.num_channels; c++)
1068
                    {
1069
                        var buffer_ptr = bufferObj.buffer.getChannelData(c);
1070
                        for (var n=0; n<waveObj.num_samples; n++)
1071
                        {
1072
                            buffer_ptr[n] = waveObj.decoded_data[c][n];
1073
                        }
1074
                    }
1075

    
1076
                    delete waveObj;
1077
                } else {
1078
                    audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) {
1079
                        bufferObj.buffer = decodedData;
1080
                    }, function(e){
1081
                        // Should only be called if there was an error, but sometimes gets called continuously
1082
                        // Check here if the error is genuine
1083
                        if (bufferObj.xmlRequest.response == undefined) {
1084
                            // Genuine error
1085
                            console.log('FATAL - Error loading buffer on '+audioObj.id);
1086
                            if (request.status == 404)
1087
                            {
1088
                                console.log('FATAL - Fragment '+audioObj.id+' 404 error');
1089
                                console.log('URL: '+audioObj.url);
1090
                                errorSessionDump('Fragment '+audioObj.id+' 404 error');
1091
                            }
1092
                            this.parent.status = -1;
1093
                        }
1094
                    });
1095
                }
1096
                if (bufferObj.buffer != undefined)
1097
                {
1098
                    bufferObj.status = 2;
1099
                    calculateLoudness(bufferObj,"I");
1100
                }
1101
                        };
1102
            
1103
            // Create callback for any error in loading
1104
            this.xmlRequest.onerror = function() {
1105
                this.parent.status = -1;
1106
                for (var i=0; i<this.parent.users.length; i++)
1107
                {
1108
                    this.parent.users[i].state = -1;
1109
                    if (this.parent.users[i].interfaceDOM != null)
1110
                    {
1111
                        this.parent.users[i].bufferLoaded(this);
1112
                    }
1113
                }
1114
            }
1115
            
1116
                        this.progress = 0;
1117
                        this.progressCallback = function(event){
1118
                                if (event.lengthComputable)
1119
                                {
1120
                                        this.parent.progress = event.loaded / event.total;
1121
                                        for (var i=0; i<this.parent.users.length; i++)
1122
                                        {
1123
                                                if(this.parent.users[i].interfaceDOM != null)
1124
                                                {
1125
                                                        if (typeof this.parent.users[i].interfaceDOM.updateLoading === "function")
1126
                                                        {
1127
                                                                this.parent.users[i].interfaceDOM.updateLoading(this.parent.progress*100);
1128
                                                        }
1129
                                                }
1130
                                        }
1131
                                }
1132
                        };
1133
                        this.xmlRequest.addEventListener("progress", this.progressCallback);
1134
            this.status = 1;
1135
                        this.xmlRequest.send();
1136
                };
1137
        
1138
        this.registerAudioObject = function(audioObject)
1139
        {
1140
            // Called by an audioObject to register to the buffer for use
1141
            // First check if already in the register pool
1142
            for (var objects of this.users)
1143
            {
1144
                if (audioObject.id == objects.id){return 0;}
1145
            }
1146
            this.users.push(audioObject);
1147
            if (this.status == 3 || this.status == -1)
1148
            {
1149
                // The buffer is already ready, trigger bufferLoaded
1150
                audioObject.bufferLoaded(this);
1151
            }
1152
        }
1153
        };
1154
        
1155
        this.play = function(id) {
1156
                // Start the timer and set the audioEngine state to playing (1)
1157
                if (this.status == 0 && this.loopPlayback) {
1158
                        // Check if all audioObjects are ready
1159
                        if(this.checkAllReady())
1160
                        {
1161
                                this.status = 1;
1162
                                this.setSynchronousLoop();
1163
                        }
1164
                }
1165
                else
1166
                {
1167
                        this.status = 1;
1168
                }
1169
                if (this.status== 1) {
1170
                        this.timer.startTest();
1171
                        if (id == undefined) {
1172
                                id = -1;
1173
                                console.log('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
1174
                                return;
1175
                        } else {
1176
                                interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
1177
                        }
1178
                        if (this.loopPlayback) {
1179
                var setTime = audioContext.currentTime;
1180
                                for (var i=0; i<this.audioObjects.length; i++)
1181
                                {
1182
                                        this.audioObjects[i].play(setTime);
1183
                                        if (id == i) {
1184
                                                this.audioObjects[i].loopStart(setTime);
1185
                                        } else {
1186
                                                this.audioObjects[i].loopStop(setTime);
1187
                                        }
1188
                                }
1189
                        } else {
1190
                var setTime = audioContext.currentTime+0.1;
1191
                                for (var i=0; i<this.audioObjects.length; i++)
1192
                                {
1193
                                        if (i != id) {
1194
                                                this.audioObjects[i].stop(setTime);
1195
                                        } else if (i == id) {
1196
                                                this.audioObjects[id].play(setTime);
1197
                                        }
1198
                                }
1199
                        }
1200
                        interfaceContext.playhead.start();
1201
                }
1202
        };
1203
        
1204
        this.stop = function() {
1205
                // Send stop and reset command to all playback buffers
1206
                if (this.status == 1) {
1207
            var setTime = audioContext.currentTime+0.1;
1208
                        for (var i=0; i<this.audioObjects.length; i++)
1209
                        {
1210
                                this.audioObjects[i].stop(setTime);
1211
                        }
1212
                        interfaceContext.playhead.stop();
1213
                }
1214
        };
1215
        
1216
        this.newTrack = function(element) {
1217
                // Pull data from given URL into new audio buffer
1218
                // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
1219
                
1220
                // Create the audioObject with ID of the new track length;
1221
                audioObjectId = this.audioObjects.length;
1222
                this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
1223

    
1224
                // Check if audioObject buffer is currently stored by full URL
1225
                var URL = testState.currentStateMap.hostURL + element.url;
1226
                var buffer = null;
1227
                for (var i=0; i<this.buffers.length; i++)
1228
                {
1229
                        if (URL == this.buffers[i].url)
1230
                        {
1231
                                buffer = this.buffers[i];
1232
                                break;
1233
                        }
1234
                }
1235
                if (buffer == null)
1236
                {
1237
                        console.log("[WARN]: Buffer was not loaded in pre-test! "+URL);
1238
                        buffer = new this.bufferObj();
1239
            this.buffers.push(buffer);
1240
                        buffer.getMedia(URL);
1241
                }
1242
                this.audioObjects[audioObjectId].specification = element;
1243
                this.audioObjects[audioObjectId].url = URL;
1244
                // Obtain store node
1245
                var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
1246
                for (var i=0; i<aeNodes.length; i++)
1247
                {
1248
                        if(aeNodes[i].getAttribute("ref") == element.id)
1249
                        {
1250
                                this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
1251
                                break;
1252
                        }
1253
                }
1254
        buffer.registerAudioObject(this.audioObjects[audioObjectId]);
1255
                return this.audioObjects[audioObjectId];
1256
        };
1257
        
1258
        this.newTestPage = function(audioHolderObject,store) {
1259
                this.pageStore = store;
1260
                this.status = 0;
1261
                this.audioObjectsReady = false;
1262
                this.metric.reset();
1263
                for (var i=0; i < this.buffers.length; i++)
1264
                {
1265
                        this.buffers[i].users = [];
1266
                }
1267
                this.audioObjects = [];
1268
        this.timer = new timer();
1269
        this.loopPlayback = audioHolderObject.loop;
1270
        };
1271
        
1272
        this.checkAllPlayed = function() {
1273
                arr = [];
1274
                for (var id=0; id<this.audioObjects.length; id++) {
1275
                        if (this.audioObjects[id].metric.wasListenedTo == false) {
1276
                                arr.push(this.audioObjects[id].id);
1277
                        }
1278
                }
1279
                return arr;
1280
        };
1281
        
1282
        this.checkAllReady = function() {
1283
                var ready = true;
1284
                for (var i=0; i<this.audioObjects.length; i++) {
1285
                        if (this.audioObjects[i].state == 0) {
1286
                                // Track not ready
1287
                                console.log('WAIT -- audioObject '+i+' not ready yet!');
1288
                                ready = false;
1289
                        };
1290
                }
1291
                return ready;
1292
        };
1293
        
1294
        this.setSynchronousLoop = function() {
1295
                // Pads the signals so they are all exactly the same length
1296
                var length = 0;
1297
                var maxId;
1298
                for (var i=0; i<this.audioObjects.length; i++)
1299
                {
1300
                        if (length < this.audioObjects[i].buffer.buffer.length)
1301
                        {
1302
                                length = this.audioObjects[i].buffer.buffer.length;
1303
                                maxId = i;
1304
                        }
1305
                }
1306
                // Extract the audio and zero-pad
1307
                for (var i=0; i<this.audioObjects.length; i++)
1308
                {
1309
                        var orig = this.audioObjects[i].buffer.buffer;
1310
                        var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate);
1311
                        for (var c=0; c<orig.numberOfChannels; c++)
1312
                        {
1313
                                var inData = hold.getChannelData(c);
1314
                                var outData = orig.getChannelData(c);
1315
                                for (var n=0; n<orig.length; n++)
1316
                                {inData[n] = outData[n];}
1317
                        }
1318
                        hold.playbackGain = orig.playbackGain;
1319
                        hold.lufs = orig.lufs;
1320
                        this.audioObjects[i].buffer.buffer = hold;
1321
                }
1322
        };
1323
    
1324
    this.exportXML = function()
1325
    {
1326
        
1327
    };
1328
        
1329
}
1330

    
1331
function audioObject(id) {
1332
        // The main buffer object with common control nodes to the AudioEngine
1333
        
1334
        this.specification;
1335
        this.id = id;
1336
        this.state = 0; // 0 - no data, 1 - ready
1337
        this.url = null; // Hold the URL given for the output back to the results.
1338
        this.metric = new metricTracker(this);
1339
        this.storeDOM = null;
1340
        
1341
        // Bindings for GUI
1342
        this.interfaceDOM = null;
1343
        this.commentDOM = null;
1344
        
1345
        // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
1346
        this.bufferNode = undefined;
1347
        this.outputGain = audioContext.createGain();
1348
        
1349
        this.onplayGain = 1.0;
1350
        
1351
        // Connect buffer to the audio graph
1352
        this.outputGain.connect(audioEngineContext.outputGain);
1353
        
1354
        // the audiobuffer is not designed for multi-start playback
1355
        // When stopeed, the buffer node is deleted and recreated with the stored buffer.
1356
        this.buffer;
1357
        
1358
        this.bufferLoaded = function(callee)
1359
        {
1360
                // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
1361
                // audioObject and trigger the interfaceDOM.enable() function for user feedback
1362
        if (callee.status == -1) {
1363
            // ERROR
1364
            this.state = -1;
1365
            if (this.interfaceDOM != null) {this.interfaceDOM.error();}
1366
            this.buffer = callee;
1367
            return;
1368
        }
1369
                if (audioEngineContext.loopPlayback){
1370
                        // First copy the buffer into this.buffer
1371
                        this.buffer = new audioEngineContext.bufferObj();
1372
                        this.buffer.url = callee.url;
1373
                        this.buffer.buffer = audioContext.createBuffer(callee.buffer.numberOfChannels, callee.buffer.length, callee.buffer.sampleRate);
1374
                        for (var c=0; c<callee.buffer.numberOfChannels; c++)
1375
                        {
1376
                                var src = callee.buffer.getChannelData(c);
1377
                                var dst = this.buffer.buffer.getChannelData(c);
1378
                                for (var n=0; n<src.length; n++)
1379
                                {
1380
                                        dst[n] = src[n];
1381
                                }
1382
                        }
1383
                } else {
1384
                        this.buffer = callee;
1385
                }
1386
                this.state = 1;
1387
                this.buffer.buffer.playbackGain = callee.buffer.playbackGain;
1388
                this.buffer.buffer.lufs = callee.buffer.lufs;
1389
                var targetLUFS = this.specification.parent.loudness || specification.loudness;
1390
                if (typeof targetLUFS === "number")
1391
                {
1392
                        this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
1393
                } else {
1394
                        this.buffer.buffer.playbackGain = 1.0;
1395
                }
1396
                if (this.interfaceDOM != null) {
1397
                        this.interfaceDOM.enable();
1398
                }
1399
                this.onplayGain = decibelToLinear(this.specification.gain)*this.buffer.buffer.playbackGain;
1400
                this.storeDOM.setAttribute('playGain',linearToDecibel(this.onplayGain));
1401
        };
1402
        
1403
        this.bindInterface = function(interfaceObject)
1404
        {
1405
                this.interfaceDOM = interfaceObject;
1406
                this.metric.initialise(interfaceObject.getValue());
1407
                if (this.state == 1)
1408
                {
1409
                        this.interfaceDOM.enable();
1410
                } else if (this.state == -1) {
1411
            // ERROR
1412
            this.interfaceDOM.error();
1413
            return;
1414
        }
1415
                this.storeDOM.setAttribute('presentedId',interfaceObject.getPresentedId());
1416
        };
1417
    
1418
        this.loopStart = function(setTime) {
1419
                this.outputGain.gain.linearRampToValueAtTime(this.onplayGain,setTime);
1420
                this.metric.startListening(audioEngineContext.timer.getTestTime());
1421
        this.interfaceDOM.startPlayback();
1422
        };
1423
        
1424
        this.loopStop = function(setTime) {
1425
                if (this.outputGain.gain.value != 0.0) {
1426
                        this.outputGain.gain.linearRampToValueAtTime(0.0,setTime);
1427
                        this.metric.stopListening(audioEngineContext.timer.getTestTime());
1428
                }
1429
        this.interfaceDOM.stopPlayback();
1430
        };
1431
        
1432
        this.play = function(startTime) {
1433
                if (this.bufferNode == undefined && this.buffer.buffer != undefined) {
1434
                        this.bufferNode = audioContext.createBufferSource();
1435
                        this.bufferNode.owner = this;
1436
                        this.bufferNode.connect(this.outputGain);
1437
                        this.bufferNode.buffer = this.buffer.buffer;
1438
                        this.bufferNode.loop = audioEngineContext.loopPlayback;
1439
                        this.bufferNode.onended = function(event) {
1440
                                // Safari does not like using 'this' to reference the calling object!
1441
                                //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
1442
                if (event.currentTarget != null) {
1443
                                    event.currentTarget.owner.stop(audioContext.currentTime+1);
1444
                }
1445
                        };
1446
                        if (this.bufferNode.loop == false) {
1447
                                this.metric.startListening(audioEngineContext.timer.getTestTime());
1448
                this.outputGain.gain.setValueAtTime(this.onplayGain,startTime);
1449
                this.interfaceDOM.startPlayback();
1450
                        } else {
1451
                 this.outputGain.gain.setValueAtTime(0.0,startTime);
1452
            }
1453
                        this.bufferNode.start(startTime);
1454
            this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
1455
                }
1456
        };
1457
        
1458
        this.stop = function(stopTime) {
1459
        this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
1460
                if (this.bufferNode != undefined)
1461
                {
1462
                        this.metric.stopListening(audioEngineContext.timer.getTestTime(),this.getCurrentPosition());
1463
                        this.bufferNode.stop(stopTime);
1464
                        this.bufferNode = undefined;
1465
                }
1466
        this.outputGain.gain.value = 0.0;
1467
        this.interfaceDOM.stopPlayback();
1468
        };
1469
        
1470
        this.getCurrentPosition = function() {
1471
                var time = audioEngineContext.timer.getTestTime();
1472
                if (this.bufferNode != undefined) {
1473
            var position = (time - this.bufferNode.playbackStartTime)%this.buffer.buffer.duration;
1474
            if (isNaN(position)){return 0;}
1475
            return position;
1476
                } else {
1477
                        return 0;
1478
                }
1479
        };
1480
        
1481
        this.exportXMLDOM = function() {
1482
                var file = storage.document.createElement('file');
1483
                var buf;
1484
                if(typeof(this.buffer) !== "undefined"){
1485
                        buf = this.buffer.buffer;
1486
                } else {
1487
                        buf = {};
1488
                }
1489
                file.setAttribute('sampleRate', buf.sampleRate);
1490
                file.setAttribute('channels', buf.numberOfChannels);
1491
                file.setAttribute('sampleCount', buf.length);
1492
                file.setAttribute('duration', buf.duration);
1493
                this.storeDOM.appendChild(file);
1494
                if (this.specification.type != 'outside-reference') {
1495
                        var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
1496
                        if (interfaceXML != null)
1497
                        {
1498
                                if (interfaceXML.length == undefined) {
1499
                                        this.storeDOM.appendChild(interfaceXML);
1500
                                } else {
1501
                                        for (var i=0; i<interfaceXML.length; i++)
1502
                                        {
1503
                                                this.storeDOM.appendChild(interfaceXML[i]);
1504
                                        }
1505
                                }
1506
                        }
1507
                        if (this.commentDOM != null) {
1508
                                this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
1509
                        }
1510
                }
1511
                var nodes = this.metric.exportXMLDOM();
1512
                var mroot = this.storeDOM.getElementsByTagName('metric')[0];
1513
                for (var i=0; i<nodes.length; i++)
1514
                {
1515
                        mroot.appendChild(nodes[i]);
1516
                }
1517
        };
1518
}
1519

    
1520
function timer()
1521
{
1522
        /* Timer object used in audioEngine to keep track of session timings
1523
         * Uses the timer of the web audio API, so sample resolution
1524
         */
1525
        this.testStarted = false;
1526
        this.testStartTime = 0;
1527
        this.testDuration = 0;
1528
        this.minimumTestTime = 0; // No minimum test time
1529
        this.startTest = function()
1530
        {
1531
                if (this.testStarted == false)
1532
                {
1533
                        this.testStartTime = audioContext.currentTime;
1534
                        this.testStarted = true;
1535
                        this.updateTestTime();
1536
                        audioEngineContext.metric.initialiseTest();
1537
                }
1538
        };
1539
        this.stopTest = function()
1540
        {
1541
                if (this.testStarted)
1542
                {
1543
                        this.testDuration = this.getTestTime();
1544
                        this.testStarted = false;
1545
                } else {
1546
                        console.log('ERR: Test tried to end before beginning');
1547
                }
1548
        };
1549
        this.updateTestTime = function()
1550
        {
1551
                if (this.testStarted)
1552
                {
1553
                        this.testDuration = audioContext.currentTime - this.testStartTime;
1554
                }
1555
        };
1556
        this.getTestTime = function()
1557
        {
1558
                this.updateTestTime();
1559
                return this.testDuration;
1560
        };
1561
}
1562

    
1563
function sessionMetrics(engine,specification)
1564
{
1565
        /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
1566
         */
1567
        this.engine = engine;
1568
        this.lastClicked = -1;
1569
        this.data = -1;
1570
        this.reset = function() {
1571
                this.lastClicked = -1;
1572
                this.data = -1;
1573
        };
1574
        
1575
        this.enableElementInitialPosition = false;
1576
        this.enableElementListenTracker = false;
1577
        this.enableElementTimer = false;
1578
        this.enableElementTracker = false;
1579
        this.enableFlagListenedTo = false;
1580
        this.enableFlagMoved = false;
1581
        this.enableTestTimer = false;
1582
        // Obtain the metrics enabled
1583
        for (var i=0; i<specification.metrics.enabled.length; i++)
1584
        {
1585
                var node = specification.metrics.enabled[i];
1586
                switch(node)
1587
                {
1588
                case 'testTimer':
1589
                        this.enableTestTimer = true;
1590
                        break;
1591
                case 'elementTimer':
1592
                        this.enableElementTimer = true;
1593
                        break;
1594
                case 'elementTracker':
1595
                        this.enableElementTracker = true;
1596
                        break;
1597
                case 'elementListenTracker':
1598
                        this.enableElementListenTracker = true;
1599
                        break;
1600
                case 'elementInitialPosition':
1601
                        this.enableElementInitialPosition = true;
1602
                        break;
1603
                case 'elementFlagListenedTo':
1604
                        this.enableFlagListenedTo = true;
1605
                        break;
1606
                case 'elementFlagMoved':
1607
                        this.enableFlagMoved = true;
1608
                        break;
1609
                case 'elementFlagComments':
1610
                        this.enableFlagComments = true;
1611
                        break;
1612
                }
1613
        }
1614
        this.initialiseTest = function(){};
1615
}
1616

    
1617
function metricTracker(caller)
1618
{
1619
        /* Custom object to track and collect metric data
1620
         * Used only inside the audioObjects object.
1621
         */
1622
        
1623
        this.listenedTimer = 0;
1624
        this.listenStart = 0;
1625
        this.listenHold = false;
1626
        this.initialPosition = -1;
1627
        this.movementTracker = [];
1628
        this.listenTracker =[];
1629
        this.wasListenedTo = false;
1630
        this.wasMoved = false;
1631
        this.hasComments = false;
1632
        this.parent = caller;
1633
        
1634
        this.initialise = function(position)
1635
        {
1636
                if (this.initialPosition == -1) {
1637
                        this.initialPosition = position;
1638
                        this.moved(0,position);
1639
                }
1640
        };
1641
        
1642
        this.moved = function(time,position)
1643
        {
1644
                if (time > 0) {this.wasMoved = true;}
1645
                this.movementTracker[this.movementTracker.length] = [time, position];
1646
        };
1647
        
1648
        this.startListening = function(time)
1649
        {
1650
                if (this.listenHold == false)
1651
                {
1652
                        this.wasListenedTo = true;
1653
                        this.listenStart = time;
1654
                        this.listenHold = true;
1655
                        
1656
                        var evnt = document.createElement('event');
1657
                        var testTime = document.createElement('testTime');
1658
                        testTime.setAttribute('start',time);
1659
                        var bufferTime = document.createElement('bufferTime');
1660
                        bufferTime.setAttribute('start',this.parent.getCurrentPosition());
1661
                        evnt.appendChild(testTime);
1662
                        evnt.appendChild(bufferTime);
1663
                        this.listenTracker.push(evnt);
1664
                        
1665
                        console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
1666
                }
1667
        };
1668
        
1669
        this.stopListening = function(time,bufferStopTime)
1670
        {
1671
                if (this.listenHold == true)
1672
                {
1673
                        var diff = time - this.listenStart;
1674
                        this.listenedTimer += (diff);
1675
                        this.listenStart = 0;
1676
                        this.listenHold = false;
1677
                        
1678
                        var evnt = this.listenTracker[this.listenTracker.length-1];
1679
                        var testTime = evnt.getElementsByTagName('testTime')[0];
1680
                        var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
1681
                        testTime.setAttribute('stop',time);
1682
                        if (bufferStopTime == undefined) {
1683
                                bufferTime.setAttribute('stop',this.parent.getCurrentPosition());
1684
                        } else {
1685
                                bufferTime.setAttribute('stop',bufferStopTime);
1686
                        }
1687
                        console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
1688
                }
1689
        };
1690
        
1691
        this.exportXMLDOM = function() {
1692
                var storeDOM = [];
1693
                if (audioEngineContext.metric.enableElementTimer) {
1694
                        var mElementTimer = storage.document.createElement('metricresult');
1695
                        mElementTimer.setAttribute('name','enableElementTimer');
1696
                        mElementTimer.textContent = this.listenedTimer;
1697
                        storeDOM.push(mElementTimer);
1698
                }
1699
                if (audioEngineContext.metric.enableElementTracker) {
1700
                        var elementTrackerFull = storage.document.createElement('metricResult');
1701
                        elementTrackerFull.setAttribute('name','elementTrackerFull');
1702
                        for (var k=0; k<this.movementTracker.length; k++)
1703
                        {
1704
                                var timePos = storage.document.createElement('movement');
1705
                timePos.setAttribute("time",this.movementTracker[k][0]);
1706
                timePos.setAttribute("value",this.movementTracker[k][1]);
1707
                                elementTrackerFull.appendChild(timePos);
1708
                        }
1709
                        storeDOM.push(elementTrackerFull);
1710
                }
1711
                if (audioEngineContext.metric.enableElementListenTracker) {
1712
                        var elementListenTracker = storage.document.createElement('metricResult');
1713
                        elementListenTracker.setAttribute('name','elementListenTracker');
1714
                        for (var k=0; k<this.listenTracker.length; k++) {
1715
                                elementListenTracker.appendChild(this.listenTracker[k]);
1716
                        }
1717
                        storeDOM.push(elementListenTracker);
1718
                }
1719
                if (audioEngineContext.metric.enableElementInitialPosition) {
1720
                        var elementInitial = storage.document.createElement('metricResult');
1721
                        elementInitial.setAttribute('name','elementInitialPosition');
1722
                        elementInitial.textContent = this.initialPosition;
1723
                        storeDOM.push(elementInitial);
1724
                }
1725
                if (audioEngineContext.metric.enableFlagListenedTo) {
1726
                        var flagListenedTo = storage.document.createElement('metricResult');
1727
                        flagListenedTo.setAttribute('name','elementFlagListenedTo');
1728
                        flagListenedTo.textContent = this.wasListenedTo;
1729
                        storeDOM.push(flagListenedTo);
1730
                }
1731
                if (audioEngineContext.metric.enableFlagMoved) {
1732
                        var flagMoved = storage.document.createElement('metricResult');
1733
                        flagMoved.setAttribute('name','elementFlagMoved');
1734
                        flagMoved.textContent = this.wasMoved;
1735
                        storeDOM.push(flagMoved);
1736
                }
1737
                if (audioEngineContext.metric.enableFlagComments) {
1738
                        var flagComments = storage.document.createElement('metricResult');
1739
                        flagComments.setAttribute('name','elementFlagComments');
1740
                        if (this.parent.commentDOM == null)
1741
                                {flag.textContent = 'false';}
1742
                        else if (this.parent.commentDOM.textContent.length == 0) 
1743
                                {flag.textContent = 'false';}
1744
                        else 
1745
                                {flag.textContet = 'true';}
1746
                        storeDOM.push(flagComments);
1747
                }
1748
                return storeDOM;
1749
        };
1750
}
1751

    
1752
function randomiseOrder(input)
1753
{
1754
        // This takes an array of information and randomises the order
1755
        var N = input.length;
1756
        
1757
        var inputSequence = []; // For safety purposes: keep track of randomisation
1758
        for (var counter = 0; counter < N; ++counter) 
1759
                inputSequence.push(counter) // Fill array
1760
        var inputSequenceClone = inputSequence.slice(0);
1761
        
1762
        var holdArr = [];
1763
        var outputSequence = [];
1764
        for (var n=0; n<N; n++)
1765
        {
1766
                // First pick a random number
1767
                var r = Math.random();
1768
                // Multiply and floor by the number of elements left
1769
                r = Math.floor(r*input.length);
1770
                // Pick out that element and delete from the array
1771
                holdArr.push(input.splice(r,1)[0]);
1772
                // Do the same with sequence
1773
                outputSequence.push(inputSequence.splice(r,1)[0]);
1774
        }
1775
        console.log(inputSequenceClone.toString()); // print original array to console
1776
        console.log(outputSequence.toString());         // print randomised array to console
1777
        return holdArr;
1778
}
1779

    
1780
function returnDateNode()
1781
{
1782
        // Create an XML Node for the Date and Time a test was conducted
1783
        // Structure is
1784
        // <datetime> 
1785
        //        <date year="##" month="##" day="##">DD/MM/YY</date>
1786
        //        <time hour="##" minute="##" sec="##">HH:MM:SS</time>
1787
        // </datetime>
1788
        var dateTime = new Date();
1789
        var year = document.createAttribute('year');
1790
        var month = document.createAttribute('month');
1791
        var day = document.createAttribute('day');
1792
        var hour = document.createAttribute('hour');
1793
        var minute = document.createAttribute('minute');
1794
        var secs = document.createAttribute('secs');
1795
        
1796
        year.nodeValue = dateTime.getFullYear();
1797
        month.nodeValue = dateTime.getMonth()+1;
1798
        day.nodeValue = dateTime.getDate();
1799
        hour.nodeValue = dateTime.getHours();
1800
        minute.nodeValue = dateTime.getMinutes();
1801
        secs.nodeValue = dateTime.getSeconds();
1802
        
1803
        var hold = document.createElement("datetime");
1804
        var date = document.createElement("date");
1805
        date.textContent = year.nodeValue+'/'+month.nodeValue+'/'+day.nodeValue;
1806
        var time = document.createElement("time");
1807
        time.textContent = hour.nodeValue+':'+minute.nodeValue+':'+secs.nodeValue;
1808
        
1809
        date.setAttributeNode(year);
1810
        date.setAttributeNode(month);
1811
        date.setAttributeNode(day);
1812
        time.setAttributeNode(hour);
1813
        time.setAttributeNode(minute);
1814
        time.setAttributeNode(secs);
1815
        
1816
        hold.appendChild(date);
1817
        hold.appendChild(time);
1818
        return hold;
1819
        
1820
}
1821

    
1822
function Specification() {
1823
        // Handles the decoding of the project specification XML into a simple JavaScript Object.
1824
        
1825
        this.interface = null;
1826
        this.projectReturn = "null";
1827
        this.randomiseOrder = null;
1828
        this.testPages = null;
1829
        this.pages = [];
1830
        this.metrics = null;
1831
        this.interfaces = null;
1832
        this.loudness = null;
1833
        this.errors = [];
1834
        this.schema = null;
1835
        
1836
        this.processAttribute = function(attribute,schema)
1837
        {
1838
                // attribute is the string returned from getAttribute on the XML
1839
                // schema is the <xs:attribute> node
1840
                if (schema.getAttribute('name') == undefined && schema.getAttribute('ref') != undefined)
1841
                {
1842
                        schema = this.schema.getAllElementsByName(schema.getAttribute('ref'))[0];
1843
                }
1844
                var defaultOpt = schema.getAttribute('default');
1845
                if (attribute == null) {
1846
                        attribute = defaultOpt;
1847
                }
1848
                var dataType = schema.getAttribute('type');
1849
                if (typeof dataType == "string") { dataType = dataType.substr(3);}
1850
                else {dataType = "string";}
1851
                if (attribute == null)
1852
                {
1853
                        return attribute;
1854
                }
1855
                switch(dataType)
1856
                {
1857
                case "boolean":
1858
                        if (attribute == 'true'){attribute = true;}else{attribute=false;}
1859
                        break;
1860
                case "negativeInteger":
1861
                case "positiveInteger":
1862
                case "nonNegativeInteger":
1863
                case "nonPositiveInteger":
1864
                case "integer":
1865
                case "decimal":
1866
                case "short":
1867
                        attribute = Number(attribute);
1868
                        break;
1869
                case "string":
1870
                default:
1871
                        attribute = String(attribute);
1872
                        break;
1873
                }
1874
                return attribute;
1875
        };
1876
        
1877
        this.decode = function(projectXML) {
1878
                this.errors = [];
1879
                // projectXML - DOM Parsed document
1880
                this.projectXML = projectXML.childNodes[0];
1881
                var setupNode = projectXML.getElementsByTagName('setup')[0];
1882
                var schemaSetup = this.schema.getAllElementsByName('setup')[0];
1883
                // First decode the attributes
1884
                var attributes = schemaSetup.getAllElementsByTagName('xs:attribute');
1885
                for (var i in attributes)
1886
                {
1887
                        if (isNaN(Number(i)) == true){break;}
1888
                        var attributeName = attributes[i].getAttribute('name');
1889
                        var projectAttr = setupNode.getAttribute(attributeName);
1890
                        projectAttr = this.processAttribute(projectAttr,attributes[i]);
1891
                        switch(typeof projectAttr)
1892
                        {
1893
                        case "number":
1894
                        case "boolean":
1895
                                eval('this.'+attributeName+' = '+projectAttr);
1896
                                break;
1897
                        case "string":
1898
                                eval('this.'+attributeName+' = "'+projectAttr+'"');
1899
                                break;
1900
                        }
1901
                        
1902
                }
1903
                
1904
                this.metrics = new this.metricNode();
1905
                
1906
                this.metrics.decode(this,setupNode.getElementsByTagName('metric')[0]);
1907
                
1908
                // Now process the survey node options
1909
                var survey = setupNode.getElementsByTagName('survey');
1910
                for (var i in survey) {
1911
                        if (isNaN(Number(i)) == true){break;}
1912
                        var location = survey[i].getAttribute('location');
1913
                        if (location == 'pre' || location == 'before')
1914
                        {
1915
                                if (this.preTest != null){this.errors.push("Already a pre/before test survey defined! Ignoring second!!");}
1916
                                else {
1917
                                        this.preTest = new this.surveyNode();
1918
                                        this.preTest.decode(this,survey[i]);
1919
                                }
1920
                        } else if (location == 'post' || location == 'after') {
1921
                                if (this.postTest != null){this.errors.push("Already a post/after test survey defined! Ignoring second!!");}
1922
                                else {
1923
                                        this.postTest = new this.surveyNode();
1924
                                        this.postTest.decode(this,survey[i]);
1925
                                }
1926
                        }
1927
                }
1928
                
1929
                var interfaceNode = setupNode.getElementsByTagName('interface');
1930
                if (interfaceNode.length > 1)
1931
                {
1932
                        this.errors.push("Only one <interface> node in the <setup> node allowed! Others except first ingnored!");
1933
                }
1934
                this.interfaces = new this.interfaceNode();
1935
                if (interfaceNode.length != 0)
1936
                {
1937
                        interfaceNode = interfaceNode[0];
1938
                        this.interfaces.decode(this,interfaceNode,this.schema.getAllElementsByName('interface')[1]);
1939
                }
1940
                
1941
                // Page tags
1942
                var pageTags = projectXML.getElementsByTagName('page');
1943
                var pageSchema = this.schema.getAllElementsByName('page')[0];
1944
                for (var i=0; i<pageTags.length; i++)
1945
                {
1946
                        var node = new this.page();
1947
                        node.decode(this,pageTags[i],pageSchema);
1948
                        this.pages.push(node);
1949
                }
1950
        };
1951
        
1952
        this.encode = function()
1953
        {
1954
                var RootDocument = document.implementation.createDocument(null,"waet");
1955
                var root = RootDocument.children[0];
1956
        root.setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance");
1957
        root.setAttribute("xsi:noNamespaceSchemaLocation","test-schema.xsd");
1958
                // Build setup node
1959
        var setup = RootDocument.createElement("setup");
1960
        var schemaSetup = this.schema.getAllElementsByName('setup')[0];
1961
        // First decode the attributes
1962
        var attributes = schemaSetup.getAllElementsByTagName('xs:attribute');
1963
        for (var i=0; i<attributes.length; i++)
1964
        {
1965
            var name = attributes[i].getAttribute("name");
1966
            if (name == undefined) {
1967
                name = attributes[i].getAttribute("ref");
1968
            }
1969
            if(eval("this."+name+" != undefined") || attributes[i].getAttribute("use") == "required")
1970
            {
1971
                eval("setup.setAttribute('"+name+"',this."+name+")");
1972
            }
1973
        }
1974
        root.appendChild(setup);
1975
        // Survey node
1976
        setup.appendChild(this.preTest.encode(RootDocument));
1977
        setup.appendChild(this.postTest.encode(RootDocument));
1978
        setup.appendChild(this.metrics.encode(RootDocument));
1979
        setup.appendChild(this.interfaces.encode(RootDocument));
1980
        for (var page of this.pages)
1981
        {
1982
            root.appendChild(page.encode(RootDocument));
1983
        }
1984
                return RootDocument;
1985
        };
1986
        
1987
        this.surveyNode = function() {
1988
                this.location = null;
1989
                this.options = [];
1990
                this.schema = specification.schema.getAllElementsByName('survey')[0];
1991
                
1992
                this.OptionNode = function() {
1993
                        this.type = undefined;
1994
                        this.schema = specification.schema.getAllElementsByName('surveyentry')[0];
1995
                        this.id = undefined;
1996
            this.name = undefined;
1997
                        this.mandatory = undefined;
1998
                        this.statement = undefined;
1999
                        this.boxsize = undefined;
2000
                        this.options = [];
2001
                        this.min = undefined;
2002
                        this.max = undefined;
2003
                        this.step = undefined;
2004
                        
2005
                        this.decode = function(parent,child)
2006
                        {
2007
                                var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
2008
                                for (var i in attributeMap){
2009
                                        if(isNaN(Number(i)) == true){break;}
2010
                                        var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
2011
                                        var projectAttr = child.getAttribute(attributeName);
2012
                                        projectAttr = parent.processAttribute(projectAttr,attributeMap[i]);
2013
                                        switch(typeof projectAttr)
2014
                                        {
2015
                                        case "number":
2016
                                        case "boolean":
2017
                                                eval('this.'+attributeName+' = '+projectAttr);
2018
                                                break;
2019
                                        case "string":
2020
                                                eval('this.'+attributeName+' = "'+projectAttr+'"');
2021
                                                break;
2022
                                        }
2023
                                }
2024
                                this.statement = child.getElementsByTagName('statement')[0].textContent;
2025
                                if (this.type == "checkbox" || this.type == "radio") {
2026
                                        var children = child.getElementsByTagName('option');
2027
                                        if (children.length == null) {
2028
                                                console.log('Malformed' +child.nodeName+ 'entry');
2029
                                                this.statement = 'Malformed' +child.nodeName+ 'entry';
2030
                                                this.type = 'statement';
2031
                                        } else {
2032
                                                this.options = [];
2033
                                                for (var i in children)
2034
                                                {
2035
                                                        if (isNaN(Number(i))==true){break;}
2036
                                                        this.options.push({
2037
                                                                name: children[i].getAttribute('name'),
2038
                                                                text: children[i].textContent
2039
                                                        });
2040
                                                }
2041
                                        }
2042
                                }
2043
                        };
2044
                        
2045
                        this.exportXML = function(doc)
2046
                        {
2047
                                var node = doc.createElement('surveyentry');
2048
                                node.setAttribute('type',this.type);
2049
                                var statement = doc.createElement('statement');
2050
                                statement.textContent = this.statement;
2051
                                node.appendChild(statement);
2052
                if (this.type != "statement") {
2053
                    node.id = this.id;
2054
                    if (this.name != undefined) { node.setAttribute("name",this.name);}
2055
                    if (this.mandatory != undefined) { node.setAttribute("mandatory",this.mandatory);}
2056
                    switch(this.type)
2057
                    {
2058
                    case "question":
2059
                        if (this.boxsize != undefined) {node.setAttribute("boxsize",this.boxsize);}
2060
                        break;
2061
                    case "number":
2062
                        if (this.min != undefined) {node.setAttribute("min", this.min);}
2063
                        if (this.max != undefined) {node.setAttribute("max", this.max);}
2064
                        break;
2065
                    case "checkbox":
2066
                    case "radio":
2067
                        for (var i=0; i<this.options.length; i++)
2068
                        {
2069
                            var option = this.options[i];
2070
                            var optionNode = doc.createElement("option");
2071
                            optionNode.setAttribute("name",option.name);
2072
                            optionNode.textContent = option.text;
2073
                            node.appendChild(optionNode);
2074
                        }
2075
                        break;
2076
                    }
2077
                }
2078
                                return node;
2079
                        };
2080
                };
2081
                this.decode = function(parent,xml) {
2082
                        this.location = xml.getAttribute('location');
2083
                        if (this.location == 'before'){this.location = 'pre';}
2084
                        else if (this.location == 'after'){this.location = 'post';}
2085
                        for (var i in xml.children)
2086
                        {
2087
                                if(isNaN(Number(i))==true){break;}
2088
                                var node = new this.OptionNode();
2089
                                node.decode(parent,xml.children[i]);
2090
                                this.options.push(node);
2091
                        }
2092
                };
2093
                this.encode = function(doc) {
2094
                        var node = doc.createElement('survey');
2095
                        node.setAttribute('location',this.location);
2096
                        for (var i=0; i<this.options.length; i++)
2097
                        {
2098
                                node.appendChild(this.options[i].exportXML(doc));
2099
                        }
2100
                        return node;
2101
                };
2102
        };
2103
        
2104
        this.interfaceNode = function()
2105
        {
2106
                this.title = null;
2107
                this.name = null;
2108
                this.options = [];
2109
                this.scales = [];
2110
                this.schema = specification.schema.getAllElementsByName('interface')[1];
2111
                
2112
                this.decode = function(parent,xml) {
2113
                        this.name = xml.getAttribute('name');
2114
                        var titleNode = xml.getElementsByTagName('title');
2115
                        if (titleNode.length == 1)
2116
                        {
2117
                                this.title = titleNode[0].textContent;
2118
                        }
2119
                        var interfaceOptionNodes = xml.getElementsByTagName('interfaceoption');
2120
                        // Extract interfaceoption node schema
2121
                        var interfaceOptionNodeSchema = this.schema.getAllElementsByName('interfaceoption')[0];
2122
                        var attributeMap = interfaceOptionNodeSchema.getAllElementsByTagName('xs:attribute');
2123
                        for (var i=0; i<interfaceOptionNodes.length; i++)
2124
                        {
2125
                                var ioNode = interfaceOptionNodes[i];
2126
                                var option = {};
2127
                                for (var j=0; j<attributeMap.length; j++)
2128
                                {
2129
                                        var attributeName = attributeMap[j].getAttribute('name') || attributeMap[j].getAttribute('ref');
2130
                                        var projectAttr = ioNode.getAttribute(attributeName);
2131
                                        projectAttr = parent.processAttribute(projectAttr,attributeMap[j]);
2132
                                        switch(typeof projectAttr)
2133
                                        {
2134
                                        case "number":
2135
                                        case "boolean":
2136
                                                eval('option.'+attributeName+' = '+projectAttr);
2137
                                                break;
2138
                                        case "string":
2139
                                                eval('option.'+attributeName+' = "'+projectAttr+'"');
2140
                                                break;
2141
                                        }
2142
                                }
2143
                                this.options.push(option);
2144
                        }
2145
                        
2146
                        // Now the scales nodes
2147
                        var scaleParent = xml.getElementsByTagName('scales');
2148
                        if (scaleParent.length == 1) {
2149
                                scaleParent = scaleParent[0];
2150
                                for (var i=0; i<scaleParent.children.length; i++) {
2151
                                        var child = scaleParent.children[i];
2152
                                        this.scales.push({
2153
                                                text: child.textContent,
2154
                                                position: Number(child.getAttribute('position'))
2155
                                        });
2156
                                }
2157
                        }
2158
                };
2159
                
2160
                this.encode = function(doc) {
2161
                        var node = doc.createElement("interface");
2162
            if (typeof name == "string")
2163
                node.setAttribute("name",this.name);
2164
            for (var option of this.options)
2165
            {
2166
                var child = doc.createElement("interfaceoption");
2167
                child.setAttribute("type",option.type);
2168
                child.setAttribute("name",option.name);
2169
                node.appendChild(child);
2170
            }
2171
            if (this.scales.length != 0) {
2172
                var scales = doc.createElement("scales");
2173
                for (var scale of this.scales)
2174
                {
2175
                    var child = doc.createElement("scalelabel");
2176
                    child.setAttribute("position",scale.position);
2177
                    child.textContent = scale.text;
2178
                    scales.appendChild(child);
2179
                }
2180
                node.appendChild(scales);
2181
            }
2182
            return node;
2183
                };
2184
        };
2185
        
2186
    this.metricNode = function() {
2187
        this.enabled = [];
2188
        this.decode = function(parent, xml) {
2189
            var children = xml.getElementsByTagName('metricenable');
2190
            for (var i in children) { 
2191
                if (isNaN(Number(i)) == true){break;}
2192
                this.enabled.push(children[i].textContent);
2193
            }
2194
        }
2195
        this.encode = function(doc) {
2196
            var node = doc.createElement('metric');
2197
            for (var i in this.enabled)
2198
            {
2199
                if (isNaN(Number(i)) == true){break;}
2200
                var child = doc.createElement('metricenable');
2201
                child.textContent = this.enabled[i];
2202
                node.appendChild(child);
2203
            }
2204
            return node;
2205
        }
2206
    }
2207
    
2208
        this.page = function() {
2209
                this.presentedId = undefined;
2210
                this.id = undefined;
2211
                this.hostURL = undefined;
2212
                this.randomiseOrder = undefined;
2213
                this.loop = undefined;
2214
                this.showElementComments = undefined;
2215
                this.outsideReference = null;
2216
                this.loudness = null;
2217
        this.label = null;
2218
                this.preTest = null;
2219
                this.postTest = null;
2220
                this.interfaces = [];
2221
                this.commentBoxPrefix = "Comment on track";
2222
                this.audioElements = [];
2223
                this.commentQuestions = [];
2224
                this.schema = specification.schema.getAllElementsByName("page")[0];
2225
                
2226
                this.decode = function(parent,xml)
2227
                {
2228
                        var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
2229
                        for (var i=0; i<attributeMap.length; i++)
2230
                        {
2231
                                var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
2232
                                var projectAttr = xml.getAttribute(attributeName);
2233
                                projectAttr = parent.processAttribute(projectAttr,attributeMap[i]);
2234
                                switch(typeof projectAttr)
2235
                                {
2236
                                case "number":
2237
                                case "boolean":
2238
                                        eval('this.'+attributeName+' = '+projectAttr);
2239
                                        break;
2240
                                case "string":
2241
                                        eval('this.'+attributeName+' = "'+projectAttr+'"');
2242
                                        break;
2243
                                }
2244
                        }
2245
                        
2246
                        // Get the Comment Box Prefix
2247
                        var CBP = xml.getElementsByTagName('commentboxprefix');
2248
                        if (CBP.length != 0) {
2249
                                this.commentBoxPrefix = CBP[0].textContent;
2250
                        }
2251
                        
2252
                        // Now decode the interfaces
2253
                        var interfaceNode = xml.getElementsByTagName('interface');
2254
                        for (var i=0; i<interfaceNode.length; i++)
2255
                        {
2256
                                var node = new parent.interfaceNode();
2257
                                node.decode(this,interfaceNode[i],parent.schema.getAllElementsByName('interface')[1]);
2258
                                this.interfaces.push(node);
2259
                        }
2260
                        
2261
                        // Now process the survey node options
2262
                        var survey = xml.getElementsByTagName('survey');
2263
                        var surveySchema = parent.schema.getAllElementsByName('survey')[0];
2264
                        for (var i in survey) {
2265
                                if (isNaN(Number(i)) == true){break;}
2266
                                var location = survey[i].getAttribute('location');
2267
                                if (location == 'pre' || location == 'before')
2268
                                {
2269
                                        if (this.preTest != null){this.errors.push("Already a pre/before test survey defined! Ignoring second!!");}
2270
                                        else {
2271
                                                this.preTest = new parent.surveyNode();
2272
                                                this.preTest.decode(parent,survey[i],surveySchema);
2273
                                        }
2274
                                } else if (location == 'post' || location == 'after') {
2275
                                        if (this.postTest != null){this.errors.push("Already a post/after test survey defined! Ignoring second!!");}
2276
                                        else {
2277
                                                this.postTest = new parent.surveyNode();
2278
                                                this.postTest.decode(parent,survey[i],surveySchema);
2279
                                        }
2280
                                }
2281
                        }
2282
                        
2283
                        // Now process the audioelement tags
2284
                        var audioElements = xml.getElementsByTagName('audioelement');
2285
                        for (var i=0; i<audioElements.length; i++)
2286
                        {
2287
                                var node = new this.audioElementNode();
2288
                                node.decode(this,audioElements[i]);
2289
                                this.audioElements.push(node);
2290
                        }
2291
                        
2292
                        // Now decode the commentquestions
2293
                        var commentQuestions = xml.getElementsByTagName('commentquestion');
2294
                        for (var i=0; i<commentQuestions.length; i++)
2295
                        {
2296
                                var node = new this.commentQuestionNode();
2297
                                node.decode(parent,commentQuestions[i]);
2298
                                this.commentQuestions.push(node);
2299
                        }
2300
                };
2301
                
2302
                this.encode = function(root)
2303
                {
2304
                        var AHNode = root.createElement("page");
2305
            // First decode the attributes
2306
            var attributes = this.schema.getAllElementsByTagName('xs:attribute');
2307
            for (var i=0; i<attributes.length; i++)
2308
            {
2309
                var name = attributes[i].getAttribute("name");
2310
                if (name == undefined) {
2311
                    name = attributes[i].getAttribute("ref");
2312
                }
2313
                if(eval("this."+name+" != undefined") || attributes[i].getAttribute("use") == "required")
2314
                {
2315
                    eval("AHNode.setAttribute('"+name+"',this."+name+")");
2316
                }
2317
            }
2318
                        if(this.loudness != null) {AHNode.setAttribute("loudness",this.loudness);}
2319
            // <commentboxprefix>
2320
            var commentboxprefix = root.createElement("commentboxprefix");
2321
            commentboxprefix.textContent = this.commentBoxPrefix;
2322
            AHNode.appendChild(commentboxprefix);
2323
            
2324
                        for (var i=0; i<this.interfaces.length; i++)
2325
                        {
2326
                                AHNode.appendChild(this.interfaces[i].encode(root));
2327
                        }
2328
                        
2329
                        for (var i=0; i<this.audioElements.length; i++) {
2330
                                AHNode.appendChild(this.audioElements[i].encode(root));
2331
                        }
2332
                        // Create <CommentQuestion>
2333
                        for (var i=0; i<this.commentQuestions.length; i++)
2334
                        {
2335
                                AHNode.appendChild(this.commentQuestions[i].encode(root));
2336
                        }
2337
                        
2338
                        AHNode.appendChild(this.preTest.encode(root));
2339
            AHNode.appendChild(this.postTest.encode(root));
2340
                        return AHNode;
2341
                };
2342
                
2343
                this.commentQuestionNode = function() {
2344
                        this.id = null;
2345
            this.name = undefined;
2346
                        this.type = undefined;
2347
                        this.options = [];
2348
                        this.statement = undefined;
2349
                        this.schema = specification.schema.getAllElementsByName('commentquestion')[0];
2350
                        this.decode = function(parent,xml)
2351
                        {
2352
                                this.id = xml.id;
2353
                this.name = xml.getAttribute('name');
2354
                                this.type = xml.getAttribute('type');
2355
                                this.statement = xml.getElementsByTagName('statement')[0].textContent;
2356
                                var optNodes = xml.getElementsByTagName('option');
2357
                                for (var i=0; i<optNodes.length; i++)
2358
                                {
2359
                                        var optNode = optNodes[i];
2360
                                        this.options.push({
2361
                                                name: optNode.getAttribute('name'),
2362
                                                text: optNode.textContent
2363
                                        });
2364
                                }
2365
                        };
2366
                        
2367
                        this.encode = function(root)
2368
                        {
2369
                                var node = root.createElement("commentquestion");
2370
                node.id = this.id;
2371
                node.setAttribute("type",this.type);
2372
                if (this.name != undefined){node.setAttribute("name",this.name);}
2373
                var statement = root.createElement("statement");
2374
                statement.textContent = this.statement;
2375
                node.appendChild(statement);
2376
                for (var option of this.options)
2377
                {
2378
                    var child = root.createElement("option");
2379
                    child.setAttribute("name",option.name);
2380
                    child.textContent = option.text;
2381
                    node.appendChild(child);
2382
                }
2383
                return node;
2384
                        };
2385
                };
2386
                
2387
                this.audioElementNode = function() {
2388
                        this.url = null;
2389
                        this.id = null;
2390
            this.name = null;
2391
                        this.parent = null;
2392
                        this.type = null;
2393
                        this.marker = null;
2394
                        this.enforce = false;
2395
                        this.gain = 0.0;
2396
                        this.schema = specification.schema.getAllElementsByName('audioelement')[0];;
2397
                        this.parent = null;
2398
                        this.decode = function(parent,xml)
2399
                        {
2400
                                this.parent = parent;
2401
                                var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
2402
                                for (var i=0; i<attributeMap.length; i++)
2403
                                {
2404
                                        var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
2405
                                        var projectAttr = xml.getAttribute(attributeName);
2406
                                        projectAttr = specification.processAttribute(projectAttr,attributeMap[i]);
2407
                                        switch(typeof projectAttr)
2408
                                        {
2409
                                        case "number":
2410
                                        case "boolean":
2411
                                                eval('this.'+attributeName+' = '+projectAttr);
2412
                                                break;
2413
                                        case "string":
2414
                                                eval('this.'+attributeName+' = "'+projectAttr+'"');
2415
                                                break;
2416
                                        }
2417
                                }
2418
                                
2419
                        };
2420
                        this.encode = function(root)
2421
                        {
2422
                                var AENode = root.createElement("audioelement");
2423
                                var attributes = this.schema.getAllElementsByTagName('xs:attribute');
2424
                for (var i=0; i<attributes.length; i++)
2425
                {
2426
                    var name = attributes[i].getAttribute("name");
2427
                    if (name == undefined) {
2428
                        name = attributes[i].getAttribute("ref");
2429
                    }
2430
                    if(eval("this."+name+" != undefined") || attributes[i].getAttribute("use") == "required")
2431
                    {
2432
                        eval("AENode.setAttribute('"+name+"',this."+name+")");
2433
                    }
2434
                }
2435
                                return AENode;
2436
                        };
2437
                };
2438
        };
2439
}
2440
                        
2441
function Interface(specificationObject) {
2442
        // This handles the bindings between the interface and the audioEngineContext;
2443
        this.specification = specificationObject;
2444
        this.insertPoint = document.getElementById("topLevelBody");
2445
        
2446
        this.newPage = function(audioHolderObject,store)
2447
        {
2448
                audioEngineContext.newTestPage(audioHolderObject,store);
2449
                interfaceContext.commentBoxes.deleteCommentBoxes();
2450
                interfaceContext.deleteCommentQuestions();
2451
                loadTest(audioHolderObject,store);
2452
                if(audioHolderObject.hidden === true){ 
2453
                // work-around to have zero pages: set only one page with the attribute hidden=true and 
2454
                // it will automatically skip over.
2455
                        testState.advanceState();
2456
                }
2457
        };
2458
        
2459
        // Bounded by interface!!
2460
        // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
2461
        // For example, APE returns  the slider position normalised in a <value> tag.
2462
        this.interfaceObjects = [];
2463
        this.interfaceObject = function(){};
2464
        
2465
        this.resizeWindow = function(event)
2466
        {
2467
                popup.resize(event);
2468
                for(var i=0; i<this.commentBoxes.length; i++)
2469
                {this.commentBoxes[i].resize();}
2470
                for(var i=0; i<this.commentQuestions.length; i++)
2471
                {this.commentQuestions[i].resize();}
2472
                try
2473
                {
2474
                        resizeWindow(event);
2475
                }
2476
                catch(err)
2477
                {
2478
                        console.log("Warning - Interface does not have Resize option");
2479
                        console.log(err);
2480
                }
2481
        };
2482
        
2483
        this.returnNavigator = function()
2484
        {
2485
                var node = storage.document.createElement("navigator");
2486
                var platform = storage.document.createElement("platform");
2487
                platform.textContent = navigator.platform;
2488
                var vendor = storage.document.createElement("vendor");
2489
                vendor.textContent = navigator.vendor;
2490
                var userAgent = storage.document.createElement("uagent");
2491
                userAgent.textContent = navigator.userAgent;
2492
        var screen = storage.document.createElement("window");
2493
        screen.setAttribute('innerWidth',window.innerWidth);
2494
        screen.setAttribute('innerHeight',window.innerHeight);
2495
                node.appendChild(platform);
2496
                node.appendChild(vendor);
2497
                node.appendChild(userAgent);
2498
        node.appendChild(screen);
2499
                return node;
2500
        };
2501
        
2502
        this.commentBoxes = new function() {
2503
        this.boxes = [];
2504
        this.injectPoint = null;
2505
        this.elementCommentBox = function(audioObject) {
2506
            var element = audioObject.specification;
2507
            this.audioObject = audioObject;
2508
            this.id = audioObject.id;
2509
            var audioHolderObject = audioObject.specification.parent;
2510
            // Create document objects to hold the comment boxes
2511
            this.trackComment = document.createElement('div');
2512
            this.trackComment.className = 'comment-div';
2513
            this.trackComment.id = 'comment-div-'+audioObject.id;
2514
            // Create a string next to each comment asking for a comment
2515
            this.trackString = document.createElement('span');
2516
            this.trackString.innerHTML = audioHolderObject.commentBoxPrefix+' '+audioObject.interfaceDOM.getPresentedId();
2517
            // Create the HTML5 comment box 'textarea'
2518
            this.trackCommentBox = document.createElement('textarea');
2519
            this.trackCommentBox.rows = '4';
2520
            this.trackCommentBox.cols = '100';
2521
            this.trackCommentBox.name = 'trackComment'+audioObject.id;
2522
            this.trackCommentBox.className = 'trackComment';
2523
            var br = document.createElement('br');
2524
            // Add to the holder.
2525
            this.trackComment.appendChild(this.trackString);
2526
            this.trackComment.appendChild(br);
2527
            this.trackComment.appendChild(this.trackCommentBox);
2528

    
2529
            this.exportXMLDOM = function() {
2530
                var root = document.createElement('comment');
2531
                var question = document.createElement('question');
2532
                question.textContent = this.trackString.textContent;
2533
                var response = document.createElement('response');
2534
                response.textContent = this.trackCommentBox.value;
2535
                console.log("Comment frag-"+this.id+": "+response.textContent);
2536
                root.appendChild(question);
2537
                root.appendChild(response);
2538
                return root;
2539
            };
2540
            this.resize = function()
2541
            {
2542
                var boxwidth = (window.innerWidth-100)/2;
2543
                if (boxwidth >= 600)
2544
                {
2545
                    boxwidth = 600;
2546
                }
2547
                else if (boxwidth < 400)
2548
                {
2549
                    boxwidth = 400;
2550
                }
2551
                this.trackComment.style.width = boxwidth+"px";
2552
                this.trackCommentBox.style.width = boxwidth-6+"px";
2553
            };
2554
            this.resize();
2555
        };
2556
        this.createCommentBox = function(audioObject) {
2557
            var node = new this.elementCommentBox(audioObject);
2558
            this.boxes.push(node);
2559
            audioObject.commentDOM = node;
2560
            return node;
2561
        };
2562
        this.sortCommentBoxes = function() {
2563
            this.boxes.sort(function(a,b){return a.id - b.id;});
2564
        };
2565

    
2566
        this.showCommentBoxes = function(inject, sort) {
2567
            this.injectPoint = inject;
2568
            if (sort) {this.sortCommentBoxes();}
2569
            for (var box of this.boxes) {
2570
                inject.appendChild(box.trackComment);
2571
            }
2572
        };
2573

    
2574
        this.deleteCommentBoxes = function() {
2575
            if (this.injectPoint != null) {
2576
                for (var box of this.boxes) {
2577
                    this.injectPoint.removeChild(box.trackComment);
2578
                }
2579
                this.injectPoint = null;
2580
            }
2581
            this.boxes = [];
2582
        };
2583
    }
2584
        
2585
        this.commentQuestions = [];
2586
        
2587
        this.commentBox = function(commentQuestion) {
2588
                this.specification = commentQuestion;
2589
                // Create document objects to hold the comment boxes
2590
                this.holder = document.createElement('div');
2591
                this.holder.className = 'comment-div';
2592
                // Create a string next to each comment asking for a comment
2593
                this.string = document.createElement('span');
2594
                this.string.innerHTML = commentQuestion.statement;
2595
                // Create the HTML5 comment box 'textarea'
2596
                this.textArea = document.createElement('textarea');
2597
                this.textArea.rows = '4';
2598
                this.textArea.cols = '100';
2599
                this.textArea.className = 'trackComment';
2600
                var br = document.createElement('br');
2601
                // Add to the holder.
2602
                this.holder.appendChild(this.string);
2603
                this.holder.appendChild(br);
2604
                this.holder.appendChild(this.textArea);
2605
                
2606
                this.exportXMLDOM = function(storePoint) {
2607
                        var root = storePoint.parent.document.createElement('comment');
2608
                        root.id = this.specification.id;
2609
                        root.setAttribute('type',this.specification.type);
2610
                        console.log("Question: "+this.string.textContent);
2611
                        console.log("Response: "+root.textContent);
2612
            var question = storePoint.parent.document.createElement('question');
2613
            question.textContent = this.string.textContent;
2614
            var response = storePoint.parent.document.createElement('response');
2615
            response.textContent = this.textArea.value;
2616
            root.appendChild(question);
2617
            root.appendChild(response);
2618
            storePoint.XMLDOM.appendChild(root);
2619
                        return root;
2620
                };
2621
                this.resize = function()
2622
                {
2623
                        var boxwidth = (window.innerWidth-100)/2;
2624
                        if (boxwidth >= 600)
2625
                        {
2626
                                boxwidth = 600;
2627
                        }
2628
                        else if (boxwidth < 400)
2629
                        {
2630
                                boxwidth = 400;
2631
                        }
2632
                        this.holder.style.width = boxwidth+"px";
2633
                        this.textArea.style.width = boxwidth-6+"px";
2634
                };
2635
                this.resize();
2636
        };
2637
        
2638
        this.radioBox = function(commentQuestion) {
2639
                this.specification = commentQuestion;
2640
                // Create document objects to hold the comment boxes
2641
                this.holder = document.createElement('div');
2642
                this.holder.className = 'comment-div';
2643
                // Create a string next to each comment asking for a comment
2644
                this.string = document.createElement('span');
2645
                this.string.innerHTML = commentQuestion.statement;
2646
                var br = document.createElement('br');
2647
                // Add to the holder.
2648
                this.holder.appendChild(this.string);
2649
                this.holder.appendChild(br);
2650
                this.options = [];
2651
                this.inputs = document.createElement('div');
2652
                this.span = document.createElement('div');
2653
                this.inputs.align = 'center';
2654
                this.inputs.style.marginLeft = '12px';
2655
                this.span.style.marginLeft = '12px';
2656
                this.span.align = 'center';
2657
                this.span.style.marginTop = '15px';
2658
                
2659
                var optCount = commentQuestion.options.length;
2660
                for (var optNode of commentQuestion.options)
2661
                {
2662
                        var div = document.createElement('div');
2663
                        div.style.width = '80px';
2664
                        div.style.float = 'left';
2665
                        var input = document.createElement('input');
2666
                        input.type = 'radio';
2667
                        input.name = commentQuestion.id;
2668
                        input.setAttribute('setvalue',optNode.name);
2669
                        input.className = 'comment-radio';
2670
                        div.appendChild(input);
2671
                        this.inputs.appendChild(div);
2672
                        
2673
                        
2674
                        div = document.createElement('div');
2675
                        div.style.width = '80px';
2676
                        div.style.float = 'left';
2677
                        div.align = 'center';
2678
                        var span = document.createElement('span');
2679
                        span.textContent = optNode.text;
2680
                        span.className = 'comment-radio-span';
2681
                        div.appendChild(span);
2682
                        this.span.appendChild(div);
2683
                        this.options.push(input);
2684
                }
2685
                this.holder.appendChild(this.span);
2686
                this.holder.appendChild(this.inputs);
2687
                
2688
                this.exportXMLDOM = function(storePoint) {
2689
                        var root = storePoint.parent.document.createElement('comment');
2690
                        root.id = this.specification.id;
2691
                        root.setAttribute('type',this.specification.type);
2692
                        var question = document.createElement('question');
2693
                        question.textContent = this.string.textContent;
2694
                        var response = document.createElement('response');
2695
                        var i=0;
2696
                        while(this.options[i].checked == false) {
2697
                                i++;
2698
                                if (i >= this.options.length) {
2699
                                        break;
2700
                                }
2701
                        }
2702
                        if (i >= this.options.length) {
2703
                                response.textContent = 'null';
2704
                        } else {
2705
                                response.textContent = this.options[i].getAttribute('setvalue');
2706
                                response.setAttribute('number',i);
2707
                        }
2708
                        console.log('Comment: '+question.textContent);
2709
                        console.log('Response: '+response.textContent);
2710
                        root.appendChild(question);
2711
                        root.appendChild(response);
2712
            storePoint.XMLDOM.appendChild(root);
2713
                        return root;
2714
                };
2715
                this.resize = function()
2716
                {
2717
                        var boxwidth = (window.innerWidth-100)/2;
2718
                        if (boxwidth >= 600)
2719
                        {
2720
                                boxwidth = 600;
2721
                        }
2722
                        else if (boxwidth < 400)
2723
                        {
2724
                                boxwidth = 400;
2725
                        }
2726
                        this.holder.style.width = boxwidth+"px";
2727
                        var text = this.holder.children[2];
2728
                        var options = this.holder.children[3];
2729
                        var optCount = options.children.length;
2730
                        var spanMargin = Math.floor(((boxwidth-20-(optCount*80))/(optCount))/2)+'px';
2731
                        var options = options.firstChild;
2732
                        var text = text.firstChild;
2733
                        options.style.marginRight = spanMargin;
2734
                        options.style.marginLeft = spanMargin;
2735
                        text.style.marginRight = spanMargin;
2736
                        text.style.marginLeft = spanMargin;
2737
                        while(options.nextSibling != undefined)
2738
                        {
2739
                                options = options.nextSibling;
2740
                                text = text.nextSibling;
2741
                                options.style.marginRight = spanMargin;
2742
                                options.style.marginLeft = spanMargin;
2743
                                text.style.marginRight = spanMargin;
2744
                                text.style.marginLeft = spanMargin;
2745
                        }
2746
                };
2747
                this.resize();
2748
        };
2749
        
2750
        this.checkboxBox = function(commentQuestion) {
2751
                this.specification = commentQuestion;
2752
                // Create document objects to hold the comment boxes
2753
                this.holder = document.createElement('div');
2754
                this.holder.className = 'comment-div';
2755
                // Create a string next to each comment asking for a comment
2756
                this.string = document.createElement('span');
2757
                this.string.innerHTML = commentQuestion.statement;
2758
                var br = document.createElement('br');
2759
                // Add to the holder.
2760
                this.holder.appendChild(this.string);
2761
                this.holder.appendChild(br);
2762
                this.options = [];
2763
                this.inputs = document.createElement('div');
2764
                this.span = document.createElement('div');
2765
                this.inputs.align = 'center';
2766
                this.inputs.style.marginLeft = '12px';
2767
                this.span.style.marginLeft = '12px';
2768
                this.span.align = 'center';
2769
                this.span.style.marginTop = '15px';
2770
                
2771
                var optCount = commentQuestion.options.length;
2772
                for (var i=0; i<optCount; i++)
2773
                {
2774
                        var div = document.createElement('div');
2775
                        div.style.width = '80px';
2776
                        div.style.float = 'left';
2777
                        var input = document.createElement('input');
2778
                        input.type = 'checkbox';
2779
                        input.name = commentQuestion.id;
2780
                        input.setAttribute('setvalue',commentQuestion.options[i].name);
2781
                        input.className = 'comment-radio';
2782
                        div.appendChild(input);
2783
                        this.inputs.appendChild(div);
2784
                        
2785
                        
2786
                        div = document.createElement('div');
2787
                        div.style.width = '80px';
2788
                        div.style.float = 'left';
2789
                        div.align = 'center';
2790
                        var span = document.createElement('span');
2791
                        span.textContent = commentQuestion.options[i].text;
2792
                        span.className = 'comment-radio-span';
2793
                        div.appendChild(span);
2794
                        this.span.appendChild(div);
2795
                        this.options.push(input);
2796
                }
2797
                this.holder.appendChild(this.span);
2798
                this.holder.appendChild(this.inputs);
2799
                
2800
                this.exportXMLDOM = function(storePoint) {
2801
                        var root = storePoint.parent.document.createElement('comment');
2802
                        root.id = this.specification.id;
2803
                        root.setAttribute('type',this.specification.type);
2804
                        var question = document.createElement('question');
2805
                        question.textContent = this.string.textContent;
2806
                        root.appendChild(question);
2807
                        console.log('Comment: '+question.textContent);
2808
                        for (var i=0; i<this.options.length; i++) {
2809
                                var response = document.createElement('response');
2810
                                response.textContent = this.options[i].checked;
2811
                                response.setAttribute('name',this.options[i].getAttribute('setvalue'));
2812
                                root.appendChild(response);
2813
                                console.log('Response '+response.getAttribute('name') +': '+response.textContent);
2814
                        }
2815
            storePoint.XMLDOM.appendChild(root);
2816
                        return root;
2817
                };
2818
                this.resize = function()
2819
                {
2820
                        var boxwidth = (window.innerWidth-100)/2;
2821
                        if (boxwidth >= 600)
2822
                        {
2823
                                boxwidth = 600;
2824
                        }
2825
                        else if (boxwidth < 400)
2826
                        {
2827
                                boxwidth = 400;
2828
                        }
2829
                        this.holder.style.width = boxwidth+"px";
2830
                        var text = this.holder.children[2];
2831
                        var options = this.holder.children[3];
2832
                        var optCount = options.children.length;
2833
                        var spanMargin = Math.floor(((boxwidth-20-(optCount*80))/(optCount))/2)+'px';
2834
                        var options = options.firstChild;
2835
                        var text = text.firstChild;
2836
                        options.style.marginRight = spanMargin;
2837
                        options.style.marginLeft = spanMargin;
2838
                        text.style.marginRight = spanMargin;
2839
                        text.style.marginLeft = spanMargin;
2840
                        while(options.nextSibling != undefined)
2841
                        {
2842
                                options = options.nextSibling;
2843
                                text = text.nextSibling;
2844
                                options.style.marginRight = spanMargin;
2845
                                options.style.marginLeft = spanMargin;
2846
                                text.style.marginRight = spanMargin;
2847
                                text.style.marginLeft = spanMargin;
2848
                        }
2849
                };
2850
                this.resize();
2851
        };
2852
        
2853
        this.createCommentQuestion = function(element) {
2854
                var node;
2855
                if (element.type == 'question') {
2856
                        node = new this.commentBox(element);
2857
                } else if (element.type == 'radio') {
2858
                        node = new this.radioBox(element);
2859
                } else if (element.type == 'checkbox') {
2860
                        node = new this.checkboxBox(element);
2861
                }
2862
                this.commentQuestions.push(node);
2863
                return node;
2864
        };
2865
        
2866
        this.deleteCommentQuestions = function()
2867
        {
2868
                this.commentQuestions = [];
2869
        };
2870
        
2871
        this.playhead = new function()
2872
        {
2873
                this.object = document.createElement('div');
2874
                this.object.className = 'playhead';
2875
                this.object.align = 'left';
2876
                var curTime = document.createElement('div');
2877
                curTime.style.width = '50px';
2878
                this.curTimeSpan = document.createElement('span');
2879
                this.curTimeSpan.textContent = '00:00';
2880
                curTime.appendChild(this.curTimeSpan);
2881
                this.object.appendChild(curTime);
2882
                this.scrubberTrack = document.createElement('div');
2883
                this.scrubberTrack.className = 'playhead-scrub-track';
2884
                
2885
                this.scrubberHead = document.createElement('div');
2886
                this.scrubberHead.id = 'playhead-scrubber';
2887
                this.scrubberTrack.appendChild(this.scrubberHead);
2888
                this.object.appendChild(this.scrubberTrack);
2889
                
2890
                this.timePerPixel = 0;
2891
                this.maxTime = 0;
2892
                
2893
                this.playbackObject;
2894
                
2895
                this.setTimePerPixel = function(audioObject) {
2896
                        //maxTime must be in seconds
2897
                        this.playbackObject = audioObject;
2898
                        this.maxTime = audioObject.buffer.buffer.duration;
2899
                        var width = 490; //500 - 10, 5 each side of the tracker head
2900
                        this.timePerPixel = this.maxTime/490;
2901
                        if (this.maxTime < 60) {
2902
                                this.curTimeSpan.textContent = '0.00';
2903
                        } else {
2904
                                this.curTimeSpan.textContent = '00:00';
2905
                        }
2906
                };
2907
                
2908
                this.update = function() {
2909
                        // Update the playhead position, startPlay must be called
2910
                        if (this.timePerPixel > 0) {
2911
                                var time = this.playbackObject.getCurrentPosition();
2912
                                if (time > 0 && time < this.maxTime) {
2913
                                        var width = 490;
2914
                                        var pix = Math.floor(time/this.timePerPixel);
2915
                                        this.scrubberHead.style.left = pix+'px';
2916
                                        if (this.maxTime > 60.0) {
2917
                                                var secs = time%60;
2918
                                                var mins = Math.floor((time-secs)/60);
2919
                                                secs = secs.toString();
2920
                                                secs = secs.substr(0,2);
2921
                                                mins = mins.toString();
2922
                                                this.curTimeSpan.textContent = mins+':'+secs;
2923
                                        } else {
2924
                                                time = time.toString();
2925
                                                this.curTimeSpan.textContent = time.substr(0,4);
2926
                                        }
2927
                                } else {
2928
                                        this.scrubberHead.style.left = '0px';
2929
                                        if (this.maxTime < 60) {
2930
                                                this.curTimeSpan.textContent = '0.00';
2931
                                        } else {
2932
                                                this.curTimeSpan.textContent = '00:00';
2933
                                        }
2934
                                }
2935
                        }
2936
                };
2937
                
2938
                this.interval = undefined;
2939
                
2940
                this.start = function() {
2941
                        if (this.playbackObject != undefined && this.interval == undefined) {
2942
                                if (this.maxTime < 60) {
2943
                                        this.interval = setInterval(function(){interfaceContext.playhead.update();},10);
2944
                                } else {
2945
                                        this.interval = setInterval(function(){interfaceContext.playhead.update();},100);
2946
                                }
2947
                        }
2948
                };
2949
                this.stop = function() {
2950
                        clearInterval(this.interval);
2951
                        this.interval = undefined;
2952
            this.scrubberHead.style.left = '0px';
2953
                        if (this.maxTime < 60) {
2954
                                this.curTimeSpan.textContent = '0.00';
2955
                        } else {
2956
                                this.curTimeSpan.textContent = '00:00';
2957
                        }
2958
                };
2959
        };
2960
    
2961
    this.volume = new function()
2962
    {
2963
        // An in-built volume module which can be viewed on page
2964
        // Includes trackers on page-by-page data
2965
        // Volume does NOT reset to 0dB on each page load
2966
        this.valueLin = 1.0;
2967
        this.valueDB = 0.0;
2968
        this.object = document.createElement('div');
2969
        this.object.id = 'master-volume-holder';
2970
        this.slider = document.createElement('input');
2971
        this.slider.id = 'master-volume-control';
2972
        this.slider.type = 'range';
2973
        this.valueText = document.createElement('span');
2974
        this.valueText.id = 'master-volume-feedback';
2975
        this.valueText.textContent = '0dB';
2976
        
2977
        this.slider.min = -60;
2978
        this.slider.max = 12;
2979
        this.slider.value = 0;
2980
        this.slider.step = 1;
2981
        this.slider.onmousemove = function(event)
2982
        {
2983
            interfaceContext.volume.valueDB = event.currentTarget.value;
2984
            interfaceContext.volume.valueLin = decibelToLinear(interfaceContext.volume.valueDB);
2985
            interfaceContext.volume.valueText.textContent = interfaceContext.volume.valueDB+'dB';
2986
            audioEngineContext.outputGain.gain.value = interfaceContext.volume.valueLin;
2987
        }
2988
        this.slider.onmouseup = function(event)
2989
        {
2990
            var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
2991
            if (storePoint.length == 0)
2992
            {
2993
                storePoint = storage.document.createElement('metricresult');
2994
                storePoint.setAttribute('name','volumeTracker');
2995
                testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
2996
            }
2997
            else {
2998
                storePoint = storePoint[0];
2999
            }
3000
            var node = storage.document.createElement('movement');
3001
            node.setAttribute('test-time',audioEngineContext.timer.getTestTime());
3002
            node.setAttribute('volume',interfaceContext.volume.valueDB);
3003
            node.setAttribute('format','dBFS');
3004
            storePoint.appendChild(node);
3005
        }
3006
        
3007
        var title = document.createElement('div');
3008
        title.innerHTML = '<span>Master Volume Control</span>';
3009
        title.style.fontSize = '0.75em';
3010
        title.style.width = "100%";
3011
        title.align = 'center';
3012
        this.object.appendChild(title);
3013
        
3014
        this.object.appendChild(this.slider);
3015
        this.object.appendChild(this.valueText);
3016
    }
3017
        // Global Checkers
3018
        // These functions will help enforce the checkers
3019
        this.checkHiddenAnchor = function()
3020
        {
3021
                for (var ao of audioEngineContext.audioObjects)
3022
                {
3023
                        if (ao.specification.type == "anchor")
3024
                        {
3025
                                if (ao.interfaceDOM.getValue() > (ao.specification.marker/100) && ao.specification.marker > 0) {
3026
                                        // Anchor is not set below
3027
                                        console.log('Anchor node not below marker value');
3028
                                        alert('Please keep listening');
3029
                    this.storeErrorNode('Anchor node not below marker value');
3030
                                        return false;
3031
                                }
3032
                        }
3033
                }
3034
                return true;
3035
        };
3036
        
3037
        this.checkHiddenReference = function()
3038
        {
3039
                for (var ao of audioEngineContext.audioObjects)
3040
                {
3041
                        if (ao.specification.type == "reference")
3042
                        {
3043
                                if (ao.interfaceDOM.getValue() < (ao.specification.marker/100) && ao.specification.marker > 0) {
3044
                                        // Anchor is not set below
3045
                                        console.log('Reference node not above marker value');
3046
                    this.storeErrorNode('Reference node not above marker value');
3047
                                        alert('Please keep listening');
3048
                                        return false;
3049
                                }
3050
                        }
3051
                }
3052
                return true;
3053
        };
3054
        
3055
        this.checkFragmentsFullyPlayed = function ()
3056
        {
3057
                // Checks the entire file has been played back
3058
                // NOTE ! This will return true IF playback is Looped!!!
3059
                if (audioEngineContext.loopPlayback)
3060
                {
3061
                        console.log("WARNING - Looped source: Cannot check fragments are fully played");
3062
                        return true;
3063
                }
3064
                var check_pass = true;
3065
                var error_obj = [];
3066
                for (var i = 0; i<audioEngineContext.audioObjects.length; i++)
3067
                {
3068
                        var object = audioEngineContext.audioObjects[i];
3069
                        var time = object.buffer.buffer.duration;
3070
                        var metric = object.metric;
3071
                        var passed = false;
3072
                        for (var j=0; j<metric.listenTracker.length; j++)
3073
                        {
3074
                                var bt = metric.listenTracker[j].getElementsByTagName('buffertime');
3075
                                var start_time = Number(bt[0].getAttribute('start'));
3076
                                var stop_time = Number(bt[0].getAttribute('stop'));
3077
                                var delta = stop_time - start_time;
3078
                                if (delta >= time)
3079
                                {
3080
                                        passed = true;
3081
                                        break;
3082
                                }
3083
                        }
3084
                        if (passed == false)
3085
                        {
3086
                                check_pass = false;
3087
                                console.log("Continue listening to track-"+object.interfaceDOM.getPresentedId());
3088
                                error_obj.push(object.interfaceDOM.getPresentedId());
3089
                        }
3090
                }
3091
                if (check_pass == false)
3092
                {
3093
                        var str_start = "You have not completely listened to fragments ";
3094
                        for (var i=0; i<error_obj.length; i++)
3095
                        {
3096
                                str_start += error_obj[i];
3097
                                if (i != error_obj.length-1)
3098
                                {
3099
                                        str_start += ', ';
3100
                                }
3101
                        }
3102
                        str_start += ". Please keep listening";
3103
                        console.log("[ALERT]: "+str_start);
3104
            this.storeErrorNode("[ALERT]: "+str_start);
3105
                        alert(str_start);
3106
                }
3107
        };
3108
        this.checkAllMoved = function()
3109
        {
3110
                var str = "You have not moved ";
3111
                var failed = [];
3112
                for (var ao of audioEngineContext.audioObjects)
3113
                {
3114
                        if(ao.metric.wasMoved == false && ao.interfaceDOM.canMove() == true)
3115
                        {
3116
                                failed.push(ao.interfaceDOM.getPresentedId());
3117
                        }
3118
                }
3119
                if (failed.length == 0)
3120
                {
3121
                        return true;
3122
                } else if (failed.length == 1)
3123
                {
3124
                        str += 'track '+failed[0];
3125
                } else {
3126
                        str += 'tracks ';
3127
                        for (var i=0; i<failed.length-1; i++)
3128
                        {
3129
                                str += failed[i]+', ';
3130
                        }
3131
                        str += 'and '+failed[i];
3132
                }
3133
                str +='.';
3134
                alert(str);
3135
                console.log(str);
3136
                this.storeErrorNode(str);
3137
                return false;
3138
        };
3139
        this.checkOneFragmentSelected = function(){
3140
                console.log("checkOneFragmentSelected");
3141
                var str = "You should select an answer before continuing";
3142
                var isInactive = 0;
3143
                for (var ao of audioEngineContext.audioObjects)
3144
                {
3145
                        if(ao.specification.inactive === true)
3146
                        {
3147
                                ++isInactive;
3148
                        }
3149
                }
3150
                if(this.comparator.selected === null && isInactive===0){
3151
                        alert(str);
3152
                        return false;
3153
                } else {
3154
                        return true;
3155
                }
3156
         };
3157
        this.checkAllPlayed = function()
3158
        {
3159
                var str = "You have not played ";
3160
                var failed = [];
3161
                for (var ao of audioEngineContext.audioObjects)
3162
                {
3163
                        if(ao.metric.wasListenedTo == false)
3164
                        {
3165
                                failed.push(ao.interfaceDOM.getPresentedId());
3166
                        }
3167
                }
3168
                if (failed.length == 0)
3169
                {
3170
                        return true;
3171
                } else if (failed.length == 1)
3172
                {
3173
                        str += 'track '+failed[0];
3174
                } else {
3175
                        str += 'tracks ';
3176
                        for (var i=0; i<failed.length-1; i++)
3177
                        {
3178
                                str += failed[i]+', ';
3179
                        }
3180
                        str += 'and '+failed[i];
3181
                }
3182
                str +='.';
3183
                alert(str);
3184
                console.log(str);
3185
        this.storeErrorNode(str);
3186
                return false;
3187
        };
3188
    
3189
    this.storeErrorNode = function(errorMessage)
3190
    {
3191
        var time = audioEngineContext.timer.getTestTime();
3192
        var node = storage.document.createElement('error');
3193
        node.setAttribute('time',time);
3194
        node.textContent = errorMessage;
3195
        testState.currentStore.XMLDOM.appendChild(node);
3196
    };
3197
}
3198

    
3199
function Storage()
3200
{
3201
        // Holds results in XML format until ready for collection
3202
        this.globalPreTest = null;
3203
        this.globalPostTest = null;
3204
        this.testPages = [];
3205
        this.document = null;
3206
        this.root = null;
3207
        this.state = 0;
3208
        
3209
        this.initialise = function(existingStore)
3210
        {
3211
        if (existingStore == undefined) {
3212
            // We need to get the sessionKey
3213
            this.SessionKey.generateKey();
3214
            this.document = document.implementation.createDocument(null,"waetresult");
3215
            this.root = this.document.childNodes[0];
3216
            var projectDocument = specification.projectXML;
3217
            projectDocument.setAttribute('file-name',url);
3218
            this.root.appendChild(projectDocument);
3219
            this.root.appendChild(returnDateNode());
3220
            this.root.appendChild(interfaceContext.returnNavigator());
3221
        } else {
3222
            this.document = existingStore;
3223
            this.root = existingStore.children[0];
3224
            this.SessionKey.key = this.root.getAttribute("key");
3225
        }
3226
        if (specification.preTest != undefined){this.globalPreTest = new this.surveyNode(this,this.root,specification.preTest);}
3227
        if (specification.postTest != undefined){this.globalPostTest = new this.surveyNode(this,this.root,specification.postTest);}
3228
        };
3229
    
3230
    this.SessionKey = {
3231
        key: null,
3232
        request: new XMLHttpRequest(),
3233
        parent: this,
3234
        handleEvent: function() {
3235
            var parse = new DOMParser();
3236
            var xml = parse.parseFromString(this.request.response,"text/xml");
3237
            if (xml.getAllElementsByTagName("state")[0].textContent == "OK") {
3238
                this.key = xml.getAllElementsByTagName("key")[0].textContent;
3239
                this.parent.root.setAttribute("key",this.key);
3240
                this.parent.root.setAttribute("state","empty");
3241
            } else {
3242
                this.generateKey();
3243
            }
3244
        },
3245
        generateKey: function() {
3246
            var temp_key = randomString(32);
3247
            this.request.open("GET","keygen.php?key="+temp_key,true);
3248
            this.request.addEventListener("load",this);
3249
            this.request.send();
3250
        },
3251
        update: function() {
3252
            this.parent.root.setAttribute("state","update");
3253
            var xmlhttp = new XMLHttpRequest();
3254
            xmlhttp.open("POST",specification.projectReturn+"?key="+this.key);
3255
            xmlhttp.setRequestHeader('Content-Type', 'text/xml');
3256
            xmlhttp.onerror = function(){
3257
                console.log('Error updating file to server!');
3258
            };
3259
            var hold = document.createElement("div");
3260
            var clone = this.parent.root.cloneNode(true);
3261
            hold.appendChild(clone);
3262
            xmlhttp.onload = function() {
3263
                if (this.status >= 300) {
3264
                    console.log("WARNING - Could not update at this time");
3265
                } else {
3266
                    var parser = new DOMParser();
3267
                    var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
3268
                    var response = xmlDoc.getElementsByTagName('response')[0];
3269
                    if (response.getAttribute("state") == "OK") {
3270
                        var file = response.getElementsByTagName("file")[0];
3271
                        console.log("Intermediate save: OK, written "+file.getAttribute("bytes")+"B");
3272
                    } else {
3273
                        var message = response.getElementsByTagName("message");
3274
                        console.log("Intermediate save: Error! "+message.textContent);
3275
                    }
3276
                }
3277
            }
3278
            xmlhttp.send([hold.innerHTML]);
3279
        }
3280
    }
3281
        
3282
        this.createTestPageStore = function(specification)
3283
        {
3284
                var store = new this.pageNode(this,specification);
3285
                this.testPages.push(store);
3286
                return this.testPages[this.testPages.length-1];
3287
        };
3288
        
3289
        this.surveyNode = function(parent,root,specification)
3290
        {
3291
                this.specification = specification;
3292
                this.parent = parent;
3293
        this.state = "empty";
3294
                this.XMLDOM = this.parent.document.createElement('survey');
3295
                this.XMLDOM.setAttribute('location',this.specification.location);
3296
        this.XMLDOM.setAttribute("state",this.state);
3297
                for (var optNode of this.specification.options)
3298
                {
3299
                        if (optNode.type != 'statement')
3300
                        {
3301
                                var node = this.parent.document.createElement('surveyresult');
3302
                                node.setAttribute("ref",optNode.id);
3303
                                node.setAttribute('type',optNode.type);
3304
                                this.XMLDOM.appendChild(node);
3305
                        }
3306
                }
3307
                root.appendChild(this.XMLDOM);
3308
                
3309
                this.postResult = function(node)
3310
                {
3311
                        // From popup: node is the popupOption node containing both spec. and results
3312
                        // ID is the position
3313
                        if (node.specification.type == 'statement'){return;}
3314
                        var surveyresult = this.XMLDOM.children[0];
3315
            while(surveyresult != null) {
3316
                if (surveyresult.getAttribute("ref") == node.specification.id)
3317
                {
3318
                    break;
3319
                }
3320
                surveyresult = surveyresult.nextElementSibling;
3321
            }
3322
                        switch(node.specification.type)
3323
                        {
3324
                        case "number":
3325
                        case "question":
3326
                                var child = this.parent.document.createElement('response');
3327
                                child.textContent = node.response;
3328
                                surveyresult.appendChild(child);
3329
                                break;
3330
                        case "radio":
3331
                                var child = this.parent.document.createElement('response');
3332
                                child.setAttribute('name',node.response.name);
3333
                                child.textContent = node.response.text;
3334
                                surveyresult.appendChild(child);
3335
                                break;
3336
                        case "checkbox":
3337
                                for (var i=0; i<node.response.length; i++)
3338
                                {
3339
                                        var checkNode = this.parent.document.createElement('response');
3340
                                        checkNode.setAttribute('name',node.response[i].name);
3341
                                        checkNode.setAttribute('checked',node.response[i].checked);
3342
                                        surveyresult.appendChild(checkNode);
3343
                                }
3344
                                break;
3345
                        }
3346
                };
3347
        this.complete = function() {
3348
            this.state = "complete";
3349
            this.XMLDOM.setAttribute("state",this.state);
3350
        }
3351
        };
3352
        
3353
        this.pageNode = function(parent,specification)
3354
        {
3355
                // Create one store per test page
3356
                this.specification = specification;
3357
                this.parent = parent;
3358
        this.state = "empty";
3359
                this.XMLDOM = this.parent.document.createElement('page');
3360
                this.XMLDOM.setAttribute('ref',specification.id);
3361
                this.XMLDOM.setAttribute('presentedId',specification.presentedId);
3362
        this.XMLDOM.setAttribute("state",this.state);
3363
                if (specification.preTest != undefined){this.preTest = new this.parent.surveyNode(this.parent,this.XMLDOM,this.specification.preTest);}
3364
                if (specification.postTest != undefined){this.postTest = new this.parent.surveyNode(this.parent,this.XMLDOM,this.specification.postTest);}
3365
                
3366
                // Add any page metrics
3367
                var page_metric = this.parent.document.createElement('metric');
3368
                this.XMLDOM.appendChild(page_metric);
3369
                
3370
                // Add the audioelement
3371
                for (var element of this.specification.audioElements)
3372
                {
3373
                        var aeNode = this.parent.document.createElement('audioelement');
3374
                        aeNode.setAttribute('ref',element.id);
3375
            if (element.name != undefined){aeNode.setAttribute('name',element.name)};
3376
                        aeNode.setAttribute('type',element.type);
3377
                        aeNode.setAttribute('url', element.url);
3378
                        aeNode.setAttribute('gain', element.gain);
3379
                        if (element.type == 'anchor' || element.type == 'reference')
3380
                        {
3381
                                if (element.marker > 0)
3382
                                {
3383
                                        aeNode.setAttribute('marker',element.marker);
3384
                                }
3385
                        }
3386
                        var ae_metric = this.parent.document.createElement('metric');
3387
                        aeNode.appendChild(ae_metric); 
3388
                        this.XMLDOM.appendChild(aeNode);
3389
                }
3390
                
3391
                this.parent.root.appendChild(this.XMLDOM);
3392
        
3393
        this.complete = function() {
3394
            this.state = "complete";
3395
            this.XMLDOM.setAttribute("state","complete");
3396
        }
3397
        };
3398
    this.update = function() {
3399
        this.SessionKey.update();
3400
    }
3401
        this.finish = function()
3402
        {
3403
                if (this.state == 0)
3404
                {
3405
            this.update();
3406
                }
3407
                this.state = 1;
3408
                return this.root;
3409
        };
3410
}