changeset 2899:231d2186f3fc

Merge branch 'master' of https://github.com/BrechtDeMan/WebAudioEvaluationTool
author Dave Moffat <me@davemoffat.com>
date Fri, 14 Jul 2017 15:39:24 +0100
parents 56e72cd18404 (current diff) 5d5542e01fe1 (diff)
children dbfe0bbde246 4028cf5afef1
files index.html test_create/custom.css
diffstat 53 files changed, 5511 insertions(+), 4443 deletions(-) [+]
line wrap: on
line diff
--- a/css/core.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/css/core.css	Fri Jul 14 15:39:24 2017 +0100
@@ -8,6 +8,12 @@
     margin-bottom: 10px;
     font-size: 2em;
 }
+div#footer {
+    position: fixed;
+    bottom: 0px;
+    width: 100%;
+    text-align: center;
+}
 div.indicator-box {
     position: absolute;
     left: 150px;
@@ -65,9 +71,10 @@
 div#popupResponse {
     width: inherit;
     min-height: 50px;
-    max-height: 320px;
+    max-height: 270px;
     overflow: auto;
     position: relative;
+    margin-bottom: 10px;
 }
 button.popupButton {
     /* Button for popup window
@@ -200,7 +207,7 @@
     position: absolute;
     top: 20px;
     left: 50px;
-    width: 250px%;
+    width: 250px;
     padding: 5px;
 }
 div#master-volume-root {
@@ -211,17 +218,16 @@
     height: 40px;
 }
 input#master-volume-control {
-    width: 200px;
+    width: 190px;
     height: 25px;
     float: left;
     margin: 0px;
     padding: 0px;
 }
 span#master-volume-feedback {
-    width: 45px;
     height: 25px;
-    margin-left: 5px;
-    float: left;
+    margin: 0px 5px;
+    float: right;
 }
 div.error-colour {
     background-color: #FF8F8F;
@@ -262,3 +268,26 @@
 div.comment-slider-text-holder span {
     margin: 0px 5px;
 }
+div.comment-checkbox-inputs-holder {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-around;
+    margin: 10px 5px;
+}
+div.comment-checkbox-inputs-flex {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    align-items: center;
+}
+div.comment-box-playing {
+    background-color: #FFDDDD;
+}
+div#imageController {
+    align-content: center;
+    text-align: center;
+    height: 250px;
+}
+div#imageController img {
+    max-height: 250px;
+}
Binary file docs/Instructions/img/interface-AB.png has changed
Binary file docs/Instructions/img/interface-ABC.png has changed
Binary file docs/Instructions/img/interface-ABX.png has changed
Binary file docs/Instructions/img/interface-MUSHRA.png has changed
Binary file docs/Instructions/img/interface-ape.png has changed
Binary file docs/Instructions/img/interface-discrete.png has changed
Binary file docs/Instructions/img/interface-horizontal.png has changed
Binary file docs/Instructions/img/interface-timeline.png has changed
--- a/index.html	Fri Jul 14 15:37:53 2017 +0100
+++ b/index.html	Fri Jul 14 15:39:24 2017 +0100
@@ -24,7 +24,7 @@
 
 <body>
     <div id='topLevelBody'>
-        <h1>Web Audio Evaluation Tool</h1>
+        <h1><a target="_blank" href="https://github.com/BrechtDeMan/WebAudioEvaluationTool">Web Audio Evaluation Toolbox (v1.2.1)</a></h1>
         <h2>Start menu </h2>
         <ul>
             <li><a href="test.html?url=tests/examples/APE_example.xml" target="_blank">APE interface test example</a></li>
@@ -51,6 +51,7 @@
         <button id="popup-previous" class="popupButton">Back</button>
     </div>
     <div class="testHalt" style="visibility: hidden"></div>
+    <div id="footer"><a target="_blank" href="https://github.com/BrechtDeMan/WebAudioEvaluationTool">Web Audio Evaluation Toolbox (v1.2.2-pre)</a></div>
 </body>
 
 </html>
--- a/interfaces/AB.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/AB.css	Fri Jul 14 15:39:24 2017 +0100
@@ -2,6 +2,10 @@
     /* Set the background colour (note US English spelling) to grey*/
     background-color: #fff
 }
+div#feedbackHolder {
+    display: flex;
+    flex-direction: column;
+}
 div.pageTitle {
     width: auto;
     height: 20px;
@@ -34,13 +38,20 @@
     position: inherit;
     margin: 0px 5px;
 }
+div#box-holders {
+    width: 100%;
+    text-align: center;
+}
+div#playback-holder {
+    float: none;
+}
 div.comparator-holder {
     width: 260px;
     height: 300px;
     border: black 1px solid;
-    float: left;
     padding-top: 5px;
     margin: 25px;
+    display: inline-block;
 }
 div.comparator-selector {
     width: 248px;
@@ -49,12 +60,26 @@
     position: relative;
     background-color: #FF0000;
     border-radius: 20px;
+    text-align: center;
+    display: block;
+    margin: auto;
+}
+div.comparator-image {
+    background-color: rgba(255, 255, 255, 0);
+}
+img.comparator-image {
+    width: inherit;
+    height: inherit;
+    z-index: -1;
+    position: absolute;
+    display: inline;
+    right: 0px;
 }
 div.disabled {
     background-color: #AAA;
 }
 div.selected {
-    background-color: #008000;
+    background-color: rgba(0, 200, 0, 0.4);
 }
 div.comparator-selector span {
     font-size: 4em;
@@ -72,8 +97,3 @@
     float: left;
     margin: 0px 5px;
 }
-div#master-volume-holder {
-    position: absolute;
-    top: 10px;
-    left: 120px;
-}
--- a/interfaces/AB.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/AB.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,6 +1,6 @@
 // Once this is loaded and parsed, begin execution
 loadInterface();
-
+/*globals window, interfaceContext, testState, Interface, audioEngineContext, console, document, specification, $, storage*/
 function loadInterface() {
     // Get the dimensions of the screen available to the page
     var width = window.innerWidth;
@@ -10,34 +10,6 @@
     // Custom comparator Object
     Interface.prototype.comparator = null;
 
-    Interface.prototype.checkScaleRange = function (min, max) {
-        var page = testState.getCurrentTestPage();
-        var audioObjects = audioEngineContext.audioObjects;
-        var state = true;
-        var str = "Please keep listening. ";
-        var minRanking = Infinity;
-        var maxRanking = -Infinity;
-        for (var ao of audioObjects) {
-            var rank = ao.interfaceDOM.getValue();
-            if (rank < minRanking) {
-                minRanking = rank;
-            }
-            if (rank > maxRanking) {
-                maxRanking = rank;
-            }
-        }
-        if (maxRanking * 100 < max) {
-            str += "At least one fragment must be selected."
-            state = false;
-        }
-        if (!state) {
-            console.log(str);
-            this.storeErrorNode(str);
-            interfaceContext.lightbox.post("Message", str);
-        }
-        return state;
-    }
-
     // The injection point into the HTML page
     interfaceContext.insertPoint = document.getElementById("topLevelBody");
     var testContent = document.createElement('div');
@@ -52,7 +24,7 @@
     titleSpan.id = "test-title";
 
     // Set title to that defined in XML, else set to default
-    if (titleAttr != undefined) {
+    if (titleAttr !== undefined) {
         titleSpan.textContent = titleAttr;
     } else {
         titleSpan.textContent = 'Listening test';
@@ -63,7 +35,8 @@
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
     pagetitle.align = "center";
-    var titleSpan = document.createElement('span');
+
+    titleSpan = document.createElement('span');
     titleSpan.id = "pageTitle";
     pagetitle.appendChild(titleSpan);
 
@@ -100,9 +73,7 @@
 
     // Construct the AB Boxes
     var boxes = document.createElement('div');
-    boxes.align = "center";
     boxes.id = "box-holders";
-    boxes.style.float = "left";
 
     var submit = document.createElement('button');
     submit.id = "submit";
@@ -150,13 +121,18 @@
 
     // Set the page title
     if (typeof audioHolderObject.title == "string" && audioHolderObject.title.length > 0) {
-        document.getElementById("test-title").textContent = audioHolderObject.title
+        document.getElementById("test-title").textContent = audioHolderObject.title;
     }
 
-    if (interfaceObj.title != null) {
+    if (interfaceObj.title !== undefined) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined) {
+        feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("box-holders"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     var interfaceOptions = interfaceObj.options;
     // Clear the interfaceElements
     {
@@ -182,7 +158,7 @@
             switch (option.name) {
                 case "playhead":
                     var playbackHolder = document.getElementById('playback-holder');
-                    if (playbackHolder == null) {
+                    if (playbackHolder === null) {
                         playbackHolder = document.createElement('div');
                         playbackHolder.id = 'playback-holder';
                         playbackHolder.style.width = "100%";
@@ -194,7 +170,7 @@
                     break;
                 case "page-count":
                     var pagecountHolder = document.getElementById('page-count');
-                    if (pagecountHolder == null) {
+                    if (pagecountHolder === null) {
                         pagecountHolder = document.createElement('div');
                         pagecountHolder.id = 'page-count';
                         document.getElementById('interface-buttons').appendChild(pagecountHolder);
@@ -202,7 +178,7 @@
                     pagecountHolder.innerHTML = '<span>Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + '</span>';
                     break;
                 case "volume":
-                    if (document.getElementById('master-volume-holder-float') == null) {
+                    if (document.getElementById('master-volume-holder-float') === null) {
                         feedbackHolder.appendChild(interfaceContext.volume.object);
                     }
                     break;
@@ -237,6 +213,13 @@
         this.box.id = 'comparator-' + text;
         this.selector = document.createElement('div');
         this.selector.className = 'comparator-selector disabled';
+        if (audioElement.specification.image) {
+            this.selector.className += " comparator-image";
+            var image = document.createElement("img");
+            image.src = audioElement.specification.image;
+            image.className = "comparator-image";
+            this.selector.appendChild(image);
+        }
         var selectorText = document.createElement('span');
         selectorText.textContent = text;
         this.selector.appendChild(selectorText);
@@ -246,51 +229,50 @@
         this.playback.textContent = "Listen";
         this.box.appendChild(this.selector);
         this.box.appendChild(this.playback);
-        this.selector.onclick = function (event) {
+        this.selectorClicked = function () {
+            var i;
             var time = audioEngineContext.timer.getTestTime();
-            if ($(event.currentTarget).hasClass('disabled')) {
+            if (this.parent.state !== 1) {
+                interfaceContext.lightbox.post("Message", "Please wait for the sample to load");
                 console.log("Please wait until sample has loaded");
                 return;
             }
-            if (audioEngineContext.status == 0) {
+            if (audioEngineContext.status === 0) {
                 interfaceContext.lightbox.post("Message", "Please listen to the samples before making a selection");
                 console.log("Please listen to the samples before making a selection");
                 return;
             }
-            var id = event.currentTarget.parentElement.getAttribute('track-id');
-            interfaceContext.comparator.selected = id;
-            if ($(event.currentTarget).hasClass("selected")) {
-                $(".comparator-selector").removeClass('selected');
-                for (var i = 0; i < interfaceContext.comparator.comparators.length; i++) {
-                    var obj = interfaceContext.comparator.comparators[i];
-                    obj.parent.metric.moved(time, 0);
-                    obj.value = 0;
+            interfaceContext.comparator.selected = this.id;
+            $(".comparator-selector").removeClass('selected');
+            $(this.selector).addClass('selected');
+            this.comparator.comparators.forEach(function (a) {
+                if (a !== this) {
+                    a.value = 0;
+                } else {
+                    a.value = 1;
                 }
-            } else {
-                $(".comparator-selector").removeClass('selected');
-                $(event.currentTarget).addClass('selected');
-                for (var i = 0; i < interfaceContext.comparator.comparators.length; i++) {
-                    var obj = interfaceContext.comparator.comparators[i];
-                    if (i == id) {
-                        obj.value = 1;
-                    } else {
-                        obj.value = 0;
-                    }
-                    obj.parent.metric.moved(time, obj.value);
-                }
-                console.log("Selected " + id + ' (' + time + ')');
-            }
+                a.parent.metric.moved(time, a.value);
+            }, this);
+            console.log("Selected " + this.id + ' (' + time + ')');
         };
         this.playback.setAttribute("playstate", "ready");
-        this.playback.onclick = function (event) {
-            var id = event.currentTarget.parentElement.getAttribute('track-id');
-            if (event.currentTarget.getAttribute("playstate") == "ready") {
-                audioEngineContext.play(id);
-            } else if (event.currentTarget.getAttribute("playstate") == "playing") {
+        this.playbackClicked = function () {
+            if (this.playback.getAttribute("playstate") == "ready") {
+                audioEngineContext.play(this.id);
+            } else if (this.playback.getAttribute("playstate") == "playing") {
                 audioEngineContext.stop();
             }
 
         };
+        this.handleEvent = function (event) {
+            if (event.currentTarget === this.selector) {
+                this.selectorClicked();
+            } else if (event.currentTarget === this.playback) {
+                this.playbackClicked();
+            }
+        };
+        this.playback.addEventListener("click", this);
+        this.selector.addEventListener("click", this);
 
         this.enable = function () {
             if (this.parent.state == 1) {
@@ -311,23 +293,34 @@
             // audioObject has an error!!
             this.playback.textContent = "Error";
             $(this.playback).addClass("error-colour");
-        }
+        };
         this.startPlayback = function () {
             if (this.parent.specification.parent.playOne || specification.playOne) {
                 $('.comparator-button').text('Wait');
                 $('.comparator-button').attr("disabled", "true");
-                $(this.playback).css("disabled", "false");
+                $(this.playback).removeAttr("disabled");
             } else {
                 $('.comparator-button').text('Listen');
             }
             $(this.playback).text('Stop');
             this.playback.setAttribute("playstate", "playing");
+            interfaceContext.commentBoxes.highlightById(audioElement.id);
         };
         this.stopPlayback = function () {
             if (this.playback.getAttribute("playstate") == "playing") {
-                $('.comparator-button').text('Listen');
-                $('.comparator-button').removeAttr("disabled");
+                $(this.playback).text('Listen');
+                $(this.playback).removeAttr("disabled");
                 this.playback.setAttribute("playstate", "ready");
+                if (this.parent.specification.parent.playOne || specification.playOne) {
+                    $('.comparator-button').text('Listen');
+                    $('.comparator-button').removeAttr("disabled");
+                }
+            }
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === audioElement.id;
+            });
+            if (box) {
+                box.highlight(false);
             }
         };
         this.exportXMLDOM = function (audioObject) {
@@ -369,6 +362,11 @@
                 label = interfaceContext.getLabel(labelType, index, audioHolderObject.labelStart);
             }
             var node = new this.comparatorBox(audioObject, index, label);
+            Object.defineProperties(node, {
+                'comparator': {
+                    'value': this
+                }
+            });
             audioObject.bindInterface(node);
             this.comparators.push(node);
             this.boxHolders.appendChild(node.box);
@@ -387,12 +385,9 @@
         boxW = numObj * 312;
         diff = window.innerWidth - boxW;
     }
-    document.getElementById('box-holders').style.marginLeft = diff / 2 + 'px';
-    document.getElementById('box-holders').style.marginRight = diff / 2 + 'px';
-    document.getElementById('box-holders').style.width = boxW + 'px';
 
     var outsideRef = document.getElementById('outside-reference');
-    if (outsideRef != null) {
+    if (outsideRef !== null) {
         outsideRef.style.left = (window.innerWidth - 120) / 2 + 'px';
     }
 }
@@ -400,42 +395,47 @@
 function buttonSubmitClick() {
     var checks = testState.currentStateMap.interfaces[0].options,
         canContinue = true;
+    
+    if (interfaceContext.checkFragmentMinPlays() === false) {
+    return;
+}
 
     for (var i = 0; i < checks.length; i++) {
         if (checks[i].type == 'check') {
+            var checkState;
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
+                    if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    var checkState = interfaceContext.checkFragmentsFullyPlayed();
-                    if (checkState == false) {
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
+                    if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllMoved();
-                    if (checkState == false) {
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
+                    if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllCommented();
-                    if (checkState == false) {
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
+                    if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
-                    if (checkState == false) {
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
+                    if (checkState === false) {
                         canContinue = false;
                     }
                     break;
@@ -455,7 +455,7 @@
             playback.click();
             // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
         } else {
-            if (audioEngineContext.timer.testStarted == false) {
+            if (audioEngineContext.timer.testStarted === false) {
                 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!');
                 return;
             }
--- a/interfaces/ABX.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/ABX.css	Fri Jul 14 15:39:24 2017 +0100
@@ -20,13 +20,20 @@
     height: 40px;
     font-size: 1.2em;
 }
+div#box-holders {
+    width: 100%;
+    text-align: center;
+}
+div#playback-holder {
+    float: none;
+}
 div.comparator-holder {
     width: 260px;
     height: 300px;
     border: black 1px solid;
-    float: left;
     padding-top: 5px;
     margin: 25px;
+    display: inline-block;
 }
 div.comparator-selector {
     width: 248px;
@@ -35,12 +42,26 @@
     position: relative;
     background-color: #FF0000;
     border-radius: 20px;
+    text-align: center;
+    display: block;
+    margin: auto;
+}
+div.comparator-image {
+    background-color: rgba(255, 255, 255, 0);
+}
+img.comparator-image {
+    width: inherit;
+    height: inherit;
+    z-index: -1;
+    position: absolute;
+    display: inline;
+    right: 0px;
 }
 div.disabled {
     background-color: #AAA;
 }
 div.selected {
-    background-color: #008000;
+    background-color: rgba(0, 200, 0, 0.4);
 }
 div.comparator-selector.inactive {
     background-color: yellow !important;
@@ -61,8 +82,3 @@
     float: left;
     margin: 0px 5px;
 }
-div#master-volume-holder {
-    position: absolute;
-    top: 10px;
-    left: 120px;
-}
--- a/interfaces/ABX.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/ABX.js	Fri Jul 14 15:39:24 2017 +0100
@@ -4,6 +4,7 @@
  */
 
 // Once this is loaded and parsed, begin execution
+/* globals interfaceContext, Interface, testState, audioEngineContext, console, document, window, feedbackHolder, $, specification, storage*/
 loadInterface();
 
 function loadInterface() {
@@ -12,36 +13,8 @@
 
     interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema
 
-    Interface.prototype.checkScaleRange = function (min, max) {
-        var page = testState.getCurrentTestPage();
-        var audioObjects = audioEngineContext.audioObjects;
-        var state = true;
-        var str = "Please keep listening. ";
-        var minRanking = Infinity;
-        var maxRanking = -Infinity;
-        for (var ao of audioObjects) {
-            var rank = ao.interfaceDOM.getValue();
-            if (rank < minRanking) {
-                minRanking = rank;
-            }
-            if (rank > maxRanking) {
-                maxRanking = rank;
-            }
-        }
-        if (maxRanking * 100 < max) {
-            str += "At least one fragment must be selected."
-            state = false;
-        }
-        if (!state) {
-            console.log(str);
-            this.storeErrorNode(str);
-            interfaceContext.lightbox.post("Message", str);
-        }
-        return state;
-    }
-
     // Custom comparator Object
-    Interface.prototype.comparator = null;
+    interfaceContext.comparator = null;
 
     // The injection point into the HTML page
     interfaceContext.insertPoint = document.getElementById("topLevelBody");
@@ -57,7 +30,7 @@
     titleSpan.id = "test-title";
 
     // Set title to that defined in XML, else set to default
-    if (titleAttr != undefined) {
+    if (titleAttr !== undefined) {
         titleSpan.textContent = titleAttr;
     } else {
         titleSpan.textContent = 'Listening test';
@@ -68,7 +41,8 @@
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
     pagetitle.align = "center";
-    var titleSpan = document.createElement('span');
+
+    titleSpan = document.createElement('span');
     titleSpan.id = "pageTitle";
     pagetitle.appendChild(titleSpan);
 
@@ -131,7 +105,7 @@
     // Load the full interface
     testState.initialise();
     testState.advanceState();
-};
+}
 
 function loadTest(page) {
     // Called each time a new test page is to be build. The page specification node is the only item passed in
@@ -148,13 +122,18 @@
 
     // Set the page title
     if (typeof page.title == "string" && page.title.length > 0) {
-        document.getElementById("test-title").textContent = page.title
+        document.getElementById("test-title").textContent = page.title;
     }
 
-    if (interfaceObj.title != null) {
+    if (interfaceObj.title !== null) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined) {
+        feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("box-holders"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     interfaceContext.comparator = new comparator(page);
 
     var interfaceOptions = interfaceObj.options;
@@ -163,7 +142,7 @@
             switch (option.name) {
                 case "playhead":
                     var playbackHolder = document.getElementById('playback-holder');
-                    if (playbackHolder == null) {
+                    if (playbackHolder === null) {
                         playbackHolder = document.createElement('div');
                         playbackHolder.style.width = "100%";
                         playbackHolder.style.float = "left";
@@ -174,7 +153,7 @@
                     break;
                 case "page-count":
                     var pagecountHolder = document.getElementById('page-count');
-                    if (pagecountHolder == null) {
+                    if (pagecountHolder === null) {
                         pagecountHolder = document.createElement('div');
                         pagecountHolder.id = 'page-count';
                     }
@@ -183,7 +162,7 @@
                     inject.appendChild(pagecountHolder);
                     break;
                 case "volume":
-                    if (document.getElementById('master-volume-holder') == null) {
+                    if (document.getElementById('master-volume-holder') === null) {
                         feedbackHolder.appendChild(interfaceContext.volume.object);
                     }
                     break;
@@ -229,62 +208,65 @@
         this.playback.className = 'comparator-button';
         this.playback.disabled = true;
         this.playback.textContent = "Listen";
+        if (element.specification.image) {
+            this.selector.className += " comparator-image";
+            var image = document.createElement("img");
+            image.src = element.specification.image;
+            image.className = "comparator-image";
+            this.selector.appendChild(image);
+        } else if (label === "X") {
+            this.selector.classList.add('inactive');
+        }
         this.box.appendChild(this.selector);
         this.box.appendChild(this.playback);
-        this.selector.onclick = function (event) {
-            var label = event.currentTarget.children[0].textContent;
+        this.selectorClicked = function (event) {
             if (label == "X" || label == "x") {
                 return;
             }
             var time = audioEngineContext.timer.getTestTime();
-            if ($(event.currentTarget).hasClass('disabled')) {
+            if (this.disabled) {
+                interfaceContext.lightbox.post("Message", "Please wait until sample has loaded");
                 console.log("Please wait until sample has loaded");
                 return;
             }
-            if (audioEngineContext.status == 0) {
+            if (audioEngineContext.status === 0) {
                 interfaceContext.lightbox.post("Message", "Please listen to the samples before making a selection");
                 console.log("Please listen to the samples before making a selection");
                 return;
             }
-            var id = event.currentTarget.parentElement.getAttribute('track-id');
-            interfaceContext.comparator.selected = id;
-            if ($(event.currentTarget).hasClass("selected")) {
-                $(".comparator-selector").removeClass('selected');
-                for (var i = 0; i < interfaceContext.comparator.pair.length; i++) {
-                    var obj = interfaceContext.comparator.pair[i];
-                    obj.parent.metric.moved(time, 0);
-                    obj.value = 0;
-                }
-            } else {
-                $(".comparator-selector").removeClass('selected');
-                $(event.currentTarget).addClass('selected');
-                for (var i = 0; i < interfaceContext.comparator.pair.length; i++) {
-                    var obj = interfaceContext.comparator.pair[i];
-                    if (i == id) {
-                        obj.value = 1;
-                    } else {
-                        obj.value = 0;
-                    }
-                    obj.parent.metric.moved(time, obj.value);
-                }
-                console.log("Selected " + id + ' (' + time + ')');
-            }
+            interfaceContext.comparator.selected = this.id;
+            $(".comparator-selector").removeClass('selected');
+            $(this.selector).addClass('selected');
+            interfaceContext.comparator.pair.forEach(function (obj) {
+                obj.value = 1.0 * (obj === this);
+                obj.parent.metric.moved(time, obj.value);
+            }, this);
+            console.log("Selected " + this.id + ' (' + time + ')');
         };
         this.playback.setAttribute("playstate", "ready");
-        this.playback.onclick = function (event) {
-            var id = event.currentTarget.parentElement.getAttribute('track-id');
-            if (event.currentTarget.getAttribute("playstate") == "ready") {
-                audioEngineContext.play(id);
+        this.playbackClicked = function (event) {
+            if (this.playback.getAttribute("playstate") == "ready") {
+                audioEngineContext.play(this.id);
             } else if (event.currentTarget.getAttribute("playstate") == "playing") {
                 audioEngineContext.stop();
             }
 
         };
+        this.handleEvent = function (event) {
+            if (event.currentTarget === this.playback) {
+                this.playbackClicked(event);
+            } else if (event.currentTarget === this.selector) {
+                this.selectorClicked(event);
+            }
+        };
+        this.playback.addEventListener("click", this);
+        this.selector.addEventListener("click", this);
         this.enable = function () {
             // This is used to tell the interface object that playback of this node is ready
             if (this.parent.state == 1) {
                 $(this.selector).removeClass('disabled');
                 this.playback.disabled = false;
+                this.disabled = false;
             }
         };
         this.updateLoading = function (progress) {
@@ -312,6 +294,7 @@
             }
             $(this.playback).text('Stop');
             this.playback.setAttribute("playstate", "playing");
+            interfaceContext.commentBoxes.highlightById(element.id);
         };
         this.stopPlayback = function () {
             if (this.playback.getAttribute("playstate") == "playing") {
@@ -319,6 +302,12 @@
                 $('.comparator-button').removeAttr("disabled");
                 this.playback.setAttribute("playstate", "ready");
             }
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === element.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
         };
         this.getValue = function () {
             // Return the current value of the object. If there is no value, return 0
@@ -345,7 +334,7 @@
         };
         this.error = function () {
             // If there is an error with the audioObject, this will be called to indicate a failure
-        }
+        };
     };
     // Ensure there are only two comparisons per page
     if (page.audioElements.length != 2) {
@@ -353,31 +342,42 @@
         return;
     }
     // Build the three audio elements
+
+    function buildElement(index, audioObject) {
+        var label;
+        switch (index) {
+            case 0:
+                label = "A";
+                break;
+            case 1:
+                label = "B";
+                break;
+            default:
+                label = "X";
+                break;
+        }
+        var node = new this.interfaceObject(audioObject, label);
+        audioObject.bindInterface(node);
+        return node;
+    }
+
     this.pair = [];
     this.X = null;
     this.boxHolders = document.getElementById('box-holders');
-    for (var index = 0; index < page.audioElements.length; index++) {
-        var element = page.audioElements[index];
+    var node;
+    page.audioElements.forEach(function (element, index) {
         if (element.type != 'normal') {
             console.log("WARNING - ABX can only have normal elements. Page " + page.id + ", Element " + element.id);
             element.type = "normal";
         }
-        var audioObject = audioEngineContext.newTrack(element);
-        var label;
-        if (index == 0) {
-            label = "A";
-        } else {
-            label = "B";
-        }
-        var node = new this.interfaceObject(audioObject, label);
-        audioObject.bindInterface(node);
+        node = buildElement.call(this, index, audioEngineContext.newTrack(element));
         this.pair.push(node);
         this.boxHolders.appendChild(node.box);
-    }
+    }, this);
     var elementId = Math.floor(Math.random() * 2); //Randomly pick A or B to be X
-    var element = new page.audioElementNode(specification);
+    var element = page.addAudioElement();
     for (var atr in page.audioElements[elementId]) {
-        eval("element." + atr + " = page.audioElements[elementId]." + atr);
+        element[atr] = page.audioElements[elementId][atr];
     }
     element.id += "-X";
     if (typeof element.name == "string") {
@@ -397,18 +397,9 @@
     aeNode.appendChild(storage.document.createElement('metric'));
     root.appendChild(aeNode);
     // Build the 'X' element
+    var label;
     var audioObject = audioEngineContext.newTrack(element);
-    var label;
-    switch (audioObject.specification.parent.label) {
-        case "letter":
-            label = "x";
-            break;
-        default:
-            label = "X";
-            break;
-    }
-    var node = new this.interfaceObject(audioObject, label);
-    node.box.children[0].classList.add('inactive');
+    node = buildElement.call(this, 3, audioObject);
     audioObject.bindInterface(node);
     this.X = node;
     this.boxHolders.appendChild(node.box);
@@ -433,43 +424,33 @@
     var checks = testState.currentStateMap.interfaces[0].options,
         canContinue = true;
 
+    if (interfaceContext.checkFragmentMinPlays() === false) {
+        return;
+    }
+
     for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
+
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    var checkState = interfaceContext.checkFragmentsFullyPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllMoved();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllCommented();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    console.log("WARNING - Check 'scalerange' does not make sense in AB/ABX! Ignoring!");
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
@@ -477,17 +458,19 @@
             }
 
         }
-        if (!canContinue) {
+        if (checkState === false) {
+            canContinue = false;
             break;
         }
     }
+
     if (canContinue) {
         if (audioEngineContext.status == 1) {
             var playback = document.getElementById('playback-button');
             playback.click();
             // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
         } else {
-            if (audioEngineContext.timer.testStarted == false) {
+            if (audioEngineContext.timer.testStarted === false) {
                 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please listen to a sample to begin the test!');
                 return;
             }
--- a/interfaces/ape.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/ape.css	Fri Jul 14 15:39:24 2017 +0100
@@ -89,3 +89,11 @@
     top: 10px;
     left: 120px;
 }
+div.imageController {
+    align-content: center;
+    text-align: center;
+    height: 250px;
+}
+div.imageController img {
+    max-height: 250px;
+}
--- a/interfaces/ape.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/ape.js	Fri Jul 14 15:39:24 2017 +0100
@@ -3,7 +3,8 @@
  *  Create the APE interface
  */
 
-
+/*globals window,interfaceContext, document, audioEngineContext, console, $, Interface, testState, storage, specification */
+/*globals metricTracker */
 // Once this is loaded and parsed, begin execution
 loadInterface();
 
@@ -21,7 +22,7 @@
 
     // Bindings for interfaceContext
     interfaceContext.checkAllPlayed = function () {
-        hasBeenPlayed = audioEngineContext.checkAllPlayed();
+        var hasBeenPlayed = audioEngineContext.checkAllPlayed();
         if (hasBeenPlayed.length > 0) // if a fragment has not been played yet
         {
             var str = "";
@@ -53,14 +54,14 @@
             var interfaceTID = [];
             for (var j = 0; j < this.interfaceSliders[i].metrics.length; j++) {
                 var ao_id = this.interfaceSliders[i].sliders[j].getAttribute("trackIndex");
-                if (this.interfaceSliders[i].metrics[j].wasMoved == false && audioEngineContext.audioObjects[ao_id].interfaceDOM.canMove()) {
+                if (this.interfaceSliders[i].metrics[j].wasMoved === false && audioEngineContext.audioObjects[ao_id].interfaceDOM.canMove()) {
                     state = false;
                     interfaceTID.push(j);
                 }
             }
-            if (interfaceTID.length != 0) {
+            if (interfaceTID.length !== 0) {
                 var interfaceName = this.interfaceSliders[i].interfaceObject.title;
-                if (interfaceName == undefined) {
+                if (interfaceName === undefined) {
                     str += 'On axis ' + String(i + 1) + ' you must move ';
                 } else {
                     str += 'On axis "' + interfaceName + '" you must move ';
@@ -76,7 +77,7 @@
                 }
             }
         }
-        if (state != true) {
+        if (state !== true) {
             this.storeErrorNode(str);
             interfaceContext.lightbox.post("Message", str);
             console.log(str);
@@ -84,75 +85,38 @@
         return state;
     };
 
-    Interface.prototype.checkAllCommented = function () {
+    interfaceContext.checkScaleRange = function () {
         var audioObjs = audioEngineContext.audioObjects;
         var audioHolder = testState.stateMap[testState.stateIndex];
-        var state = true;
-        if (audioHolder.elementComments) {
-            var strNums = [];
-            for (var i = 0; i < audioObjs.length; i++) {
-                if (audioObjs[i].commentDOM.trackCommentBox.value.length == 0) {
-                    state = false;
-                    strNums.push(i);
-                }
-            }
-            if (state == false) {
-                var str = "";
-                if (strNums.length > 1) {
-
-                    for (var i = 0; i < strNums.length; i++) {
-                        var ao_id = audioEngineContext.audioObjects[strNums[i]].interfaceDOM.getPresentedId();
-                        str = str + (ao_id); // start from 1
-                        if (i < strNums.length - 2) {
-                            str += ", ";
-                        } else if (i == strNums.length - 2) {
-                            str += " or ";
-                        }
-                    }
-                    str = 'You have not commented on fragments ' + str + ' yet. Please listen, rate and comment all samples before submitting.';
-                } else {
-                    str = 'You have not commented on fragment ' + (audioEngineContext.audioObjects[strNums[0]].interfaceDOM.getPresentedId()) + ' yet. Please listen, rate and comment all samples before submitting.';
-                }
-                this.storeErrorNode(str);
-                interfaceContext.lightbox.post("Message", str);
-                console.log(str);
-            }
-        }
-        return state;
-    };
-
-    Interface.prototype.checkScaleRange = function () {
-        var audioObjs = audioEngineContext.audioObjects;
-        var audioHolder = testState.stateMap[testState.stateIndex];
+        var interfaceObject = this.interfaceSliders[0].interfaceObject;
         var state = true;
         var str = '';
-        for (var i = 0; i < this.interfaceSliders.length; i++) {
-            var minScale;
-            var maxScale;
-            var interfaceObject = interfaceContext.interfaceSliders[0].interfaceObject;
-            for (var j = 0; j < interfaceObject.options.length; j++) {
-                if (interfaceObject.options[j].check == "scalerange") {
-                    minScale = interfaceObject.options[j].min;
-                    maxScale = interfaceObject.options[j].max;
-                    break;
-                }
+        this.interfaceSliders.forEach(function (sliderHolder, i) {
+            var scales = (function () {
+                var scaleRange = interfaceObject.options.find(function (a) {
+                    return a.name == "scalerange";
+                });
+                return {
+                    min: scaleRange.min,
+                    max: scaleRange.max
+                };
+            })();
+            var range = sliderHolder.sliders.reduce(function (a, b) {
+                var v = convSliderPosToRate(b) * 100.0;
+                return {
+                    min: Math.min(a.min, v),
+                    max: Math.max(a.max, v)
+                };
+            }, {
+                min: 100,
+                max: 0
+            });
+            if (range.min >= scales.min || range.max <= scales.max) {
+                state = false;
+                str += 'On axis "' + sliderHolder.interfaceObject.title + '" you have not used the full width of the scale. ';
             }
-            var minRanking = convSliderPosToRate(this.interfaceSliders[i].sliders[0]);
-            var maxRanking = minRanking;
-            for (var j = 1; j < this.interfaceSliders[i].sliders.length; j++) {
-                var ranking = convSliderPosToRate(this.interfaceSliders[i].sliders[j]);
-                if (ranking < minRanking) {
-                    minRanking = ranking;
-                } else if (ranking > maxRanking) {
-                    maxRanking = ranking;
-                }
-            }
-            if (minRanking > minScale || maxRanking < maxScale) {
-                state = false;
-                str += 'On axis "' + this.interfaceSliders[i].interfaceObject.title + '" you have not used the full width of the scale. ';
-            }
-        }
-        if (state != true) {
+        });
+        if (state !== true) {
             this.storeErrorNode(str);
             interfaceContext.lightbox.post("Message", str);
             console.log(str);
@@ -163,13 +127,13 @@
     Interface.prototype.objectSelected = null;
     Interface.prototype.objectMoved = false;
     Interface.prototype.selectObject = function (object) {
-        if (this.objectSelected == null) {
+        if (this.objectSelected === null) {
             this.objectSelected = object;
             this.objectMoved = false;
         }
     };
     Interface.prototype.moveObject = function () {
-        if (this.objectMoved == false) {
+        if (this.objectMoved === false) {
             this.objectMoved = true;
         }
     };
@@ -198,7 +162,7 @@
     titleSpan.id = "test-title";
 
     // Set title to that defined in XML, else set to default
-    if (titleAttr != undefined) {
+    if (titleAttr !== undefined) {
         titleSpan.textContent = titleAttr;
     } else {
         titleSpan.textContent = 'Listening test';
@@ -274,12 +238,12 @@
     sliderHolder.innerHTML = "";
 
     // Set labelType if default to number
-    if (audioHolderObject.label == "default" || audioHolderObject.label == "") {
+    if (audioHolderObject.label === "default" || audioHolderObject.label === "") {
         audioHolderObject.label = "number";
     }
     // Set the page title
     if (typeof audioHolderObject.title == "string" && audioHolderObject.title.length > 0) {
-        document.getElementById("test-title").textContent = audioHolderObject.title
+        document.getElementById("test-title").textContent = audioHolderObject.title;
     }
 
 
@@ -287,17 +251,17 @@
     document.getElementById("outside-reference-holder").innerHTML = "";
 
     var interfaceObj = interfaceContext.getCombinedInterfaces(audioHolderObject);
-    for (var k = 0; k < interfaceObj.length; k++) {
+    interfaceObj.forEach(function (interfaceObjectInstance) {
         // Create the div box to center align
-        interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObj[k]));
-    }
+        interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObjectInstance, audioHolderObject));
+    });
     interfaceObj.forEach(function (interface) {
-        for (var option of interface.options) {
+        interface.options.forEach(function (option) {
             if (option.type == "show") {
                 switch (option.name) {
                     case "playhead":
                         var playbackHolder = document.getElementById('playback-holder');
-                        if (playbackHolder == null) {
+                        if (playbackHolder === null) {
                             playbackHolder = document.createElement('div');
                             playbackHolder.style.width = "100%";
                             playbackHolder.align = 'center';
@@ -307,7 +271,7 @@
                         break;
                     case "page-count":
                         var pagecountHolder = document.getElementById('page-count');
-                        if (pagecountHolder == null) {
+                        if (pagecountHolder === null) {
                             pagecountHolder = document.createElement('div');
                             pagecountHolder.id = 'page-count';
                         }
@@ -316,7 +280,7 @@
                         inject.appendChild(pagecountHolder);
                         break;
                     case "volume":
-                        if (document.getElementById('master-volume-holder') == null) {
+                        if (document.getElementById('master-volume-holder') === null) {
                             feedbackHolder.appendChild(interfaceContext.volume.object);
                         }
                         break;
@@ -325,7 +289,7 @@
                         break;
                 }
             }
-        }
+        });
     });
 
     var commentBoxPrefix = "Comment on fragment";
@@ -334,7 +298,7 @@
 
     var loopPlayback = audioHolderObject.loop;
 
-    currentTestHolder = document.createElement('audioHolder');
+    var currentTestHolder = document.createElement('audioHolder');
     currentTestHolder.id = audioHolderObject.id;
     currentTestHolder.repeatCount = audioHolderObject.repeatCount;
 
@@ -372,7 +336,7 @@
     $('.slider').mousemove(function (event) {
         event.preventDefault();
         var obj = interfaceContext.getSelectedObject();
-        if (obj == null) {
+        if (obj === null) {
             return;
         }
         var move = event.clientX - 6;
@@ -386,7 +350,7 @@
     $('.slider').on('touchmove', null, function (event) {
         event.preventDefault();
         var obj = interfaceContext.getSelectedObject();
-        if (obj == null) {
+        if (obj === null) {
             return;
         }
         var move = event.originalEvent.targetTouches[0].clientX - 6;
@@ -400,14 +364,15 @@
     $(document).mouseup(function (event) {
         event.preventDefault();
         var obj = interfaceContext.getSelectedObject();
-        if (obj == null) {
+        if (obj === null) {
             return;
         }
         var interfaceID = obj.parentElement.getAttribute("interfaceid");
         var trackID = obj.getAttribute("trackindex");
-        if (interfaceContext.hasSelectedObjectMoved() == true) {
+        var id;
+        if (interfaceContext.hasSelectedObjectMoved() === true) {
             var l = $(obj).css("left");
-            var id = obj.getAttribute('trackIndex');
+            id = obj.getAttribute('trackIndex');
             var time = audioEngineContext.timer.getTestTime();
             var rate = convSliderPosToRate(obj);
             audioEngineContext.audioObjects[id].metric.moved(time, rate);
@@ -415,7 +380,7 @@
             console.log("slider " + id + " moved to " + rate + ' (' + time + ')');
             obj.setAttribute("slider-value", convSliderPosToRate(obj));
         } else {
-            var id = Number(obj.attributes['trackIndex'].value);
+            id = Number(obj.attributes.trackIndex.value);
             //audioEngineContext.metric.sliderPlayed(id);
             audioEngineContext.play(id);
         }
@@ -424,12 +389,12 @@
 
     $('.slider').on('touchend', null, function (event) {
         var obj = interfaceContext.getSelectedObject();
-        if (obj == null) {
+        if (obj === null) {
             return;
         }
         var interfaceID = obj.parentElement.getAttribute("interfaceid");
         var trackID = obj.getAttribute("trackindex");
-        if (interfaceContext.hasSelectedObjectMoved() == true) {
+        if (interfaceContext.hasSelectedObjectMoved() === true) {
             var l = $(obj).css("left");
             var id = obj.getAttribute('trackIndex');
             var time = audioEngineContext.timer.getTestTime();
@@ -446,7 +411,7 @@
         for (var i = 0; i < interfaceList[k].options.length; i++) {
             if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'playhead') {
                 var playbackHolder = document.getElementById('playback-holder');
-                if (playbackHolder == null) {
+                if (playbackHolder === null) {
                     playbackHolder = document.createElement('div');
                     playbackHolder.id = "playback-holder";
                     playbackHolder.style.width = "100%";
@@ -456,7 +421,7 @@
                 }
             } else if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'page-count') {
                 var pagecountHolder = document.getElementById('page-count');
-                if (pagecountHolder == null) {
+                if (pagecountHolder === null) {
                     pagecountHolder = document.createElement('div');
                     pagecountHolder.id = 'page-count';
                 }
@@ -464,7 +429,7 @@
                 var inject = document.getElementById('interface-buttons');
                 inject.appendChild(pagecountHolder);
             } else if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'volume') {
-                if (document.getElementById('master-volume-holder') == null) {
+                if (document.getElementById('master-volume-holder') === null) {
                     feedbackHolder.appendChild(interfaceContext.volume.object);
                 }
             } else if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'comments') {
@@ -482,7 +447,7 @@
     //testWaitIndicator();
 }
 
-function interfaceSliderHolder(interfaceObject) {
+function interfaceSliderHolder(interfaceObject, page) {
     this.sliders = [];
     this.metrics = [];
     this.id = document.getElementsByClassName("sliderCanvasDiv").length;
@@ -491,13 +456,28 @@
     this.sliderDOM = document.createElement('div');
     this.sliderDOM.className = 'sliderCanvasDiv';
     this.sliderDOM.id = 'sliderCanvasHolder-' + this.id;
+    this.imageHolder = (function () {
+        var imageController = {};
+        imageController.root = document.createElement("div");
+        imageController.root.className = "imageController";
+        imageController.img = document.createElement("img");
+        imageController.root.appendChild(imageController.img);
+        imageController.setImage = function (src) {
+            imageController.img.src = "";
+            if (typeof src !== "string" || src.length === undefined) {
+                return;
+            }
+            imageController.img.src = src;
+        };
+        return imageController;
+    })();
 
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
     pagetitle.align = "center";
     var titleSpan = document.createElement('span');
     titleSpan.id = "pageTitle-" + this.id;
-    if (interfaceObject.title != undefined && typeof interfaceObject.title == "string") {
+    if (interfaceObject.title !== undefined && typeof interfaceObject.title == "string") {
         titleSpan.textContent = interfaceObject.title;
     } else {
         titleSpan.textContent = "Axis " + String(this.id + 1);
@@ -505,9 +485,15 @@
     pagetitle.appendChild(titleSpan);
     this.sliderDOM.appendChild(pagetitle);
 
+    if (interfaceObject.image !== undefined || page.audioElements.some(function (a) {
+            return a.image !== undefined;
+        })) {
+        this.sliderDOM.appendChild(this.imageHolder.root);
+        this.imageHolder.setImage(interfaceObject.image);
+    }
     // Create the slider box to hold the slider elements
     this.canvas = document.createElement('div');
-    if (this.name != undefined)
+    if (this.name !== undefined)
         this.canvas.id = 'slider-' + this.name;
     else
         this.canvas.id = 'slider-' + this.id;
@@ -531,16 +517,16 @@
     var positionScale = this.canvas.style.width.substr(0, this.canvas.style.width.length - 2);
     var offset = 50;
     var dest = document.getElementById("slider-holder").appendChild(this.sliderDOM);
-    for (var scaleObj of interfaceObject.scales) {
+    interfaceObject.scales.forEach(function (scaleObj) {
         var position = Number(scaleObj.position) * 0.01;
         var pixelPosition = (position * $(this.canvas).width()) + offset;
         var scaleDOM = document.createElement('span');
         scaleDOM.className = "ape-marker-text";
         scaleDOM.textContent = scaleObj.text;
-        scaleDOM.setAttribute('value', position)
+        scaleDOM.setAttribute('value', position);
         this.scale.appendChild(scaleDOM);
         scaleDOM.style.left = Math.floor((pixelPosition - ($(scaleDOM).width() / 2))) + 'px';
-    }
+    }, this);
 
     this.createSliderObject = function (audioObject, label) {
         var trackObj = document.createElement('div');
@@ -548,7 +534,7 @@
         trackObj.className = 'track-slider track-slider-disabled track-slider-' + audioObject.id;
         trackObj.id = 'track-slider-' + this.id + '-' + audioObject.id;
         trackObj.setAttribute('trackIndex', audioObject.id);
-        if (this.name != undefined) {
+        if (this.name !== undefined) {
             trackObj.setAttribute('interface-name', this.name);
         } else {
             trackObj.setAttribute('interface-name', this.id);
@@ -572,25 +558,36 @@
     };
 
     this.resize = function (event) {
-        var width = window.innerWidth;
         var sliderDiv = this.canvas;
         var sliderScaleDiv = this.scale;
         var width = $(sliderDiv).width();
         var marginsize = 50;
         // Move sliders into new position
-        for (var index = 0; index < this.sliders.length; index++) {
-            var pix = Number(this.sliders[index].getAttribute("slider-value")) * width;
-            this.sliders[index].style.left = (pix + marginsize) + 'px';
-        }
+        this.sliders.forEach(function (slider, index) {
+            var pix = Number(slider.getAttribute("slider-value")) * width;
+            slider.style.left = (pix + marginsize) + 'px';
+        });
 
         // Move scale labels
         for (var index = 0; index < this.scale.children.length; index++) {
             var scaleObj = this.scale.children[index];
-            var position = Number(scaleObj.attributes['value'].value);
+            var position = Number(scaleObj.attributes.value.value);
             var pixelPosition = (position * width) + marginsize;
             scaleObj.style.left = Math.floor((pixelPosition - ($(scaleObj).width() / 2))) + 'px';
         }
     };
+
+    this.playing = function (id) {
+        var node = audioEngineContext.audioObjects.find(function (a) {
+            return a.id == id;
+        });
+        if (node === undefined) {
+            this.imageHolder.setImage(interfaceObject.image || "");
+            return;
+        }
+        var imgurl = node.specification.image || interfaceObject.image || "";
+        this.imageHolder.setImage(imgurl);
+    }
 }
 
 function sliderObject(audioObject, interfaceObjects, index) {
@@ -626,8 +623,7 @@
         $('.track-slider').removeClass('track-slider-playing');
         var name = ".track-slider-" + this.parent.id;
         $(name).addClass('track-slider-playing');
-        $('.comment-div').removeClass('comment-box-playing');
-        $('#comment-div-' + this.parent.id).addClass('comment-box-playing');
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
         $('.outside-reference').removeClass('track-slider-playing');
         this.playing = true;
 
@@ -635,15 +631,23 @@
             $('.track-slider').addClass('track-slider-disabled');
             $('.outside-reference').addClass('track-slider-disabled');
         }
+        interfaceContext.interfaceSliders.forEach(function (ts) {
+            ts.playing(this.parent.id);
+        }, this);
     };
     this.stopPlayback = function () {
         if (this.playing) {
             this.playing = false;
             var name = ".track-slider-" + this.parent.id;
             $(name).removeClass('track-slider-playing');
-            $('#comment-div-' + this.parent.id).removeClass('comment-box-playing');
             $('.track-slider').removeClass('track-slider-disabled');
             $('.outside-reference').removeClass('track-slider-disabled');
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === audioObject.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
         }
     };
     this.exportXMLDOM = function (audioObject) {
@@ -651,7 +655,9 @@
         var obj = [];
         $(this.trackSliderObjects).each(function (i, trackObj) {
             var node = storage.document.createElement('value');
-            node.setAttribute("interface-name", trackObj.getAttribute("interface-name"));
+            if (trackObj.getAttribute("interface-name") !== "null") {
+                node.setAttribute("interface-name", trackObj.getAttribute("interface-name"));
+            }
             node.textContent = convSliderPosToRate(trackObj);
             obj.push(node);
         });
@@ -671,7 +677,7 @@
         // audioObject has an error!!
         this.playback.textContent = "Error";
         $(this.playback).addClass("error-colour");
-    }
+    };
 }
 
 function outsideReferenceDOM(audioObject, index, inject) {
@@ -684,16 +690,13 @@
     this.outsideReferenceHolder.appendChild(outsideReferenceHolderspan);
     this.outsideReferenceHolder.setAttribute('track-id', index);
 
-    this.outsideReferenceHolder.onclick = function (event) {
-        audioEngineContext.play(event.currentTarget.getAttribute('track-id'));
+    this.handleEvent = function (event) {
+        audioEngineContext.play(audioObject.id);
         $('.track-slider').removeClass('track-slider-playing');
         $('.comment-div').removeClass('comment-box-playing');
-        if (event.currentTarget.nodeName == 'DIV') {
-            $(event.currentTarget).addClass('track-slider-playing');
-        } else {
-            $(event.currentTarget.parentElement).addClass('track-slider-playing');
-        }
+        $(this.outsideReferenceHolder).addClass('track-slider-playing');
     };
+    this.outsideReferenceHolder.addEventListener("click", this.handleEvent);
     inject.appendChild(this.outsideReferenceHolder);
     this.enable = function () {
         if (this.parent.state == 1) {
@@ -733,7 +736,7 @@
         // audioObject has an error!!
         this.outsideReferenceHolder.textContent = "Error";
         $(this.outsideReferenceHolder).addClass("error-colour");
-    }
+    };
 }
 
 function buttonSubmitClick() {
@@ -741,50 +744,39 @@
         canContinue = true;
 
     // Check that the anchor and reference objects are correctly placed
-    if (interfaceContext.checkHiddenAnchor() == false) {
+    if (interfaceContext.checkHiddenAnchor() === false) {
         return;
     }
-    if (interfaceContext.checkHiddenReference() == false) {
+    if (interfaceContext.checkHiddenReference() === false) {
+        return;
+    }
+    if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
 
     for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    var checkState = interfaceContext.checkFragmentsFullyPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllMoved();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllCommented();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale is used to its full width outlined by the node
-                    var checkState = interfaceContext.checkScaleRange();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].name + " is not supported on this interface");
@@ -792,7 +784,8 @@
             }
 
         }
-        if (!canContinue) {
+        if (checkState === false) {
+            canContinue = false;
             break;
         }
     }
@@ -803,7 +796,7 @@
             playback.click();
             // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
         } else {
-            if (audioEngineContext.timer.testStarted == false) {
+            if (audioEngineContext.timer.testStarted === false) {
                 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click a fragment to begin the test!');
                 return;
             }
@@ -847,9 +840,9 @@
     var audioelements = store.getElementsByTagName("audioelement");
     for (var i = 0; i < audioelements.length; i++) {
         // Have to append the metric specific nodes
-        if (pageSpecification.outsideReference == null || pageSpecification.outsideReference.id != audioelements[i].id) {
+        if (pageSpecification.outsideReference === undefined || pageSpecification.outsideReference.id != audioelements[i].id) {
             var inject = audioelements[i].getElementsByTagName("metric");
-            if (inject.length == 0) {
+            if (inject.length === 0) {
                 inject = storage.document.createElement("metric");
             } else {
                 inject = inject[0];
@@ -859,9 +852,10 @@
                 for (var j = 0; j < mrnodes.length; j++) {
                     var name = mrnodes[j].getAttribute("name");
                     if (name == "elementTracker" || name == "elementTrackerFull" || name == "elementInitialPosition" || name == "elementFlagMoved") {
-                        mrnodes[j].setAttribute("interface-name", interfaceContext.interfaceSliders[k].name);
+                        if (interfaceContext.interfaceSliders[k].name !== null) {
+                            mrnodes[j].setAttribute("interface-name", interfaceContext.interfaceSliders[k].name);
+                        }
                         mrnodes[j].setAttribute("interface-id", k);
-                        inject.appendChild(mrnodes[j]);
                     }
                 }
             }
--- a/interfaces/discrete.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/discrete.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,3 +1,4 @@
+/* globals interfaceContext, document, window, $, specification, audioEngineContext, console, window, testState, storage */
 // Once this is loaded and parsed, begin execution
 loadInterface();
 
@@ -19,7 +20,7 @@
     titleSpan.id = "test-title";
 
     // Set title to that defined in XML, else set to default
-    if (titleAttr != undefined) {
+    if (titleAttr !== undefined) {
         titleSpan.textContent = titleAttr;
     } else {
         titleSpan.textContent = 'Listening test';
@@ -30,7 +31,8 @@
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
     pagetitle.align = "center";
-    var titleSpan = document.createElement('span');
+
+    titleSpan = document.createElement('span');
     titleSpan.id = "pageTitle";
     pagetitle.appendChild(titleSpan);
 
@@ -112,7 +114,7 @@
     // Load the full interface
     testState.initialise();
     testState.advanceState();
-};
+}
 
 function loadTest(page) {
     // Called each time a new test page is to be build. The page specification node is the only item passed in
@@ -128,13 +130,20 @@
 
     // Set the page title
     if (typeof page.title == "string" && page.title.length > 0) {
-        document.getElementById("test-title").textContent = page.title
+        document.getElementById("test-title").textContent = page.title;
     }
 
-    if (interfaceObj.title != null) {
+    if (interfaceObj.title !== null) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined || page.audioElements.some(function (elem) {
+            return elem.image !== undefined;
+        })) {
+        document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     document.getElementById("outside-reference-holder").innerHTML = "";
 
@@ -142,17 +151,17 @@
     sliderBox.innerHTML = "";
 
     var commentBoxPrefix = "Comment on track";
-    if (interfaceObj.commentBoxPrefix != undefined) {
+    if (interfaceObj.commentBoxPrefix !== undefined) {
         commentBoxPrefix = interfaceObj.commentBoxPrefix;
     }
     var loopPlayback = page.loop;
 
-    for (var option of interfaceObj.options) {
+    interfaceObj.options.forEach(function (option) {
         if (option.type == "show") {
             switch (option.name) {
                 case "playhead":
                     var playbackHolder = document.getElementById('playback-holder');
-                    if (playbackHolder == null) {
+                    if (playbackHolder === null) {
                         playbackHolder = document.createElement('div');
                         playbackHolder.style.width = "100%";
                         playbackHolder.align = 'center';
@@ -162,7 +171,7 @@
                     break;
                 case "page-count":
                     var pagecountHolder = document.getElementById('page-count');
-                    if (pagecountHolder == null) {
+                    if (pagecountHolder === null) {
                         pagecountHolder = document.createElement('div');
                         pagecountHolder.id = 'page-count';
                     }
@@ -171,7 +180,7 @@
                     inject.appendChild(pagecountHolder);
                     break;
                 case "volume":
-                    if (document.getElementById('master-volume-holder') == null) {
+                    if (document.getElementById('master-volume-holder') === null) {
                         feedbackHolder.appendChild(interfaceContext.volume.object);
                     }
                     break;
@@ -180,7 +189,7 @@
                     break;
             }
         }
-    }
+    });
 
     // Find all the audioElements from the audioHolder
     var index = 0;
@@ -223,9 +232,9 @@
     // An example node, you can make this however you want for each audioElement.
     // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following
     // You attach them by calling audioObject.bindInterface( )
-    if (interfaceScales == null || interfaceScales.length == 0) {
+    if (interfaceScales === null || interfaceScales.length === 0) {
         console.log("WARNING: The discrete radio's are built depending on the number of scale points specified! Ensure you have some specified. Defaulting to 5 for now!");
-        numOptions = 5;
+        var numOptions = 5;
     }
     this.parent = audioObject;
 
@@ -246,6 +255,23 @@
 
     this.discreteHolder.className = "track-slider-range";
     this.discreteHolder.style.width = window.innerWidth - 500 + 'px';
+    this.radioClicked = function (event) {
+        var time = audioEngineContext.timer.getTestTime();
+        if (audioEngineContext.status === 0) {
+            event.currentTarget.checked = false;
+            return;
+        }
+        var id = this.parent.id;
+        var position = this.getValue();
+        this.parent.metric.moved(time, position);
+        console.log('slider ' + id + ' moved to ' + position + ' (' + time + ')');
+
+    };
+    this.handleEvent = function (event) {
+        if (event.currentTarget.getAttribute("name") === this.parent.specification.id) {
+            this.radioClicked(event);
+        }
+    };
     for (var i = 0; i < interfaceScales.length; i++) {
         var node = document.createElement('input');
         node.setAttribute('type', 'radio');
@@ -256,17 +282,7 @@
         node.setAttribute('id', audioObject.specification.id + '-' + String(i));
         this.discretes.push(node);
         this.discreteHolder.appendChild(node);
-        node.onclick = function (event) {
-            if (audioEngineContext.status == 0) {
-                event.currentTarget.checked = false;
-                return;
-            }
-            var time = audioEngineContext.timer.getTestTime();
-            var id = Number(event.currentTarget.parentNode.parentNode.getAttribute('trackIndex'));
-            var value = event.currentTarget.getAttribute('position') / 100.0;
-            audioEngineContext.audioObjects[id].metric.moved(time, value);
-            console.log('slider ' + id + ' moved to ' + value + ' (' + time + ')');
-        };
+        node.addEventListener("click", this);
     }
 
     this.play.className = 'track-slider-button';
@@ -300,9 +316,9 @@
         this.play.disabled = false;
         this.play.textContent = "Play";
         $(this.slider).removeClass('track-slider-disabled');
-        for (var radio of this.discretes) {
-            radio.disabled = false;
-        }
+        this.discretes.forEach(function (elem) {
+            elem.disabled = false;
+        });
     };
     this.updateLoading = function (progress) {
         // progress is a value from 0 to 100 indicating the current download state of media files
@@ -322,14 +338,18 @@
         $(this.holder).addClass('track-slider-playing');
         var outsideReference = document.getElementById('outside-reference');
         this.play.textContent = "Listening";
-        if (outsideReference != null) {
+        if (outsideReference !== null) {
             $(outsideReference).removeClass('track-slider-playing');
         }
         if (this.parent.specification.parent.playOne || specification.playOne) {
             $('.track-slider-button').text = "Wait";
             $('.track-slider-button').attr("disabled", "true");
         }
-    }
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
+        if (audioObject.specification.image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.image);
+        }
+    };
     this.stopPlayback = function () {
         // Called by audioObject when playback stops
         if (this.play.getAttribute("playstate") == "playing") {
@@ -338,19 +358,29 @@
             $('.track-slider-button').text = "Play";
             this.play.textContent = "Play";
             $('.track-slider-button').removeAttr("disabled");
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === audioObject.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
+            if (audioObject.specification.parent.interfaces[0].image !== undefined) {
+                interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image);
+            } else {
+                interfaceContext.imageHolder.setImage("");
+            }
         }
-    }
+    };
 
     this.getValue = function () {
         // Return the current value of the object. If there is no value, return -1
-        var value = -1;
-        for (var i = 0; i < this.discretes.length; i++) {
-            if (this.discretes[i].checked == true) {
-                value = this.discretes[i].getAttribute('position') / 100.0;
-                break;
-            }
+        var checkedElement = this.discretes.find(function (elem) {
+            return elem.checked;
+        });
+        if (checkedElement === undefined) {
+            return -1;
         }
-        return value;
+        return checkedElement.getAttribute("position") / 100.0;
     };
     this.getPresentedId = function () {
         // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
@@ -374,8 +404,8 @@
         // audioObject has an error!!
         this.playback.textContent = "Error";
         $(this.playback).addClass("error-colour");
-    }
-};
+    };
+}
 
 function resizeWindow(event) {
     // Called on every window resize event, use this to scale your page properly
@@ -415,7 +445,7 @@
     textHolder.innerHTML = "";
     ctx.fillStyle = "#000000";
     ctx.setLineDash([1, 4]);
-    for (var scale of scales) {
+    scales.forEach(function (scale) {
         var posPercent = scale.position / 100.0;
         var posPix = Math.round(width * posPercent);
         if (posPix <= 0) {
@@ -437,7 +467,7 @@
         textHolder.appendChild(text);
         text.style.width = $(text.children[0]).width() + 'px';
         text.style.left = (posPix + 150 - ($(text).width() / 2)) + 'px';
-    }
+    });
 }
 
 function buttonSubmitClick() // TODO: Only when all songs have been played!
@@ -446,57 +476,48 @@
         canContinue = true;
 
     // Check that the anchor and reference objects are correctly placed
-    if (interfaceContext.checkHiddenAnchor() == false) {
+    if (interfaceContext.checkHiddenAnchor() === false) {
         return;
     }
-    if (interfaceContext.checkHiddenReference() == false) {
+    if (interfaceContext.checkHiddenReference() === false) {
+        return;
+    }
+    if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
 
     for (var i = 0; i < checks.length; i++) {
+        var checkState;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllMoved();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllCommented();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
                     break;
             }
-
+            if (checkState === false) {
+                canContinue = false;
+            }
         }
         if (!canContinue) {
             break;
@@ -509,7 +530,7 @@
             playback.click();
             // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
         } else {
-            if (audioEngineContext.timer.testStarted == false) {
+            if (audioEngineContext.timer.testStarted === false) {
                 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!');
                 return;
             }
--- a/interfaces/horizontal-sliders.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/horizontal-sliders.css	Fri Jul 14 15:39:24 2017 +0100
@@ -76,7 +76,7 @@
     margin: 0px 5px;
 }
 div.track-slider-playing {
-    background-color: #FFDDDD;
+    background-color: rgba(255, 201, 201, 0.5);
 }
 input.track-slider-range {
     float: left;
@@ -115,3 +115,6 @@
     top: 10px;
     left: 120px;
 }
+div.comment-box-playing {
+    background-color: #FFDDDD;
+}
--- a/interfaces/horizontal-sliders.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/horizontal-sliders.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,4 +1,5 @@
 // Once this is loaded and parsed, begin execution
+/*globals interfaceContext, window, document, specification, audioEngineContext, console, testState, $, storage */
 loadInterface();
 
 function loadInterface() {
@@ -19,7 +20,7 @@
     titleSpan.id = "test-title";
 
     // Set title to that defined in XML, else set to default
-    if (titleAttr != undefined) {
+    if (titleAttr !== undefined) {
         titleSpan.textContent = titleAttr;
     } else {
         titleSpan.textContent = 'Listening test';
@@ -30,7 +31,8 @@
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
     pagetitle.align = "center";
-    var titleSpan = document.createElement('span');
+
+    titleSpan = document.createElement('span');
     titleSpan.id = "pageTitle";
     pagetitle.appendChild(titleSpan);
 
@@ -111,7 +113,7 @@
     // Load the full interface
     testState.initialise();
     testState.advanceState();
-};
+}
 
 function loadTest(page) {
     // Called each time a new test page is to be build. The page specification node is the only item passed in
@@ -128,13 +130,20 @@
 
     // Set the page title
     if (typeof page.title == "string" && page.title.length > 0) {
-        document.getElementById("test-title").textContent = page.title
+        document.getElementById("test-title").textContent = page.title;
     }
 
-    if (interfaceObj.title != null) {
+    if (interfaceObj.title !== null) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined || page.audioElements.some(function (elem) {
+            return elem.image !== undefined;
+        })) {
+        document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     document.getElementById("outside-reference-holder").innerHTML = "";
 
@@ -142,7 +151,7 @@
     sliderBox.innerHTML = "";
 
     var commentBoxPrefix = "Comment on track";
-    if (interfaceObj.commentBoxPrefix != undefined) {
+    if (interfaceObj.commentBoxPrefix !== undefined) {
         commentBoxPrefix = interfaceObj.commentBoxPrefix;
     }
     var loopPlayback = page.loop;
@@ -186,12 +195,12 @@
         }
 
     });
-    for (var option of interfaceObj.options) {
+    interfaceObj.options.forEach(function (option) {
         if (option.type == "show") {
             switch (option.name) {
                 case "playhead":
                     var playbackHolder = document.getElementById('playback-holder');
-                    if (playbackHolder == null) {
+                    if (playbackHolder === null) {
                         playbackHolder = document.createElement('div');
                         playbackHolder.style.width = "100%";
                         playbackHolder.align = 'center';
@@ -201,7 +210,7 @@
                     break;
                 case "page-count":
                     var pagecountHolder = document.getElementById('page-count');
-                    if (pagecountHolder == null) {
+                    if (pagecountHolder === null) {
                         pagecountHolder = document.createElement('div');
                         pagecountHolder.id = 'page-count';
                     }
@@ -210,7 +219,7 @@
                     inject.appendChild(pagecountHolder);
                     break;
                 case "volume":
-                    if (document.getElementById('master-volume-holder') == null) {
+                    if (document.getElementById('master-volume-holder') === null) {
                         feedbackHolder.appendChild(interfaceContext.volume.object);
                     }
                     break;
@@ -219,7 +228,7 @@
                     break;
             }
         }
-    }
+    });
     // Auto-align
     resizeWindow(null);
 }
@@ -228,6 +237,7 @@
     // An example node, you can make this however you want for each audioElement.
     // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following
     // You attach them by calling audioObject.bindInterface( )
+    var playing = false;
     this.parent = audioObject;
 
     this.holder = document.createElement('div');
@@ -266,9 +276,9 @@
     this.play.onclick = function (event) {
         var id = Number(event.currentTarget.value);
         //audioEngineContext.metric.sliderPlayed(id);
-        if (event.currentTarget.getAttribute("playstate") == "ready") {
+        if (!playing) {
             audioEngineContext.play(id);
-        } else if (event.currentTarget.getAttribute("playstate") == "playing") {
+        } else if (playing) {
             audioEngineContext.stop();
         }
     };
@@ -287,18 +297,35 @@
     };
     this.startPlayback = function () {
         // Called when playback has begun
-        this.play.setAttribute("playstate", "playing");
+        playing = true;
+        this.play.textContent = "Stop";
         $(".track-slider").removeClass('track-slider-playing');
         $(this.holder).addClass('track-slider-playing');
         var outsideReference = document.getElementById('outside-reference');
-        if (outsideReference != null) {
+        if (outsideReference !== null) {
             $(outsideReference).removeClass('track-slider-playing');
         }
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
+        if (audioObject.specification.image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.image);
+        }
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
-        this.play.setAttribute("playstate", "ready");
+        playing = false;
+        this.play.textContent = "Play";
         $(this.holder).removeClass('track-slider-playing');
+        var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+            return a.id === audioObject.id;
+        });
+        if (box) {
+            box.highlight(false);
+        }
+        if (audioObject.specification.parent.interfaces[0].image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image);
+        } else {
+            interfaceContext.imageHolder.setImage("");
+        }
     };
     this.getValue = function () {
         // Return the current value of the object. If there is no value, return 0
@@ -326,8 +353,8 @@
         // audioObject has an error!!
         this.playback.textContent = "Error";
         $(this.playback).addClass("error-colour");
-    }
-};
+    };
+}
 
 function resizeWindow(event) {
     // Called on every window resize event, use this to scale your page properly
@@ -367,7 +394,7 @@
     textHolder.innerHTML = "";
     ctx.fillStyle = "#000000";
     ctx.setLineDash([1, 4]);
-    for (var scale of scales) {
+    scales.forEach(function (scale) {
         var posPercent = scale.position / 100.0;
         var posPix = Math.round(width * posPercent);
         if (posPix <= 0) {
@@ -389,7 +416,7 @@
         textHolder.appendChild(text);
         text.style.width = Math.ceil($(text).width()) + 'px';
         text.style.left = (posPix + 100 - ($(text).width() / 2)) + 'px';
-    }
+    });
 }
 
 function buttonSubmitClick() // TODO: Only when all songs have been played!
@@ -398,59 +425,49 @@
         canContinue = true;
 
     // Check that the anchor and reference objects are correctly placed
-    if (interfaceContext.checkHiddenAnchor() == false) {
+    if (interfaceContext.checkHiddenAnchor() === false) {
         return;
     }
-    if (interfaceContext.checkHiddenReference() == false) {
+    if (interfaceContext.checkHiddenReference() === false) {
+        return;
+    }
+    if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
 
     for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllMoved();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllCommented();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
+
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
                     break;
             }
-
         }
-        if (!canContinue) {
+        if (checkState === false) {
+            canContinue = false;
             break;
         }
     }
@@ -461,7 +478,7 @@
             playback.click();
             // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
         } else {
-            if (audioEngineContext.timer.testStarted == false) {
+            if (audioEngineContext.timer.testStarted === false) {
                 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!');
                 return;
             }
--- a/interfaces/interfaces.json	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/interfaces.json	Fri Jul 14 15:39:24 2017 +0100
@@ -30,6 +30,10 @@
             "name": "timeline",
             "scripts": ["interfaces/timeline.js"],
             "css": ["interfaces/timeline.css"]
+        }, {
+            "name": "ordinal",
+            "scripts": ["interfaces/ordinal.js"],
+            "css": ["interfaces/ordinal.css"]
         }
     ]
 }
--- a/interfaces/mushra.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/mushra.css	Fri Jul 14 15:39:24 2017 +0100
@@ -7,52 +7,43 @@
     /* Set the background colour (note US English spelling) to grey*/
     background-color: #ddd
 }
-
 div.pageTitle {
     width: auto;
     height: 20px;
     margin: 10px 0px;
 }
-
 div.pageTitle span {
     font-size: 1.5em;
 }
-
 button {
     /* Specify any button structure or style */
     min-width: 20px;
     background-color: #ddd
 }
-
 div#slider-holder {
     height: inherit;
     position: absolute;
     left: 0px;
     z-index: 3;
 }
-
 div#scale-holder {
     height: inherit;
     position: absolute;
     left: 0px;
     z-index: 2;
 }
-
 div#scale-text-holder {
     position: relative;
     width: 100px;
     float: left;
 }
-
 div.scale-text {
     position: absolute;
 }
-
 canvas#scale-canvas {
     position: relative;
     float: left;
 }
-
 div.track-slider {
     float: left;
     width: 94px;
@@ -62,27 +53,22 @@
     padding: 2px;
     margin-left: 50px;
 }
-
 div#outside-reference-holder {
     display: flex;
     align-content: center;
     justify-content: center;
     margin-bottom: 5px;
 }
-
 button.outside-reference {
     position: inherit;
     margin: 0px 5px;
 }
-
 div.track-slider-playing {
-    background-color: #FFDDDD;
+    background-color: rgba(255, 201, 201, 0.5);
 }
-
 input.track-slider-range {
     margin: 2px 0px;
 }
-
 input[type=range][orient=vertical] {
     writing-mode: bt-lr;
     /* IE */
@@ -92,7 +78,6 @@
     padding: 0 5px;
     color: rgb(255, 144, 144);
 }
-
 input[type=range]::-webkit-slider-runnable-track {
     width: 8px;
     cursor: pointer;
@@ -100,7 +85,6 @@
     border-radius: 4px;
     border: 1px solid #000;
 }
-
 input[type=range]::-moz-range-track {
     width: 8px;
     cursor: pointer;
@@ -108,79 +92,63 @@
     border-radius: 4px;
     border: 1px solid #000;
 }
-
 input[type=range]::-ms-track {
     cursor: pointer;
     background: #fff;
     border-radius: 4px;
     border: 1px solid #000;
 }
-
 input.track-slider-not-moved[type=range]::-webkit-slider-runnable-track {
     background: #aaa;
 }
-
 input.track-slider-not-moved[type=range]::-moz-range-track {
     background: #aaa;
 }
-
 input[type=range]::-moz-range-thumb {
     margin-left: -7px;
     cursor: pointer;
     margin-top: -1px;
     box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
 }
-
 input[type=range]::-webkit-slider-thumb {
     cursor: pointer;
     margin-top: -1px;
     margin-left: -4px;
 }
-
 input[type=range]::-ms-thumb {
     cursor: pointer;
     margin-top: -1px;
     margin-left: -4px;
 }
-
 input[type=range]::-ms-tooltip {
     visibility: hidden;
 }
-
 input.track-slider-range-disabled {}
-
 input.track-slider-range-disabled[type=range]::-webkit-slider-runnable-track {
     cursor: not-allowed;
 }
-
 input.track-slider-range-disabled[type=range]::-moz-range-track {
     cursor: not-allowed;
 }
-
 input.track-slider-range-disabled[type=range]::-ms-track {
     cursor: not-allowed;
 }
-
 input.track-slider-range-disabled[type=range]::-moz-range-thumb {
     cursor: not-allowed;
     background-color: #888;
 }
-
 input.track-slider-range-disabled[type=range]::-webkit-slider-thumb {
     cursor: not-allowed;
     background-color: #888;
 }
-
 input.track-slider-range-disabled[type=range]::-ms-thumb {
     cursor: not-allowed;
     background-color: #888;
 }
-
 div#page-count {
     float: left;
     margin: 0px 5px;
 }
-
 div#master-volume-holder {
     position: absolute;
     top: 10px;
--- a/interfaces/mushra.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/mushra.js	Fri Jul 14 15:39:24 2017 +0100
@@ -2,7 +2,7 @@
  *  mushra.js
  *  Create the MUSHRA interface
  */
-
+/*globals window, interfaceContext, document, $, specification, audioEngineContext, console, testState, storage */
 // Once this is loaded and parsed, begin execution
 loadInterface();
 
@@ -25,7 +25,7 @@
     titleSpan.id = "test-title";
 
     // Set title to that defined in XML, else set to default
-    if (titleAttr != undefined) {
+    if (titleAttr !== undefined) {
         titleSpan.textContent = titleAttr;
     } else {
         titleSpan.textContent = 'Listening test';
@@ -36,7 +36,8 @@
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
     pagetitle.align = "center";
-    var titleSpan = document.createElement('span');
+
+    titleSpan = document.createElement('span');
     titleSpan.id = "pageTitle";
     pagetitle.appendChild(titleSpan);
 
@@ -49,7 +50,7 @@
     var playback = document.createElement("button");
     playback.innerHTML = 'Stop';
     playback.id = 'playback-button';
-    playback.style.float = 'left';
+    playback.style.display = 'inline-block';
     // onclick function. Check if it is playing or not, call the correct function in the
     // audioEngine, change the button text to reflect the next state.
     playback.onclick = function () {
@@ -65,7 +66,7 @@
     submit.innerHTML = 'Next';
     submit.onclick = buttonSubmitClick;
     submit.id = 'submit-button';
-    submit.style.float = 'left';
+    submit.style.display = 'inline-block';
     // Append the interface buttons into the interfaceButtons object.
     interfaceButtons.appendChild(playback);
     interfaceButtons.appendChild(submit);
@@ -129,13 +130,20 @@
 
     // Set the page title
     if (typeof audioHolderObject.title == "string" && audioHolderObject.title.length > 0) {
-        document.getElementById("test-title").textContent = audioHolderObject.title
+        document.getElementById("test-title").textContent = audioHolderObject.title;
     }
 
-    if (interfaceObj.title != null) {
+    if (interfaceObj.title !== null) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined || audioHolderObject.audioElements.some(function (elem) {
+            return elem.image !== undefined;
+        })) {
+        document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     var outsideReferenceHolder = document.getElementById("outside-reference-holder");
     outsideReferenceHolder.innerHTML = "";
@@ -144,12 +152,12 @@
     sliderBox.innerHTML = "";
 
     var commentBoxPrefix = "Comment on track";
-    if (interfaceObj.commentBoxPrefix != undefined) {
+    if (interfaceObj.commentBoxPrefix !== undefined) {
         commentBoxPrefix = interfaceObj.commentBoxPrefix;
     }
     var loopPlayback = audioHolderObject.loop;
 
-    currentTestHolder = document.createElement('audioHolder');
+    var currentTestHolder = document.createElement('audioHolder');
     currentTestHolder.id = audioHolderObject.id;
     currentTestHolder.repeatCount = audioHolderObject.repeatCount;
 
@@ -192,18 +200,18 @@
     if (testState.currentStateMap.restrictMovement) {
         $(".track-slider-range").addClass("track-slider-range-disabled");
         $(".track-slider-range").each(function (i, e) {
-            e.disabled = true
+            e.disabled = true;
         });
     }
 
 
     var interfaceOptions = interfaceObj.options;
-    for (var option of interfaceOptions) {
+    interfaceOptions.forEach(function (option) {
         if (option.type == "show") {
             switch (option.name) {
                 case "playhead":
                     var playbackHolder = document.getElementById('playback-holder');
-                    if (playbackHolder == null) {
+                    if (playbackHolder === null) {
                         playbackHolder = document.createElement('div');
                         playbackHolder.style.width = "100%";
                         playbackHolder.align = 'center';
@@ -213,25 +221,53 @@
                     break;
                 case "page-count":
                     var pagecountHolder = document.getElementById('page-count');
-                    if (pagecountHolder == null) {
+                    if (pagecountHolder === null) {
                         pagecountHolder = document.createElement('div');
                         pagecountHolder.id = 'page-count';
+                        pagecountHolder.style.display = 'inline-block';
                     }
                     pagecountHolder.innerHTML = '<span>Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + '</span>';
                     var inject = document.getElementById('interface-buttons');
                     inject.appendChild(pagecountHolder);
                     break;
                 case "volume":
-                    if (document.getElementById('master-volume-holder') == null) {
+                    if (document.getElementById('master-volume-holder') === null) {
                         feedbackHolder.appendChild(interfaceContext.volume.object);
                     }
                     break;
                 case "comments":
                     interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true);
                     break;
+                case "fragmentSort":
+                    var button = document.getElementById('sort');
+                    if (button === null) {
+                        button = document.createElement("button");
+                        button.id = 'sort';
+                        button.textContent = "Sort";
+                        button.style.display = 'inline-block';
+                        var container = document.getElementById("interface-buttons");
+                        var neighbour = container.lastElementChild;
+                        container.appendChild(button);
+                        button.onclick = function () {
+                            var sortIndex = interfaceContext.sortFragmentsByScore();
+                            var sliderBox = document.getElementById("slider-holder");
+                            var nodes = audioEngineContext.audioObjects.filter(function (ao) {
+                                return ao.specification.type !== "outside-reference";
+                            });
+                            var i;
+                            nodes.forEach(function (ao) {
+                                sliderBox.removeChild(ao.interfaceDOM.holder);
+                            });
+                            for (i = 0; i < nodes.length; i++) {
+                                var j = sortIndex[i];
+                                sliderBox.appendChild(nodes[j].interfaceDOM.holder);
+                            }
+                        };
+                    }
+                    break;
             }
         }
-    }
+    });
 
     $(audioHolderObject.commentQuestions).each(function (index, element) {
         var node = interfaceContext.createCommentQuestion(element);
@@ -257,7 +293,7 @@
     this.holder.appendChild(this.slider);
     this.holder.appendChild(this.play);
     this.holder.align = "center";
-    if (label == 0) {
+    if (label === 0) {
         this.holder.style.marginLeft = '0px';
     }
     this.holder.setAttribute('trackIndex', audioObject.id);
@@ -315,8 +351,9 @@
         this.play.setAttribute("playstate", "playing");
         $(".track-slider").removeClass('track-slider-playing');
         $(this.holder).addClass('track-slider-playing');
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
         var outsideReference = document.getElementById('outside-reference');
-        if (outsideReference != null) {
+        if (outsideReference !== null) {
             $(outsideReference).removeClass('track-slider-playing');
         }
         this.play.textContent = "Stop";
@@ -336,6 +373,9 @@
                 });
             }
         }
+        if (audioObject.specification.image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.image);
+        }
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
@@ -346,14 +386,25 @@
             $(this.slider).addClass("track-slider-range-disabled");
             this.slider.setAttribute("disabled", "true");
         }
+        var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+            return a.id === audioObject.id;
+        });
+        if (box) {
+            box.highlight(false);
+        }
+        if (audioObject.specification.parent.interfaces[0].image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image);
+        } else {
+            interfaceContext.imageHolder.setImage("");
+        }
     };
     this.getValue = function () {
         return this.slider.value;
     };
 
-    this.resize = function (event) {
-        this.holder.style.height = window.innerHeight - 200 + 'px';
-        this.slider.style.height = window.innerHeight - 250 + 'px';
+    this.resize = function (event, height) {
+        this.holder.style.height = height - 20 + 'px';
+        this.slider.style.height = height - 70 + 'px';
     };
     this.updateLoading = function (progress) {
         progress = String(progress);
@@ -374,15 +425,21 @@
         // audioObject has an error!!
         this.playback.textContent = "Error";
         $(this.playback).addClass("error-colour");
-    }
+    };
 }
 
 function resizeWindow(event) {
     // Function called when the window has been resized.
     // MANDATORY FUNCTION
 
-    var outsideRef = document.getElementById('outside-reference');
-    if (outsideRef != null) {
+    var outsideRef = document.getElementById('outside-reference'),
+        imageHeight = 0,
+        minHeight = Math.max(Math.floor(window.screen.height * 0.33), 200),
+        maxHeight = Math.floor(window.screen.height * 0.5);
+    if (document.getElementById("imageController")) {
+        imageHeight = $(interfaceContext.imageHolder.root).height();
+    }
+    if (outsideRef !== null) {
         outsideRef.style.left = (window.innerWidth - 120) / 2 + 'px';
     }
 
@@ -390,18 +447,21 @@
     var numObj = document.getElementsByClassName('track-slider').length;
     var totalWidth = (numObj - 1) * 150 + 100;
     var diff = (window.innerWidth - totalWidth) / 2;
-    document.getElementById('slider').style.height = window.innerHeight - 180 + 'px';
+    var height = window.innerHeight - 180 - imageHeight;
+    height = Math.min(height, maxHeight);
+    height = Math.max(height, minHeight);
+    document.getElementById('slider').style.height = height + 'px';
     if (diff <= 0) {
         diff = 0;
     }
     document.getElementById('slider-holder').style.marginLeft = diff + 'px';
     for (var i in audioEngineContext.audioObjects) {
         if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference') {
-            audioEngineContext.audioObjects[i].interfaceDOM.resize(event);
+            audioEngineContext.audioObjects[i].interfaceDOM.resize(event, height);
         }
     }
     document.getElementById('scale-holder').style.marginLeft = (diff - 100) + 'px';
-    document.getElementById('scale-text-holder').style.height = window.innerHeight - 194 + 'px';
+    document.getElementById('scale-text-holder').style.height = height - 14 + 'px';
     // Cheers edge for making me delete a canvas every resize.
     var canvas = document.getElementById('scale-canvas');
     var new_canvas = document.createElement("canvas");
@@ -409,7 +469,7 @@
     canvas.parentElement.appendChild(new_canvas);
     canvas.parentElement.removeChild(canvas);
     new_canvas.width = totalWidth;
-    new_canvas.height = window.innerHeight - 194;
+    new_canvas.height = height - 14;
     drawScale();
 }
 
@@ -428,7 +488,7 @@
     var textHolder = document.getElementById('scale-text-holder');
     textHolder.innerHTML = "";
     var lastHeight = 0;
-    for (var scale of scales) {
+    scales.forEach(function (scale) {
         var posPercent = scale.position / 100.0;
         var posPix = (1 - posPercent) * (draw_heights[1] - draw_heights[0]) + draw_heights[0];
         ctx.fillStyle = "#000000";
@@ -446,7 +506,7 @@
         text.style.top = (posPix - 9) + 'px';
         text.style.left = 100 - ($(text).width() + 3) + 'px';
         lastHeight = posPix;
-    }
+    });
 }
 
 function buttonSubmitClick() // TODO: Only when all songs have been played!
@@ -455,51 +515,40 @@
         canContinue = true;
 
     // Check that the anchor and reference objects are correctly placed
-    if (interfaceContext.checkHiddenAnchor() == false) {
+    if (interfaceContext.checkHiddenAnchor() === false) {
         return;
     }
-    if (interfaceContext.checkHiddenReference() == false) {
+    if (interfaceContext.checkHiddenReference() === false) {
+        return;
+    }
+    if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
 
     for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    var checkState = interfaceContext.checkAllPlayed();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllMoved();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    var checkState = interfaceContext.checkAllCommented();
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
-                    if (checkState == false) {
-                        canContinue = false;
-                    }
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
@@ -507,7 +556,8 @@
             }
 
         }
-        if (!canContinue) {
+        if (checkState === false) {
+            canContinue = false;
             break;
         }
     }
@@ -518,7 +568,7 @@
             playback.click();
             // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
         } else {
-            if (audioEngineContext.timer.testStarted == false) {
+            if (audioEngineContext.timer.testStarted === false) {
                 interfaceContext.lightbox.post("Message", 'You have not started the test! Please press start to begin the test!');
                 return;
             }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interfaces/ordinal.css	Fri Jul 14 15:39:24 2017 +0100
@@ -0,0 +1,36 @@
+[draggable] {
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+    user-select: none;
+    /* Required to make elements draggable in old WebKit */
+    -khtml-user-drag: element;
+    -webkit-user-drag: element;
+}
+.ordinal-element {
+    width: 250px;
+    height: 250px;
+    background: #bbffbb;
+    border: 2px #050 solid;
+    border-radius: 10px;
+    float: left;
+    margin: 10px 5px;
+    text-align: center;
+    cursor: move;
+}
+.disabled {
+    background-color: grey;
+}
+.playing {
+    background-color: #ffbbbb;
+    border: 2px #500 solid;
+}
+.dragging {
+    opacity: 0.4;
+}
+.over {
+    border-style: dashed;
+}
+.ordinal-element-label {
+    font-size: 2em;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interfaces/ordinal.js	Fri Jul 14 15:39:24 2017 +0100
@@ -0,0 +1,487 @@
+/**
+ * WAET Blank Template
+ * Use this to start building your custom interface
+ */
+/*globals interfaceContext, window, document, specification, audioEngineContext, console, testState, $, storage */
+// Once this is loaded and parsed, begin execution
+loadInterface();
+
+function loadInterface() {
+    // Use this to do any one-time page / element construction. For instance, placing any stationary text objects,
+    // holding div's, or setting up any nodes which are present for the entire test sequence
+
+    // The injection point into the HTML page
+    interfaceContext.insertPoint = document.getElementById("topLevelBody");
+    var testContent = document.createElement('div');
+    testContent.id = 'testContent';
+
+    // Create the top div for the Title element
+    var titleAttr = specification.title;
+    var title = document.createElement('div');
+    title.className = "title";
+    title.align = "center";
+    var titleSpan = document.createElement('span');
+    titleSpan.id = "test-title";
+
+    // Set title to that defined in XML, else set to default
+    if (titleAttr !== undefined) {
+        titleSpan.textContent = titleAttr;
+    } else {
+        titleSpan.textContent = 'Listening test';
+    }
+    // Insert the titleSpan element into the title div element.
+    title.appendChild(titleSpan);
+
+    var pagetitle = document.createElement('div');
+    pagetitle.className = "pageTitle";
+    pagetitle.align = "center";
+
+    titleSpan = document.createElement('span');
+    titleSpan.id = "pageTitle";
+    pagetitle.appendChild(titleSpan);
+
+    // Create Interface buttons!
+    var interfaceButtons = document.createElement('div');
+    interfaceButtons.id = 'interface-buttons';
+    interfaceButtons.style.height = '25px';
+
+    // Create playback start/stop points
+    var playback = document.createElement("button");
+    playback.innerHTML = 'Stop';
+    playback.id = 'playback-button';
+    playback.style.float = 'left';
+    // onclick function. Check if it is playing or not, call the correct function in the
+    // audioEngine, change the button text to reflect the next state.
+    playback.onclick = function () {
+        if (audioEngineContext.status == 1) {
+            audioEngineContext.stop();
+            this.innerHTML = 'Stop';
+            var time = audioEngineContext.timer.getTestTime();
+            console.log('Stopped at ' + time); // DEBUG/SAFETY
+        }
+    };
+    // Create Submit (save) button
+    var submit = document.createElement("button");
+    submit.innerHTML = 'Next';
+    submit.onclick = buttonSubmitClick;
+    submit.id = 'submit-button';
+    submit.style.float = 'left';
+    // Append the interface buttons into the interfaceButtons object.
+    interfaceButtons.appendChild(playback);
+    interfaceButtons.appendChild(submit);
+
+    // Create outside reference holder
+    var outsideRef = document.createElement("div");
+    outsideRef.id = "outside-reference-holder";
+
+    // Create a slider box
+    var slider = document.createElement("div");
+    slider.id = "slider";
+    slider.style.height = "300px";
+
+    // Global parent for the comment boxes on the page
+    var feedbackHolder = document.createElement('div');
+    feedbackHolder.id = 'feedbackHolder';
+
+    testContent.style.zIndex = 1;
+    interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema
+
+    // Inject into HTML
+    testContent.appendChild(title); // Insert the title
+    testContent.appendChild(pagetitle);
+    testContent.appendChild(interfaceButtons);
+    testContent.appendChild(outsideRef);
+    testContent.appendChild(slider);
+    testContent.appendChild(feedbackHolder);
+    interfaceContext.insertPoint.appendChild(testContent);
+
+    // Load the full interface
+    testState.initialise();
+    testState.advanceState();
+}
+
+function loadTest(page) {
+    // Called each time a new test page is to be build. The page specification node is the only item passed in
+    var id = page.id;
+
+    var feedbackHolder = document.getElementById('feedbackHolder');
+    feedbackHolder.innerHTML = "";
+
+    var interfaceObj = interfaceContext.getCombinedInterfaces(page);
+    if (interfaceObj.length > 1) {
+        console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
+    }
+    interfaceObj = interfaceObj[0];
+
+    // Set the page title
+    if (typeof page.title == "string" && page.title.length > 0) {
+        document.getElementById("test-title").textContent = page.title;
+    }
+
+    if (interfaceObj.title !== null) {
+        document.getElementById("pageTitle").textContent = interfaceObj.title;
+    }
+
+    if (interfaceObj.image !== undefined) {
+        feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+    // Delete outside reference
+    document.getElementById("outside-reference-holder").innerHTML = "";
+
+    var sliderBox = document.getElementById('slider');
+    sliderBox.innerHTML = "";
+
+    var commentBoxPrefix = "Comment on track";
+    if (interfaceObj.commentBoxPrefix !== undefined) {
+        commentBoxPrefix = interfaceObj.commentBoxPrefix;
+    }
+
+    $(page.commentQuestions).each(function (index, element) {
+        var node = interfaceContext.createCommentQuestion(element);
+        feedbackHolder.appendChild(node.holder);
+    });
+
+    var index = 0;
+    var labelType = page.label;
+    if (labelType == "default") {
+        labelType = "number";
+    }
+    page.audioElements.forEach(function (element, pageIndex) {
+        var audioObject = audioEngineContext.newTrack(element);
+        if (element.type == 'outside-reference') {
+            // Construct outside reference;
+            var orNode = new interfaceContext.outsideReferenceDOM(audioObject, index, document.getElementById("outside-reference-holder"));
+            audioObject.bindInterface(orNode);
+        } else {
+            // Create a slider per track
+            var label = interfaceContext.getLabel(labelType, index, page.labelStart);
+            var sliderObj = new interfaceObject(audioObject, label);
+
+            sliderBox.appendChild(sliderObj.root);
+            audioObject.bindInterface(sliderObj);
+            interfaceContext.commentBoxes.createCommentBox(audioObject);
+            index += 1;
+        }
+    });
+    interfaceObj.options.forEach(function (option) {
+        if (option.type == "show") {
+            switch (option.name) {
+                case "playhead":
+                    var playbackHolder = document.getElementById('playback-holder');
+                    if (playbackHolder === null) {
+                        playbackHolder = document.createElement('div');
+                        playbackHolder.style.width = "100%";
+                        playbackHolder.align = 'center';
+                        playbackHolder.appendChild(interfaceContext.playhead.object);
+                        feedbackHolder.appendChild(playbackHolder);
+                    }
+                    break;
+                case "page-count":
+                    var pagecountHolder = document.getElementById('page-count');
+                    if (pagecountHolder === null) {
+                        pagecountHolder = document.createElement('div');
+                        pagecountHolder.id = 'page-count';
+                    }
+                    pagecountHolder.innerHTML = '<span>Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + '</span>';
+                    var inject = document.getElementById('interface-buttons');
+                    inject.appendChild(pagecountHolder);
+                    break;
+                case "volume":
+                    if (document.getElementById('master-volume-holder') === null) {
+                        feedbackHolder.appendChild(interfaceContext.volume.object);
+                    }
+                    break;
+                case "comments":
+                    interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true);
+                    break;
+            }
+        }
+    });
+    resizeWindow();
+}
+
+function interfaceObject(audioObject, label) {
+    var container = document.getElementById("slider");
+    var playing = false;
+    var root = document.createElement("div");
+    root.className = "ordinal-element";
+    root.draggable = "true";
+    var labelElement = document.createElement("span");
+    labelElement.className = "ordinal-element-label";
+    labelElement.textContent = label;
+    root.appendChild(labelElement);
+    root.classList.add("disabled");
+    // An example node, you can make this however you want for each audioElement.
+    // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following
+    // You attach them by calling audioObject.bindInterface( )
+    root.addEventListener("click", this, true);
+    root.addEventListener('dragstart', this, true);
+    root.addEventListener('dragenter', this, true);
+    root.addEventListener('dragover', this, true);
+    root.addEventListener('dragleave', this, true);
+    root.addEventListener('drop', this, true);
+    root.addEventListener('dragend', this, true);
+    this.handleEvent = function (event) {
+        if (event.type == "click") {
+            if (playing === false) {
+                audioEngineContext.play(audioObject.id);
+            } else {
+                audioEngineContext.stop();
+            }
+            playing = !playing;
+            return;
+        } else if (event.type == "dragstart") {
+            return dragStart.call(this, event);
+        } else if (event.type == "dragenter") {
+            return dragEnter.call(this, event);
+        } else if (event.type == "dragleave") {
+            return dragLeave.call(this, event);
+        } else if (event.type == "dragover") {
+            return dragOver.call(this, event);
+        } else if (event.type == "drop") {
+            return drop.call(this, event);
+        } else if (event.type == "dragend") {
+            return dragEnd.call(this, event);
+        }
+        throw (event);
+    };
+
+    function dragStart(e) {
+        e.currentTarget.classList.add("dragging");
+
+        e.dataTransfer.effectAllowed = 'move';
+        e.dataTransfer.setData('text/plain', audioObject.id);
+    }
+
+    function dragEnter(e) {
+        // this / e.target is the current hover target.
+        root.classList.add('over');
+    }
+
+    function dragLeave(e) {
+        root.classList.remove('over'); // this / e.target is previous target element.
+    }
+
+    function dragOver(e) {
+        if (e.preventDefault) {
+            e.preventDefault(); // Necessary. Allows us to drop.
+        }
+
+        e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object.
+
+        var srcid = Number(e.dataTransfer.getData("text/plain"));
+        var elements = container.childNodes;
+        var srcObject = audioEngineContext.audioObjects.find(function (ao) {
+            return ao.id === srcid;
+        });
+        var src = srcObject.interfaceDOM.root;
+        if (src !== root) {
+            var srcpos = srcObject.interfaceDOM.getElementPosition();
+            var mypos = this.getElementPosition();
+            var neighbour;
+            if (srcpos <= mypos) {
+                neighbour = root.nextElementSibling;
+            } else {
+                neighbour = root;
+            }
+            if (neighbour)
+                container.insertBefore(src, neighbour);
+            else {
+                container.removeChild(src);
+                container.appendChild(src);
+            }
+
+        }
+
+        return false;
+    }
+
+    function drop(e) {
+        // this / e.target is current target element.
+
+        if (e.stopPropagation) {
+            e.stopPropagation(); // stops the browser from redirecting.
+        }
+        if (e.preventDefault) {
+            e.preventDefault(); // Necessary. Allows us to drop.
+        }
+
+        audioEngineContext.audioObjects.forEach(function (ao) {
+            ao.interfaceDOM.processMovement();
+        });
+
+        return false;
+    }
+
+    function dragEnd(e) {
+        // this/e.target is the source node.
+        $(".ordinal-element").removeClass("dragging");
+        $(".ordinal-element").removeClass("over");
+    }
+
+    this.getElementPosition = function () {
+        var elements = container.childNodes,
+            position = 0,
+            elem = elements[0];
+        while (root !== elem) {
+            position++;
+            elem = elem.nextElementSibling;
+        }
+        return position;
+    };
+
+    this.processMovement = function () {
+        var time = audioEngineContext.timer.getTestTime();
+        var pos = this.getElementPosition();
+        var rank = pos / (audioEngineContext.audioObjects.length - 1);
+        audioObject.metric.moved(time, rank);
+        console.log('slider ' + audioObject.id + ' moved to ' + rank + ' (' + time + ')');
+    };
+
+    this.enable = function () {
+        // This is used to tell the interface object that playback of this node is ready
+        root.classList.remove("disabled");
+        labelElement.textContent = label;
+    };
+    this.updateLoading = function (progress) {
+        // progress is a value from 0 to 100 indicating the current download state of media files
+        labelElement.textContent = String(progress);
+    };
+    this.startPlayback = function () {
+        // Called when playback has begun
+        root.classList.add("playing");
+        if (audioObject.commentDOM) {
+            audioObject.commentDOM.trackComment.classList.add("comment-box-playing");
+        }
+    };
+    this.stopPlayback = function () {
+        // Called when playback has stopped. This gets called even if playback never started!
+        root.classList.remove("playing");
+        playing = false;
+        if (audioObject.commentDOM) {
+            audioObject.commentDOM.trackComment.classList.remove("comment-box-playing");
+        }
+    };
+    this.getValue = function () {
+        // Return the current value of the object. If there is no value, return 0
+        var pos = this.getElementPosition();
+        var rank = pos / (audioEngineContext.audioObjects.length - 1);
+    };
+    this.getPresentedId = function () {
+        // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
+        return label;
+    };
+    this.canMove = function () {
+        // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale.
+        // These are checked primarily if the interface check option 'fragmentMoved' is enabled.
+        return true;
+    };
+    this.exportXMLDOM = function (audioObject) {
+        // Called by the audioObject holding this element to export the interface <value> node.
+        // If there is no value node (such as outside reference), return null
+        // If there are multiple value nodes (such as multiple scale / 2D scales), return an array of nodes with each value node having an 'interfaceName' attribute
+        // Use storage.document.createElement('value'); to generate the XML node.
+        var node = storage.document.createElement('value');
+        node.textContent = this.getValue();
+        return node;
+
+    };
+    this.error = function () {
+        // If there is an error with the audioObject, this will be called to indicate a failure
+        root.classList.remove("disabled");
+        labelElement.textContent = "Error";
+    };
+    Object.defineProperties(this, {
+        "root": {
+            "get": function () {
+                return root;
+            },
+            "set": function () {}
+        }
+    });
+}
+
+function resizeWindow(event) {
+    // Called on every window resize event, use this to scale your page properly
+    var w = $("#slider").width();
+    var N = audioEngineContext.audioObjects.length;
+    w /= N;
+    w -= 14;
+    w = Math.floor(w);
+    $(".ordinal-element").width(w);
+}
+
+function buttonSubmitClick() // TODO: Only when all songs have been played!
+{
+    var checks = testState.currentStateMap.interfaces[0].options,
+        canContinue = true;
+
+    // Check that the anchor and reference objects are correctly placed
+    if (interfaceContext.checkHiddenAnchor() === false) {
+        return;
+    }
+    if (interfaceContext.checkHiddenReference() === false) {
+        return;
+    }
+
+    for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
+        if (checks[i].type == 'check') {
+            switch (checks[i].name) {
+                case 'fragmentPlayed':
+                    // Check if all fragments have been played
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
+                    break;
+                case 'fragmentFullPlayback':
+                    // Check all fragments have been played to their full length
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
+                    console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
+                    break;
+                case 'fragmentMoved':
+                    // Check all fragment sliders have been moved.
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
+                    break;
+                case 'fragmentComments':
+                    // Check all fragment sliders have been moved.
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
+                    break;
+                case 'scalerange':
+                    // Check the scale has been used effectively
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
+
+                    break;
+                default:
+                    console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
+                    break;
+            }
+        }
+        if (checkState === false) {
+            canContinue = false;
+            break;
+        }
+    }
+
+    if (canContinue) {
+        if (audioEngineContext.status == 1) {
+            var playback = document.getElementById('playback-button');
+            playback.click();
+            // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
+        } else {
+            if (audioEngineContext.timer.testStarted === false) {
+                interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!');
+                return;
+            }
+        }
+        testState.advanceState();
+    }
+}
+
+function pageXMLSave(store, pageSpecification) {
+    // MANDATORY
+    // Saves a specific test page
+    // You can use this space to add any extra nodes to your XML <audioHolder> saves
+    // Get the current <page> information in store (remember to appendChild your data to it)
+    // pageSpecification is the current page node configuration
+    // To create new XML nodes, use storage.document.createElement();
+}
--- a/interfaces/timeline.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/timeline.css	Fri Jul 14 15:39:24 2017 +0100
@@ -3,17 +3,16 @@
     justify-content: center;
 }
 div.timeline-element-content {
-    max-width: 800px;
+    width: 90%;
     min-width: 200px;
     border: 1px solid black;
     margin: 10px 0px;
     padding: 20px;
 }
 div.timeline-element-canvas-holder {
-    display: flex;
     width: inherit;
     height: 160px;
-    margin-left: 50px;
+    margin: auto;
 }
 canvas.canvas-layer1 {
     position: absolute;
@@ -36,10 +35,12 @@
 canvas.canvas-disabled {
     background-color: gray;
 }
-div.timeline-element-comment-holder {
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
+div.timeline-element-comment-holder {}
+img.timeline-element-image {
+    height: 150px;
+    max-width: 20%;
+    display: inline-block;
+    float: right;
 }
 div.comment-entry {
     border: 1px solid #444444;
@@ -49,14 +50,22 @@
     padding: 5px;
     height: 80px;
     border-radius: 10px;
-    display: flex;
-    flex-direction: column;
+    display: inline-block;
+    text-align: center;
 }
 div.comment-entry-header {
-    display: flex;
-    justify-content: space-between;
+    display: table;
+    text-align: unset;
+    width: 100%;
+}
+div.comment-entry-header span {
+    float: left;
+}
+div.comment-entry-header button {
+    float: right;
 }
 textarea.comment-entry-text {
     resize: none;
     margin: auto;
+    width: 90%;
 }
--- a/interfaces/timeline.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/interfaces/timeline.js	Fri Jul 14 15:39:24 2017 +0100
@@ -2,7 +2,7 @@
  * WAET Timeline
  * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing.
  */
-
+/*globals interfaceContext, window, document, console, audioEngineContext, testState, $, storage */
 // Once this is loaded and parsed, begin execution
 loadInterface();
 
@@ -77,7 +77,7 @@
     // Load the full interface
     testState.initialise();
     testState.advanceState();
-};
+}
 
 function loadTest(page) {
     // Called each time a new test page is to be build. The page specification node is the only item passed in
@@ -94,16 +94,21 @@
         document.getElementById("test-title").textContent = page.title;
     }
 
-    if (interfaceObj.title != null) {
+    if (interfaceObj.title !== null) {
         document.getElementById("page-title").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined) {
+        document.getElementById("timeline-test-content").parentElement.insertBefore(interfaceContext.imageHolder.root, document.getElementById("timeline-test-content"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     var outsideReferenceHolder = document.getElementById("outside-reference-holder");
     outsideReferenceHolder.innerHTML = "";
 
     var commentBoxPrefix = "Comment on track";
-    if (interfaceObj.commentBoxPrefix != undefined) {
+    if (interfaceObj.commentBoxPrefix !== undefined) {
         commentBoxPrefix = interfaceObj.commentBoxPrefix;
     }
     var index = 0;
@@ -116,7 +121,7 @@
         var audioObject = audioEngineContext.newTrack(element);
         if (page.audioElements.type == 'outside-reference') {
             var refNode = interfaceContext.outsideReferenceDOM(audioObject, index, outsideReferenceHolder);
-            audioObject.bindInterface(orNode);
+            audioObject.bindInterface(refNode);
         } else {
             var label = interfaceContext.getLabel(labelType, index, page.labelStart);
             var node = new interfaceObject(audioObject, label);
@@ -169,7 +174,7 @@
             var titleHolder = document.createElement("div");
             titleHolder.className = "comment-entry-header";
             this.title = document.createElement("span");
-            if (str != undefined) {
+            if (str !== undefined) {
                 this.title.textContent = str;
             } else {
                 this.title.textContent = "Time: " + time.toFixed(2) + "s";
@@ -186,7 +191,7 @@
                 handleEvent: function () {
                     this.parent.parent.deleteComment(this.parent);
                 }
-            }
+            };
             this.clear.DOM.textContent = "Delete";
             this.clear.DOM.addEventListener("click", this.clear);
             titleHolder.appendChild(this.clear.DOM);
@@ -199,7 +204,7 @@
                 elem_w = Math.max(elem_w, 190);
                 this.DOM.style.width = elem_w + "px";
                 this.textarea.style.width = (elem_w - 5) + "px";
-            }
+            };
             this.buildXML = function (root) {
                 //storage.document.createElement();
                 var node = storage.document.createElement("comment");
@@ -211,7 +216,7 @@
                 node.appendChild(question);
                 node.appendChild(comment);
                 root.appendChild(node);
-            }
+            };
             this.resize();
         },
         newComment: function (time) {
@@ -240,7 +245,7 @@
                 this.deleteComment(this.list[0]);
             }
         }
-    }
+    };
 
     this.canvas = {
         parent: this,
@@ -249,7 +254,8 @@
         layer2: document.createElement("canvas"),
         layer3: document.createElement("canvas"),
         layer4: document.createElement("canvas"),
-        resize: function (w) {
+        resize: function () {
+            var w = $(this.layer1.parentElement).width();
             this.layer1.width = w;
             this.layer2.width = w;
             this.layer3.width = w;
@@ -283,7 +289,7 @@
             }
         },
         drawWaveform: function () {
-            if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
+            if (this.parent.parent === undefined || this.parent.parent.buffer === undefined) {
                 return;
             }
             var buffer = this.parent.parent.buffer.buffer;
@@ -342,7 +348,7 @@
             context.stroke();
         },
         drawMarkers: function () {
-            if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
+            if (this.parent.parent === undefined || this.parent.parent.buffer === undefined) {
                 return;
             }
             var context = this.layer3.getContext("2d");
@@ -362,23 +368,35 @@
             var context = canvas.getContext("2d");
             context.clearRect(0, 0, canvas.width, canvas.height);
         }
-    }
+    };
     this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled";
     this.canvas.layer2.className = "timeline-element-canvas canvas-layer2";
     this.canvas.layer3.className = "timeline-element-canvas canvas-layer3";
     this.canvas.layer4.className = "timeline-element-canvas canvas-layer3";
-    this.canvas.layer1.height = "150";
-    this.canvas.layer2.height = "150";
-    this.canvas.layer3.height = "150";
-    this.canvas.layer4.height = "150";
-    canvasHolder.appendChild(this.canvas.layer1);
-    canvasHolder.appendChild(this.canvas.layer2);
-    canvasHolder.appendChild(this.canvas.layer3);
-    canvasHolder.appendChild(this.canvas.layer4);
+    this.canvas.layer1.height = "160";
+    this.canvas.layer2.height = "160";
+    this.canvas.layer3.height = "160";
+    this.canvas.layer4.height = "160";
+    var canvasDiv = document.createElement("div");
+    canvasDiv.appendChild(this.canvas.layer1);
+    canvasDiv.appendChild(this.canvas.layer2);
+    canvasDiv.appendChild(this.canvas.layer3);
+    canvasDiv.appendChild(this.canvas.layer4);
+    canvasHolder.appendChild(canvasDiv);
     this.canvas.layer1.addEventListener("mousemove", this.canvas);
     this.canvas.layer1.addEventListener("mouseleave", this.canvas);
     this.canvas.layer1.addEventListener("click", this.canvas);
 
+    if (audioObject.specification.image) {
+        canvasDiv.style.width = "80%";
+        var image = document.createElement("img");
+        image.src = audioObject.specification.image;
+        image.className = "timeline-element-image";
+        canvasHolder.appendChild(image);
+    } else {
+        canvasDiv.style.width = "100%";
+    }
+
     var canvasIntervalID = null;
 
     this.playButton = {
@@ -393,7 +411,7 @@
                 audioEngineContext.stop();
             }
         }
-    }
+    };
     this.playButton.DOM.addEventListener("click", this.playButton);
     this.playButton.DOM.className = "timeline-button timeline-button-disabled";
     this.playButton.DOM.disabled = true;
@@ -402,13 +420,8 @@
     buttonHolder.appendChild(this.playButton.DOM);
 
     this.resize = function () {
-        var w = window.innerWidth;
-        w = Math.min(w, 800);
-        w = Math.max(w, 200);
-        root.style.width = w + "px";
-        var c_w = w - 100;
-        this.canvas.resize(c_w);
-    }
+        this.canvas.resize();
+    };
 
     this.enable = function () {
         // This is used to tell the interface object that playback of this node is ready
@@ -428,14 +441,27 @@
     };
     this.startPlayback = function () {
         // Called when playback has begun
-        canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100);
+        var animate = function () {
+            this.canvas.drawTicker.call(this.canvas);
+            if (this.playButton.DOM.textContent == "Stop") {
+                window.requestAnimationFrame(animate);
+            }
+        }.bind(this);
         this.playButton.DOM.textContent = "Stop";
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
+        canvasIntervalID = window.requestAnimationFrame(animate);
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
         window.clearInterval(canvasIntervalID);
         this.canvas.clearCanvas(this.canvas.layer2);
         this.playButton.DOM.textContent = "Play";
+        var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+            return a.id === audioObject.id;
+        });
+        if (box) {
+            box.highlight(false);
+        }
     };
     this.getValue = function () {
         // Return the current value of the object. If there is no value, return 0
@@ -459,8 +485,8 @@
     };
     this.error = function () {
         // If there is an error with the audioObject, this will be called to indicate a failure
-    }
-};
+    };
+}
 
 function resizeWindow(event) {
     // Called on every window resize event, use this to scale your page properly
@@ -470,32 +496,35 @@
 }
 
 function buttonSubmitClick() {
-    if (audioEngineContext.timer.testStarted == false) {
+    if (audioEngineContext.timer.testStarted === false) {
         interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!');
         return;
     }
-    var checks = testState.currentStateMap.interfaces[0],
+    var checks = testState.currentStateMap.interfaces[0].options,
         canContinue = true;
+    if (interfaceContext.checkFragmentMinPlays() === false) {
+        return;
+    }
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     //Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     //Check if all fragments have played to their full length
-                    checkState = interfaceContext.checkFragmentsFullyPlayed();
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
                     break;
             }
-            if (checkState == false) {
+            if (checkState === false) {
                 canContinue = false;
             }
         }
--- a/js/WAVE.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/js/WAVE.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,6 +1,7 @@
 // Decode and perform WAVE file byte level manipulation
+/*globals console, Uint8Array, Float32Array, Float64Array */
 
-find_subarray = function (arr, subarr) {
+function find_subarray(arr, subarr) {
     var arr_length = arr.length;
     var subarr_length = subarr.length;
     var last_check_index = arr_length - subarr_length;
@@ -15,7 +16,7 @@
             return i;
         }
     return -1;
-};
+}
 
 function convertToInteger(arr) {
     var value = 0;
@@ -35,24 +36,24 @@
 
 function WAVE() {
     // The WAVE file object
-    this.status == 'WAVE_DECLARED'
+    this.status = 'WAVE_DECLARED';
 
     this.decoded_data = null;
 
     this.RIFF = String(); //ChunkID
-    this.size; //ChunkSize
-    this.FT_Header; //Format
-    this.fmt_marker; //Subchunk1ID
-    this.formatDataLength; //Subchunk1Size
-    this.type; //AudioFormat
-    this.num_channels; //NumChannels
-    this.sample_rate; //SampleRate
-    this.byte_rate; //ByteRate
-    this.block_align; //BlockAlign
-    this.bits_per_sample; //BitsPerSample
-    this.data_header; //Subchunk2ID
-    this.data_size; //Subchunk2Size
-    this.num_samples;
+    this.size = undefined; //ChunkSize
+    this.FT_Header = undefined; //Format
+    this.fmt_marker = undefined; //Subchunk1ID
+    this.formatDataLength = undefined; //Subchunk1Size
+    this.type = undefined; //AudioFormat
+    this.num_channels = undefined; //NumChannels
+    this.sample_rate = undefined; //SampleRate
+    this.byte_rate = undefined; //ByteRate
+    this.block_align = undefined; //BlockAlign
+    this.bits_per_sample = undefined; //BitsPerSample
+    this.data_header = undefined; //Subchunk2ID
+    this.data_size = undefined; //Subchunk2Size
+    this.num_samples = undefined;
 
     this.open = function (IOArrayBuffer) {
         var IOView8 = new Uint8Array(IOArrayBuffer);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/js/angular.min.js	Fri Jul 14 15:39:24 2017 +0100
@@ -0,0 +1,332 @@
+/*
+ AngularJS v1.6.4
+ (c) 2010-2017 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(x){'use strict';function L(a,b){b=b||Error;return function(){var d=arguments[0],c;c="["+(a?a+":":"")+d+"] http://errors.angularjs.org/1.6.4/"+(a?a+"/":"")+d;for(d=1;d<arguments.length;d++){c=c+(1==d?"?":"&")+"p"+(d-1)+"=";var e=encodeURIComponent,f;f=arguments[d];f="function"==typeof f?f.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof f?"undefined":"string"!=typeof f?JSON.stringify(f):f;c+=e(f)}return new b(c)}}function me(a){if(C(a))u(a.objectMaxDepth)&&(Ic.objectMaxDepth=Sb(a.objectMaxDepth)?
+a.objectMaxDepth:NaN);else return Ic}function Sb(a){return ba(a)&&0<a}function qa(a){if(null==a||Wa(a))return!1;if(H(a)||F(a)||B&&a instanceof B)return!0;var b="length"in Object(a)&&a.length;return ba(b)&&(0<=b&&(b-1 in a||a instanceof Array)||"function"===typeof a.item)}function q(a,b,d){var c,e;if(a)if(D(a))for(c in a)"prototype"!==c&&"length"!==c&&"name"!==c&&a.hasOwnProperty(c)&&b.call(d,a[c],c,a);else if(H(a)||qa(a)){var f="object"!==typeof a;c=0;for(e=a.length;c<e;c++)(f||c in a)&&b.call(d,
+a[c],c,a)}else if(a.forEach&&a.forEach!==q)a.forEach(b,d,a);else if(Jc(a))for(c in a)b.call(d,a[c],c,a);else if("function"===typeof a.hasOwnProperty)for(c in a)a.hasOwnProperty(c)&&b.call(d,a[c],c,a);else for(c in a)ua.call(a,c)&&b.call(d,a[c],c,a);return a}function Kc(a,b,d){for(var c=Object.keys(a).sort(),e=0;e<c.length;e++)b.call(d,a[c[e]],c[e]);return c}function Lc(a){return function(b,d){a(d,b)}}function ne(){return++qb}function Tb(a,b,d){for(var c=a.$$hashKey,e=0,f=b.length;e<f;++e){var g=b[e];
+if(C(g)||D(g))for(var h=Object.keys(g),k=0,l=h.length;k<l;k++){var m=h[k],n=g[m];d&&C(n)?ga(n)?a[m]=new Date(n.valueOf()):Xa(n)?a[m]=new RegExp(n):n.nodeName?a[m]=n.cloneNode(!0):Ub(n)?a[m]=n.clone():(C(a[m])||(a[m]=H(n)?[]:{}),Tb(a[m],[n],!0)):a[m]=n}}c?a.$$hashKey=c:delete a.$$hashKey;return a}function S(a){return Tb(a,va.call(arguments,1),!1)}function oe(a){return Tb(a,va.call(arguments,1),!0)}function Z(a){return parseInt(a,10)}function Vb(a,b){return S(Object.create(a),b)}function z(){}function Ya(a){return a}
+function la(a){return function(){return a}}function Wb(a){return D(a.toString)&&a.toString!==ma}function w(a){return"undefined"===typeof a}function u(a){return"undefined"!==typeof a}function C(a){return null!==a&&"object"===typeof a}function Jc(a){return null!==a&&"object"===typeof a&&!Mc(a)}function F(a){return"string"===typeof a}function ba(a){return"number"===typeof a}function ga(a){return"[object Date]"===ma.call(a)}function D(a){return"function"===typeof a}function Xa(a){return"[object RegExp]"===
+ma.call(a)}function Wa(a){return a&&a.window===a}function Za(a){return a&&a.$evalAsync&&a.$watch}function Ha(a){return"boolean"===typeof a}function pe(a){return a&&ba(a.length)&&qe.test(ma.call(a))}function Ub(a){return!(!a||!(a.nodeName||a.prop&&a.attr&&a.find))}function re(a){var b={};a=a.split(",");var d;for(d=0;d<a.length;d++)b[a[d]]=!0;return b}function wa(a){return Q(a.nodeName||a[0]&&a[0].nodeName)}function $a(a,b){var d=a.indexOf(b);0<=d&&a.splice(d,1);return d}function ra(a,b,d){function c(a,
+b,c){c--;if(0>c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f<g;f++)b.push(e(a[f],c))}else if(Jc(a))for(f in a)b[f]=e(a[f],c);else if(a&&"function"===typeof a.hasOwnProperty)for(f in a)a.hasOwnProperty(f)&&(b[f]=e(a[f],c));else for(f in a)ua.call(a,f)&&(b[f]=e(a[f],c));d?b.$$hashKey=d:delete b.$$hashKey;return b}function e(a,b){if(!C(a))return a;var d=g.indexOf(a);if(-1!==d)return h[d];if(Wa(a)||Za(a))throw Fa("cpws");var d=!1,e=f(a);void 0===e&&(e=H(a)?[]:Object.create(Mc(a)),
+d=!0);g.push(a);h.push(e);return d?c(a,e,b):e}function f(a){switch(ma.call(a)){case "[object Int8Array]":case "[object Int16Array]":case "[object Int32Array]":case "[object Float32Array]":case "[object Float64Array]":case "[object Uint8Array]":case "[object Uint8ClampedArray]":case "[object Uint16Array]":case "[object Uint32Array]":return new a.constructor(e(a.buffer),a.byteOffset,a.length);case "[object ArrayBuffer]":if(!a.slice){var b=new ArrayBuffer(a.byteLength);(new Uint8Array(b)).set(new Uint8Array(a));
+return b}return a.slice(0);case "[object Boolean]":case "[object Number]":case "[object String]":case "[object Date]":return new a.constructor(a.valueOf());case "[object RegExp]":return b=new RegExp(a.source,a.toString().match(/[^/]*$/)[0]),b.lastIndex=a.lastIndex,b;case "[object Blob]":return new a.constructor([a],{type:a.type})}if(D(a.cloneNode))return a.cloneNode(!0)}var g=[],h=[];d=Sb(d)?d:NaN;if(b){if(pe(b)||"[object ArrayBuffer]"===ma.call(b))throw Fa("cpta");if(a===b)throw Fa("cpi");H(b)?b.length=
+0:q(b,function(a,c){"$$hashKey"!==c&&delete b[c]});g.push(a);h.push(b);return c(a,b,d)}return e(a,d)}function Xb(a,b){return a===b||a!==a&&b!==b}function sa(a,b){if(a===b)return!0;if(null===a||null===b)return!1;if(a!==a&&b!==b)return!0;var d=typeof a,c;if(d===typeof b&&"object"===d)if(H(a)){if(!H(b))return!1;if((d=a.length)===b.length){for(c=0;c<d;c++)if(!sa(a[c],b[c]))return!1;return!0}}else{if(ga(a))return ga(b)?Xb(a.getTime(),b.getTime()):!1;if(Xa(a))return Xa(b)?a.toString()===b.toString():!1;
+if(Za(a)||Za(b)||Wa(a)||Wa(b)||H(b)||ga(b)||Xa(b))return!1;d=V();for(c in a)if("$"!==c.charAt(0)&&!D(a[c])){if(!sa(a[c],b[c]))return!1;d[c]=!0}for(c in b)if(!(c in d)&&"$"!==c.charAt(0)&&u(b[c])&&!D(b[c]))return!1;return!0}return!1}function ab(a,b,d){return a.concat(va.call(b,d))}function bb(a,b){var d=2<arguments.length?va.call(arguments,2):[];return!D(b)||b instanceof RegExp?b:d.length?function(){return arguments.length?b.apply(a,ab(d,arguments,0)):b.apply(a,d)}:function(){return arguments.length?
+b.apply(a,arguments):b.call(a)}}function Nc(a,b){var d=b;"string"===typeof a&&"$"===a.charAt(0)&&"$"===a.charAt(1)?d=void 0:Wa(b)?d="$WINDOW":b&&x.document===b?d="$DOCUMENT":Za(b)&&(d="$SCOPE");return d}function cb(a,b){if(!w(a))return ba(b)||(b=b?2:null),JSON.stringify(a,Nc,b)}function Oc(a){return F(a)?JSON.parse(a):a}function Pc(a,b){a=a.replace(se,"");var d=Date.parse("Jan 01, 1970 00:00:00 "+a)/6E4;return da(d)?b:d}function Yb(a,b,d){d=d?-1:1;var c=a.getTimezoneOffset();b=Pc(b,c);d*=b-c;a=new Date(a.getTime());
+a.setMinutes(a.getMinutes()+d);return a}function xa(a){a=B(a).clone();try{a.empty()}catch(b){}var d=B("<div>").append(a).html();try{return a[0].nodeType===Ia?Q(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+Q(b)})}catch(c){return Q(d)}}function Qc(a){try{return decodeURIComponent(a)}catch(b){}}function Rc(a){var b={};q((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Qc(e),u(e)&&(f=
+u(f)?Qc(f):!0,ua.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Zb(a){var b=[];q(a,function(a,c){H(a)?q(a,function(a){b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))}):b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))});return b.length?b.join("&"):""}function db(a){return $(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function $(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,
+b?"%20":"+")}function te(a,b){var d,c,e=Ja.length;for(c=0;c<e;++c)if(d=Ja[c]+b,F(d=a.getAttribute(d)))return d;return null}function ue(a,b){var d,c,e={};q(Ja,function(b){b+="app";!d&&a.hasAttribute&&a.hasAttribute(b)&&(d=a,c=a.getAttribute(b))});q(Ja,function(b){b+="app";var e;!d&&(e=a.querySelector("["+b.replace(":","\\:")+"]"))&&(d=e,c=e.getAttribute(b))});d&&(ve?(e.strictDi=null!==te(d,"strict-di"),b(d,c?[c]:[],e)):x.console.error("Angular: disabling automatic bootstrap. <script> protocol indicates an extension, document.location.href does not match."))}
+function Sc(a,b,d){C(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=B(a);if(a.injector()){var c=a[0]===x.document?"document":xa(a);throw Fa("btstrpd",c.replace(/</,"&lt;").replace(/>/,"&gt;"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",
+d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;x&&e.test(x.name)&&(d.debugInfoEnabled=!0,x.name=x.name.replace(e,""));if(x&&!f.test(x.name))return c();x.name=x.name.replace(f,"");ea.resumeBootstrap=function(a){q(a,function(a){b.push(a)});return c()};D(ea.resumeDeferredBootstrap)&&ea.resumeDeferredBootstrap()}function we(){x.name="NG_ENABLE_DEBUG_INFO!"+x.name;x.location.reload()}function xe(a){a=ea.element(a).injector();if(!a)throw Fa("test");return a.get("$$testability")}
+function Tc(a,b){b=b||"_";return a.replace(ye,function(a,c){return(c?b:"")+a.toLowerCase()})}function ze(){var a;if(!Uc){var b=rb();(na=w(b)?x.jQuery:b?x[b]:void 0)&&na.fn.on?(B=na,S(na.fn,{scope:Na.scope,isolateScope:Na.isolateScope,controller:Na.controller,injector:Na.injector,inheritedData:Na.inheritedData}),a=na.cleanData,na.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=na._data(f,"events"))&&c.$destroy&&na(f).triggerHandler("$destroy");a(b)}):B=W;ea.element=B;Uc=!0}}function fb(a,
+b,d){if(!a)throw Fa("areq",b||"?",d||"required");return a}function sb(a,b,d){d&&H(a)&&(a=a[a.length-1]);fb(D(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ka(a,b){if("hasOwnProperty"===a)throw Fa("badname",b);}function Vc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g<f;g++)c=b[g],a&&(a=(e=a)[c]);return!d&&D(a)?bb(e,a):a}function tb(a){for(var b=a[0],d=a[a.length-1],c,e=1;b!==d&&(b=b.nextSibling);e++)if(c||a[e]!==
+b)c||(c=B(va.call(a,0,e))),c.push(b);return c||a}function V(){return Object.create(null)}function $b(a){if(null==a)return"";switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=!Wb(a)||H(a)||ga(a)?cb(a):a.toString()}return a}function Ae(a){function b(a,b,c){return a[b]||(a[b]=c())}var d=L("$injector"),c=L("ng");a=b(a,"angular",Object);a.$$minErr=a.$$minErr||L;return b(a,"module",function(){var a={};return function(f,g,h){var k={};if("hasOwnProperty"===f)throw c("badname","module");
+g&&a.hasOwnProperty(f)&&(a[f]=null);return b(a,f,function(){function a(b,c,d,f){f||(f=e);return function(){f[d||"push"]([b,c,arguments]);return v}}function b(a,c,d){d||(d=e);return function(b,e){e&&D(e)&&(e.$$moduleName=f);d.push([a,c,arguments]);return v}}if(!g)throw d("nomod",f);var e=[],p=[],r=[],J=a("$injector","invoke","push",p),v={_invokeQueue:e,_configBlocks:p,_runBlocks:r,info:function(a){if(u(a)){if(!C(a))throw c("aobj","value");k=a;return this}return k},requires:g,name:f,provider:b("$provide",
+"provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide","decorator",p),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),component:b("$compileProvider","component"),config:J,run:function(a){r.push(a);return this}};h&&J(h);return v})}})}function pa(a,b){if(H(a)){b=
+b||[];for(var d=0,c=a.length;d<c;d++)b[d]=a[d]}else if(C(a))for(d in b=b||{},a)if("$"!==d.charAt(0)||"$"!==d.charAt(1))b[d]=a[d];return b||a}function Be(a,b){var d=[];Sb(b)&&(a=ra(a,null,b));return JSON.stringify(a,function(a,b){b=Nc(a,b);if(C(b)){if(0<=d.indexOf(b))return"...";d.push(b)}return b})}function Ce(a){S(a,{errorHandlingConfig:me,bootstrap:Sc,copy:ra,extend:S,merge:oe,equals:sa,element:B,forEach:q,injector:eb,noop:z,bind:bb,toJson:cb,fromJson:Oc,identity:Ya,isUndefined:w,isDefined:u,isString:F,
+isFunction:D,isObject:C,isNumber:ba,isElement:Ub,isArray:H,version:De,isDate:ga,lowercase:Q,uppercase:ub,callbacks:{$$counter:0},getTestability:xe,reloadWithDebugInfo:we,$$minErr:L,$$csp:Ga,$$encodeUriSegment:db,$$encodeUriQuery:$,$$stringify:$b});ac=Ae(x);ac("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Ee});a.provider("$compile",Wc).directive({a:Fe,input:Xc,textarea:Xc,form:Ge,script:He,select:Ie,option:Je,ngBind:Ke,ngBindHtml:Le,ngBindTemplate:Me,ngClass:Ne,ngClassEven:Oe,
+ngClassOdd:Pe,ngCloak:Qe,ngController:Re,ngForm:Se,ngHide:Te,ngIf:Ue,ngInclude:Ve,ngInit:We,ngNonBindable:Xe,ngPluralize:Ye,ngRepeat:Ze,ngShow:$e,ngStyle:af,ngSwitch:bf,ngSwitchWhen:cf,ngSwitchDefault:df,ngOptions:ef,ngTransclude:ff,ngModel:gf,ngList:hf,ngChange:jf,pattern:Yc,ngPattern:Yc,required:Zc,ngRequired:Zc,minlength:$c,ngMinlength:$c,maxlength:ad,ngMaxlength:ad,ngValue:kf,ngModelOptions:lf}).directive({ngInclude:mf}).directive(vb).directive(bd);a.provider({$anchorScroll:nf,$animate:of,$animateCss:pf,
+$$animateJs:qf,$$animateQueue:rf,$$AnimateRunner:sf,$$animateAsyncRun:tf,$browser:uf,$cacheFactory:vf,$controller:wf,$document:xf,$$isDocumentHidden:yf,$exceptionHandler:zf,$filter:cd,$$forceReflow:Af,$interpolate:Bf,$interval:Cf,$http:Df,$httpParamSerializer:Ef,$httpParamSerializerJQLike:Ff,$httpBackend:Gf,$xhrFactory:Hf,$jsonpCallbacks:If,$location:Jf,$log:Kf,$parse:Lf,$rootScope:Mf,$q:Nf,$$q:Of,$sce:Pf,$sceDelegate:Qf,$sniffer:Rf,$templateCache:Sf,$templateRequest:Tf,$$testability:Uf,$timeout:Vf,
+$window:Wf,$$rAF:Xf,$$jqLite:Yf,$$Map:Zf,$$cookieReader:$f})}]).info({angularVersion:"1.6.4"})}function gb(a,b){return b.toUpperCase()}function wb(a){return a.replace(ag,gb)}function bc(a){a=a.nodeType;return 1===a||!a||9===a}function dd(a,b){var d,c,e=b.createDocumentFragment(),f=[];if(cc.test(a)){d=e.appendChild(b.createElement("div"));c=(bg.exec(a)||["",""])[1].toLowerCase();c=ha[c]||ha._default;d.innerHTML=c[1]+a.replace(cg,"<$1></$2>")+c[2];for(c=c[0];c--;)d=d.lastChild;f=ab(f,d.childNodes);
+d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";q(f,function(a){e.appendChild(a)});return e}function W(a){if(a instanceof W)return a;var b;F(a)&&(a=T(a),b=!0);if(!(this instanceof W)){if(b&&"<"!==a.charAt(0))throw dc("nosel");return new W(a)}if(b){b=x.document;var d;a=(d=dg.exec(a))?[b.createElement(d[1])]:(d=dd(a,b))?d.childNodes:[];ec(this,a)}else D(a)?ed(a):ec(this,a)}function fc(a){return a.cloneNode(!0)}function xb(a,b){!b&&bc(a)&&B.cleanData([a]);
+a.querySelectorAll&&B.cleanData(a.querySelectorAll("*"))}function fd(a,b,d,c){if(u(c))throw dc("offargs");var e=(c=yb(a))&&c.events,f=c&&c.handle;if(f)if(b){var g=function(b){var c=e[b];u(d)&&$a(c||[],d);u(d)&&c&&0<c.length||(a.removeEventListener(b,f),delete e[b])};q(b.split(" "),function(a){g(a);zb[a]&&g(zb[a])})}else for(b in e)"$destroy"!==b&&a.removeEventListener(b,f),delete e[b]}function gc(a,b){var d=a.ng339,c=d&&hb[d];c&&(b?delete c.data[b]:(c.handle&&(c.events.$destroy&&c.handle({},"$destroy"),
+fd(a)),delete hb[d],a.ng339=void 0))}function yb(a,b){var d=a.ng339,d=d&&hb[d];b&&!d&&(a.ng339=d=++eg,d=hb[d]={events:{},data:{},handle:void 0});return d}function hc(a,b,d){if(bc(a)){var c,e=u(d),f=!e&&b&&!C(b),g=!b;a=(a=yb(a,!f))&&a.data;if(e)a[wb(b)]=d;else{if(g)return a;if(f)return a&&a[wb(b)];for(c in b)a[wb(c)]=b[c]}}}function Ab(a,b){return a.getAttribute?-1<(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+b+" "):!1}function Bb(a,b){b&&a.setAttribute&&q(b.split(" "),
+function(b){a.setAttribute("class",T((" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+T(b)+" "," ")))})}function Cb(a,b){if(b&&a.setAttribute){var d=(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");q(b.split(" "),function(a){a=T(a);-1===d.indexOf(" "+a+" ")&&(d+=a+" ")});a.setAttribute("class",T(d))}}function ec(a,b){if(b)if(b.nodeType)a[a.length++]=b;else{var d=b.length;if("number"===typeof d&&b.window!==b){if(d)for(var c=0;c<d;c++)a[a.length++]=b[c]}else a[a.length++]=
+b}}function gd(a,b){return Db(a,"$"+(b||"ngController")+"Controller")}function Db(a,b,d){9===a.nodeType&&(a=a.documentElement);for(b=H(b)?b:[b];a;){for(var c=0,e=b.length;c<e;c++)if(u(d=B.data(a,b[c])))return d;a=a.parentNode||11===a.nodeType&&a.host}}function hd(a){for(xb(a,!0);a.firstChild;)a.removeChild(a.firstChild)}function Eb(a,b){b||xb(a);var d=a.parentNode;d&&d.removeChild(a)}function fg(a,b){b=b||x;if("complete"===b.document.readyState)b.setTimeout(a);else B(b).on("load",a)}function ed(a){function b(){x.document.removeEventListener("DOMContentLoaded",
+b);x.removeEventListener("load",b);a()}"complete"===x.document.readyState?x.setTimeout(a):(x.document.addEventListener("DOMContentLoaded",b),x.addEventListener("load",b))}function id(a,b){var d=Fb[b.toLowerCase()];return d&&jd[wa(a)]&&d}function gg(a,b){var d=function(c,d){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=b[d||c.type],g=f?f.length:0;if(g){if(w(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=
+!0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};var k=f.specialHandlerWrapper||hg;1<g&&(f=pa(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||k(a,c,f[l])}};d.elem=a;return d}function hg(a,b,d){d.call(a,b)}function ig(a,b,d){var c=b.relatedTarget;c&&(c===a||jg.call(a,c))||d.call(a,b)}function Yf(){this.$get=function(){return S(W,{hasClass:function(a,b){a.attr&&(a=a[0]);return Ab(a,b)},addClass:function(a,
+b){a.attr&&(a=a[0]);return Cb(a,b)},removeClass:function(a,b){a.attr&&(a=a[0]);return Bb(a,b)}})}}function Pa(a,b){var d=a&&a.$$hashKey;if(d)return"function"===typeof d&&(d=a.$$hashKey()),d;d=typeof a;return d="function"===d||"object"===d&&null!==a?a.$$hashKey=d+":"+(b||ne)():d+":"+a}function kd(){this._keys=[];this._values=[];this._lastKey=NaN;this._lastIndex=-1}function ld(a){a=Function.prototype.toString.call(a).replace(kg,"");return a.match(lg)||a.match(mg)}function ng(a){return(a=ld(a))?"function("+
+(a[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function eb(a,b){function d(a){return function(b,c){if(C(b))q(b,Lc(a));else return a(b,c)}}function c(a,b){Ka(a,"service");if(D(b)||H(b))b=p.instantiate(b);if(!b.$get)throw ya("pget",a);return n[a+"Provider"]=b}function e(a,b){return function(){var c=v.invoke(b,this);if(w(c))throw ya("undef",a);return c}}function f(a,b,d){return c(a,{$get:!1!==d?e(a,b):b})}function g(a){fb(w(a)||H(a),"modulesToLoad","not an array");var b=[],c;q(a,function(a){function d(a){var b,
+c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=p.get(e[0]);f[e[1]].apply(f,e[2])}}if(!m.get(a)){m.set(a,!0);try{F(a)?(c=ac(a),v.modules[a]=c,b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):D(a)?b.push(p.invoke(a)):H(a)?b.push(p.invoke(a)):sb(a,"module")}catch(e){throw H(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1===e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),ya("modulerr",a,e.stack||e.message||e);}}});return b}function h(a,c){function d(b,e){if(a.hasOwnProperty(b)){if(a[b]===
+k)throw ya("cdep",b+" <- "+l.join(" <- "));return a[b]}try{return l.unshift(b),a[b]=k,a[b]=c(b,e),a[b]}catch(f){throw a[b]===k&&delete a[b],f;}finally{l.shift()}}function e(a,c,f){var g=[];a=eb.$$annotate(a,b,f);for(var k=0,h=a.length;k<h;k++){var l=a[k];if("string"!==typeof l)throw ya("itkn",l);g.push(c&&c.hasOwnProperty(l)?c[l]:d(l,f))}return g}return{invoke:function(a,b,c,d){"string"===typeof c&&(d=c,c=null);c=e(a,c,d);H(a)&&(a=a[a.length-1]);d=a;if(za||"function"!==typeof d)d=!1;else{var f=d.$$ngIsClass;
+Ha(f)||(f=d.$$ngIsClass=/^(?:class\b|constructor\()/.test(Function.prototype.toString.call(d)));d=f}return d?(c.unshift(null),new (Function.prototype.bind.apply(a,c))):a.apply(b,c)},instantiate:function(a,b,c){var d=H(a)?a[a.length-1]:a;a=e(a,b,c);a.unshift(null);return new (Function.prototype.bind.apply(d,a))},get:d,annotate:eb.$$annotate,has:function(b){return n.hasOwnProperty(b+"Provider")||a.hasOwnProperty(b)}}}b=!0===b;var k={},l=[],m=new Gb,n={$provide:{provider:d(c),factory:d(f),service:d(function(a,
+b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:d(function(a,b){return f(a,la(b),!1)}),constant:d(function(a,b){Ka(a,"constant");n[a]=b;r[a]=b}),decorator:function(a,b){var c=p.get(a+"Provider"),d=c.$get;c.$get=function(){var a=v.invoke(d,c);return v.invoke(b,null,{$delegate:a})}}}},p=n.$injector=h(n,function(a,b){ea.isString(b)&&l.push(b);throw ya("unpr",l.join(" <- "));}),r={},J=h(r,function(a,b){var c=p.get(a+"Provider",b);return v.invoke(c.$get,c,void 0,a)}),v=J;n.$injectorProvider=
+{$get:la(J)};v.modules=p.modules=V();var t=g(a),v=J.get("$injector");v.strictDi=b;q(t,function(a){a&&v.invoke(a)});return v}function nf(){var a=!0;this.disableAutoScrolling=function(){a=!1};this.$get=["$window","$location","$rootScope",function(b,d,c){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===wa(a))return b=a,!0});return b}function f(a){if(a){a.scrollIntoView();var c;c=g.yOffset;D(c)?c=c():Ub(c)?(c=c[0],c="fixed"!==b.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):
+ba(c)||(c=0);c&&(a=a.getBoundingClientRect().top,b.scrollBy(0,a-c))}else b.scrollTo(0,0)}function g(a){a=F(a)?a:ba(a)?a.toString():d.hash();var b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var h=b.document;a&&c.$watch(function(){return d.hash()},function(a,b){a===b&&""===a||fg(function(){c.$evalAsync(g)})});return g}]}function ib(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;H(a)&&(a=a.join(" "));H(b)&&(b=b.join(" "));return a+" "+b}function og(a){F(a)&&
+(a=a.split(" "));var b=V();q(a,function(a){a.length&&(b[a]=!0)});return b}function ia(a){return C(a)?a:{}}function pg(a,b,d,c){function e(a){try{a.apply(null,va.call(arguments,1))}finally{if(J--,0===J)for(;v.length;)try{v.pop()()}catch(b){d.error(b)}}}function f(){Oa=null;h()}function g(){t=I();t=w(t)?null:t;sa(t,G)&&(t=G);M=G=t}function h(){var a=M;g();if(N!==k.url()||a!==t)N=k.url(),M=t,q(K,function(a){a(k.url(),t)})}var k=this,l=a.location,m=a.history,n=a.setTimeout,p=a.clearTimeout,r={};k.isMock=
+!1;var J=0,v=[];k.$$completeOutstandingRequest=e;k.$$incOutstandingRequestCount=function(){J++};k.notifyWhenNoOutstandingRequests=function(a){0===J?a():v.push(a)};var t,M,N=l.href,A=b.find("base"),Oa=null,I=c.history?function(){try{return m.state}catch(a){}}:z;g();k.url=function(b,d,e){w(e)&&(e=null);l!==a.location&&(l=a.location);m!==a.history&&(m=a.history);if(b){var f=M===e;if(N===b&&(!c.history||f))return k;var h=N&&Aa(N)===Aa(b);N=b;M=e;!c.history||h&&f?(h||(Oa=b),d?l.replace(b):h?(d=l,e=b.indexOf("#"),
+e=-1===e?"":b.substr(e),d.hash=e):l.href=b,l.href!==b&&(Oa=b)):(m[d?"replaceState":"pushState"](e,"",b),g());Oa&&(Oa=b);return k}return Oa||l.href.replace(/%27/g,"'")};k.state=function(){return t};var K=[],E=!1,G=null;k.onUrlChange=function(b){if(!E){if(c.history)B(a).on("popstate",f);B(a).on("hashchange",f);E=!0}K.push(b);return b};k.$$applicationDestroyed=function(){B(a).off("hashchange popstate",f)};k.$$checkUrlChange=h;k.baseHref=function(){var a=A.attr("href");return a?a.replace(/^(https?:)?\/\/[^/]*/,
+""):""};k.defer=function(a,b){var c;J++;c=n(function(){delete r[c];e(a)},b||0);r[c]=!0;return c};k.defer.cancel=function(a){return r[a]?(delete r[a],p(a),e(z),!0):!1}}function uf(){this.$get=["$window","$log","$sniffer","$document",function(a,b,d,c){return new pg(a,c,b,d)}]}function vf(){this.$get=function(){function a(a,c){function e(a){a!==n&&(p?p===a&&(p=a.n):p=a,f(a.n,a.p),f(a,n),n=a,n.n=null)}function f(a,b){a!==b&&(a&&(a.p=b),b&&(b.n=a))}if(a in b)throw L("$cacheFactory")("iid",a);var g=0,h=
+S({},c,{id:a}),k=V(),l=c&&c.capacity||Number.MAX_VALUE,m=V(),n=null,p=null;return b[a]={put:function(a,b){if(!w(b)){if(l<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}a in k||g++;k[a]=b;g>l&&this.remove(p.key);return b}},get:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;e(b)}return k[a]},remove:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;b===n&&(n=b.p);b===p&&(p=b.n);f(b.n,b.p);delete m[a]}a in k&&(delete k[a],g--)},removeAll:function(){k=V();g=0;m=V();n=p=null},destroy:function(){m=
+h=k=null;delete b[a]},info:function(){return S({},h,{size:g})}}}var b={};a.info=function(){var a={};q(b,function(b,e){a[e]=b.info()});return a};a.get=function(a){return b[a]};return a}}function Sf(){this.$get=["$cacheFactory",function(a){return a("templates")}]}function Wc(a,b){function d(a,b,c){var d=/^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/,e=V();q(a,function(a,f){if(a in n)e[f]=n[a];else{var g=a.match(d);if(!g)throw fa("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");
+e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f};g[4]&&(n[a]=e[f])}});return e}function c(a){var b=a.charAt(0);if(!b||b!==Q(b))throw fa("baddir",a);if(a!==a.trim())throw fa("baddir",a);}function e(a){var b=a.require||a.controller&&a.name;!H(b)&&C(b)&&q(b,function(a,c){var d=a.match(l);a.substring(d[0].length)||(b[c]=d[0]+c)});return b}var f={},g=/^\s*directive:\s*([\w-]+)\s+(.*)$/,h=/(([\w-]+)(?::([^;]+))?;?)/,k=re("ngSrc,ngSrcset,src,srcset"),l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,
+m=/^(on[a-z]+|formaction)$/,n=V();this.directive=function N(b,d){fb(b,"name");Ka(b,"directive");F(b)?(c(b),fb(d,"directiveFactory"),f.hasOwnProperty(b)||(f[b]=[],a.factory(b+"Directive",["$injector","$exceptionHandler",function(a,c){var d=[];q(f[b],function(f,g){try{var h=a.invoke(f);D(h)?h={compile:la(h)}:!h.compile&&h.link&&(h.compile=la(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||b;h.require=e(h);var k=h,l=h.restrict;if(l&&(!F(l)||!/[EACM]/.test(l)))throw fa("badrestrict",l,b);k.restrict=
+l||"EA";h.$$moduleName=f.$$moduleName;d.push(h)}catch(m){c(m)}});return d}])),f[b].push(d)):q(b,Lc(N));return this};this.component=function(a,b){function c(a){function e(b){return D(b)||H(b)?function(c,d){return a.invoke(b,this,{$element:c,$attrs:d})}:b}var f=b.template||b.templateUrl?b.template:"",g={controller:d,controllerAs:qg(b.controller)||b.controllerAs||"$ctrl",template:e(f),templateUrl:e(b.templateUrl),transclude:b.transclude,scope:{},bindToController:b.bindings||{},restrict:"E",require:b.require};
+q(b,function(a,b){"$"===b.charAt(0)&&(g[b]=a)});return g}var d=b.controller||function(){};q(b,function(a,b){"$"===b.charAt(0)&&(c[b]=a,D(d)&&(d[b]=a))});c.$inject=["$injector"];return this.directive(a,c)};this.aHrefSanitizationWhitelist=function(a){return u(a)?(b.aHrefSanitizationWhitelist(a),this):b.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(a){return u(a)?(b.imgSrcSanitizationWhitelist(a),this):b.imgSrcSanitizationWhitelist()};var p=!0;this.debugInfoEnabled=function(a){return u(a)?
+(p=a,this):p};var r=!1;this.preAssignBindingsEnabled=function(a){return u(a)?(r=a,this):r};var J=10;this.onChangesTtl=function(a){return arguments.length?(J=a,this):J};var v=!0;this.commentDirectivesEnabled=function(a){return arguments.length?(v=a,this):v};var t=!0;this.cssClassDirectivesEnabled=function(a){return arguments.length?(t=a,this):t};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$sce","$animate","$$sanitizeUri",function(a,
+b,c,e,n,E,G,y,O,X){function P(){try{if(!--ya)throw ia=void 0,fa("infchng",J);G.$apply(function(){for(var a=[],b=0,c=ia.length;b<c;++b)try{ia[b]()}catch(d){a.push(d)}ia=void 0;if(a.length)throw a;})}finally{ya++}}function s(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a}function R(a,b,c){ta.innerHTML="<span "+b+">";b=ta.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function La(a,
+b){try{a.addClass(b)}catch(c){}}function ca(a,b,c,d,e){a instanceof B||(a=B(a));var f=Ma(a,b,a,c,d,e);ca.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw fa("multilink");fb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==wa(d)&&ma.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?B(ha(g,B("<div>").append(a).html())):
+c?Na.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);ca.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Ma(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,n,p,r;if(K)for(r=Array(c.length),m=0;m<h.length;m+=3)f=h[m],r[f]=c[f];else r=c;m=0;for(n=h.length;m<n;)k=r[h[m++]],c=h[m++],f=h[m++],c?(c.scope?(l=a.$new(),ca.$$addScopeInfo(B(k),l)):l=a,p=c.transcludeOnThisElement?ja(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?ja(a,b):null,c(f,l,
+k,d,p)):f&&f(a,k.childNodes,void 0,e)}for(var h=[],k=H(a)||a instanceof B,l,m,n,p,K,r=0;r<a.length;r++){l=new s;11===za&&L(a,r,k);m=jc(a[r],[],l,0===r?d:void 0,e);(f=m.length?W(m,a[r],l,b,c,null,[],[],f):null)&&f.scope&&ca.$$addScopeClass(l.$$element);l=f&&f.terminal||!(n=a[r].childNodes)||!n.length?null:Ma(n,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||l)h.push(r,f,l),p=!0,K=K||f;f=null}return p?g:null}function L(a,b,c){var d=a[b],e=d.parentNode,f;if(d.nodeType===
+Ia)for(;;){f=e?d.nextSibling:a[b+1];if(!f||f.nodeType!==Ia)break;d.nodeValue+=f.nodeValue;f.parentNode&&f.parentNode.removeChild(f);c&&f===a[b+1]&&a.splice(b+1,1)}}function ja(a,b,c){function d(e,f,g,h,k){e||(e=a.$new(!1,k),e.$$transcluded=!0);return b(e,f,{parentBoundTranscludeFn:c,transcludeControllers:g,futureParentElement:h})}var e=d.$$slots=V(),f;for(f in b.$$slots)e[f]=b.$$slots[f]?ja(a,b.$$slots[f],c):null;return d}function jc(a,b,c,d,e){var f=c.$attr,g;switch(a.nodeType){case 1:g=wa(a);Y(b,
+Ba(g),"E",d,e);for(var k,l,m,n,p=a.attributes,K=0,r=p&&p.length;K<r;K++){var G=!1,E=!1;k=p[K];l=k.name;m=k.value;k=Ba(l);(n=Ja.test(k))&&(l=l.replace(md,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()}));(k=k.match(Ka))&&Z(k[1])&&(G=l,E=l.substr(0,l.length-5)+"end",l=l.substr(0,l.length-6));k=Ba(l.toLowerCase());f[k]=l;if(n||!c.hasOwnProperty(k))c[k]=m,id(a,k)&&(c[k]=!0);pa(a,b,m,k,n);Y(b,k,"A",d,e,G,E)}"input"===g&&"hidden"===a.getAttribute("type")&&a.setAttribute("autocomplete",
+"off");if(!Ga)break;f=a.className;C(f)&&(f=f.animVal);if(F(f)&&""!==f)for(;a=h.exec(f);)k=Ba(a[2]),Y(b,k,"C",d,e)&&(c[k]=T(a[3])),f=f.substr(a.index+a[0].length);break;case Ia:la(b,a.nodeValue);break;case 8:if(!Fa)break;jb(a,b,c,d,e)}b.sort(ea);return b}function jb(a,b,c,d,e){try{var f=g.exec(a.nodeValue);if(f){var h=Ba(f[1]);Y(b,h,"M",d,e)&&(c[h]=T(f[2]))}}catch(k){}}function nd(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw fa("uterdir",b,c);1===a.nodeType&&(a.hasAttribute(b)&&
+e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return B(d)}function od(a,b,c){return function(d,e,f,g,h){e=nd(e[0],b,c);return a(d,e,f,g,h)}}function kc(a,b,c,d,e,f){var g;return a?ca(b,c,d,e,f):function(){g||(g=ca(b,c,d,e,f),b=c=f=null);return g.apply(this,arguments)}}function W(a,b,d,e,f,g,h,k,l){function m(a,b,c,d){if(a){c&&(a=od(a,c,d));a.require=y.require;a.directiveName=P;if(E===y||y.$$isolateScope)a=qa(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=od(b,c,d));b.require=
+y.require;b.directiveName=P;if(E===y||y.$$isolateScope)b=qa(b,{isolateScope:!0});k.push(b)}}function n(a,e,f,g,l){function m(a,b,c,d){var e;Za(a)||(d=c,c=b,b=a,a=void 0);X&&(e=O);c||(c=X?P.parent():P);if(d){var f=l.$$slots[d];if(f)return f(a,b,e,c,R);if(w(f))throw fa("noslot",d,xa(P));}else return l(a,b,e,c,R)}var p,y,t,v,J,O,N,P;b===f?(g=d,P=d.$$element):(P=B(f),g=new s(P,d));J=e;E?v=e.$new(!0):K&&(J=e.$parent);l&&(N=m,N.$$boundTransclude=l,N.isSlotFilled=function(a){return!!l.$$slots[a]});G&&(O=
+ba(P,g,N,G,v,e,E));E&&(ca.$$addScopeInfo(P,v,!0,!(I&&(I===E||I===E.$$originalDirective))),ca.$$addScopeClass(P,!0),v.$$isolateBindings=E.$$isolateBindings,y=na(e,g,v,v.$$isolateBindings,E),y.removeWatches&&v.$on("$destroy",y.removeWatches));for(p in O){y=G[p];t=O[p];var Hb=y.$$bindings.bindToController;if(r){t.bindingInfo=Hb?na(J,g,t.instance,Hb,y):{};var A=t();A!==t.instance&&(t.instance=A,P.data("$"+y.name+"Controller",A),t.bindingInfo.removeWatches&&t.bindingInfo.removeWatches(),t.bindingInfo=
+na(J,g,t.instance,Hb,y))}else t.instance=t(),P.data("$"+y.name+"Controller",t.instance),t.bindingInfo=na(J,g,t.instance,Hb,y)}q(G,function(a,b){var c=a.require;a.bindToController&&!H(c)&&C(c)&&S(O[b].instance,U(b,c,P,O))});q(O,function(a){var b=a.instance;if(D(b.$onChanges))try{b.$onChanges(a.bindingInfo.initialChanges)}catch(d){c(d)}if(D(b.$onInit))try{b.$onInit()}catch(e){c(e)}D(b.$doCheck)&&(J.$watch(function(){b.$doCheck()}),b.$doCheck());D(b.$onDestroy)&&J.$on("$destroy",function(){b.$onDestroy()})});
+p=0;for(y=h.length;p<y;p++)t=h[p],ra(t,t.isolateScope?v:e,P,g,t.require&&U(t.directiveName,t.require,P,O),N);var R=e;E&&(E.template||null===E.templateUrl)&&(R=v);a&&a(R,f.childNodes,void 0,l);for(p=k.length-1;0<=p;p--)t=k[p],ra(t,t.isolateScope?v:e,P,g,t.require&&U(t.directiveName,t.require,P,O),N);q(O,function(a){a=a.instance;D(a.$postLink)&&a.$postLink()})}l=l||{};for(var p=-Number.MAX_VALUE,K=l.newScopeDirective,G=l.controllerDirectives,E=l.newIsolateScopeDirective,I=l.templateDirective,t=l.nonTlbTranscludeDirective,
+J=!1,O=!1,X=l.hasElementTranscludeDirective,v=d.$$element=B(b),y,P,N,A=e,R,u=!1,La=!1,x,z=0,F=a.length;z<F;z++){y=a[z];var Ma=y.$$start,L=y.$$end;Ma&&(v=nd(b,Ma,L));N=void 0;if(p>y.priority)break;if(x=y.scope)y.templateUrl||(C(x)?($("new/isolated scope",E||K,y,v),E=y):$("new/isolated scope",E,y,v)),K=K||y;P=y.name;if(!u&&(y.replace&&(y.templateUrl||y.template)||y.transclude&&!y.$$tlb)){for(x=z+1;u=a[x++];)if(u.transclude&&!u.$$tlb||u.replace&&(u.templateUrl||u.template)){La=!0;break}u=!0}!y.templateUrl&&
+y.controller&&(G=G||V(),$("'"+P+"' controller",G[P],y,v),G[P]=y);if(x=y.transclude)if(J=!0,y.$$tlb||($("transclusion",t,y,v),t=y),"element"===x)X=!0,p=y.priority,N=v,v=d.$$element=B(ca.$$createComment(P,d[P])),b=v[0],ka(f,va.call(N,0),b),N[0].$$parentNode=N[0].parentNode,A=kc(La,N,e,p,g&&g.name,{nonTlbTranscludeDirective:t});else{var ja=V();if(C(x)){N=[];var Q=V(),jb=V();q(x,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Q[a]=b;ja[b]=null;jb[b]=c});q(v.contents(),function(a){var b=Q[Ba(wa(a))];
+b?(jb[b]=!0,ja[b]=ja[b]||[],ja[b].push(a)):N.push(a)});q(jb,function(a,b){if(!a)throw fa("reqslot",b);});for(var ic in ja)ja[ic]&&(ja[ic]=kc(La,ja[ic],e))}else N=B(fc(b)).contents();v.empty();A=kc(La,N,e,void 0,void 0,{needsNewScope:y.$$isolateScope||y.$$newScope});A.$$slots=ja}if(y.template)if(O=!0,$("template",I,y,v),I=y,x=D(y.template)?y.template(v,d):y.template,x=Ea(x),y.replace){g=y;N=cc.test(x)?pd(ha(y.templateNamespace,T(x))):[];b=N[0];if(1!==N.length||1!==b.nodeType)throw fa("tplrt",P,"");
+ka(f,v,b);F={$attr:{}};x=jc(b,[],F);var Y=a.splice(z+1,a.length-(z+1));(E||K)&&aa(x,E,K);a=a.concat(x).concat(Y);da(d,F);F=a.length}else v.html(x);if(y.templateUrl)O=!0,$("template",I,y,v),I=y,y.replace&&(g=y),n=ga(a.splice(z,a.length-z),v,d,f,J&&A,h,k,{controllerDirectives:G,newScopeDirective:K!==y&&K,newIsolateScopeDirective:E,templateDirective:I,nonTlbTranscludeDirective:t}),F=a.length;else if(y.compile)try{R=y.compile(v,d,A);var Z=y.$$originalDirective||y;D(R)?m(null,bb(Z,R),Ma,L):R&&m(bb(Z,R.pre),
+bb(Z,R.post),Ma,L)}catch(ea){c(ea,xa(v))}y.terminal&&(n.terminal=!0,p=Math.max(p,y.priority))}n.scope=K&&!0===K.scope;n.transcludeOnThisElement=J;n.templateOnThisElement=O;n.transclude=A;l.hasElementTranscludeDirective=X;return n}function U(a,b,c,d){var e;if(F(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e=g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw fa("ctreq",b,a);}else if(H(b))for(e=
+[],g=0,f=b.length;g<f;g++)e[g]=U(a,b[g],c,d);else C(b)&&(e={},q(b,function(b,f){e[f]=U(a,b,c,d)}));return e||null}function ba(a,b,c,d,e,f,g){var h=V(),k;for(k in d){var l=d[k],m={$scope:l===g||l.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},n=l.controller;"@"===n&&(n=b[l.name]);m=E(n,m,!0,l.controllerAs);h[l.name]=m;a.data("$"+l.name+"Controller",m.instance)}return h}function aa(a,b,c){for(var d=0,e=a.length;d<e;d++)a[d]=Vb(a[d],{$$isolateScope:b,$$newScope:c})}function Y(b,c,e,g,h,k,l){if(c===
+h)return null;var m=null;if(f.hasOwnProperty(c)){h=a.get(c+"Directive");for(var n=0,p=h.length;n<p;n++)if(c=h[n],(w(g)||g>c.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=Vb(c,{$$start:k,$$end:l}));if(!c.$$bindings){var K=m=c,r=c.name,t={isolateScope:null,bindToController:null};C(K.scope)&&(!0===K.bindToController?(t.bindToController=d(K.scope,r,!0),t.isolateScope={}):t.isolateScope=d(K.scope,r,!1));C(K.bindToController)&&(t.bindToController=d(K.bindToController,r,!0));if(t.bindToController&&!K.controller)throw fa("noctrl",
+r);m=m.$$bindings=t;C(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function Z(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d<e;d++)if(b=c[d],b.multiElement)return!0;return!1}function da(a,b){var c=b.$attr,d=a.$attr;q(a,function(d,e){"$"!==e.charAt(0)&&(b[e]&&b[e]!==d&&(d=d.length?d+(("style"===e?";":" ")+b[e]):b[e]),a.$set(e,d,!0,c[e]))});q(b,function(b,e){a.hasOwnProperty(e)||"$"===e.charAt(0)||(a[e]=b,"class"!==e&&"style"!==e&&(d[e]=c[e]))})}
+function ga(a,b,d,f,g,h,k,l){var m=[],n,p,K=b[0],r=a.shift(),t=Vb(r,{templateUrl:null,transclude:null,replace:null,$$originalDirective:r}),y=D(r.templateUrl)?r.templateUrl(b,d):r.templateUrl,E=r.templateNamespace;b.empty();e(y).then(function(c){var e,G;c=Ea(c);if(r.replace){c=cc.test(c)?pd(ha(E,T(c))):[];e=c[0];if(1!==c.length||1!==e.nodeType)throw fa("tplrt",r.name,y);c={$attr:{}};ka(f,b,e);var I=jc(e,[],c);C(r.scope)&&aa(I,!0);a=I.concat(a);da(d,c)}else e=K,b.html(c);a.unshift(t);n=W(a,e,d,g,b,
+r,h,k,l);q(f,function(a,c){a===e&&(f[c]=b[0])});for(p=Ma(b[0].childNodes,g);m.length;){c=m.shift();G=m.shift();var v=m.shift(),J=m.shift(),I=b[0];if(!c.$$destroyed){if(G!==K){var O=G.className;l.hasElementTranscludeDirective&&r.replace||(I=fc(e));ka(v,B(G),I);La(B(I),O)}G=n.transcludeOnThisElement?ja(c,n.transclude,J):J;n(p,c,I,f,G)}}m=null}).catch(function(a){a instanceof Error&&c(a)});return function(a,b,c,d,e){a=e;b.$$destroyed||(m?m.push(b,c,d,a):(n.transcludeOnThisElement&&(a=ja(b,n.transclude,
+e)),n(p,b,c,d,a)))}}function ea(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function $(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw fa("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,xa(d));}function la(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&ca.$$addBindingClass(a);return function(a,c){var e=c.parent();b||ca.$$addBindingClass(e);ca.$$addBindingInfo(e,d.expressions);
+a.$watch(d,function(a){c[0].nodeValue=a})}}})}function ha(a,b){a=Q(a||"html");switch(a){case "svg":case "math":var c=x.document.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function oa(a,b){if("srcdoc"===b)return y.HTML;var c=wa(a);if("src"===b||"ngSrc"===b){if(-1===["img","video","audio","source","track"].indexOf(c))return y.RESOURCE_URL}else if("xlinkHref"===b||"form"===c&&"action"===b||"link"===c&&"href"===b)return y.RESOURCE_URL}function pa(a,
+c,d,e,f){var g=oa(a,e),h=k[e]||f,l=b(d,!f,g,h);if(l){if("multiple"===e&&"select"===wa(a))throw fa("selmulti",xa(a));if(m.test(e))throw fa("nodomevents");c.push({priority:100,compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=V());var k=f[e];k!==d&&(l=k&&b(k,!0,g,h),d=k);l&&(f[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function ka(a,b,c){var d=b[0],e=
+b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]===d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=x.document.createDocumentFragment();for(g=0;g<e;g++)a.appendChild(b[g]);B.hasData(d)&&(B.data(c,B.data(d)),B(d).off("$destroy"));B.cleanData(a.querySelectorAll("*"));for(g=1;g<e;g++)delete b[g];b[0]=c;b.length=1}function qa(a,b){return S(function(){return a.apply(null,arguments)},
+a,b)}function ra(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,xa(d))}}function na(a,c,d,e,f){function g(b,c,e){D(d.$onChanges)&&!Xb(c,e)&&(ia||(a.$$postDigest(P),ia=[]),m||(m={},ia.push(h)),m[b]&&(e=m[b].previousValue),m[b]=new Ib(e,c))}function h(){d.$onChanges(m);m=void 0}var k=[],l={},m;q(e,function(e,h){var m=e.attrName,p=e.optional,r,t,y,G;switch(e.mode){case "@":p||ua.call(c,m)||(d[h]=c[m]=void 0);p=c.$observe(m,function(a){if(F(a)||Ha(a))g(h,a,d[h]),d[h]=a});c.$$observers[m].$$scope=a;r=c[m];
+F(r)?d[h]=b(r)(a):Ha(r)&&(d[h]=r);l[h]=new Ib(lc,d[h]);k.push(p);break;case "=":if(!ua.call(c,m)){if(p)break;c[m]=void 0}if(p&&!c[m])break;t=n(c[m]);G=t.literal?sa:Xb;y=t.assign||function(){r=d[h]=t(a);throw fa("nonassign",c[m],m,f.name);};r=d[h]=t(a);p=function(b){G(b,d[h])||(G(b,r)?y(a,b=d[h]):d[h]=b);return r=b};p.$stateful=!0;p=e.collection?a.$watchCollection(c[m],p):a.$watch(n(c[m],p),null,t.literal);k.push(p);break;case "<":if(!ua.call(c,m)){if(p)break;c[m]=void 0}if(p&&!c[m])break;t=n(c[m]);
+var E=t.literal,I=d[h]=t(a);l[h]=new Ib(lc,d[h]);p=a.$watch(t,function(a,b){if(b===a){if(b===I||E&&sa(b,I))return;b=I}g(h,a,b);d[h]=a},E);k.push(p);break;case "&":t=c.hasOwnProperty(m)?n(c[m]):z;if(t===z&&p)break;d[h]=function(b){return t(a,b)}}});return{initialChanges:l,removeWatches:k.length&&function(){for(var a=0,b=k.length;a<b;++a)k[a]()}}}var Ca=/^\w/,ta=x.document.createElement("div"),Fa=v,Ga=t,ya=J,ia;s.prototype={$normalize:Ba,$addClass:function(a){a&&0<a.length&&O.addClass(this.$$element,
+a)},$removeClass:function(a){a&&0<a.length&&O.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=qd(a,b);c&&c.length&&O.addClass(this.$$element,c);(c=qd(b,a))&&c.length&&O.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=id(this.$$element[0],a),g=rd[a],h=a;f?(this.$$element.prop(a,b),e=f):g&&(this[g]=b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=Tc(a,"-"));f=wa(this.$$element);if("a"===f&&("href"===a||"xlinkHref"===a)||"img"===f&&"src"===a)this[a]=
+b=X(b,"src"===a);else if("img"===f&&"srcset"===a&&u(b)){for(var f="",g=T(b),k=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,k=/\s/.test(g)?k:/(,)/,g=g.split(k),k=Math.floor(g.length/2),l=0;l<k;l++)var m=2*l,f=f+X(T(g[m]),!0),f=f+(" "+T(g[m+1]));g=T(g[2*l]).split(/\s/);f+=X(T(g[0]),!0);2===g.length&&(f+=" "+T(g[1]));this[a]=b=f}!1!==d&&(null===b||w(b)?this.$$element.removeAttr(e):Ca.test(e)?this.$$element.attr(e,b):R(this.$$element[0],e,b));(a=this.$$observers)&&q(a[h],function(a){try{a(b)}catch(d){c(d)}})},
+$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=V()),e=d[a]||(d[a]=[]);e.push(b);G.$evalAsync(function(){e.$$inter||!c.hasOwnProperty(a)||w(c[a])||b(c[a])});return function(){$a(e,b)}}};var Aa=b.startSymbol(),Da=b.endSymbol(),Ea="{{"===Aa&&"}}"===Da?Ya:function(a){return a.replace(/\{\{/g,Aa).replace(/}}/g,Da)},Ja=/^ngAttr[A-Z]/,Ka=/^(.+)Start$/;ca.$$addBindingInfo=p?function(a,b){var c=a.data("$binding")||[];H(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:z;ca.$$addBindingClass=
+p?function(a){La(a,"ng-binding")}:z;ca.$$addScopeInfo=p?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:z;ca.$$addScopeClass=p?function(a,b){La(a,b?"ng-isolate-scope":"ng-scope")}:z;ca.$$createComment=function(a,b){var c="";p&&(c=" "+(a||"")+": ",b&&(c+=b+" "));return x.document.createComment(c)};return ca}]}function Ib(a,b){this.previousValue=a;this.currentValue=b}function Ba(a){return a.replace(md,"").replace(rg,gb)}function qd(a,b){var d="",c=a.split(/\s+/),
+e=b.split(/\s+/),f=0;a:for(;f<c.length;f++){for(var g=c[f],h=0;h<e.length;h++)if(g===e[h])continue a;d+=(0<d.length?" ":"")+g}return d}function pd(a){a=B(a);var b=a.length;if(1>=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Ia&&""===d.nodeValue.trim())&&sg.call(a,b,1)}return a}function qg(a,b){if(b&&F(b))return b;if(F(a)){var d=sd.exec(a);if(d)return d[3]}}function wf(){var a={},b=!1;this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,c){Ka(b,"controller");C(b)?
+S(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!C(a.$scope))throw L("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,n;h=!0===h;k&&F(k)&&(n=k);if(F(f)){k=f.match(sd);if(!k)throw td("ctrlfmt",f);m=k[1];n=n||k[3];f=a.hasOwnProperty(m)?a[m]:Vc(g.$scope,m,!0)||(b?Vc(c,m,!0):void 0);if(!f)throw td("ctrlreg",m);sb(f,m,!0)}if(h)return h=(H(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),n&&e(g,n,
+l,m||f.name),S(function(){var a=d.invoke(f,l,g,m);a!==l&&(C(a)||D(a))&&(l=a,n&&e(g,n,l,m||f.name));return l},{instance:l,identifier:n});l=d.instantiate(f,g,m);n&&e(g,n,l,m||f.name);return l}}]}function xf(){this.$get=["$window",function(a){return B(a.document)}]}function yf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function zf(){this.$get=
+["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function mc(a){return C(a)?ga(a)?a.toISOString():cb(a):a}function Ef(){this.$get=function(){return function(a){if(!a)return"";var b=[];Kc(a,function(a,c){null===a||w(a)||(H(a)?q(a,function(a){b.push($(c)+"="+$(mc(a)))}):b.push($(c)+"="+$(mc(a))))});return b.join("&")}}}function Ff(){this.$get=function(){return function(a){function b(a,e,f){null===a||w(a)||(H(a)?q(a,function(a,c){b(a,e+"["+(C(a)?c:"")+"]")}):C(a)&&!ga(a)?Kc(a,function(a,
+c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push($(e)+"="+$(mc(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function nc(a,b){if(F(a)){var d=a.replace(tg,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf(ud))||(c=(c=d.match(ug))&&vg[c[0]].test(d));if(c)try{a=Oc(d)}catch(e){throw oc("baddata",a,e);}}}return a}function vd(a){var b=V(),d;F(a)?q(a.split("\n"),function(a){d=a.indexOf(":");var e=Q(T(a.substr(0,d)));a=T(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):C(a)&&q(a,function(a,
+d){var f=Q(d),g=T(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function wd(a){var b;return function(d){b||(b=vd(a));return d?(d=b[Q(d)],void 0===d&&(d=null),d):b}}function xd(a,b,d,c){if(D(c))return c(a,b,d);q(c,function(c){a=c(a,b,d)});return a}function Df(){var a=this.defaults={transformResponse:[nc],transformRequest:[function(a){return C(a)&&"[object File]"!==ma.call(a)&&"[object Blob]"!==ma.call(a)&&"[object FormData]"!==ma.call(a)?cb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},
+post:pa(pc),put:pa(pc),patch:pa(pc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return u(a)?(b=!!a,this):b};var d=this.interceptors=[];this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(c,e,f,g,h,k,l,m){function n(b){function d(a,b){for(var c=0,e=b.length;c<e;){var f=b[c++],g=b[c++];a=a.then(f,g)}b.length=0;return a}
+function e(a,b){var c,d={};q(a,function(a,e){D(a)?(c=a(b),null!=c&&(d[e]=c)):d[e]=a});return d}function f(a){var b=S({},a);b.data=xd(a.data,a.headers,a.status,g.transformResponse);a=a.status;return 200<=a&&300>a?b:k.reject(b)}if(!C(b))throw L("$http")("badreq",b);if(!F(m.valueOf(b.url)))throw L("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers=
+function(b){var c=a.headers,d=S({},b.headers),f,g,h,c=S({},c.common,c[Q(b.method)]);a:for(f in c){g=Q(f);for(h in d)if(Q(h)===g)continue a;d[f]=c[f]}return e(d,pa(b))}(b);g.method=ub(g.method);g.paramSerializer=F(g.paramSerializer)?l.get(g.paramSerializer):g.paramSerializer;c.$$incOutstandingRequestCount();var h=[],n=[];b=k.resolve(g);q(t,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&n.push(a.response,a.responseError)});b=d(b,h);b=b.then(function(b){var c=
+b.headers,d=xd(b.data,wd(c),void 0,b.transformRequest);w(d)&&q(c,function(a,b){"content-type"===Q(b)&&delete c[b]});w(b.withCredentials)&&!w(a.withCredentials)&&(b.withCredentials=a.withCredentials);return p(b,d).then(f,f)});b=d(b,n);return b=b.finally(function(){c.$$completeOutstandingRequest(z)})}function p(c,d){function g(a){if(a){var c={};q(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function l(a,c,d,e){function f(){p(c,a,d,e)}O&&
+(200<=a&&300>a?O.put(R,[a,c,vd(d),e]):O.remove(R));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function p(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?G.resolve:G.reject)({data:a,status:b,headers:wd(d),config:c,statusText:e})}function K(a){p(a.data,a.status,pa(a.headers()),a.statusText)}function t(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var G=k.defer(),y=G.promise,O,X,P=c.headers,s="jsonp"===Q(c.method),R=c.url;s?R=m.getTrustedResourceUrl(R):F(R)||(R=m.valueOf(R));R=r(R,
+c.paramSerializer(c.params));s&&(R=J(R,c.jsonpCallbackParam));n.pendingRequests.push(c);y.then(t,t);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=C(c.cache)?c.cache:C(a.cache)?a.cache:v);O&&(X=O.get(R),u(X)?X&&D(X.then)?X.then(K,K):H(X)?p(X[1],X[0],pa(X[2]),X[3]):p(X,200,{},"OK"):O.put(R,y));w(X)&&((X=yd(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(P[c.xsrfHeaderName||a.xsrfHeaderName]=X),e(c.method,R,d,l,P,c.timeout,c.withCredentials,c.responseType,g(c.eventHandlers),
+g(c.uploadEventHandlers)));return y}function r(a,b){0<b.length&&(a+=(-1===a.indexOf("?")?"?":"&")+b);return a}function J(a,b){if(/[&?][^=]+=JSON_CALLBACK/.test(a))throw oc("badjsonp",a);if((new RegExp("[&?]"+b+"=")).test(a))throw oc("badjsonp",b,a);return a+=(-1===a.indexOf("?")?"?":"&")+b+"=JSON_CALLBACK"}var v=g("$http");a.paramSerializer=F(a.paramSerializer)?l.get(a.paramSerializer):a.paramSerializer;var t=[];q(d,function(a){t.unshift(F(a)?l.get(a):l.invoke(a))});n.pendingRequests=[];(function(a){q(arguments,
+function(a){n[a]=function(b,c){return n(S({},c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){q(arguments,function(a){n[a]=function(b,c,d){return n(S({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");n.defaults=a;return n}]}function Hf(){this.$get=function(){return function(){return new x.XMLHttpRequest}}}function Gf(){this.$get=["$browser","$jsonpCallbacks","$document","$xhrFactory",function(a,b,d,c){return wg(a,c,a.defer,b,d[0])}]}function wg(a,b,d,c,e){function f(a,
+b,d){a=a.replace("JSON_CALLBACK",b);var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m);f.removeEventListener("error",m);e.body.removeChild(f);f=null;var g=-1,r="unknown";a&&("load"!==a.type||c.wasCalled(b)||(a={type:"error"}),r=a.type,g="error"===a.type?404:200);d&&d(g,r)};f.addEventListener("load",m);f.addEventListener("error",m);e.body.appendChild(f);return m}return function(e,h,k,l,m,n,p,r,J,v){function t(){N&&N();A&&
+A.abort()}h=h||a.url();if("jsonp"===Q(e))var M=c.createCallback(h),N=f(h,M,function(a,b){var e=200===a&&c.getResponse(M);u(I)&&d.cancel(I);N=A=null;l(a,e,"",b);c.removeCallback(M)});else{var A=b(e,h);A.open(e,h,!0);q(m,function(a,b){u(a)&&A.setRequestHeader(b,a)});A.onload=function(){var a=A.statusText||"",b="response"in A?A.response:A.responseText,c=1223===A.status?204:A.status;0===c&&(c=b?200:"file"===Ca(h).protocol?404:0);var e=A.getAllResponseHeaders();u(I)&&d.cancel(I);N=A=null;l(c,b,e,a)};e=
+function(){u(I)&&d.cancel(I);N=A=null;l(-1,null,null,"")};A.onerror=e;A.onabort=e;A.ontimeout=e;q(J,function(a,b){A.addEventListener(b,a)});q(v,function(a,b){A.upload.addEventListener(b,a)});p&&(A.withCredentials=!0);if(r)try{A.responseType=r}catch(s){if("json"!==r)throw s;}A.send(w(k)?null:k)}if(0<n)var I=d(t,n);else n&&D(n.then)&&n.then(t)}}function Bf(){var a="{{",b="}}";this.startSymbol=function(b){return b?(a=b,this):a};this.endSymbol=function(a){return a?(b=a,this):b};this.$get=["$parse","$exceptionHandler",
+"$sce",function(d,c,e){function f(a){return"\\\\\\"+a}function g(c){return c.replace(n,a).replace(p,b)}function h(a,b,c,d){var e=a.$watch(function(a){e();return d(a)},b,c);return e}function k(f,k,n,p){function M(a){try{var b=a;a=n?e.getTrusted(n,b):e.valueOf(b);return p&&!u(a)?a:$b(a)}catch(d){c(Da.interr(f,d))}}if(!f.length||-1===f.indexOf(a)){var q;k||(k=g(f),q=la(k),q.exp=f,q.expressions=[],q.$$watchDelegate=h);return q}p=!!p;var A,s,I=0,K=[],E=[];q=f.length;for(var G=[],y=[];I<q;)if(-1!==(A=f.indexOf(a,
+I))&&-1!==(s=f.indexOf(b,A+l)))I!==A&&G.push(g(f.substring(I,A))),I=f.substring(A+l,s),K.push(I),E.push(d(I,M)),I=s+m,y.push(G.length),G.push("");else{I!==q&&G.push(g(f.substring(I)));break}n&&1<G.length&&Da.throwNoconcat(f);if(!k||K.length){var O=function(a){for(var b=0,c=K.length;b<c;b++){if(p&&w(a[b]))return;G[y[b]]=a[b]}return G.join("")};return S(function(a){var b=0,d=K.length,e=Array(d);try{for(;b<d;b++)e[b]=E[b](a);return O(e)}catch(g){c(Da.interr(f,g))}},{exp:f,expressions:K,$$watchDelegate:function(a,
+b){var c;return a.$watchGroup(E,function(d,e){var f=O(d);D(b)&&b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=a.length,m=b.length,n=new RegExp(a.replace(/./g,f),"g"),p=new RegExp(b.replace(/./g,f),"g");k.startSymbol=function(){return a};k.endSymbol=function(){return b};return k}]}function Cf(){this.$get=["$rootScope","$window","$q","$$q","$browser",function(a,b,d,c,e){function f(f,k,l,m){function n(){p?f.apply(null,r):f(t)}var p=4<arguments.length,r=p?va.call(arguments,4):[],J=b.setInterval,v=b.clearInterval,
+t=0,M=u(m)&&!m,q=(M?c:d).defer(),A=q.promise;l=u(l)?l:0;A.$$intervalId=J(function(){M?e.defer(n):a.$evalAsync(n);q.notify(t++);0<l&&t>=l&&(q.resolve(t),v(A.$$intervalId),delete g[A.$$intervalId]);M||a.$apply()},k);g[A.$$intervalId]=q;return A}var g={};f.cancel=function(a){return a&&a.$$intervalId in g?(g[a.$$intervalId].promise.catch(z),g[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),delete g[a.$$intervalId],!0):!1};return f}]}function qc(a){a=a.split("/");for(var b=a.length;b--;)a[b]=
+db(a[b]);return a.join("/")}function zd(a,b){var d=Ca(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=Z(d.port)||xg[d.protocol]||null}function Ad(a,b){if(yg.test(a))throw kb("badpath",a);var d="/"!==a.charAt(0);d&&(a="/"+a);var c=Ca(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=Rc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function rc(a,b){return a.slice(0,b.length)===
+b}function ka(a,b){if(rc(b,a))return b.substr(a.length)}function Aa(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function lb(a){return a.replace(/(#.+)|#$/,"$1")}function sc(a,b,d){this.$$html5=!0;d=d||"";zd(a,this);this.$$parse=function(a){var d=ka(b,a);if(!F(d))throw kb("ipthprfx",a,b);Ad(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Zb(this.$$search),d=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(a?"?"+a:"")+d;this.$$absUrl=b+
+this.$$url.substr(1);this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;u(f=ka(a,c))?(g=f,g=d&&u(f=ka(d,f))?b+(ka("/",f)||f):a+g):u(f=ka(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function tc(a,b,d){zd(a,this);this.$$parse=function(c){var e=ka(a,c)||ka(b,c),f;w(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",w(e)&&(a=c,this.replace())):(f=ka(d,e),w(f)&&(f=e));Ad(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;rc(f,
+e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?d+this.$$url:"");this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(b,d){return Aa(a)===Aa(b)?(this.$$parse(b),!0):!1}}function Bd(a,b,d){this.$$html5=!0;tc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),
+!0;var f,g;a===Aa(c)?f=c:(g=ka(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url;this.$$urlUpdatedByLocation=!0}}function Jb(a){return function(){return this[a]}}function Cd(a,b){return function(d){if(w(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Jf(){var a="!",b={enabled:!1,requireBase:!0,rewriteLinks:!0};
+this.hashPrefix=function(b){return u(b)?(a=b,this):a};this.html5Mode=function(a){if(Ha(a))return b.enabled=a,this;if(C(a)){Ha(a.enabled)&&(b.enabled=a.enabled);Ha(a.requireBase)&&(b.requireBase=a.requireBase);if(Ha(a.rewriteLinks)||F(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state=
+f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,b)}var l,m;m=c.baseHref();var n=c.url(),p;if(b.enabled){if(!m&&b.requireBase)throw kb("nobase");p=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?sc:Bd}else p=Aa(n),m=tc;var r=p.substr(0,Aa(p).lastIndexOf("/")+1);l=new m(p,r,"#"+a);l.$$parseLinkUrl(n,n);l.$$state=c.state();var J=/^\s*(javascript|mailto):/i;f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&
+2!==a.which&&2!==a.button){for(var h=B(a.target);"a"!==wa(h[0]);)if(h[0]===f[0]||!(h=h.parent())[0])return;if(!F(e)||!w(h.attr(e))){var e=h.prop("href"),k=h.attr("href")||h.attr("xlink:href");C(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=Ca(e.animVal).href);J.test(e)||!e||h.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(e,k)||(a.preventDefault(),l.absUrl()!==c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=!0))}}});lb(l.absUrl())!==lb(n)&&c.url(l.absUrl(),!0);var v=!0;
+c.onUrlChange(function(a,b){rc(a,r)?(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=lb(a);l.$$parse(a);l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(v=!1,k(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(v||l.$$urlUpdatedByLocation){l.$$urlUpdatedByLocation=!1;var a=lb(c.url()),b=lb(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(v||m)v=
+!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),k(a,f)))})}l.$$replace=!1});return l}]}function Kf(){var a=!0,b=this;this.debugEnabled=function(b){return u(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&
+(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||z;a=!1;try{a=!!e.apply}catch(f){}return a?function(){var a=[];q(arguments,function(b){a.push(c(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}var f=za||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function zg(a){return a+""}function Ag(a,
+b){return"undefined"!==typeof a?a:b}function Dd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function U(a,b){var d,c,e;switch(a.type){case s.Program:d=!0;q(a.body,function(a){U(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:U(a.argument,b);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;
+a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:U(a.test,b);U(a.alternate,b);U(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=!1;a.toWatch=[a];break;case s.MemberExpression:U(a.object,b);a.computed&&U(a.property,b);a.constant=a.object.constant&&
+(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=e=a.filter?!b(a.callee.name).$stateful:!1;c=[];q(a.arguments,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=e?c:[a];break;case s.AssignmentExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];q(a.elements,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=
+d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];q(a.properties,function(a){U(a.value,b);d=d&&a.value.constant&&!a.computed;a.value.constant||c.push.apply(c,a.value.toWatch);a.computed&&(U(a.key,b),a.key.constant||c.push.apply(c,a.key.toWatch))});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1;a.toWatch=[];break;case s.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Ed(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Fd(a){return a.type===
+s.Identifier||a.type===s.MemberExpression}function Gd(a){if(1===a.body.length&&Fd(a.body[0].expression))return{type:s.AssignmentExpression,left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function Hd(a){this.$filter=a}function Id(a){this.$filter=a}function uc(a,b,d){this.ast=new s(a,d);this.astCompiler=d.csp?new Id(b):new Hd(b)}function vc(a){return D(a.valueOf)?a.valueOf():Bg.call(a)}function Lf(){var a=V(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=
+function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=vc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function g(a,b,c,d,e){var g=d.inputs,h;if(1===g.length){var k=f,g=g[0];return a.$watch(function(a){var b=g(a);f(b,k,d.literal)||(h=d(a,void 0,void 0,[b]),k=b&&vc(b));return h},b,c,e)}for(var l=[],m=[],n=0,E=g.length;n<E;n++)l[n]=f,m[n]=null;return a.$watch(function(a){for(var b=
+!1,c=0,e=g.length;c<e;c++){var k=g[c](a);if(b||(b=!f(k,l[c],d.literal)))m[c]=k,l[c]=k&&vc(k)}b&&(h=d(a,void 0,void 0,m));return h},b,c,e)}function h(a,b,c,d,e){function f(a){return d(a)}function h(a,c,d){n=a;D(b)&&b(a,c,d);l(a)&&d.$$postDigest(function(){l(n)&&m()})}var l=d.literal?k:u,m,n;return m=d.inputs?g(a,h,c,d,e):a.$watch(f,h,c)}function k(a){var b=!0;q(a,function(a){u(a)||(b=!1)});return b}function l(a,b,c,d){var e=a.$watch(function(a){e();return d(a)},b,c);return e}function m(a,b){function c(d,
+e,g,h){g=f&&h?h[0]:a(d,e,g,h);return b(g,d,e)}function d(c,e,g,k){g=f&&k?k[0]:a(c,e,g,k);c=b(g,c,e);return h(g)?c:g}if(!b)return a;var e=a.$$watchDelegate,f=!1,h=a.literal?k:u,l=a.oneTime?d:c;l.literal=a.literal;l.oneTime=a.oneTime;f=!a.inputs;e&&e!==g?(l.$$watchDelegate=e,l.inputs=a.inputs):b.$stateful||(l.$$watchDelegate=g,l.inputs=a.inputs?a.inputs:[a]);return l}var n={csp:Ga().noUnsafeEval,literals:ra(b),isIdentifierStart:D(d)&&d,isIdentifierContinue:D(c)&&c};return function(b,c){var d,f,k;switch(typeof b){case "string":return k=
+b=b.trim(),d=a[k],d||(":"===b.charAt(0)&&":"===b.charAt(1)&&(f=!0,b=b.substring(2)),d=new wc(n),d=(new uc(d,e,n)).parse(b),d.constant?d.$$watchDelegate=l:f?(d.oneTime=!0,d.$$watchDelegate=h):d.inputs&&(d.$$watchDelegate=g),a[k]=d),m(d,c);case "function":return m(b,c);default:return m(z,c)}}}]}function Nf(){var a=!0;this.$get=["$rootScope","$exceptionHandler",function(b,d){return Jd(function(a){b.$evalAsync(a)},d,a)}];this.errorOnUnhandledRejections=function(b){return u(b)?(a=b,this):a}}function Of(){var a=
+!0;this.$get=["$browser","$exceptionHandler",function(b,d){return Jd(function(a){b.defer(a)},d,a)}];this.errorOnUnhandledRejections=function(b){return u(b)?(a=b,this):a}}function Jd(a,b,d){function c(){return new e}function e(){var a=this.promise=new f;this.resolve=function(b){k(a,b)};this.reject=function(b){m(a,b)};this.notify=function(b){p(a,b)}}function f(){this.$$state={status:0}}function g(){for(;!s&&A.length;){var a=A.shift();if(!a.pur){a.pur=!0;var c=a.value,c="Possibly unhandled rejection: "+
+("function"===typeof c?c.toString().replace(/ \{[\s\S]*$/,""):w(c)?"undefined":"string"!==typeof c?Be(c,void 0):c);a.value instanceof Error?b(a.value,c):b(c)}}}function h(b){!d||b.pending||2!==b.status||b.pur||(0===s&&0===A.length&&a(g),A.push(b));!b.processScheduled&&b.pending&&(b.processScheduled=!0,++s,a(function(){var c,e,f;f=b.pending;b.processScheduled=!1;b.pending=void 0;try{for(var h=0,l=f.length;h<l;++h){b.pur=!0;e=f[h][0];c=f[h][b.status];try{D(c)?k(e,c(b.value)):1===b.status?k(e,b.value):
+m(e,b.value)}catch(n){m(e,n)}}}finally{--s,d&&0===s&&a(g)}}))}function k(a,b){a.$$state.status||(b===a?n(a,M("qcycle",b)):l(a,b))}function l(a,b){function c(b){g||(g=!0,l(a,b))}function d(b){g||(g=!0,n(a,b))}function e(b){p(a,b)}var f,g=!1;try{if(C(b)||D(b))f=b.then;D(f)?(a.$$state.status=-1,f.call(b,c,d,e)):(a.$$state.value=b,a.$$state.status=1,h(a.$$state))}catch(k){d(k)}}function m(a,b){a.$$state.status||n(a,b)}function n(a,b){a.$$state.value=b;a.$$state.status=2;h(a.$$state)}function p(c,d){var e=
+c.$$state.pending;0>=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;f<g;f++){c=e[f][0];a=e[f][3];try{p(c,D(a)?a(d):d)}catch(h){b(h)}}})}function r(a){var b=new f;m(b,a);return b}function J(a,b,c){var d=null;try{D(c)&&(d=c())}catch(e){return r(e)}return d&&D(d.then)?d.then(function(){return b(a)},r):b(a)}function v(a,b,c,d){var e=new f;k(e,a);return e.then(b,c,d)}function t(a){if(!D(a))throw M("norslvr",a);var b=new f;a(function(a){k(b,a)},function(a){m(b,a)});return b}var M=
+L("$q",TypeError),s=0,A=[];S(f.prototype,{then:function(a,b,c){if(w(a)&&w(b)&&w(c))return this;var d=new f;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&h(this.$$state);return d},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return J(b,u,a)},function(b){return J(b,r,a)},b)}});var u=v;t.prototype=f.prototype;t.defer=c;t.reject=r;t.when=v;t.resolve=u;t.all=function(a){var b=new f,c=0,d=H(a)?
+[]:{};q(a,function(a,e){c++;v(a).then(function(a){d[e]=a;--c||k(b,d)},function(a){m(b,a)})});0===c&&k(b,d);return b};t.race=function(a){var b=c();q(a,function(a){v(a).then(b.resolve,b.reject)});return b.promise};return t}function Xf(){this.$get=["$window","$timeout",function(a,b){var d=a.requestAnimationFrame||a.webkitRequestAnimationFrame,c=a.cancelAnimationFrame||a.webkitCancelAnimationFrame||a.webkitCancelRequestAnimationFrame,e=!!d,f=e?function(a){var b=d(a);return function(){c(b)}}:function(a){var c=
+b(a,16.66,!1);return function(){b.cancel(c)}};f.supported=e;return f}]}function Mf(){function a(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++qb;this.$$ChildScope=null}b.prototype=a;return b}var b=10,d=L("$rootScope"),c=null,e=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$exceptionHandler","$parse","$browser",function(f,g,h){function k(a){a.currentScope.$$destroyed=
+!0}function l(a){9===za&&(a.$$childHead&&l(a.$$childHead),a.$$nextSibling&&l(a.$$nextSibling));a.$parent=a.$$nextSibling=a.$$prevSibling=a.$$childHead=a.$$childTail=a.$root=a.$$watchers=null}function m(){this.$id=++qb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$$isolateBindings=null}function n(a){if(M.$$phase)throw d("inprog",
+M.$$phase);M.$$phase=a}function p(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function r(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function J(){}function v(){for(;u.length;)try{u.shift()()}catch(a){f(a)}e=null}function t(){null===e&&(e=h.defer(function(){M.$apply(v)}))}m.prototype={constructor:m,$new:function(b,c){var d;c=c||this;b?(d=new m,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=a(this)),d=new this.$$ChildScope);
+d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(b||c!==this)&&d.$on("$destroy",k);return d},$watch:function(a,b,d,e){var f=g(a);if(f.$$watchDelegate)return f.$$watchDelegate(this,b,d,f,a);var h=this,k=h.$$watchers,l={fn:b,last:J,get:f,exp:e||a,eq:!!d};c=null;D(b)||(l.fn=z);k||(k=h.$$watchers=[],k.$$digestWatchIndex=-1);k.unshift(l);k.$$digestWatchIndex++;p(this,1);return function(){var a=$a(k,l);0<=a&&(p(h,-1),
+a<k.$$digestWatchIndex&&k.$$digestWatchIndex--);c=null}},$watchGroup:function(a,b){function c(){h=!1;k?(k=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});q(a,function(a,b){var k=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},
+$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!w(e)){if(C(e))if(qa(e))for(f!==n&&(f=n,t=f.length=0,l++),a=e.length,t!==a&&(l++,f.length=t=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==p&&(f=p={},t=0,l++);a=0;for(b in e)ua.call(e,b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(t++,f[b]=g,l++));if(t>a)for(b in l++,f)ua.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1<b.length,l=0,m=
+g(a,c),n=[],p={},r=!0,t=0;return this.$watch(m,function(){r?(r=!1,b(e,e,d)):b(e,h,d);if(k)if(C(e))if(qa(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else for(a in h={},e)ua.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var a,g,k,l,m,p,r,t=b,q,u=[],w,x;n("$digest");h.$$checkUrlChange();this===M&&null!==e&&(h.defer.cancel(e),v());c=null;do{r=!1;q=this;for(p=0;p<s.length;p++){try{x=s[p],l=x.fn,l(x.scope,x.locals)}catch(z){f(z)}c=null}s.length=0;a:do{if(p=q.$$watchers)for(p.$$digestWatchIndex=
+p.length;p.$$digestWatchIndex--;)try{if(a=p[p.$$digestWatchIndex])if(m=a.get,(g=m(q))!==(k=a.last)&&!(a.eq?sa(g,k):da(g)&&da(k)))r=!0,c=a,a.last=a.eq?ra(g,null):g,l=a.fn,l(g,k===J?g:k,q),5>t&&(w=4-t,u[w]||(u[w]=[]),u[w].push({msg:D(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:k}));else if(a===c){r=!1;break a}}catch(B){f(B)}if(!(p=q.$$watchersCount&&q.$$childHead||q!==this&&q.$$nextSibling))for(;q!==this&&!(p=q.$$nextSibling);)q=q.$parent}while(q=p);if((r||s.length)&&!t--)throw M.$$phase=
+null,d("infdig",b,u);}while(r||s.length);for(M.$$phase=null;I<A.length;)try{A[I++]()}catch(F){f(F)}A.length=I=0;h.$$checkUrlChange()},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===M&&h.$$applicationDestroyed();p(this,-this.$$watchersCount);for(var b in this.$$listenerCount)r(this,this.$$listenerCount[b],b);a&&a.$$childHead===this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail===this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&
+(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=z;this.$on=this.$watch=this.$watchGroup=function(){return z};this.$$listeners={};this.$$nextSibling=null;l(this)}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){M.$$phase||s.length||h.defer(function(){s.length&&M.$digest()});s.push({scope:this,fn:g(a),locals:b})},$$postDigest:function(a){A.push(a)},
+$apply:function(a){try{n("$apply");try{return this.$eval(a)}finally{M.$$phase=null}}catch(b){f(b)}finally{try{M.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&u.push(b);a=g(a);t()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,r(e,1,a))}},$emit:function(a,
+b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=ab([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;l<m;l++)if(d[l])try{d[l].apply(null,k)}catch(n){f(n)}else d.splice(l,1),l--,m--;if(g)return h.currentScope=null,h;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=
+!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var g=ab([e],arguments,1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,g)}catch(l){f(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var M=new m,s=M.$$asyncQueue=[],A=M.$$postDigestQueue=[],u=M.$$applyAsyncQueue=[],I=0;return M}]}function Ee(){var a=
+/^\s*(https?|ftp|mailto|tel|file):/,b=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(b){return u(b)?(a=b,this):a};this.imgSrcSanitizationWhitelist=function(a){return u(a)?(b=a,this):b};this.$get=function(){return function(d,c){var e=c?b:a,f;f=Ca(d).href;return""===f||f.match(e)?d:"unsafe:"+f}}}function Cg(a){if("self"===a)return a;if(F(a)){if(-1<a.indexOf("***"))throw ta("iwcard",a);a=Kd(a).replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^:/.?&;]*");return new RegExp("^"+
+a+"$")}if(Xa(a))return new RegExp("^"+a.source+"$");throw ta("imatcher");}function Ld(a){var b=[];u(a)&&q(a,function(a){b.push(Cg(a))});return b}function Qf(){this.SCE_CONTEXTS=oa;var a=["self"],b=[];this.resourceUrlWhitelist=function(b){arguments.length&&(a=Ld(b));return a};this.resourceUrlBlacklist=function(a){arguments.length&&(b=Ld(a));return b};this.$get=["$injector",function(d){function c(a,b){return"self"===a?yd(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=
+function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var f=function(a){throw ta("unsafe");};d.has("$sanitize")&&(f=d.get("$sanitize"));var g=e(),h={};h[oa.HTML]=e(g);h[oa.CSS]=e(g);h[oa.URL]=e(g);h[oa.JS]=e(g);h[oa.RESOURCE_URL]=e(h[oa.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw ta("icontext",a,b);if(null===b||w(b)||
+""===b)return b;if("string"!==typeof b)throw ta("itype",a);return new c(b)},getTrusted:function(d,e){if(null===e||w(e)||""===e)return e;var g=h.hasOwnProperty(d)?h[d]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();if(d===oa.RESOURCE_URL){var g=Ca(e.toString()),n,p,r=!1;n=0;for(p=a.length;n<p;n++)if(c(a[n],g)){r=!0;break}if(r)for(n=0,p=b.length;n<p;n++)if(c(b[n],g)){r=!1;break}if(r)return e;throw ta("insecurl",e.toString());}if(d===oa.HTML)return f(e);throw ta("unsafe");},valueOf:function(a){return a instanceof
+g?a.$$unwrapTrustedValue():a}}}]}function Pf(){var a=!0;this.enabled=function(b){arguments.length&&(a=!!b);return a};this.$get=["$parse","$sceDelegate",function(b,d){if(a&&8>za)throw ta("iequirks");var c=pa(oa);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ya);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,
+f=c.getTrusted,g=c.trustAs;q(oa,function(a,b){var d=Q(b);c[("parse_as_"+d).replace(xc,gb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(xc,gb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(xc,gb)]=function(b){return g(a,b)}});return c}]}function Rf(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=Z((/android (\d+)/.exec(Q((a.navigator||
+{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},h=g.body&&g.body.style,k=!1,l=!1;h&&(k=!!("transition"in h||"webkitTransition"in h),l=!!("animation"in h||"webkitAnimation"in h));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&za)return!1;if(w(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ga(),transitions:k,animations:l,android:e}}]}function Tf(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler",
+"$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(h,k){g.totalPendingRequests++;if(!F(h)||w(d.get(h)))h=f.getTrustedResourceUrl(h);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==nc}):l===nc&&(l=null);return c.get(h,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){d.put(h,a.data);return a.data},function(a){k||(a=Dg("tpload",h,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=
+0;return g}]}function Uf(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];q(a,function(a){var c=ea.element(a).data("$binding");c&&q(c,function(c){d?(new RegExp("(^|\\s)"+Kd(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var k=a.querySelectorAll("["+g[h]+"model"+(d?"=":"*=")+'"'+b+
+'"]');if(k.length)return k}},getLocation:function(){return d.url()},setLocation:function(b){b!==d.url()&&(d.url(b),a.$digest())},whenStable:function(a){b.notifyWhenNoOutstandingRequests(a)}}}]}function Vf(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(a,b,d,c,e){function f(f,k,l){D(f)||(l=k,k=f,f=z);var m=va.call(arguments,3),n=u(l)&&!l,p=(n?c:d).defer(),r=p.promise,q;q=b.defer(function(){try{p.resolve(f.apply(null,m))}catch(b){p.reject(b),e(b)}finally{delete g[r.$$timeoutId]}n||
+a.$apply()},k);r.$$timeoutId=q;g[q]=p;return r}var g={};f.cancel=function(a){return a&&a.$$timeoutId in g?(g[a.$$timeoutId].promise.catch(z),g[a.$$timeoutId].reject("canceled"),delete g[a.$$timeoutId],b.defer.cancel(a.$$timeoutId)):!1};return f}]}function Ca(a){za&&(aa.setAttribute("href",a),a=aa.href);aa.setAttribute("href",a);return{href:aa.href,protocol:aa.protocol?aa.protocol.replace(/:$/,""):"",host:aa.host,search:aa.search?aa.search.replace(/^\?/,""):"",hash:aa.hash?aa.hash.replace(/^#/,""):
+"",hostname:aa.hostname,port:aa.port,pathname:"/"===aa.pathname.charAt(0)?aa.pathname:"/"+aa.pathname}}function yd(a){a=F(a)?Ca(a):a;return a.protocol===Md.protocol&&a.host===Md.host}function Wf(){this.$get=la(x)}function Nd(a){function b(a){try{return decodeURIComponent(a)}catch(b){return a}}var d=a[0]||{},c={},e="";return function(){var a,g,h,k,l;try{a=d.cookie||""}catch(m){a=""}if(a!==e)for(e=a,a=e.split("; "),c={},h=0;h<a.length;h++)g=a[h],k=g.indexOf("="),0<k&&(l=b(g.substring(0,k)),w(c[l])&&
+(c[l]=b(g.substring(k+1))));return c}}function $f(){this.$get=Nd}function cd(a){function b(d,c){if(C(d)){var e={};q(d,function(a,c){e[c]=b(c,a)});return e}return a.factory(d+"Filter",c)}this.register=b;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];b("currency",Od);b("date",Pd);b("filter",Eg);b("json",Fg);b("limitTo",Gg);b("lowercase",Hg);b("number",Qd);b("orderBy",Rd);b("uppercase",Ig)}function Eg(){return function(a,b,d,c){if(!qa(a)){if(null==a)return a;throw L("filter")("notarray",
+a);}c=c||"$";var e;switch(yc(b)){case "function":break;case "boolean":case "null":case "number":case "string":e=!0;case "object":b=Jg(b,d,c,e);break;default:return a}return Array.prototype.filter.call(a,b)}}function Jg(a,b,d,c){var e=C(a)&&d in a;!0===b?b=sa:D(b)||(b=function(a,b){if(w(a))return!1;if(null===a||null===b)return a===b;if(C(b)||C(a)&&!Wb(a))return!1;a=Q(""+a);b=Q(""+b);return-1!==a.indexOf(b)});return function(f){return e&&!C(f)?Ea(f,a[d],b,d,!1):Ea(f,a,b,d,c)}}function Ea(a,b,d,c,e,
+f){var g=yc(a),h=yc(b);if("string"===h&&"!"===b.charAt(0))return!Ea(a,b.substring(1),d,c,e);if(H(a))return a.some(function(a){return Ea(a,b,d,c,e)});switch(g){case "object":var k;if(e){for(k in a)if(k.charAt&&"$"!==k.charAt(0)&&Ea(a[k],b,d,c,!0))return!0;return f?!1:Ea(a,b,d,c,!1)}if("object"===h){for(k in b)if(f=b[k],!D(f)&&!w(f)&&(g=k===c,!Ea(g?a:a[k],f,d,c,g,g)))return!1;return!0}return d(a,b);case "function":return!1;default:return d(a,b)}}function yc(a){return null===a?"null":typeof a}function Od(a){var b=
+a.NUMBER_FORMATS;return function(a,c,e){w(c)&&(c=b.CURRENCY_SYM);w(e)&&(e=b.PATTERNS[1].maxFrac);return null==a?a:Sd(a,b.PATTERNS[1],b.GROUP_SEP,b.DECIMAL_SEP,e).replace(/\u00A4/g,c)}}function Qd(a){var b=a.NUMBER_FORMATS;return function(a,c){return null==a?a:Sd(a,b.PATTERNS[0],b.GROUP_SEP,b.DECIMAL_SEP,c)}}function Kg(a){var b=0,d,c,e,f,g;-1<(c=a.indexOf(Td))&&(a=a.replace(Td,""));0<(e=a.search(/e/i))?(0>c&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===zc;e++);
+if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===zc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Ud&&(d=d.splice(0,Ud-1),b=c-1,c=1);return{d:d,e:b,i:c}}function Lg(a,b,d,c){var e=a.d,f=e.length-a.i;b=w(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0<d){e.splice(Math.max(a.i,d));for(var g=d;g<e.length;g++)e[g]=0}else for(f=Math.max(0,f),a.i=1,e.length=Math.max(1,d=b+1),e[0]=0,g=1;g<d;g++)e[g]=0;if(5<=c)if(0>d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-
+1]++;for(;f<Math.max(0,b);f++)e.push(0);if(b=e.reduceRight(function(a,b,c,d){b+=a;d[c]=b%10;return Math.floor(b/10)},0))e.unshift(b),a.i++}function Sd(a,b,d,c,e){if(!F(a)&&!ba(a)||isNaN(a))return"";var f=!isFinite(a),g=!1,h=Math.abs(a)+"",k="";if(f)k="\u221e";else{g=Kg(h);Lg(g,e,b.minFrac,b.maxFrac);k=g.d;h=g.i;e=g.e;f=[];for(g=k.reduce(function(a,b){return a&&!b},!0);0>h;)k.unshift(0),h++;0<h?f=k.splice(h,k.length):(f=k,k=[0]);h=[];for(k.length>=b.lgSize&&h.unshift(k.splice(-b.lgSize,k.length).join(""));k.length>
+b.gSize;)h.unshift(k.splice(-b.gSize,k.length).join(""));k.length&&h.unshift(k.join(""));k=h.join(d);f.length&&(k+=c+f.join(""));e&&(k+="e+"+e)}return 0>a&&!g?b.negPre+k+b.negSuf:b.posPre+k+b.posSuf}function Kb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length<b;)a=zc+a;d&&(a=a.substr(a.length-b));return e+a}function Y(a,b,d,c,e){d=d||0;return function(f){f=f["get"+a]();if(0<d||f>-d)f+=d;0===f&&-12===d&&(f=12);return Kb(f,b,c,e)}}function mb(a,b,d){return function(c,e){var f=
+c["get"+a](),g=ub((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Vd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Wd(a){return function(b){var d=Vd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Kb(b,a)}}function Ac(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Pd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,
+k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Z(b[9]+b[10]),g=Z(b[9]+b[11]));h.call(a,Z(b[1]),Z(b[2])-1,Z(b[3]));f=Z(b[4]||0)-f;g=Z(b[5]||0)-g;h=Z(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;F(c)&&(c=Mg.test(c)?Z(c):b(c));ba(c)&&(c=new Date(c));if(!ga(c)||!isFinite(c.getTime()))return c;
+for(;d;)(l=Ng.exec(d))?(h=ab(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=Pc(f,m),c=Yb(c,f,!0));q(h,function(b){k=Og[b];g+=k?k(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Fg(){return function(a,b){w(b)&&(b=2);return cb(a,b)}}function Gg(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):Z(b);if(da(b))return a;ba(a)&&(a=a.toString());if(!qa(a))return a;d=!d||isNaN(d)?0:Z(d);d=0>d?Math.max(0,a.length+
+d):d;return 0<=b?Bc(a,d,d+b):0===d?Bc(a,b,a.length):Bc(a,Math.max(0,d+b),d)}}function Bc(a,b,d){return F(a)?a.slice(b,d):va.call(a,b,d)}function Rd(a){function b(b){return b.map(function(b){var c=1,d=Ya;if(D(b))d=b;else if(F(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}
+function c(a,b){var c=0,d=a.type,k=b.type;if(d===k){var k=a.value,l=b.value;"string"===d?(k=k.toLowerCase(),l=l.toLowerCase()):"object"===d&&(C(k)&&(k=a.index),C(l)&&(l=b.index));k!==l&&(c=k<l?-1:1)}else c=d<k?-1:1;return c}return function(a,f,g,h){if(null==a)return a;if(!qa(a))throw L("orderBy")("notarray",a);H(f)||(f=[f]);0===f.length&&(f=["+"]);var k=b(f),l=g?-1:1,m=D(h)?h:c;a=Array.prototype.map.call(a,function(a,b){return{value:a,tieBreaker:{value:b,type:"number",index:b},predicateValues:k.map(function(c){var e=
+c.get(a);c=typeof e;if(null===e)c="string",e="null";else if("object"===c)a:{if(D(e.valueOf)&&(e=e.valueOf(),d(e)))break a;Wb(e)&&(e=e.toString(),d(e))}return{value:e,type:c,index:b}})}});a.sort(function(a,b){for(var c=0,d=k.length;c<d;c++){var e=m(a.predicateValues[c],b.predicateValues[c]);if(e)return e*k[c].descending*l}return m(a.tieBreaker,b.tieBreaker)*l});return a=a.map(function(a){return a.value})}}function Qa(a){D(a)&&(a={link:a});a.restrict=a.restrict||"AC";return la(a)}function Lb(a,b,d,
+c,e){this.$$controls=[];this.$error={};this.$$success={};this.$pending=void 0;this.$name=e(b.name||b.ngForm||"")(d);this.$dirty=!1;this.$valid=this.$pristine=!0;this.$submitted=this.$invalid=!1;this.$$parentForm=Mb;this.$$element=a;this.$$animate=c;Xd(this)}function Xd(a){a.$$classCache={};a.$$classCache[Yd]=!(a.$$classCache[nb]=a.$$element.hasClass(nb))}function Zd(a){function b(a,b,c){c&&!a.$$classCache[b]?(a.$$animate.addClass(a.$$element,b),a.$$classCache[b]=!0):!c&&a.$$classCache[b]&&(a.$$animate.removeClass(a.$$element,
+b),a.$$classCache[b]=!1)}function d(a,c,d){c=c?"-"+Tc(c,"-"):"";b(a,nb+c,!0===d);b(a,Yd+c,!1===d)}var c=a.set,e=a.unset;a.clazz.prototype.$setValidity=function(a,g,h){w(g)?(this.$pending||(this.$pending={}),c(this.$pending,a,h)):(this.$pending&&e(this.$pending,a,h),$d(this.$pending)&&(this.$pending=void 0));Ha(g)?g?(e(this.$error,a,h),c(this.$$success,a,h)):(c(this.$error,a,h),e(this.$$success,a,h)):(e(this.$error,a,h),e(this.$$success,a,h));this.$pending?(b(this,"ng-pending",!0),this.$valid=this.$invalid=
+void 0,d(this,"",null)):(b(this,"ng-pending",!1),this.$valid=$d(this.$error),this.$invalid=!this.$valid,d(this,"",this.$valid));g=this.$pending&&this.$pending[a]?void 0:this.$error[a]?!1:this.$$success[a]?!0:null;d(this,a,g);this.$$parentForm.$setValidity(a,g,this)}}function $d(a){if(a)for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}function Cc(a){a.$formatters.push(function(b){return a.$isEmpty(b)?b:b.toString()})}function Ra(a,b,d,c,e,f){var g=Q(b[0].type);if(!e.android){var h=!1;b.on("compositionstart",
+function(){h=!0});b.on("compositionend",function(){h=!1;l()})}var k,l=function(a){k&&(f.defer.cancel(k),k=null);if(!h){var e=b.val();a=a&&a.type;"password"===g||d.ngTrim&&"false"===d.ngTrim||(e=T(e));(c.$viewValue!==e||""===e&&c.$$hasNativeValidators)&&c.$setViewValue(e,a)}};if(e.hasEvent("input"))b.on("input",l);else{var m=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.value===c||l(a)}))};b.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut",
+m)}b.on("change",l);if(ae[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!k){var b=this.validity,c=b.badInput,d=b.typeMismatch;k=f.defer(function(){k=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Nb(a,b){return function(d,c){var e,f;if(ga(d))return d;if(F(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(Pg.test(d))return new Date(d);
+a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},q(e,function(a,c){c<b.length&&(f[b[c]]=+a)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function ob(a,b,d,c){return function(e,f,g,h,k,l,m){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function p(a){return u(a)&&!ga(a)?d(a)||
+void 0:a}Dc(e,f,g,h);Ra(e,f,g,h,k,l);var r=h&&h.$options.getOption("timezone"),q;h.$$parserName=a;h.$parsers.push(function(a){if(h.$isEmpty(a))return null;if(b.test(a))return a=d(a,q),r&&(a=Yb(a,r)),a});h.$formatters.push(function(a){if(a&&!ga(a))throw pb("datefmt",a);if(n(a))return(q=a)&&r&&(q=Yb(q,r,!0)),m("date")(a,c,r);q=null;return""});if(u(g.min)||g.ngMin){var v;h.$validators.min=function(a){return!n(a)||w(v)||d(a)>=v};g.$observe("min",function(a){v=p(a);h.$validate()})}if(u(g.max)||g.ngMax){var t;
+h.$validators.max=function(a){return!n(a)||w(t)||d(a)<=t};g.$observe("max",function(a){t=p(a);h.$validate()})}}}function Dc(a,b,d,c){(c.$$hasNativeValidators=C(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};return c.badInput||c.typeMismatch?void 0:a})}function be(a){a.$$parserName="number";a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(Qg.test(b))return parseFloat(b)});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!ba(b))throw pb("numfmt",b);b=b.toString()}return b})}
+function Sa(a){u(a)&&!ba(a)&&(a=parseFloat(a));return da(a)?void 0:a}function Ec(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1<a&&1>a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ce(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Ec(a):0,h=e?Ec(b):0,k=f?Ec(d):0,g=Math.max(g,h,k),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function de(a,b,d,c,e){if(u(c)){a=a(c);if(!a.constant)throw pb("constexpr",
+d,c);return a(b)}return e}function Fc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e===b[m])continue a;c.push(e)}return c}function c(a){var b=a;H(a)?b=a.map(c).join(" "):C(a)&&(b=Object.keys(a).filter(function(b){return a[b]}).join(" "));return b}a="ngClass"+a;var e;return["$parse",function(f){return{restrict:"AC",link:function(g,h,k){function l(a,b){var c=[];q(a,function(a){if(0<b||n[a])n[a]=(n[a]||
+0)+b,n[a]===+(0<b)&&c.push(a)});return c.join(" ")}function m(a){if(a===b){var c=r,c=l(c&&c.split(" "),1);k.$addClass(c)}else c=r,c=l(c&&c.split(" "),-1),k.$removeClass(c);p=a}var n=h.data("$classCounts"),p=!0,r;n||(n=V(),h.data("$classCounts",n));"ngClass"!==a&&(e||(e=f("$index",function(a){return a&1})),g.$watch(e,m));g.$watch(f(k[a],c),function(a){F(a)||(a=c(a));if(p===b){var e=a,f=r&&r.split(" "),g=e&&e.split(" "),e=d(f,g),f=d(g,f),e=l(e,-1),f=l(f,1);k.$addClass(f);k.$removeClass(e)}r=a})}}}]}
+function Ob(a,b,d,c,e,f,g,h,k){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=void 0;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=void 0;this.$name=k(d.name||"",!1)(a);this.$$parentForm=Mb;this.$options=Pb;this.$$parsedNgModel=e(d.ngModel);this.$$parsedNgModelAssign=this.$$parsedNgModel.assign;
+this.$$ngModelGet=this.$$parsedNgModel;this.$$ngModelSet=this.$$parsedNgModelAssign;this.$$pendingDebounce=null;this.$$parserValid=void 0;this.$$currentValidationRunId=0;Object.defineProperty(this,"$$scope",{value:a});this.$$attr=d;this.$$element=c;this.$$animate=f;this.$$timeout=g;this.$$parse=e;this.$$q=h;this.$$exceptionHandler=b;Xd(this);Rg(this)}function Rg(a){a.$$scope.$watch(function(b){b=a.$$ngModelGet(b);if(b!==a.$modelValue&&(a.$modelValue===a.$modelValue||b===b)){a.$modelValue=a.$$rawModelValue=
+b;a.$$parserValid=void 0;for(var d=a.$formatters,c=d.length,e=b;c--;)e=d[c](e);a.$viewValue!==e&&(a.$$updateEmptyClasses(e),a.$viewValue=a.$$lastCommittedViewValue=e,a.$render(),a.$$runValidators(a.$modelValue,a.$viewValue,z))}return b})}function Gc(a){this.$$options=a}function ee(a,b){q(b,function(b,c){u(a[c])||(a[c]=b)})}function Ta(a,b){a.prop("selected",b);a.attr("selected",b)}var Sg=/^\/(.+)\/([a-z]*)$/,ua=Object.prototype.hasOwnProperty,Ic={objectMaxDepth:5},Q=function(a){return F(a)?a.toLowerCase():
+a},ub=function(a){return F(a)?a.toUpperCase():a},za,B,na,va=[].slice,sg=[].splice,Tg=[].push,ma=Object.prototype.toString,Mc=Object.getPrototypeOf,Fa=L("ng"),ea=x.angular||(x.angular={}),ac,qb=0;za=x.document.documentMode;var da=Number.isNaN||function(a){return a!==a};z.$inject=[];Ya.$inject=[];var H=Array.isArray,qe=/^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/,T=function(a){return F(a)?a.trim():a},Kd=function(a){return a.replace(/([-()[\]{}+?*.$^|,:#<!\\])/g,
+"\\$1").replace(/\x08/g,"\\x08")},Ga=function(){if(!u(Ga.rules)){var a=x.document.querySelector("[ng-csp]")||x.document.querySelector("[data-ng-csp]");if(a){var b=a.getAttribute("ng-csp")||a.getAttribute("data-ng-csp");Ga.rules={noUnsafeEval:!b||-1!==b.indexOf("no-unsafe-eval"),noInlineStyle:!b||-1!==b.indexOf("no-inline-style")}}else{a=Ga;try{new Function(""),b=!1}catch(d){b=!0}a.rules={noUnsafeEval:b,noInlineStyle:!1}}}return Ga.rules},rb=function(){if(u(rb.name_))return rb.name_;var a,b,d=Ja.length,
+c,e;for(b=0;b<d;++b)if(c=Ja[b],a=x.document.querySelector("["+c.replace(":","\\:")+"jq]")){e=a.getAttribute(c+"jq");break}return rb.name_=e},se=/:/g,Ja=["ng-","data-ng-","ng:","x-ng-"],ve=function(a){var b=a.currentScript;if(!b)return!0;if(!(b instanceof x.HTMLScriptElement||b instanceof x.SVGScriptElement))return!1;b=b.attributes;return[b.getNamedItem("src"),b.getNamedItem("href"),b.getNamedItem("xlink:href")].every(function(b){if(!b)return!0;if(!b.value)return!1;var c=a.createElement("a");c.href=
+b.value;if(a.location.origin===c.origin)return!0;switch(c.protocol){case "http:":case "https:":case "ftp:":case "blob:":case "file:":case "data:":return!0;default:return!1}})}(x.document),ye=/[A-Z]/g,Uc=!1,Ia=3,De={full:"1.6.4",major:1,minor:6,dot:4,codeName:"phenomenal-footnote"};W.expando="ng339";var hb=W.cache={},eg=1;W._data=function(a){return this.cache[a[this.expando]]||{}};var ag=/-([a-z])/g,Ug=/^-ms-/,zb={mouseleave:"mouseout",mouseenter:"mouseover"},dc=L("jqLite"),dg=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,
+cc=/<|&#?\w+;/,bg=/<([\w:-]+)/,cg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ha={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ha.optgroup=ha.option;ha.tbody=ha.tfoot=ha.colgroup=ha.caption=ha.thead;ha.th=ha.td;var jg=x.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&
+16)},Na=W.prototype={ready:ed,toString:function(){var a=[];q(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?B(this[a]):B(this[this.length+a])},length:0,push:Tg,sort:[].sort,splice:[].splice},Fb={};q("multiple selected checked disabled readOnly required open".split(" "),function(a){Fb[Q(a)]=a});var jd={};q("input select option textarea button form details".split(" "),function(a){jd[a]=!0});var rd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",
+ngPattern:"pattern",ngStep:"step"};q({data:hc,removeData:gc,hasData:function(a){for(var b in hb[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b<d;b++)gc(a[b])}},function(a,b){W[b]=a});q({data:hc,inheritedData:Db,scope:function(a){return B.data(a,"$scope")||Db(a.parentNode||a,["$isolateScope","$scope"])},isolateScope:function(a){return B.data(a,"$isolateScope")||B.data(a,"$isolateScopeNoTemplate")},controller:gd,injector:function(a){return Db(a,"$injector")},removeAttr:function(a,
+b){a.removeAttribute(b)},hasClass:Ab,css:function(a,b,d){b=wb(b.replace(Ug,"ms-"));if(u(d))a.style[b]=d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Ia&&2!==c&&8!==c&&a.getAttribute){var c=Q(b),e=Fb[c];if(u(d))null===d||!1===d&&e?a.removeAttribute(b):a.setAttribute(b,e?c:d);else return a=a.getAttribute(b),e&&null!==a&&(a=c),null===a?void 0:a}},prop:function(a,b,d){if(u(d))a[b]=d;else return a[b]},text:function(){function a(a,d){if(w(d)){var c=a.nodeType;return 1===c||c===Ia?
+a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(w(b)){if(a.multiple&&"select"===wa(a)){var d=[];q(a.options,function(a){a.selected&&d.push(a.value||a.text)});return d}return a.value}a.value=b},html:function(a,b){if(w(b))return a.innerHTML;xb(a,!0);a.innerHTML=b},empty:hd},function(a,b){W.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==hd&&w(2===a.length&&a!==Ab&&a!==gd?b:c)){if(C(b)){for(e=0;e<g;e++)if(a===hc)a(this[e],b);else for(f in b)a(this[e],f,b[f]);return this}e=
+a.$dv;g=w(e)?Math.min(g,1):g;for(f=0;f<g;f++){var h=a(this[f],b,c);e=e?e+h:h}return e}for(e=0;e<g;e++)a(this[e],b,c);return this}});q({removeData:gc,on:function(a,b,d,c){if(u(c))throw dc("onargs");if(bc(a)){c=yb(a,!0);var e=c.events,f=c.handle;f||(f=c.handle=gg(a,e));c=0<=b.indexOf(" ")?b.split(" "):[b];for(var g=c.length,h=function(b,c,g){var h=e[b];h||(h=e[b]=[],h.specialHandlerWrapper=c,"$destroy"===b||g||a.addEventListener(b,f));h.push(d)};g--;)b=c[g],zb[b]?(h(zb[b],ig),h(b,void 0,!0)):h(b)}},
+off:fd,one:function(a,b,d){a=B(a);a.on(b,function e(){a.off(b,d);a.off(b,e)});a.on(b,d)},replaceWith:function(a,b){var d,c=a.parentNode;xb(a);q(new W(b),function(b){d?c.insertBefore(b,d.nextSibling):c.replaceChild(b,a);d=b})},children:function(a){var b=[];q(a.childNodes,function(a){1===a.nodeType&&b.push(a)});return b},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,b){var d=a.nodeType;if(1===d||11===d){b=new W(b);for(var d=0,c=b.length;d<c;d++)a.appendChild(b[d])}},
+prepend:function(a,b){if(1===a.nodeType){var d=a.firstChild;q(new W(b),function(b){a.insertBefore(b,d)})}},wrap:function(a,b){var d=B(b).eq(0).clone()[0],c=a.parentNode;c&&c.replaceChild(d,a);d.appendChild(a)},remove:Eb,detach:function(a){Eb(a,!0)},after:function(a,b){var d=a,c=a.parentNode;if(c){b=new W(b);for(var e=0,f=b.length;e<f;e++){var g=b[e];c.insertBefore(g,d.nextSibling);d=g}}},addClass:Cb,removeClass:Bb,toggleClass:function(a,b,d){b&&q(b.split(" "),function(b){var e=d;w(e)&&(e=!Ab(a,b));
+(e?Cb:Bb)(a,b)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,b){return a.getElementsByTagName?a.getElementsByTagName(b):[]},clone:fc,triggerHandler:function(a,b,d){var c,e,f=b.type||b,g=yb(a);if(g=(g=g&&g.events)&&g[f])c={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===
+this.immediatePropagationStopped},stopPropagation:z,type:f,target:a},b.type&&(c=S(c,b)),b=pa(g),e=d?[c].concat(d):[c],q(b,function(b){c.isImmediatePropagationStopped()||b.apply(a,e)})}},function(a,b){W.prototype[b]=function(b,c,e){for(var f,g=0,h=this.length;g<h;g++)w(f)?(f=a(this[g],b,c,e),u(f)&&(f=B(f))):ec(f,a(this[g],b,c,e));return u(f)?f:this}});W.prototype.bind=W.prototype.on;W.prototype.unbind=W.prototype.off;var Vg=Object.create(null);kd.prototype={_idx:function(a){if(a===this._lastKey)return this._lastIndex;
+this._lastKey=a;return this._lastIndex=this._keys.indexOf(a)},_transformKey:function(a){return da(a)?Vg:a},get:function(a){a=this._transformKey(a);a=this._idx(a);if(-1!==a)return this._values[a]},set:function(a,b){a=this._transformKey(a);var d=this._idx(a);-1===d&&(d=this._lastIndex=this._keys.length);this._keys[d]=a;this._values[d]=b},delete:function(a){a=this._transformKey(a);a=this._idx(a);if(-1===a)return!1;this._keys.splice(a,1);this._values.splice(a,1);this._lastKey=NaN;this._lastIndex=-1;return!0}};
+var Gb=kd,Zf=[function(){this.$get=[function(){return Gb}]}],lg=/^([^(]+?)=>/,mg=/^[^(]*\(\s*([^)]*)\)/m,Wg=/,/,Xg=/^\s*(_?)(\S+?)\1\s*$/,kg=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,ya=L("$injector");eb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw F(d)&&d||(d=a.name||ng(a)),ya("strictdi",d);b=ld(a);q(b[1].split(Wg),function(a){a.replace(Xg,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,sb(a[b],"fn"),c=a.slice(0,b)):sb(a,"fn",
+!0);return c};var fe=L("$animate"),qf=function(){this.$get=z},rf=function(){var a=new Gb,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=F(b)?b.split(" "):H(b)?b:[],q(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){q(b,function(b){var c=a.get(b);if(c){var d=og(b.attr("class")),e="",f="";q(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});q(b,function(a){e&&Cb(a,e);f&&Bb(a,f)});a.delete(b)}});b.length=0}return{enabled:z,
+on:z,off:z,pin:z,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.set(g,k),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},of=["$provide",function(a){var b=this,d=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw fe("notcsel",c);var f=c+"-animation";b.$$registeredAnimations[c.substr(1)]=
+f;a.factory(f,d)};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,fe("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e<d.length;e++){var l=d[e];if(1===l.nodeType){e=l;break a}}e=void 0}!e||e.parentNode||e.previousElementSibling||(d=null)}d?d.after(a):c.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.end&&
+a.end()},enter:function(d,g,h,k){g=g&&B(g);h=h&&B(h);g=g||h.parent();b(d,g,h);return a.push(d,"enter",ia(k))},move:function(d,g,h,k){g=g&&B(g);h=h&&B(h);g=g||h.parent();b(d,g,h);return a.push(d,"move",ia(k))},leave:function(b,d){return a.push(b,"leave",ia(d),function(){b.remove()})},addClass:function(b,d,e){e=ia(e);e.addClass=ib(e.addclass,d);return a.push(b,"addClass",e)},removeClass:function(b,d,e){e=ia(e);e.removeClass=ib(e.removeClass,d);return a.push(b,"removeClass",e)},setClass:function(b,d,
+e,k){k=ia(k);k.addClass=ib(k.addClass,d);k.removeClass=ib(k.removeClass,e);return a.push(b,"setClass",k)},animate:function(b,d,e,k,l){l=ia(l);l.from=l.from?S(l.from,d):d;l.to=l.to?S(l.to,e):e;l.tempClasses=ib(l.tempClasses,k||"ng-inline-animate");return a.push(b,"animate",l)}}}]}],tf=function(){this.$get=["$$rAF",function(a){function b(b){d.push(b);1<d.length||a(function(){for(var a=0;a<d.length;a++)d[a]();d=[]})}var d=[];return function(){var a=!1;b(function(){a=!0});return function(d){a?d():b(d)}}}]},
+sf=function(){this.$get=["$q","$sniffer","$$animateAsyncRun","$$isDocumentHidden","$timeout",function(a,b,d,c,e){function f(a){this.setHost(a);var b=d();this._doneCallbacks=[];this._tick=function(a){c()?e(a,0,!1):b(a)};this._state=0}f.chain=function(a,b){function c(){if(d===a.length)b(!0);else a[d](function(a){!1===a?b(!1):(d++,c())})}var d=0;c()};f.all=function(a,b){function c(f){e=e&&f;++d===a.length&&b(e)}var d=0,e=!0;q(a,function(a){a.done(c)})};f.prototype={setHost:function(a){this.host=a||{}},
+done:function(a){2===this._state?a():this._doneCallbacks.push(a)},progress:z,getPromise:function(){if(!this.promise){var b=this;this.promise=a(function(a,c){b.done(function(b){!1===b?c():a()})})}return this.promise},then:function(a,b){return this.getPromise().then(a,b)},"catch":function(a){return this.getPromise()["catch"](a)},"finally":function(a){return this.getPromise()["finally"](a)},pause:function(){this.host.pause&&this.host.pause()},resume:function(){this.host.resume&&this.host.resume()},end:function(){this.host.end&&
+this.host.end();this._resolve(!0)},cancel:function(){this.host.cancel&&this.host.cancel();this._resolve(!1)},complete:function(a){var b=this;0===b._state&&(b._state=1,b._tick(function(){b._resolve(a)}))},_resolve:function(a){2!==this._state&&(q(this._doneCallbacks,function(b){b(a)}),this._doneCallbacks.length=0,this._state=2)}};return f}]},pf=function(){this.$get=["$$rAF","$q","$$AnimateRunner",function(a,b,d){return function(b,e){function f(){a(function(){g.addClass&&(b.addClass(g.addClass),g.addClass=
+null);g.removeClass&&(b.removeClass(g.removeClass),g.removeClass=null);g.to&&(b.css(g.to),g.to=null);h||k.complete();h=!0});return k}var g=e||{};g.$$prepared||(g=ra(g));g.cleanupStyles&&(g.from=g.to=null);g.from&&(b.css(g.from),g.from=null);var h,k=new d;return{start:f,end:f}}}]},fa=L("$compile"),lc=new function(){};Wc.$inject=["$provide","$$sanitizeUriProvider"];Ib.prototype.isFirstChange=function(){return this.previousValue===lc};var md=/^((?:x|data)[:\-_])/i,rg=/[:\-_]+(.)/g,td=L("$controller"),
+sd=/^(\S+)(\s+as\s+([\w$]+))?$/,Af=function(){this.$get=["$document",function(a){return function(b){b?!b.nodeType&&b instanceof B&&(b=b[0]):b=a[0].body;return b.offsetWidth+1}}]},ud="application/json",pc={"Content-Type":ud+";charset=utf-8"},ug=/^\[|^\{(?!\{)/,vg={"[":/]$/,"{":/}$/},tg=/^\)]\}',?\n/,oc=L("$http"),Da=ea.$interpolateMinErr=L("$interpolate");Da.throwNoconcat=function(a){throw Da("noconcat",a);};Da.interr=function(a,b){return Da("interr",a,b.toString())};var If=function(){this.$get=function(){function a(a){var b=
+function(a){b.data=a;b.called=!0};b.id=a;return b}var b=ea.callbacks,d={};return{createCallback:function(c){c="_"+(b.$$counter++).toString(36);var e="angular.callbacks."+c,f=a(c);d[e]=b[c]=f;return e},wasCalled:function(a){return d[a].called},getResponse:function(a){return d[a].data},removeCallback:function(a){delete b[d[a].id];delete d[a]}}}},Yg=/^([^?#]*)(\?([^#]*))?(#(.*))?$/,xg={http:80,https:443,ftp:21},kb=L("$location"),yg=/^\s*[\\/]{2,}/,Zg={$$absUrl:"",$$html5:!1,$$replace:!1,absUrl:Jb("$$absUrl"),
+url:function(a){if(w(a))return this.$$url;var b=Yg.exec(a);(b[1]||""===a)&&this.path(decodeURIComponent(b[1]));(b[2]||b[1]||""===a)&&this.search(b[3]||"");this.hash(b[5]||"");return this},protocol:Jb("$$protocol"),host:Jb("$$host"),port:Jb("$$port"),path:Cd("$$path",function(a){a=null!==a?a.toString():"";return"/"===a.charAt(0)?a:"/"+a}),search:function(a,b){switch(arguments.length){case 0:return this.$$search;case 1:if(F(a)||ba(a))a=a.toString(),this.$$search=Rc(a);else if(C(a))a=ra(a,{}),q(a,function(b,
+c){null==b&&delete a[c]}),this.$$search=a;else throw kb("isrcharg");break;default:w(b)||null===b?delete this.$$search[a]:this.$$search[a]=b}this.$$compose();return this},hash:Cd("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};q([Bd,tc,sc],function(a){a.prototype=Object.create(Zg);a.prototype.state=function(b){if(!arguments.length)return this.$$state;if(a!==sc||!this.$$html5)throw kb("nostate");this.$$state=w(b)?null:b;this.$$urlUpdatedByLocation=
+!0;return this}});var Ua=L("$parse"),Bg={}.constructor.prototype.valueOf,Qb=V();q("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Qb[a]=!0});var $g={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},wc=function(a){this.options=a};wc.prototype={constructor:wc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();
+else if(this.isIdentifierStart(this.peekMultichar()))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var b=a+this.peek(),d=b+this.peek(2),c=Qb[b],e=Qb[d];Qb[a]||c||e?(a=e?d:c?b:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,b){return-1!==b.indexOf(a)},peek:function(a){a=
+a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?
+this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===
+a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=u(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ua("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<this.text.length;){var d=Q(this.text.charAt(this.index));if("."===d||this.isNumber(d))a+=d;else{var c=this.peek();if("e"===d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"===a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||
+c&&this.isNumber(c)||"e"!==a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,text:a,constant:!0,value:Number(a)})},readIdent:function(){var a=this.index;for(this.index+=this.peekMultichar().length;this.index<this.text.length;){var b=this.peekMultichar();if(!this.isIdentifierContinue(b))break;this.index+=b.length}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;
+for(var d="",c=a,e=!1;this.index<this.text.length;){var f=this.text.charAt(this.index),c=c+f;if(e)"u"===f?(e=this.text.substring(this.index+1,this.index+5),e.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+e+"]"),this.index+=4,d+=String.fromCharCode(parseInt(e,16))):d+=$g[f]||f,e=!1;else if("\\"===f)e=!0;else{if(f===a){this.index++;this.tokens.push({index:b,text:c,constant:!0,value:d});return}d+=f}this.index++}this.throwError("Unterminated quote",b)}};var s=function(a,b){this.lexer=
+a;this.options=b};s.Program="Program";s.ExpressionStatement="ExpressionStatement";s.AssignmentExpression="AssignmentExpression";s.ConditionalExpression="ConditionalExpression";s.LogicalExpression="LogicalExpression";s.BinaryExpression="BinaryExpression";s.UnaryExpression="UnaryExpression";s.CallExpression="CallExpression";s.MemberExpression="MemberExpression";s.Identifier="Identifier";s.Literal="Literal";s.ArrayExpression="ArrayExpression";s.Property="Property";s.ObjectExpression="ObjectExpression";
+s.ThisExpression="ThisExpression";s.LocalsExpression="LocalsExpression";s.NGValueParameter="NGValueParameter";s.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:s.Program,body:a}},expressionStatement:function(){return{type:s.ExpressionStatement,
+expression:this.filterChain()}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();if(this.expect("=")){if(!Fd(a))throw Ua("lval");a={type:s.AssignmentExpression,left:a,right:this.assignment(),operator:"="}}return a},ternary:function(){var a=this.logicalOR(),b,d;return this.expect("?")&&(b=this.expression(),this.consume(":"))?(d=this.expression(),{type:s.ConditionalExpression,
+test:a,alternate:b,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={type:s.LogicalExpression,operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:s.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),b;b=this.expect("==","!=","===","!==");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.relational()};
+return a},relational:function(){for(var a=this.additive(),b;b=this.expect("<",">","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},
+unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=ra(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:s.Literal,value:this.options.literals[this.consume().text]}:
+this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");
+return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},
+arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:s.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?
+(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}},throwError:function(a,b){throw Ua("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ua("ueoe",
+this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ua("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:s.ThisExpression},
+$locals:{type:s.LocalsExpression}}};Hd.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};U(a,b.$filter);var d="",c;this.stage="assign";if(c=Gd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Ed(a.body);b.stage="inputs";q(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var h=b.nextId();
+b.recurse(a,h);b.return_(h);b.state.inputs.push(d);a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,zg,Ag,Dd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;q(b,function(b){a.push("var "+
+b+"="+d.generateFunction(b,"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;q(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},
+recurse:function(a,b,d,c,e,f){var g,h,k=this,l,m,n;c=c||z;if(!f&&u(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case s.Program:q(a.body,function(b,c){k.recurse(b.expression,void 0,void 0,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case s.UnaryExpression:this.recurse(a.argument,void 0,
+void 0,function(a){h=a});m=a.operator+"("+this.ifDefined(h,0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right,
+b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.isNull(k.nonComputedMember("s",a.name)),
+k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));c(b);break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,void 0,function(){k.if_(k.notNull(g),function(){a.computed?(h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.computedMember(g,h),k.assign(b,
+m),d&&(d.computed=!0,d.name=h)):(e&&1!==e&&k.if_(k.isNull(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}")),m=k.nonComputedMember(g,a.property.name),k.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],q(a.arguments,function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)):
+(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){q(a.arguments,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})});m=g.name?k.member(g.context,g.name,g.computed)+"("+l.join(",")+")":h+"("+l.join(",")+")";k.assign(b,m)},function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};this.recurse(a.left,void 0,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);m=k.member(g.context,
+g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];q(a.elements,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case s.ObjectExpression:l=[];n=!1;q(a.properties,function(a){a.computed&&(n=!0)});n?(b=b||this.nextId(),this.assign(b,"{}"),q(a.properties,function(a){a.computed?(g=k.nextId(),k.recurse(a.key,g)):g=a.key.type===s.Identifier?a.key.name:""+a.key.value;h=k.nextId();
+k.recurse(a.value,h);k.assign(k.member(b,g,a.computed),h)})):(q(a.properties,function(b){k.recurse(b.value,a.constant?void 0:k.nextId(),void 0,function(a){l.push(k.escape(b.key.type===s.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case s.ThisExpression:this.assign(b,"s");c(b||"s");break;case s.LocalsExpression:this.assign(b,"l");c(b||"l");break;case s.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+
+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,
+b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,
+b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(F(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(ba(a))return a.toString();if(!0===a)return"true";if(!1===
+a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ua("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Id.prototype={compile:function(a){var b=this;U(a,b.$filter);var d,c;if(d=Gd(a))c=this.recurse(d);d=Ed(a.body);var e;d&&(e=[],q(d,function(a,c){var d=b.recurse(a);a.input=d;e.push(d);a.watchId=c}));var f=[];q(a.body,function(a){f.push(b.recurse(a.expression))});
+a=0===a.body.length?z:1===a.body.length?f[0]:function(a,b){var c;q(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+
+a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return f.identifier(a.name,b,d);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d):this.nonComputedMember(c,
+e,b,d);case s.CallExpression:return g=[],q(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var n=[],p=0;p<g.length;++p)n.push(g[p](a,c,d,f));a=e.apply(void 0,n,f);return b?{context:void 0,name:void 0,value:a}:a}:function(a,c,d,f){var n=e(a,c,d,f),p;if(null!=n.value){p=[];for(var r=0;r<g.length;++r)p.push(g[r](a,c,d,f));p=n.value.apply(n.context,p)}return b?{value:p}:p};case s.AssignmentExpression:return c=
+this.recurse(a.left,!0,1),e=this.recurse(a.right),function(a,d,f,g){var n=c(a,d,f,g);a=e(a,d,f,g);n.context[n.name]=a;return b?{value:a}:a};case s.ArrayExpression:return g=[],q(a.elements,function(a){g.push(f.recurse(a))}),function(a,c,d,e){for(var f=[],p=0;p<g.length;++p)f.push(g[p](a,c,d,e));return b?{value:f}:f};case s.ObjectExpression:return g=[],q(a.properties,function(a){a.computed?g.push({key:f.recurse(a.key),computed:!0,value:f.recurse(a.value)}):g.push({key:a.key.type===s.Identifier?a.key.name:
+""+a.key.value,computed:!1,value:f.recurse(a.value)})}),function(a,c,d,e){for(var f={},p=0;p<g.length;++p)g[p].computed?f[g[p].key(a,c,d,e)]=g[p].value(a,c,d,e):f[g[p].key]=g[p].value(a,c,d,e);return b?{value:f}:f};case s.ThisExpression:return function(a){return b?{value:a}:a};case s.LocalsExpression:return function(a,c){return b?{value:c}:c};case s.NGValueParameter:return function(a,c,d){return b?{value:d}:d}}},"unary+":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=u(d)?+d:0;return b?{value:d}:
+d}},"unary-":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=u(d)?-d:-0;return b?{value:d}:d}},"unary!":function(a,b){return function(d,c,e,f){d=!a(d,c,e,f);return b?{value:d}:d}},"binary+":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=Dd(h,c);return d?{value:h}:h}},"binary-":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=(u(h)?h:0)-(u(c)?c:0);return d?{value:h}:h}},"binary*":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)*b(c,e,f,g);
+return d?{value:c}:c}},"binary/":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)/b(c,e,f,g);return d?{value:c}:c}},"binary%":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)%b(c,e,f,g);return d?{value:c}:c}},"binary===":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)===b(c,e,f,g);return d?{value:c}:c}},"binary!==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!==b(c,e,f,g);return d?{value:c}:c}},"binary==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)==b(c,e,f,g);return d?
+{value:c}:c}},"binary!=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!=b(c,e,f,g);return d?{value:c}:c}},"binary<":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<b(c,e,f,g);return d?{value:c}:c}},"binary>":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:
+c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]=
+{});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,h){var k=a(e,f,g,h),l,m;null!=k&&(l=b(e,f,g,h),l+="",c&&1!==c&&k&&!k[l]&&(k[l]={}),m=k[l]);return d?{context:k,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};uc.prototype=
+{constructor:uc,parse:function(a){a=this.ast.ast(a);var b=this.astCompiler.compile(a);b.literal=0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression);b.constant=a.constant;return b}};var ta=L("$sce"),oa={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},xc=/_([a-z])/g,Dg=L("$compile"),aa=x.document.createElement("a"),Md=Ca(x.location.href);Nd.$inject=["$document"];
+cd.$inject=["$provide"];var Ud=22,Td=".",zc="0";Od.$inject=["$locale"];Qd.$inject=["$locale"];var Og={yyyy:Y("FullYear",4,0,!1,!0),yy:Y("FullYear",2,0,!0,!0),y:Y("FullYear",1,0,!1,!0),MMMM:mb("Month"),MMM:mb("Month",!0),MM:Y("Month",2,1),M:Y("Month",1,1),LLLL:mb("Month",!1,!0),dd:Y("Date",2),d:Y("Date",1),HH:Y("Hours",2),H:Y("Hours",1),hh:Y("Hours",2,-12),h:Y("Hours",1,-12),mm:Y("Minutes",2),m:Y("Minutes",1),ss:Y("Seconds",2),s:Y("Seconds",1),sss:Y("Milliseconds",3),EEEE:mb("Day"),EEE:mb("Day",!0),
+a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Kb(Math[0<a?"floor":"ceil"](a/60),2)+Kb(Math.abs(a%60),2))},ww:Wd(2),w:Wd(1),G:Ac,GG:Ac,GGG:Ac,GGGG:function(a,b){return 0>=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},Ng=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,Mg=/^-?\d+$/;Pd.$inject=["$locale"];var Hg=la(Q),Ig=la(ub);Rd.$inject=["$parse"];var Fe=la({restrict:"E",compile:function(a,
+b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===ma.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),vb={};q(Fb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=Ba("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});vb[c]=function(){return{restrict:"A",priority:100,link:e}}}});q(rd,function(a,b){vb[b]=
+function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(Sg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});q(["src","srcset","href"],function(a){var b=Ba("ng-"+a);vb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===ma.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b),
+za&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});var Mb={$addControl:z,$$renameControl:function(a,b){a.$name=b},$removeControl:z,$setValidity:z,$setDirty:z,$setPristine:z,$setSubmitted:z};Lb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Lb.prototype={$rollbackViewValue:function(){q(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){q(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ka(a.$name,"input");this.$$controls.push(a);
+a.$name&&(this[a.$name]=a);a.$$parentForm=this},$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];q(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);q(this.$error,function(b,d){this.$setValidity(d,null,a)},this);q(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);$a(this.$$controls,a);a.$$parentForm=Mb},$setDirty:function(){this.$$animate.removeClass(this.$$element,
+Va);this.$$animate.addClass(this.$$element,Rb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Va,Rb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;q(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){q(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){this.$$animate.addClass(this.$$element,"ng-submitted");this.$submitted=!0;this.$$parentForm.$setSubmitted()}};
+Zd({clazz:Lb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&($a(c,d),0===c.length&&delete a[b])}});var ge=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||z}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Lb,compile:function(d,f){d.addClass(Va).addClass(nb);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in
+e)){var p=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",p);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",p)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var r=g?c(n.$name):z;g&&(r(a,n),e.$observe(g,function(b){n.$name!==b&&(r(a,void 0),n.$$parentForm.$$renameControl(n,b),r=c(n.$name),r(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);r(a,void 0);S(n,Mb)})}}}}}]},Ge=ge(),
+Se=ge(!0),Pg=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,ah=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,bh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,Qg=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,he=/^(\d{4,})-(\d{2})-(\d{2})$/,ie=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,
+Hc=/^(\d{4,})-W(\d\d)$/,je=/^(\d{4,})-(\d\d)$/,ke=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ae=V();q(["date","datetime-local","month","time","week"],function(a){ae[a]=!0});var le={text:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c)},date:ob("date",he,Nb(he,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",ie,Nb(ie,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",ke,Nb(ke,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Hc,function(a,b){if(ga(a))return a;
+if(F(a)){Hc.lastIndex=0;var d=Hc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Vd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:ob("month",je,Nb(je,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Dc(a,b,d,c);be(c);Ra(a,b,d,c,e,f);var g,h;if(u(d.min)||d.ngMin)c.$validators.min=function(a){return c.$isEmpty(a)||w(g)||a>=g},d.$observe("min",function(a){g=Sa(a);c.$validate()});
+if(u(d.max)||d.ngMax)c.$validators.max=function(a){return c.$isEmpty(a)||w(h)||a<=h},d.$observe("max",function(a){h=Sa(a);c.$validate()});if(u(d.step)||d.ngStep){var k;c.$validators.step=function(a,b){return c.$isEmpty(b)||w(k)||ce(b,g||0,k)};d.$observe("step",function(a){k=Sa(a);c.$validate()})}},url:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c);c.$$parserName="url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||ah.test(d)}},email:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c);c.$$parserName=
+"email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||bh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==T(d.ngTrim);w(d.name)&&b.attr("name",++qb);b.on("click",function(a){var g;b[0].checked&&(g=d.value,e&&(g=T(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=T(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);d.$observe(a,c)}function h(a){n=Sa(a);da(c.$modelValue)||
+(m?(a=b.val(),n>a&&(a=n,b.val(a)),c.$setViewValue(a)):c.$validate())}function k(a){p=Sa(a);da(c.$modelValue)||(m?(a=b.val(),p<a&&(b.val(p),a=p<n?n:p),c.$setViewValue(a)):c.$validate())}function l(a){r=Sa(a);da(c.$modelValue)||(m&&c.$viewValue!==b.val()?c.$setViewValue(b.val()):c.$validate())}Dc(a,b,d,c);be(c);Ra(a,b,d,c,e,f);var m=c.$$hasNativeValidators&&"range"===b[0].type,n=m?0:void 0,p=m?100:void 0,r=m?1:void 0,q=b[0].validity;a=u(d.min);e=u(d.max);f=u(d.step);var s=c.$render;c.$render=m&&u(q.rangeUnderflow)&&
+u(q.rangeOverflow)?function(){s();c.$setViewValue(b.val())}:s;a&&(c.$validators.min=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||w(n)||b>=n},g("min",h));e&&(c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||w(p)||b<=p},g("max",k));f&&(c.$validators.step=m?function(){return!q.stepMismatch}:function(a,b){return c.$isEmpty(b)||w(r)||ce(b,n||0,r)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,h){var k=de(h,a,"ngTrueValue",d.ngTrueValue,!0),l=de(h,a,"ngFalseValue",
+d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return sa(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:z,button:z,submit:z,reset:z,file:z},Xc=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(le[Q(g.type)]||le.text)(e,f,g,h[0],b,a,d,c)}}}}],ch=/^(true|false|\d+)$/,
+kf=function(){function a(a,d,c){var e=u(c)?c:9===za?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return ch.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,function(b){a(d,f,b)})}}}},Ke=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=$b(a)})}}}}],Me=["$interpolate",
+"$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=w(a)?"":a})}}}}],Le=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,
+function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],jf=la({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ne=Fc("",!0),Pe=Fc("Odd",0),Oe=Fc("Even",1),Qe=Qa({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Re=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],bd={},dh={blur:!0,focus:!0};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),
+function(a){var b=Ba("ng-"+a);bd[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=d(f[b]);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};dh[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var Ue=["$animate","$compile",function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var h,k,l;d.$watch(e.ngIf,function(d){d?k||g(function(d,f){k=f;d[d.length++]=b.$$createComment("end ngIf",
+e.ngIf);h={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=tb(h.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),h=null))})}}}],Ve=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ea.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,p){var r=0,q,s,t,w=function(){s&&(s.remove(),s=null);q&&(q.$destroy(),q=
+null);t&&(d.leave(t).done(function(a){!1!==a&&(s=null)}),s=t,t=null)};c.$watch(f,function(f){var m=function(a){!1===a||!u(h)||h&&!c.$eval(h)||b()},s=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&s===r){var b=c.$new();n.template=a;a=p(b,function(a){w();d.enter(a,null,e).done(m)});q=b;t=a;q.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||s!==r||(w(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(w(),n.template=null)})}}}}],mf=["$compile",function(a){return{restrict:"ECA",
+priority:-400,require:"ngInclude",link:function(b,d,c,e){ma.call(d[0]).match(/SVG/)?(d.empty(),a(dd(e.template,x.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],We=Qa({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),hf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?T(e):e;c.$parsers.push(function(a){if(!w(a)){var b=
+[];a&&q(a.split(g),function(a){a&&b.push(f?T(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",Yd="ng-invalid",Va="ng-pristine",Rb="ng-dirty",pb=L("ngModel");Ob.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" ");Ob.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+
+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);D(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){D(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,xa(this.$$element));},$render:z,$isEmpty:function(a){return w(a)||""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,
+"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Rb);this.$$animate.addClass(this.$$element,Va)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element,Va);this.$$animate.addClass(this.$$element,Rb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=
+!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!da(this.$modelValue)){var a=this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),
+f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;q(k.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(q(k.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;q(k.$asyncValidators,function(e,g){var k=e(a,b);if(!k||!D(k.then))throw pb("nopromise",k);f(g,void 0);c.push(k.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});
+c.length?k.$$q.all(c).then(function(){g(d)},z):g(!0)}function f(a,b){h===k.$$currentValidationRunId&&k.$setValidity(a,b)}function g(a){h===k.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var h=this.$$currentValidationRunId,k=this;(function(){var a=k.$$parserName||"parse";if(w(k.$$parserValid))f(a,null);else return k.$$parserValid||(q(k.$validators,function(a,b){f(b,null)}),q(k.$asyncValidators,function(a,b){f(b,null)})),f(a,k.$$parserValid),k.$$parserValid;return!0})()?c()?e():g(!1):
+g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;if(this.$$parserValid=w(a)?void 0:!0)for(var d=0;d<this.$parsers.length;d++)if(a=this.$parsers[d](a),w(a)){this.$$parserValid=!1;break}da(this.$modelValue)&&
+(this.$modelValue=this.$$ngModelGet(this.$$scope));var c=this.$modelValue,e=this.$options.getOption("allowInvalid");this.$$rawModelValue=a;e&&(this.$modelValue=a,b.$modelValue!==c&&b.$$writeModelToScope());this.$$runValidators(a,this.$$lastCommittedViewValue,function(d){e||(b.$modelValue=d?a:void 0,b.$modelValue!==c&&b.$$writeModelToScope())})},$$writeModelToScope:function(){this.$$ngModelSet(this.$$scope,this.$modelValue);q(this.$viewChangeListeners,function(a){try{a()}catch(b){this.$$exceptionHandler(b)}},
+this)},$setViewValue:function(a,b){this.$viewValue=a;this.$options.getOption("updateOnDefault")&&this.$$debounceViewValueCommit(b)},$$debounceViewValueCommit:function(a){var b=this.$options.getOption("debounce");ba(b[a])?b=b[a]:ba(b["default"])&&(b=b["default"]);this.$$timeout.cancel(this.$$pendingDebounce);var d=this;0<b?this.$$pendingDebounce=this.$$timeout(function(){d.$commitViewValue()},b):this.$$scope.$root.$$phase?this.$commitViewValue():this.$$scope.$apply(function(){d.$commitViewValue()})},
+$overrideModelOptions:function(a){this.$options=this.$options.createChild(a)}};Zd({clazz:Ob,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]}});var gf=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:Ob,priority:1,compile:function(b){b.addClass(Va).addClass("ng-untouched").addClass(nb);return{pre:function(a,b,e,f){var g=f[0];b=f[1]||g.$$parentForm;if(f=f[2])g.$options=f.$options;g.$$initGetterSetters();b.$addControl(g);e.$observe("name",
+function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,a)});a.$on("$destroy",function(){g.$$parentForm.$removeControl(g)})},post:function(b,c,e,f){function g(){h.$setTouched()}var h=f[0];if(h.$options.getOption("updateOn"))c.on(h.$options.getOption("updateOn"),function(a){h.$$debounceViewValueCommit(a&&a.type)});c.on("blur",function(){h.$touched||(a.$$phase?b.$evalAsync(g):b.$apply(g))})}}}}}],Pb,eh=/(\s+|^)default(\s+|$)/;Gc.prototype={getOption:function(a){return this.$$options[a]},createChild:function(a){var b=
+!1;a=S({},a);q(a,function(d,c){"$inherit"===d?"*"===c?b=!0:(a[c]=this.$$options[c],"updateOn"===c&&(a.updateOnDefault=this.$$options.updateOnDefault)):"updateOn"===c&&(a.updateOnDefault=!1,a[c]=T(d.replace(eh,function(){a.updateOnDefault=!0;return" "})))},this);b&&(delete a["*"],ee(a,this.$$options));ee(a,Pb.$$options);return new Gc(a)}};Pb=new Gc({updateOn:"",updateOnDefault:!0,debounce:0,getterSetter:!1,allowInvalid:!1,timezone:null});var lf=function(){function a(a,d){this.$$attrs=a;this.$$scope=
+d}a.$inject=["$attrs","$scope"];a.prototype={$onInit:function(){var a=this.parentCtrl?this.parentCtrl.$options:Pb,d=this.$$scope.$eval(this.$$attrs.ngModelOptions);this.$options=a.createChild(d)}};return{restrict:"A",priority:10,require:{parentCtrl:"?^^ngModelOptions"},bindToController:!0,controller:a}},Xe=Qa({terminal:!0,priority:1E3}),fh=L("ngOptions"),gh=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
+ef=["$compile","$document","$parse",function(a,b,d){function c(a,b,c){function e(a,b,c,d,f){this.selectValue=a;this.viewValue=b;this.label=c;this.group=d;this.disabled=f}function f(a){var b;if(!q&&qa(a))b=a;else{b=[];for(var c in a)a.hasOwnProperty(c)&&"$"!==c.charAt(0)&&b.push(c)}return b}var n=a.match(gh);if(!n)throw fh("iexp",a,xa(b));var p=n[5]||n[7],q=n[6];a=/ as /.test(n[0])&&n[1];var s=n[9];b=d(n[2]?n[1]:p);var v=a&&d(a)||b,t=s&&d(s),u=s?function(a,b){return t(c,b)}:function(a){return Pa(a)},
+w=function(a,b){return u(a,G(a,b))},A=d(n[2]||n[1]),x=d(n[3]||""),I=d(n[4]||""),K=d(n[8]),E={},G=q?function(a,b){E[q]=b;E[p]=a;return E}:function(a){E[p]=a;return E};return{trackBy:s,getTrackByValue:w,getWatchables:d(K,function(a){var b=[];a=a||[];for(var d=f(a),e=d.length,g=0;g<e;g++){var h=a===d?g:d[g],l=a[h],h=G(l,h),l=u(l,h);b.push(l);if(n[2]||n[1])l=A(c,h),b.push(l);n[4]&&(h=I(c,h),b.push(h))}return b}),getOptions:function(){for(var a=[],b={},d=K(c)||[],g=f(d),h=g.length,n=0;n<h;n++){var p=d===
+g?n:g[n],q=G(d[p],p),r=v(c,q),p=u(r,q),t=A(c,q),E=x(c,q),q=I(c,q),r=new e(p,r,t,E,q);a.push(r);b[p]=r}return{items:a,selectValueMap:b,getOptionFromViewValue:function(a){return b[w(a)]},getViewValueFromOption:function(a){return s?ra(a.viewValue):a.viewValue}}}}}var e=x.document.createElement("option"),f=x.document.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","ngModel"],link:{pre:function(a,b,c,d){d[0].registerOption=z},post:function(d,h,k,l){function m(a){var b=(a=A.getOptionFromViewValue(a))&&
+a.element;b&&!b.selected&&(b.selected=!0);return a}function n(a,b){a.element=b;b.disabled=a.disabled;a.label!==b.label&&(b.label=a.label,b.textContent=a.label);b.value=a.selectValue}function p(){var a=A&&r.readValue();if(A)for(var b=A.items.length-1;0<=b;b--){var c=A.items[b];u(c.group)?Eb(c.element.parentNode):Eb(c.element)}A=z.getOptions();var d={};x&&h.prepend(r.emptyOption);A.items.forEach(function(a){var b;if(u(a.group)){b=d[a.group];b||(b=f.cloneNode(!1),I.appendChild(b),b.label=null===a.group?
+"null":a.group,d[a.group]=b);var c=e.cloneNode(!1)}else b=I,c=e.cloneNode(!1);b.appendChild(c);n(a,c)});h[0].appendChild(I);s.$render();s.$isEmpty(a)||(b=r.readValue(),(z.trackBy||v?sa(a,b):a===b)||(s.$setViewValue(b),s.$render()))}var r=l[0],s=l[1],v=k.multiple;l=0;for(var t=h.children(),w=t.length;l<w;l++)if(""===t[l].value){r.hasEmptyOption=!0;r.emptyOption=t.eq(l);break}var x=!!r.emptyOption;B(e.cloneNode(!1)).val("?");var A,z=c(k.ngOptions,h,d),I=b[0].createDocumentFragment();r.generateUnknownOptionValue=
+function(a){return"?"};v?(r.writeValue=function(a){var b=a&&a.map(m)||[];A.items.forEach(function(a){a.element.selected&&-1===Array.prototype.indexOf.call(b,a)&&(a.element.selected=!1)})},r.readValue=function(){var a=h.val()||[],b=[];q(a,function(a){(a=A.selectValueMap[a])&&!a.disabled&&b.push(A.getViewValueFromOption(a))});return b},z.trackBy&&d.$watchCollection(function(){if(H(s.$viewValue))return s.$viewValue.map(function(a){return z.getTrackByValue(a)})},function(){s.$render()})):(r.writeValue=
+function(a){var b=A.selectValueMap[h.val()],c=A.getOptionFromViewValue(a);b&&b.element.removeAttribute("selected");c?(h[0].value!==c.selectValue&&(r.removeUnknownOption(),r.unselectEmptyOption(),h[0].value=c.selectValue,c.element.selected=!0),c.element.setAttribute("selected","selected")):x?r.selectEmptyOption():r.unknownOption.parent().length?r.updateUnknownOption(a):r.renderUnknownOption(a)},r.readValue=function(){var a=A.selectValueMap[h.val()];return a&&!a.disabled?(r.unselectEmptyOption(),r.removeUnknownOption(),
+A.getViewValueFromOption(a)):null},z.trackBy&&d.$watch(function(){return z.getTrackByValue(s.$viewValue)},function(){s.$render()}));x&&(r.emptyOption.remove(),a(r.emptyOption)(d),8===r.emptyOption[0].nodeType?(r.hasEmptyOption=!1,r.registerOption=function(a,b){""===b.val()&&(r.hasEmptyOption=!0,r.emptyOption=b,r.emptyOption.removeClass("ng-scope"),s.$render(),b.on("$destroy",function(){r.hasEmptyOption=!1;r.emptyOption=void 0}))}):r.emptyOption.removeClass("ng-scope"));h.empty();p();d.$watchCollection(z.getWatchables,
+p)}}}}],Ye=["$locale","$interpolate","$log",function(a,b,d){var c=/{}/g,e=/^when(Minus)?(.+)$/;return{link:function(f,g,h){function k(a){g.text(a||"")}var l=h.count,m=h.$attr.when&&g.attr(h.$attr.when),n=h.offset||0,p=f.$eval(m)||{},r={},s=b.startSymbol(),v=b.endSymbol(),t=s+l+"-"+n+v,u=ea.noop,x;q(h,function(a,b){var c=e.exec(b);c&&(c=(c[1]?"-":"")+Q(c[2]),p[c]=g.attr(h.$attr[b]))});q(p,function(a,d){r[d]=b(a.replace(c,t))});f.$watch(l,function(b){var c=parseFloat(b),e=da(c);e||c in p||(c=a.pluralCat(c-
+n));c===x||e&&da(x)||(u(),e=r[c],w(e)?(null!=b&&d.debug("ngPluralize: no rule defined for '"+c+"' in "+m),u=z,k()):u=f.$watch(e,k),x=c)})}}}],Ze=["$parse","$animate","$compile",function(a,b,d){var c=L("ngRepeat"),e=function(a,b,c,d,e,m,n){a[c]=d;e&&(a[e]=m);a.$index=b;a.$first=0===b;a.$last=b===n-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(b&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,k=d.$$createComment("end ngRepeat",
+h),l=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!l)throw c("iexp",h);var m=l[1],n=l[2],p=l[3],r=l[4],l=m.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);if(!l)throw c("iidexp",m);var s=l[3]||l[1],v=l[2];if(p&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(p)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(p)))throw c("badident",p);var t,u,w,x,z={$id:Pa};r?t=a(r):(w=function(a,b){return Pa(b)},
+x=function(a){return a});return function(a,d,f,g,l){t&&(u=function(b,c,d){v&&(z[v]=b);z[s]=c;z.$index=d;return t(a,z)});var m=V();a.$watchCollection(n,function(f){var g,n,r=d[0],t,z=V(),B,D,F,C,G,E,H;p&&(a[p]=f);if(qa(f))G=f,n=u||w;else for(H in n=u||x,G=[],f)ua.call(f,H)&&"$"!==H.charAt(0)&&G.push(H);B=G.length;H=Array(B);for(g=0;g<B;g++)if(D=f===G?g:G[g],F=f[D],C=n(D,F,g),m[C])E=m[C],delete m[C],z[C]=E,H[g]=E;else{if(z[C])throw q(H,function(a){a&&a.scope&&(m[a.id]=a)}),c("dupes",h,C,F);H[g]={id:C,
+scope:void 0,clone:void 0};z[C]=!0}for(t in m){E=m[t];C=tb(E.clone);b.leave(C);if(C[0].parentNode)for(g=0,n=C.length;g<n;g++)C[g].$$NG_REMOVED=!0;E.scope.$destroy()}for(g=0;g<B;g++)if(D=f===G?g:G[g],F=f[D],E=H[g],E.scope){t=r;do t=t.nextSibling;while(t&&t.$$NG_REMOVED);E.clone[0]!==t&&b.move(tb(E.clone),null,r);r=E.clone[E.clone.length-1];e(E.scope,g,s,F,v,D,B)}else l(function(a,c){E.scope=c;var d=k.cloneNode(!1);a[a.length++]=d;b.enter(a,null,r);r=d;E.clone=a;z[E.id]=E;e(E.scope,g,s,F,v,D,B)});m=
+z})}}}}],$e=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngShow,function(b){a[b?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],Te=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngHide,function(b){a[b?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],af=Qa(function(a,b,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&q(d,function(a,c){b.css(c,"")});a&&b.css(a)},
+!0)}),bf=["$animate","$compile",function(a,b){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(d,c,e,f){var g=[],h=[],k=[],l=[],m=function(a,b){return function(c){!1!==c&&a.splice(b,1)}};d.$watch(e.ngSwitch||e.on,function(c){for(var d,e;k.length;)a.cancel(k.pop());d=0;for(e=l.length;d<e;++d){var s=tb(h[d].clone);l[d].$destroy();(k[d]=a.leave(s)).done(m(k,d))}h.length=0;l.length=0;(g=f.cases["!"+c]||f.cases["?"])&&q(g,function(c){c.transclude(function(d,e){l.push(e);
+var f=c.element;d[d.length++]=b.$$createComment("end ngSwitchWhen");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],cf=Qa({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){a=d.ngSwitchWhen.split(d.ngSwitchWhenSeparator).sort().filter(function(a,b,c){return c[b-1]!==a});q(a,function(a){c.cases["!"+a]=c.cases["!"+a]||[];c.cases["!"+a].push({transclude:e,element:b})})}}),df=Qa({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,
+b,d,c,e){c.cases["?"]=c.cases["?"]||[];c.cases["?"].push({transclude:e,element:b})}}),hh=L("ngTransclude"),ff=["$compile",function(a){return{restrict:"EAC",terminal:!0,compile:function(b){var d=a(b.contents());b.empty();return function(a,b,f,g,h){function k(){d(a,function(a){b.append(a)})}if(!h)throw hh("orphan",xa(b));f.ngTransclude===f.$attr.ngTransclude&&(f.ngTransclude="");f=f.ngTransclude||f.ngTranscludeSlot;h(function(a,c){var d;if(d=a.length)a:{d=0;for(var f=a.length;d<f;d++){var g=a[d];if(g.nodeType!==
+Ia||g.nodeValue.trim()){d=!0;break a}}d=void 0}d?b.append(a):(k(),c.$destroy())},null,f);f&&!h.isSlotFilled(f)&&k()}}}}],He=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(b,d){"text/ng-template"===d.type&&a.put(d.id,b[0].text)}}}],ih={$setViewValue:z,$render:z},jh=["$element","$scope",function(a,b){function d(){g||(g=!0,b.$$postDigest(function(){g=!1;e.ngModelCtrl.$render()}))}function c(a){h||(h=!0,b.$$postDigest(function(){b.$$destroyed||(h=!1,e.ngModelCtrl.$setViewValue(e.readValue()),
+a&&e.ngModelCtrl.$render())}))}var e=this,f=new Gb;e.selectValueMap={};e.ngModelCtrl=ih;e.multiple=!1;e.unknownOption=B(x.document.createElement("option"));e.hasEmptyOption=!1;e.emptyOption=void 0;e.renderUnknownOption=function(b){b=e.generateUnknownOptionValue(b);e.unknownOption.val(b);a.prepend(e.unknownOption);Ta(e.unknownOption,!0);a.val(b)};e.updateUnknownOption=function(b){b=e.generateUnknownOptionValue(b);e.unknownOption.val(b);Ta(e.unknownOption,!0);a.val(b)};e.generateUnknownOptionValue=
+function(a){return"? "+Pa(a)+" ?"};e.removeUnknownOption=function(){e.unknownOption.parent()&&e.unknownOption.remove()};e.selectEmptyOption=function(){e.emptyOption&&(a.val(""),Ta(e.emptyOption,!0))};e.unselectEmptyOption=function(){e.hasEmptyOption&&e.emptyOption.removeAttr("selected")};b.$on("$destroy",function(){e.renderUnknownOption=z});e.readValue=function(){var b=a.val(),b=b in e.selectValueMap?e.selectValueMap[b]:b;return e.hasOption(b)?b:null};e.writeValue=function(b){var c=a[0].options[a[0].selectedIndex];
+c&&Ta(B(c),!1);e.hasOption(b)?(e.removeUnknownOption(),c=Pa(b),a.val(c in e.selectValueMap?c:b),Ta(B(a[0].options[a[0].selectedIndex]),!0)):null==b&&e.emptyOption?(e.removeUnknownOption(),e.selectEmptyOption()):e.unknownOption.parent().length?e.updateUnknownOption(b):e.renderUnknownOption(b)};e.addOption=function(a,b){if(8!==b[0].nodeType){Ka(a,'"option value"');""===a&&(e.hasEmptyOption=!0,e.emptyOption=b);var c=f.get(a)||0;f.set(a,c+1);d()}};e.removeOption=function(a){var b=f.get(a);b&&(1===b?(f.delete(a),
+""===a&&(e.hasEmptyOption=!1,e.emptyOption=void 0)):f.set(a,b-1))};e.hasOption=function(a){return!!f.get(a)};var g=!1,h=!1;e.registerOption=function(a,b,f,g,h){if(f.$attr.ngValue){var q,s=NaN;f.$observe("value",function(a){var d,f=b.prop("selected");u(s)&&(e.removeOption(q),delete e.selectValueMap[s],d=!0);s=Pa(a);q=a;e.selectValueMap[s]=a;e.addOption(a,b);b.attr("value",s);d&&f&&c()})}else g?f.$observe("value",function(a){e.readValue();var d,f=b.prop("selected");u(q)&&(e.removeOption(q),d=!0);q=
+a;e.addOption(a,b);d&&f&&c()}):h?a.$watch(h,function(a,d){f.$set("value",a);var g=b.prop("selected");d!==a&&e.removeOption(d);e.addOption(a,b);d&&g&&c()}):e.addOption(f.value,b);f.$observe("disabled",function(a){if("true"===a||a&&b.prop("selected"))e.multiple?c(!0):(e.ngModelCtrl.$setViewValue(null),e.ngModelCtrl.$render())});b.on("$destroy",function(){var a=e.readValue(),b=f.value;e.removeOption(b);d();(e.multiple&&a&&-1!==a.indexOf(b)||a===b)&&c(!0)})}}],Ie=function(){return{restrict:"E",require:["select",
+"?ngModel"],controller:jh,priority:1,link:{pre:function(a,b,d,c){var e=c[0],f=c[1];if(f){if(e.ngModelCtrl=f,b.on("change",function(){e.removeUnknownOption();a.$apply(function(){f.$setViewValue(e.readValue())})}),d.multiple){e.multiple=!0;e.readValue=function(){var a=[];q(b.find("option"),function(b){b.selected&&!b.disabled&&(b=b.value,a.push(b in e.selectValueMap?e.selectValueMap[b]:b))});return a};e.writeValue=function(a){q(b.find("option"),function(b){var c=!!a&&(-1!==Array.prototype.indexOf.call(a,
+b.value)||-1!==Array.prototype.indexOf.call(a,e.selectValueMap[b.value]));c!==b.selected&&Ta(B(b),c)})};var g,h=NaN;a.$watch(function(){h!==f.$viewValue||sa(g,f.$viewValue)||(g=pa(f.$viewValue),f.$render());h=f.$viewValue});f.$isEmpty=function(a){return!a||0===a.length}}}else e.registerOption=z},post:function(a,b,d,c){var e=c[1];if(e){var f=c[0];e.$render=function(){f.writeValue(e.$viewValue)}}}}}},Je=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(b,d){var c,e;u(d.ngValue)||
+(u(d.value)?c=a(d.value,!0):(e=a(b.text(),!0))||d.$set("value",b.text()));return function(a,b,d){var k=b.parent();(k=k.data("$selectController")||k.parent().data("$selectController"))&&k.registerOption(a,b,d,c,e)}}}}],Zc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){c&&(d.required=!0,c.$validators.required=function(a,b){return!d.required||!c.$isEmpty(b)},d.$observe("required",function(){c.$validate()}))}}},Yc=function(){return{restrict:"A",require:"?ngModel",link:function(a,
+b,d,c){if(c){var e,f=d.ngPattern||d.pattern;d.$observe("pattern",function(a){F(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw L("ngPattern")("noregexp",f,a,xa(b));e=a||void 0;c.$validate()});c.$validators.pattern=function(a,b){return c.$isEmpty(b)||w(e)||e.test(b)}}}}},ad=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=-1;d.$observe("maxlength",function(a){a=Z(a);e=da(a)?-1:a;c.$validate()});c.$validators.maxlength=function(a,b){return 0>e||c.$isEmpty(b)||
+b.length<=e}}}}},$c=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=Z(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};x.angular.bootstrap?x.console&&console.log("WARNING: Tried to load angular more than once."):(ze(),Ce(ea),ea.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM",
+"PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5,
+6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a,
+c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),B(function(){ue(x.document,Sc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
+//# sourceMappingURL=angular.min.js.map
--- a/js/core.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/js/core.js	Fri Jul 14 15:39:24 2017 +0100
@@ -5,6 +5,10 @@
  * Also contains all global variables.
  */
 
+/*globals window, document, XMLDocument, Element, XMLHttpRequest, DOMParser, console, Blob, $, Promise, navigator */
+/*globals AudioBuffer, AudioBufferSourceNode */
+/*globals Specification, calculateLoudness, WAVE, validateXML, showdown, pageXMLSave, loadTest, resizeWindow */
+
 /* create the web audio API context and store in audioContext*/
 var audioContext; // Hold the browser web audio API
 var projectXML; // Hold the parsed setup XML
@@ -48,13 +52,13 @@
     name = String(name);
     var selected = this.documentElement.getAllElementsByName(name);
     return selected;
-}
+};
 
 Element.prototype.getAllElementsByName = function (name) {
     name = String(name);
     var selected = [];
     var node = this.firstElementChild;
-    while (node != null) {
+    while (node !== null) {
         if (node.getAttribute('name') == name) {
             selected.push(node);
         }
@@ -64,19 +68,19 @@
         node = node.nextElementSibling;
     }
     return selected;
-}
+};
 
 XMLDocument.prototype.getAllElementsByTagName = function (name) {
     name = String(name);
     var selected = this.documentElement.getAllElementsByTagName(name);
     return selected;
-}
+};
 
 Element.prototype.getAllElementsByTagName = function (name) {
     name = String(name);
     var selected = [];
     var node = this.firstElementChild;
-    while (node != null) {
+    while (node !== null) {
         if (node.nodeName == name) {
             selected.push(node);
         }
@@ -86,7 +90,7 @@
         node = node.nextElementSibling;
     }
     return selected;
-}
+};
 
 // Firefox does not have an XMLDocument.prototype.getElementsByName
 if (typeof XMLDocument.prototype.getElementsByName != "function") {
@@ -94,14 +98,14 @@
         name = String(name);
         var node = this.documentElement.firstElementChild;
         var selected = [];
-        while (node != null) {
+        while (node !== null) {
             if (node.getAttribute('name') == name) {
                 selected.push(node);
             }
             node = node.nextElementSibling;
         }
         return selected;
-    }
+    };
 }
 
 var check_dependancies = function () {
@@ -122,7 +126,7 @@
         return false;
     }
     return true;
-}
+};
 
 var onload = function () {
     // Function called once the browser has loaded all files.
@@ -131,7 +135,7 @@
     // Create a web audio API context
     // Fixed for cross-browser support
     var AudioContext = window.AudioContext || window.webkitAudioContext;
-    audioContext = new AudioContext;
+    audioContext = new AudioContext();
 
     // Create test state
     testState = new stateMachine();
@@ -152,11 +156,11 @@
         interfaceContext.resizeWindow(event);
     };
 
-    if (window.location.search.length != 0) {
+    if (window.location.search.length !== 0) {
         var search = window.location.search.split('?')[1];
         // Now split the requests into pairs
         var searchQueries = search.split('&');
-
+        var url;
         for (var i in searchQueries) {
             // Split each key-value pair
             searchQueries[i] = searchQueries[i].split('=');
@@ -165,12 +169,13 @@
             switch (key) {
                 case "url":
                     url = value;
+                    specification.url = url;
                     break;
                 case "returnURL":
                     gReturnURL = value;
                     break;
                 case "saveFilenamePrefix":
-                    gSaveFilenamePrefix = value;
+                    storage.filenamePrefix = value;
                     break;
             }
         }
@@ -188,9 +193,7 @@
     var xmlhttp = new XMLHttpRequest();
     xmlhttp.open("GET", 'xml/test-schema.xsd', true);
     xmlhttp.onload = function () {
-        schemaXSD = xmlhttp.response;
-        var parse = new DOMParser();
-        specification.schema = parse.parseFromString(xmlhttp.response, 'text/xml');
+        specification.processSchema(xmlhttp.response);
         var r = new XMLHttpRequest();
         r.open('GET', url, true);
         r.onload = function () {
@@ -204,11 +207,11 @@
             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.";
             document.getElementsByTagName('body')[0].appendChild(msg);
             document.getElementsByTagName('body')[0].appendChild(span);
-        }
+        };
         r.send();
     };
     xmlhttp.send();
-};
+}
 
 function loadProjectSpecCallback(response) {
     // Function called after asynchronous download of XML project specification
@@ -219,10 +222,11 @@
     var parse = new DOMParser();
     var responseDocument = parse.parseFromString(response, 'text/xml');
     var errorNode = responseDocument.getElementsByTagName('parsererror');
+    var msg, span;
     if (errorNode.length >= 1) {
-        var msg = document.createElement("h3");
+        msg = document.createElement("h3");
         msg.textContent = "FATAL ERROR";
-        var span = document.createElement("span");
+        span = document.createElement("span");
         span.textContent = "The XML parser returned the following errors when decoding your XML file";
         document.getElementsByTagName('body')[0].innerHTML = null;
         document.getElementsByTagName('body')[0].appendChild(msg);
@@ -230,10 +234,10 @@
         document.getElementsByTagName('body')[0].appendChild(errorNode[0]);
         return;
     }
-    if (responseDocument == undefined || responseDocument.firstChild == undefined) {
-        var msg = document.createElement("h3");
+    if (responseDocument === undefined || responseDocument.firstChild === undefined) {
+        msg = document.createElement("h3");
         msg.textContent = "FATAL ERROR";
-        var span = document.createElement("span");
+        span = document.createElement("span");
         span.textContent = "The project XML was not decoded properly, try refreshing your browser and clearing caches. If the problem persists, contact the test creator.";
         document.getElementsByTagName('body')[0].innerHTML = null;
         document.getElementsByTagName('body')[0].appendChild(msg);
@@ -246,7 +250,7 @@
         // Perform XML schema validation
         var Module = {
             xml: response,
-            schema: schemaXSD,
+            schema: specification.getSchemaString(),
             arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml']
         };
         projectXML = responseDocument;
@@ -254,16 +258,16 @@
         console.log(xmllint);
         if (xmllint != 'document.xml validates\n') {
             document.getElementsByTagName('body')[0].innerHTML = null;
-            var msg = document.createElement("h3");
+            msg = document.createElement("h3");
             msg.textContent = "FATAL ERROR";
-            var span = document.createElement("h4");
+            span = document.createElement("h4");
             span.textContent = "The XML validator returned the following errors when decoding your XML file";
             document.getElementsByTagName('body')[0].appendChild(msg);
             document.getElementsByTagName('body')[0].appendChild(span);
             xmllint = xmllint.split('\n');
             for (var i in xmllint) {
                 document.getElementsByTagName('body')[0].appendChild(document.createElement('br'));
-                var span = document.createElement("span");
+                span = document.createElement("span");
                 span.textContent = xmllint[i];
                 document.getElementsByTagName('body')[0].appendChild(span);
             }
@@ -278,15 +282,16 @@
         // document is a result
         projectXML = document.implementation.createDocument(null, "waet");
         projectXML.firstChild.appendChild(responseDocument.getElementsByTagName('waet')[0].getElementsByTagName("setup")[0].cloneNode(true));
-        var child = responseDocument.firstChild.firstChild;
-        while (child != null) {
+        var child = responseDocument.firstChild.firstChild,
+            copy;
+        while (child !== null) {
             if (child.nodeName == "survey") {
                 // One of the global survey elements
                 if (child.getAttribute("state") == "complete") {
                     // We need to remove this survey from <setup>
                     var location = child.getAttribute("location");
                     var globalSurveys = projectXML.getElementsByTagName("setup")[0].getElementsByTagName("survey")[0];
-                    while (globalSurveys != null) {
+                    while (globalSurveys !== null) {
                         if (location == "pre" || location == "before") {
                             if (globalSurveys.getAttribute("location") == "pre" || globalSurveys.getAttribute("location") == "before") {
                                 projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
@@ -302,7 +307,7 @@
                     }
                 } else {
                     // We need to complete this, so it must be regenerated by store
-                    var copy = child;
+                    copy = child;
                     child = child.previousElementSibling;
                     responseDocument.firstChild.removeChild(copy);
                 }
@@ -310,7 +315,7 @@
                 if (child.getAttribute("state") == "empty") {
                     // We need to complete this page
                     projectXML.firstChild.appendChild(responseDocument.getElementById(child.getAttribute("ref")).cloneNode(true));
-                    var copy = child;
+                    copy = child;
                     child = child.previousElementSibling;
                     responseDocument.firstChild.removeChild(copy);
                 }
@@ -323,7 +328,7 @@
         storage.initialise(responseDocument);
     }
     /// CHECK FOR SAMPLE RATE COMPATIBILITY
-    if (specification.sampleRate != undefined) {
+    if (isFinite(specification.sampleRate)) {
         if (Number(specification.sampleRate) != audioContext.sampleRate) {
             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.';
             interfaceContext.lightbox.post("Error", errStr);
@@ -335,7 +340,7 @@
     getInterfaces.open("GET", "interfaces/interfaces.json");
     getInterfaces.onerror = function (e) {
         throw (e);
-    }
+    };
     getInterfaces.onload = function () {
         if (getInterfaces.status !== 200) {
             throw (new Error(getInterfaces.status));
@@ -363,14 +368,14 @@
             css.setAttribute("href", v);
             head.appendChild(css);
         });
-    }
+    };
     getInterfaces.send();
 
-    if (gReturnURL != undefined) {
+    if (gReturnURL !== undefined) {
         console.log("returnURL Overide from " + specification.returnURL + " to " + gReturnURL);
         specification.returnURL = gReturnURL;
     }
-    if (gSaveFilenamePrefix != undefined) {
+    if (gSaveFilenamePrefix !== undefined) {
         specification.saveFilenamePrefix = gSaveFilenamePrefix;
     }
 
@@ -384,7 +389,7 @@
     // Save the data from interface into XML and send to destURL
     // If destURL is null then download XML in client
     // Now time to render file locally
-    var xmlDoc = interfaceXMLSave();
+    var xmlDoc = storage.finish();
     var parent = document.createElement("div");
     parent.appendChild(xmlDoc);
     var file = [parent.innerHTML];
@@ -403,51 +408,22 @@
         popup.popupContent.innerHTML = "<span>Please save the file below to give to your test supervisor</span><br>";
         popup.popupContent.appendChild(a);
     } else {
-        var saveUrlSuffix = "";
-        var saveFilenamePrefix = specification.saveFilenamePrefix;
-        if (typeof (saveFilenamePrefix) === "string" && saveFilenamePrefix.length > 0) {
-            saveUrlSuffix = "&saveFilenamePrefix=" + saveFilenamePrefix;
-        }
         var projectReturn = "";
         if (typeof specification.projectReturn == "string") {
             if (specification.projectReturn.substr(0, 4) == "http") {
                 projectReturn = specification.projectReturn;
             }
         }
-        var saveURL = projectReturn + "php/save.php?key=" + storage.SessionKey.key + saveUrlSuffix;
-        var xmlhttp = new XMLHttpRequest;
-        xmlhttp.open("POST", saveURL, true);
-        xmlhttp.setRequestHeader('Content-Type', 'text/xml');
-        xmlhttp.onerror = function () {
-            console.log('Error saving file to server! Presenting download locally');
+        storage.SessionKey.finish().then(function (resolved) {
+            if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) {
+                window.location = specification.returnURL;
+            } else {
+                popup.popupContent.textContent = specification.exitText;
+            }
+        }, function (message) {
+            console.log("Save: Error! " + message.textContent);
             createProjectSave("local");
-        };
-        xmlhttp.onload = function () {
-            console.log(xmlhttp);
-            if (this.status >= 300) {
-                console.log("WARNING - Could not update at this time");
-                createProjectSave("local");
-            } else {
-                var parser = new DOMParser();
-                var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
-                var response = xmlDoc.getElementsByTagName('response')[0];
-                if (response.getAttribute("state") == "OK") {
-                    window.onbeforeunload = undefined;
-                    var file = response.getElementsByTagName("file")[0];
-                    console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
-                    if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) {
-                        window.location = specification.returnURL;
-                    } else {
-                        popup.popupContent.textContent = specification.exitText;
-                    }
-                } else {
-                    var message = response.getElementsByTagName("message");
-                    console.log("Save: Error! " + message.textContent);
-                    createProjectSave("local");
-                }
-            }
-        };
-        xmlhttp.send(file);
+        });
         popup.showPopup();
         popup.popupContent.innerHTML = null;
         popup.popupContent.textContent = "Submitting. Please Wait";
@@ -517,7 +493,7 @@
 }
 
 function randomString(length) {
-    var str = ""
+    var str = "";
     for (var i = 0; i < length; i += 2) {
         var num = Math.floor(Math.random() * 1295);
         str += num.toString(36);
@@ -532,7 +508,7 @@
 
     var inputSequence = []; // For safety purposes: keep track of randomisation
     for (var counter = 0; counter < N; ++counter)
-        inputSequence.push(counter) // Fill array
+        inputSequence.push(counter); // Fill array
     var inputSequenceClone = inputSequence.slice(0);
 
     var holdArr = [];
@@ -577,6 +553,7 @@
     this.currentIndex = null;
     this.node = null;
     this.store = null;
+    var lastNodeStart;
     $(window).keypress(function (e) {
         if (e.keyCode == 13 && popup.popup.style.visibility == 'visible') {
             console.log(e);
@@ -584,6 +561,441 @@
             e.preventDefault();
         }
     });
+    // Generators & Processors //
+
+    function processConditional(node, value) {
+        function jumpToId(jumpID) {
+            var index = this.popupOptions.findIndex(function (item, index, element) {
+                if (item.specification.id == jumpID) {
+                    return true;
+                } else {
+                    return false;
+                }
+            }, this);
+            this.currentIndex = index - 1;
+        }
+        var conditionFunction;
+        if (node.specification.type === "question") {
+            conditionFunction = processQuestionConditional;
+        } else if (node.specification.type === "checkbox") {
+            conditionFunction = processCheckboxConditional;
+        } else if (node.specification.type === "radio") {
+            conditionFunction = processRadioConditional;
+        } else if (node.specification.type === "number") {
+            conditionFunction = processNumberConditional;
+        } else if (node.specification.type === "slider") {
+            conditionFunction = processSliderConditional;
+        } else {
+            return;
+        }
+        for (var i = 0; i < node.specification.conditions.length; i++) {
+            var condition = node.specification.conditions[i];
+            var pass = conditionFunction(condition, value);
+            var jumpID;
+            if (pass) {
+                jumpID = condition.jumpToOnPass;
+            } else {
+                jumpID = condition.jumpToOnFail;
+            }
+            if (jumpID !== null) {
+                jumpToId.call(this, jumpID);
+                break;
+            }
+        }
+    }
+
+    function postQuestion(node) {
+        var textArea = document.createElement('textarea');
+        switch (node.specification.boxsize) {
+            case 'small':
+                textArea.cols = "20";
+                textArea.rows = "1";
+                break;
+            case 'normal':
+                textArea.cols = "30";
+                textArea.rows = "2";
+                break;
+            case 'large':
+                textArea.cols = "40";
+                textArea.rows = "5";
+                break;
+            case 'huge':
+                textArea.cols = "50";
+                textArea.rows = "10";
+                break;
+        }
+        if (node.response === undefined) {
+            node.response = "";
+        } else {
+            textArea.value = node.response;
+        }
+        this.popupResponse.appendChild(textArea);
+        textArea.focus();
+        this.popupResponse.style.textAlign = "center";
+        this.popupResponse.style.left = "0%";
+    }
+
+    function processQuestionConditional(condition, value) {
+        switch (condition.check) {
+            case "equals":
+                // Deliberately loose check
+                if (value == condition.value) {
+                    return true;
+                }
+                break;
+            case "greaterThan":
+            case "lessThan":
+                console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING");
+                break;
+            case "contains":
+                if (value.includes(condition.value)) {
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    function processQuestion(node) {
+        var textArea = this.popupResponse.getElementsByTagName("textarea")[0];
+        if (node.specification.mandatory === true && textArea.value.length === 0) {
+            interfaceContext.lightbox.post("Error", "This question is mandatory");
+            return false;
+        }
+        // Save the text content
+        console.log("Question: " + node.specification.statement);
+        console.log("Question Response: " + textArea.value);
+        node.response = textArea.value;
+        processConditional.call(this, node, textArea.value);
+        return true;
+    }
+
+    function postCheckbox(node) {
+        if (node.response === undefined) {
+            node.response = Array(node.specification.options.length);
+        }
+        var table = document.createElement("table");
+        table.className = "popup-option-list";
+        table.border = "0";
+        node.response = [];
+        node.specification.options.forEach(function (option, index) {
+            var tr = document.createElement("tr");
+            table.appendChild(tr);
+            var td = document.createElement("td");
+            tr.appendChild(td);
+            var input = document.createElement('input');
+            input.id = option.name;
+            input.type = 'checkbox';
+            td.appendChild(input);
+
+            td = document.createElement("td");
+            tr.appendChild(td);
+            var span = document.createElement('span');
+            span.textContent = option.text;
+            td.appendChild(span);
+            tr = document.createElement('div');
+            tr.setAttribute('name', 'option');
+            tr.className = "popup-option-checbox";
+            if (node.response[index] !== undefined) {
+                if (node.response[index].checked === true) {
+                    input.checked = "true";
+                }
+            }
+            index++;
+        });
+        this.popupResponse.appendChild(table);
+    }
+
+    function processCheckbox(node) {
+        console.log("Checkbox: " + node.specification.statement);
+        var inputs = this.popupResponse.getElementsByTagName('input');
+        node.response = [];
+        var numChecked = 0,
+            i;
+        for (i = 0; i < node.specification.options.length; i++) {
+            if (inputs[i].checked) {
+                numChecked++;
+            }
+        }
+        if (node.specification.min !== undefined) {
+            if (node.specification.max === undefined) {
+                if (numChecked < node.specification.min) {
+                    var msg = "You must select at least " + node.specification.min + " option";
+                    if (node.specification.min > 1) {
+                        msg += "s";
+                    }
+                    interfaceContext.lightbox.post("Error", msg);
+                    return;
+                }
+            } else {
+                if (numChecked < node.specification.min || numChecked > node.specification.max) {
+                    if (node.specification.min == node.specification.max) {
+                        interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min);
+                    } else {
+                        interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max);
+                    }
+                    return false;
+                }
+            }
+        }
+        for (i = 0; i < node.specification.options.length; i++) {
+            node.response.push({
+                name: node.specification.options[i].name,
+                text: node.specification.options[i].text,
+                checked: inputs[i].checked
+            });
+            console.log(node.specification.options[i].name + ": " + inputs[i].checked);
+        }
+        processConditional.call(this, node, node.response);
+        return true;
+    }
+
+    function processCheckboxConditional(condition, response) {
+        switch (condition.check) {
+            case "contains":
+                for (var i = 0; i < response.length; i++) {
+                    var value = response[i];
+                    if (value.name === condition.value && value.checked) {
+                        return true;
+                    }
+                }
+                break;
+            case "equals":
+            case "greaterThan":
+            case "lessThan":
+                console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING");
+                break;
+            default:
+                console.log("Unknown condition. IGNORING");
+                break;
+        }
+        return false;
+    }
+
+    function postRadio(node) {
+        if (node.response === null) {
+            node.response = {
+                name: "",
+                text: ""
+            };
+        }
+        var table = document.createElement("table");
+        table.className = "popup-option-list";
+        table.border = "0";
+        if (node.response === null || node.response.length === 0) {
+            node.response = [];
+        }
+        node.specification.options.forEach(function (option, index) {
+            var tr = document.createElement("tr");
+            table.appendChild(tr);
+            var td = document.createElement("td");
+            tr.appendChild(td);
+            var input = document.createElement('input');
+            input.id = option.name;
+            input.type = 'radio';
+            input.name = node.specification.id;
+            td.appendChild(input);
+
+            td = document.createElement("td");
+            tr.appendChild(td);
+            var span = document.createElement('span');
+            span.textContent = option.text;
+            td.appendChild(span);
+            tr = document.createElement('div');
+            tr.setAttribute('name', 'option');
+            tr.className = "popup-option-checbox";
+            table.appendChild(tr);
+        });
+        this.popupResponse.appendChild(table);
+    }
+
+    function processRadio(node) {
+        var optHold = this.popupResponse;
+        console.log("Radio: " + node.specification.statement);
+        node.response = null;
+        var i = 0;
+        var inputs = optHold.getElementsByTagName('input');
+        while (node.response === null) {
+            if (i == inputs.length) {
+                if (node.specification.mandatory === true) {
+                    interfaceContext.lightbox.post("Error", "Please select one option");
+                    return false;
+                }
+                break;
+            }
+            if (inputs[i].checked === true) {
+                node.response = node.specification.options[i];
+                console.log("Selected: " + node.specification.options[i].name);
+            }
+            i++;
+        }
+        processConditional.call(this, node, node.response.name);
+        return true;
+    }
+
+    function processRadioConditional(condition, response) {
+        switch (condition.check) {
+            case "equals":
+                if (response === condition.value) {
+                    return true;
+                }
+                break;
+            case "contains":
+            case "greaterThan":
+            case "lessThan":
+                console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING");
+                break;
+            default:
+                console.log("Unknown condition. IGNORING");
+                break;
+        }
+        return false;
+    }
+
+    function postNumber(node) {
+        var input = document.createElement('input');
+        input.type = 'textarea';
+        if (node.specification.min !== null) {
+            input.min = node.specification.min;
+        }
+        if (node.specification.max !== null) {
+            input.max = node.specification.max;
+        }
+        if (node.specification.step !== null) {
+            input.step = node.specification.step;
+        }
+        if (node.response !== undefined) {
+            input.value = node.response;
+        }
+        this.popupResponse.appendChild(input);
+        this.popupResponse.style.textAlign = "center";
+        this.popupResponse.style.left = "0%";
+    }
+
+    function processNumber(node) {
+        var input = this.popupContent.getElementsByTagName('input')[0];
+        if (node.specification.mandatory === true && input.value.length === 0) {
+            interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number');
+            return false;
+        }
+        var enteredNumber = Number(input.value);
+        if (isNaN(enteredNumber)) {
+            interfaceContext.lightbox.post("Error", 'Please enter a valid number');
+            return false;
+        }
+        if (enteredNumber < node.specification.min && node.specification.min !== null) {
+            interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.specification.min);
+            return false;
+        }
+        if (enteredNumber > node.specification.max && node.specification.max !== null) {
+            interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.specification.max);
+            return false;
+        }
+        node.response = input.value;
+        processConditional.call(this, node, node.response);
+        return true;
+    }
+
+    function processNumberConditional(condtion, value) {
+        var condition = condition;
+        switch (condition.check) {
+            case "greaterThan":
+                if (value > Number(condition.value)) {
+                    return true;
+                }
+                break;
+            case "lessThan":
+                if (value < Number(condition.value)) {
+                    return true;
+                }
+                break;
+            case "equals":
+                if (value == condition.value) {
+                    return true;
+                }
+                break;
+            case "contains":
+                console.log("Survey Element of type 'number' cannot interpret \"contains\" conditions. IGNORING");
+                break;
+            default:
+                console.log("Unknown condition. IGNORING");
+                break;
+        }
+        return false;
+    }
+
+    function postVideo(node) {
+        var video = document.createElement("video");
+        video.src = node.specification.url;
+        this.popupResponse.appendChild(video);
+    }
+
+    function postYoutube(node) {
+        var iframe = document.createElement("iframe");
+        iframe.className = "youtube";
+        iframe.src = node.specification.url;
+        this.popupResponse.appendChild(iframe);
+    }
+
+    function postSlider(node) {
+        var hold = document.createElement('div');
+        var input = document.createElement('input');
+        input.type = 'range';
+        input.style.width = "90%";
+        if (node.specification.min !== null) {
+            input.min = node.specification.min;
+        }
+        if (node.specification.max !== null) {
+            input.max = node.specification.max;
+        }
+        if (node.response !== undefined) {
+            input.value = node.response;
+        }
+        hold.className = "survey-slider-text-holder";
+        var minText = document.createElement('span');
+        var maxText = document.createElement('span');
+        minText.textContent = node.specification.leftText;
+        maxText.textContent = node.specification.rightText;
+        hold.appendChild(minText);
+        hold.appendChild(maxText);
+        this.popupResponse.appendChild(input);
+        this.popupResponse.appendChild(hold);
+        this.popupResponse.style.textAlign = "center";
+    }
+
+    function processSlider(node) {
+        var input = this.popupContent.getElementsByTagName('input')[0];
+        node.response = input.value;
+        processConditional.call(this, node, node.response);
+        return true;
+    }
+
+    function processSliderConditional(condition, value) {
+        switch (condition.check) {
+            case "contains":
+                console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING");
+                break;
+            case "greaterThan":
+                if (value > Number(condition.value)) {
+                    return true;
+                }
+                break;
+            case "lessThan":
+                if (value < Number(condition.value)) {
+                    return true;
+                }
+                break;
+            case "equals":
+                if (value == condition.value) {
+                    return true;
+                }
+                break;
+            default:
+                console.log("Unknown condition. IGNORING");
+                break;
+        }
+        return false;
+    }
 
     this.createPopup = function () {
         // Create popup window interface
@@ -614,7 +1026,7 @@
     };
 
     this.showPopup = function () {
-        if (this.popup == null) {
+        if (this.popup === null) {
             this.createPopup();
         }
         this.popup.style.visibility = 'visible';
@@ -637,161 +1049,24 @@
         var node = this.popupOptions[this.currentIndex],
             converter = new showdown.Converter(),
             p = new DOMParser();
+        lastNodeStart = new Date();
         this.popupResponse.innerHTML = "";
         this.popupTitle.innerHTML = "";
         this.popupTitle.appendChild(p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").getElementsByTagName("body")[0].firstElementChild);
         if (node.specification.type == 'question') {
-            var textArea = document.createElement('textarea');
-            switch (node.specification.boxsize) {
-                case 'small':
-                    textArea.cols = "20";
-                    textArea.rows = "1";
-                    break;
-                case 'normal':
-                    textArea.cols = "30";
-                    textArea.rows = "2";
-                    break;
-                case 'large':
-                    textArea.cols = "40";
-                    textArea.rows = "5";
-                    break;
-                case 'huge':
-                    textArea.cols = "50";
-                    textArea.rows = "10";
-                    break;
-            }
-            if (node.response == undefined) {
-                node.response = "";
-            } else {
-                textArea.value = node.response;
-            }
-            this.popupResponse.appendChild(textArea);
-            textArea.focus();
-            this.popupResponse.style.textAlign = "center";
-            this.popupResponse.style.left = "0%";
+            postQuestion.call(this, node);
         } else if (node.specification.type == 'checkbox') {
-            if (node.response == undefined) {
-                node.response = Array(node.specification.options.length);
-            }
-            var index = 0;
-            var table = document.createElement("table");
-            table.className = "popup-option-list";
-            table.border = "0";
-            for (var option of node.specification.options) {
-                var tr = document.createElement("tr");
-                table.appendChild(tr);
-                var td = document.createElement("td");
-                tr.appendChild(td);
-                var input = document.createElement('input');
-                input.id = option.name;
-                input.type = 'checkbox';
-                td.appendChild(input);
-
-                td = document.createElement("td");
-                tr.appendChild(td);
-                var span = document.createElement('span');
-                span.textContent = option.text;
-                td.appendChild(span);
-                var tr = document.createElement('div');
-                tr.setAttribute('name', 'option');
-                tr.className = "popup-option-checbox";
-                if (node.response[index] != undefined) {
-                    if (node.response[index].checked == true) {
-                        input.checked = "true";
-                    }
-                }
-                index++;
-            }
-            this.popupResponse.appendChild(table);
+            postCheckbox.call(this, node);
         } else if (node.specification.type == 'radio') {
-            if (node.response == undefined) {
-                node.response = {
-                    name: "",
-                    text: ""
-                };
-            }
-            var index = 0;
-            var table = document.createElement("table");
-            table.className = "popup-option-list";
-            table.border = "0";
-            for (var option of node.specification.options) {
-                var tr = document.createElement("tr");
-                table.appendChild(tr);
-                var td = document.createElement("td");
-                tr.appendChild(td);
-                var input = document.createElement('input');
-                input.id = option.name;
-                input.type = 'radio';
-                input.name = node.specification.id;
-                td.appendChild(input);
-
-                td = document.createElement("td");
-                tr.appendChild(td);
-                var span = document.createElement('span');
-                span.textContent = option.text;
-                td.appendChild(span);
-                var tr = document.createElement('div');
-                tr.setAttribute('name', 'option');
-                tr.className = "popup-option-checbox";
-                if (node.response[index] != undefined) {
-                    if (node.response[index].checked == true) {
-                        input.checked = "true";
-                    }
-                }
-                index++;
-            }
-            this.popupResponse.appendChild(table);
+            postRadio.call(this, node);
         } else if (node.specification.type == 'number') {
-            var input = document.createElement('input');
-            input.type = 'textarea';
-            if (node.specification.min != null) {
-                input.min = node.specification.min;
-            }
-            if (node.specification.max != null) {
-                input.max = node.specification.max;
-            }
-            if (node.specification.step != null) {
-                input.step = node.specification.step;
-            }
-            if (node.response != undefined) {
-                input.value = node.response;
-            }
-            this.popupResponse.appendChild(input);
-            this.popupResponse.style.textAlign = "center";
-            this.popupResponse.style.left = "0%";
+            postNumber.call(this, node);
         } else if (node.specification.type == "video") {
-            var video = document.createElement("video");
-            video.src = node.specification.url;
-            this.popupResponse.appendChild(video);
+            postVideo.call(this, node);
         } else if (node.specification.type == "youtube") {
-            var iframe = document.createElement("iframe");
-            iframe.className = "youtube";
-            iframe.src = node.specification.url;
-            this.popupResponse.appendChild(iframe);
+            postYoutube.call(this, node);
         } else if (node.specification.type == "slider") {
-            var hold = document.createElement('div');
-            var input = document.createElement('input');
-            input.type = 'range';
-            input.style.width = "90%";
-            if (node.specification.min != null) {
-                input.min = node.specification.min;
-            }
-            if (node.specification.max != null) {
-                input.max = node.specification.max;
-            }
-            if (node.response != undefined) {
-                input.value = node.response;
-            }
-            hold.className = "survey-slider-text-holder";
-            var minText = document.createElement('span');
-            var maxText = document.createElement('span');
-            minText.textContent = node.specification.leftText;
-            maxText.textContent = node.specification.rightText;
-            hold.appendChild(minText);
-            hold.appendChild(maxText);
-            this.popupResponse.appendChild(input);
-            this.popupResponse.appendChild(hold);
-            this.popupResponse.style.textAlign = "center";
+            postSlider.call(this, node);
         }
         if (this.currentIndex + 1 == this.popupOptions.length) {
             if (this.node.location == "pre") {
@@ -815,12 +1090,12 @@
             this.popupOptions = [];
             this.node = node;
             this.store = store;
-            for (var opt of node.options) {
+            node.options.forEach(function (opt) {
                 this.popupOptions.push({
                     specification: opt,
                     response: null
                 });
-            }
+            }, this);
             this.currentIndex = 0;
             this.showPopup();
             this.postNode();
@@ -831,294 +1106,36 @@
 
     this.proceedClicked = function () {
         // Each time the popup button is clicked!
-        if (testState.stateIndex == 0 && specification.calibration) {
+        if (testState.stateIndex === 0 && specification.calibration) {
             interfaceContext.calibrationModuleObject.collect();
             advanceState();
             return;
         }
-        var node = this.popupOptions[this.currentIndex];
+        var node = this.popupOptions[this.currentIndex],
+            pass = true,
+            timeDelta = (new Date() - lastNodeStart) / 1000.0;
+        if (timeDelta < node.specification.minWait) {
+            interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds");
+            return;
+        }
+        node.elapsedTime = timeDelta;
         if (node.specification.type == 'question') {
             // Must extract the question data
-            var textArea = $(popup.popupContent).find('textarea')[0];
-            if (node.specification.mandatory == true && textArea.value.length == 0) {
-                interfaceContext.lightbox.post("Error", "This question is mandatory");
-                return;
-            } else {
-                // Save the text content
-                console.log("Question: " + node.specification.statement);
-                console.log("Question Response: " + textArea.value);
-                node.response = textArea.value;
-            }
-            // Perform the conditional
-            for (var condition of node.specification.conditions) {
-                var pass = false;
-                switch (condition.check) {
-                    case "equals":
-                        if (textArea.value == condition.value) {
-                            pass = true;
-                        }
-                        break;
-                    case "greaterThan":
-                    case "lessThan":
-                        console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING");
-                        break;
-                    case "contains":
-                        if (textArea.value.includes(condition.value)) {
-                            pass = true;
-                        }
-                        break;
-                }
-                var jumpID;
-                if (pass) {
-                    jumpID = condition.jumpToOnPass;
-                } else {
-                    jumpID = condition.jumpToOnFail;
-                }
-                if (jumpID != undefined) {
-                    var index = this.popupOptions.findIndex(function (item, index, element) {
-                        if (item.specification.id == jumpID) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, this);
-                    this.currentIndex = index - 1;
-                    break;
-                }
-            }
+            pass = processQuestion.call(this, node);
         } else if (node.specification.type == 'checkbox') {
             // Must extract checkbox data
-            console.log("Checkbox: " + node.specification.statement);
-            var inputs = this.popupResponse.getElementsByTagName('input');
-            node.response = [];
-            var numChecked = 0;
-            for (var i = 0; i < node.specification.options.length; i++) {
-                if (inputs[i].checked) {
-                    numChecked++;
-                }
-            }
-            if (node.specification.min != undefined) {
-                if (node.specification.max == undefined) {
-                    if (numChecked < node.specification.min) {
-                        var msg = "You must select at least " + node.specification.min + " option";
-                        if (node.specification.min > 1) {
-                            msg += "s";
-                        }
-                        interfaceContext.lightbox.post("Error", msg);
-                        return;
-                    }
-                } else {
-                    if (numChecked < node.specification.min || numChecked > node.specification.max) {
-                        if (node.specification.min == node.specification.max) {
-                            interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min);
-                        } else {
-                            interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max);
-                        }
-                        return;
-                    }
-                }
-            }
-            for (var i = 0; i < node.specification.options.length; i++) {
-                node.response.push({
-                    name: node.specification.options[i].name,
-                    text: node.specification.options[i].text,
-                    checked: inputs[i].checked
-                });
-                console.log(node.specification.options[i].name + ": " + inputs[i].checked);
-            }
+            pass = processCheckbox.call(this, node);
+        } else if (node.specification.type == "radio") {
             // Perform the conditional
-            for (var condition of node.specification.conditions) {
-                var pass = false;
-                switch (condition.check) {
-                    case "equals":
-                    case "greaterThan":
-                    case "lessThan":
-                        console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING");
-                        break;
-                    case "contains":
-                        for (var response of node.response) {
-                            if (response.name == condition.value && response.checked) {
-                                pass = true;
-                                break;
-                            }
-                        }
-                        break;
-                }
-                var jumpID;
-                if (pass) {
-                    jumpID = condition.jumpToOnPass;
-                } else {
-                    jumpID = condition.jumpToOnFail;
-                }
-                if (jumpID != undefined) {
-                    var index = this.popupOptions.findIndex(function (item, index, element) {
-                        if (item.specification.id == jumpID) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, this);
-                    this.currentIndex = index - 1;
-                    break;
-                }
-            }
-        } else if (node.specification.type == "radio") {
-            var optHold = this.popupResponse;
-            console.log("Radio: " + node.specification.statement);
-            node.response = null;
-            var i = 0;
-            var inputs = optHold.getElementsByTagName('input');
-            while (node.response == null) {
-                if (i == inputs.length) {
-                    if (node.specification.mandatory == true) {
-                        interfaceContext.lightbox.post("Error", "Please select one option");
-                        return;
-                    }
-                    break;
-                }
-                if (inputs[i].checked == true) {
-                    node.response = node.specification.options[i];
-                    console.log("Selected: " + node.specification.options[i].name);
-                }
-                i++;
-            }
+            pass = processRadio.call(this, node);
+        } else if (node.specification.type == "number") {
             // Perform the conditional
-            for (var condition of node.specification.conditions) {
-                var pass = false;
-                switch (condition.check) {
-                    case "contains":
-                    case "greaterThan":
-                    case "lessThan":
-                        console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING");
-                        break;
-                    case "equals":
-                        if (node.response.name == condition.value) {
-                            pass = true;
-                        }
-                        break;
-                }
-                var jumpID;
-                if (pass) {
-                    jumpID = condition.jumpToOnPass;
-                } else {
-                    jumpID = condition.jumpToOnFail;
-                }
-                if (jumpID != undefined) {
-                    var index = this.popupOptions.findIndex(function (item, index, element) {
-                        if (item.specification.id == jumpID) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, this);
-                    this.currentIndex = index - 1;
-                    break;
-                }
-            }
-        } else if (node.specification.type == "number") {
-            var input = this.popupContent.getElementsByTagName('input')[0];
-            if (node.mandatory == true && input.value.length == 0) {
-                interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number');
-                return;
-            }
-            var enteredNumber = Number(input.value);
-            if (isNaN(enteredNumber)) {
-                interfaceContext.lightbox.post("Error", 'Please enter a valid number');
-                return;
-            }
-            if (enteredNumber < node.min && node.min != null) {
-                interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.min);
-                return;
-            }
-            if (enteredNumber > node.max && node.max != null) {
-                interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.max);
-                return;
-            }
-            node.response = input.value;
-            // Perform the conditional
-            for (var condition of node.specification.conditions) {
-                var pass = false;
-                switch (condition.check) {
-                    case "contains":
-                        console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING");
-                        break;
-                    case "greaterThan":
-                        if (node.response > Number(condition.value)) {
-                            pass = true;
-                        }
-                        break;
-                    case "lessThan":
-                        if (node.response < Number(condition.value)) {
-                            pass = true;
-                        }
-                        break;
-                    case "equals":
-                        if (node.response == condition.value) {
-                            pass = true;
-                        }
-                        break;
-                }
-                var jumpID;
-                if (pass) {
-                    jumpID = condition.jumpToOnPass;
-                } else {
-                    jumpID = condition.jumpToOnFail;
-                }
-                if (jumpID != undefined) {
-                    var index = this.popupOptions.findIndex(function (item, index, element) {
-                        if (item.specification.id == jumpID) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, this);
-                    this.currentIndex = index - 1;
-                    break;
-                }
-            }
+            pass = processNumber.call(this, node);
         } else if (node.specification.type == 'slider') {
-            var input = this.popupContent.getElementsByTagName('input')[0];
-            node.response = input.value;
-            for (var condition of node.specification.conditions) {
-                var pass = false;
-                switch (condition.check) {
-                    case "contains":
-                        console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING");
-                        break;
-                    case "greaterThan":
-                        if (node.response > Number(condition.value)) {
-                            pass = true;
-                        }
-                        break;
-                    case "lessThan":
-                        if (node.response < Number(condition.value)) {
-                            pass = true;
-                        }
-                        break;
-                    case "equals":
-                        if (node.response == condition.value) {
-                            pass = true;
-                        }
-                        break;
-                }
-                var jumpID;
-                if (pass) {
-                    jumpID = condition.jumpToOnPass;
-                } else {
-                    jumpID = condition.jumpToOnFail;
-                }
-                if (jumpID != undefined) {
-                    var index = this.popupOptions.findIndex(function (item, index, element) {
-                        if (item.specification.id == jumpID) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, this);
-                    this.currentIndex = index - 1;
-                    break;
-                }
-            }
+            pass = processSlider.call(this, node);
+        }
+        if (pass === false) {
+            return;
         }
         this.currentIndex++;
         if (this.currentIndex < this.popupOptions.length) {
@@ -1128,9 +1145,9 @@
             this.popupTitle.innerHTML = "";
             this.popupResponse.innerHTML = "";
             this.hidePopup();
-            for (var node of this.popupOptions) {
+            this.popupOptions.forEach(function (node) {
                 this.store.postResult(node);
-            }
+            }, this);
             this.store.complete();
             advanceState();
         }
@@ -1146,7 +1163,7 @@
 
     this.resize = function (event) {
         // Called on window resize;
-        if (this.popup != null) {
+        if (this.popup !== null) {
             this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
             this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
             var blank = document.getElementsByClassName('testHalt')[0];
@@ -1156,16 +1173,16 @@
     };
     this.hideNextButton = function () {
         this.buttonProceed.style.visibility = "hidden";
-    }
+    };
     this.hidePreviousButton = function () {
         this.buttonPrevious.style.visibility = "hidden";
-    }
+    };
     this.showNextButton = function () {
         this.buttonProceed.style.visibility = "visible";
-    }
+    };
     this.showPreviousButton = function () {
         this.buttonPrevious.style.visibility = "visible";
-    }
+    };
 }
 
 function advanceState() {
@@ -1175,6 +1192,21 @@
 
 function stateMachine() {
     // Object prototype for tracking and managing the test state
+
+    function pickSubPool(pool, numElements) {
+        // Assumes each element of pool has function "alwaysInclude"
+
+        // First extract those excluded from picking process
+        var picked = [];
+        pool.forEach(function (e, i) {
+            if (e.alwaysInclude) {
+                picked.push(pool.splice(i, 1)[0]);
+            }
+        });
+
+        return picked.concat(randomSubArray(pool, numElements - picked.length));
+    }
+
     this.stateMap = [];
     this.preTestSurvey = null;
     this.postTestSurvey = null;
@@ -1186,73 +1218,65 @@
 
         // Get the data from Specification
         var pagePool = [];
-        var pageInclude = [];
-        for (var page of specification.pages) {
-            if (page.alwaysInclude) {
-                pageInclude.push(page);
-            } else {
-                pagePool.push(page);
+        specification.pages.forEach(function (page) {
+            if (page.position !== null || page.alwaysInclude) {
+                page.alwaysInclude = true;
             }
+            pagePool.push(page);
+        });
+        if (specification.numPages > 0) {
+            specification.randomiseOrder = true;
+            pagePool = pickSubPool(pagePool, specification.numPages);
         }
 
-        // Find how many are left to get
-        var numPages = specification.poolSize;
-        if (numPages > pagePool.length) {
-            console.log("WARNING - You have specified more pages in <setup poolSize> than you have created!!");
-            numPages = specification.pages.length;
-        }
-        if (specification.poolSize == 0) {
-            numPages = specification.pages.length;
-        }
-        numPages -= pageInclude.length;
+        // Now get the order of pages
+        var fixed = [];
+        pagePool.forEach(function (page) {
+            if (page.position !== undefined) {
+                fixed.push(page);
+                var i = pagePool.indexOf(page);
+                pagePool.splice(i, 1);
+            }
+        });
 
-        if (numPages > 0) {
-            // Go find the rest of the pages from the pool
-            var subarr = null;
-            if (specification.randomiseOrder) {
-                // Append a random sub-array
-                subarr = randomSubArray(pagePool, numPages);
-            } else {
-                // Append the matching number
-                subarr = pagePool.slice(0, numPages);
-            }
-            pageInclude = pageInclude.concat(subarr);
+        if (specification.randomiseOrder) {
+            pagePool = randomiseOrder(pagePool);
         }
 
-        // We now have our selected pages in pageInclude array
-        if (specification.randomiseOrder) {
-            pageInclude = randomiseOrder(pageInclude);
-        }
-        for (var i = 0; i < pageInclude.length; i++) {
-            pageInclude[i].presentedId = i;
-            this.stateMap.push(pageInclude[i]);
-            // For each selected page, we must get the sub pool
-            if (pageInclude[i].poolSize != 0 && pageInclude[i].poolSize != pageInclude[i].audioElements.length) {
-                var elemInclude = [];
-                var elemPool = [];
-                for (var elem of pageInclude[i].audioElements) {
-                    if (elem.alwaysInclude || elem.type != "normal") {
-                        elemInclude.push(elem);
-                    } else {
-                        elemPool.push(elem);
-                    }
+        // Place in the correct order
+        fixed.forEach(function (page) {
+            pagePool.splice(page.position, 0, page);
+        });
+
+        // Now process the pages
+        pagePool.forEach(function (page, i) {
+            page.presentedId = i;
+            this.stateMap.push(page);
+            var elements = page.audioElements;
+            if (page.poolSize > 0 || page.randomiseOrder) {
+                page.randomiseOrder = true;
+                if (page.poolSize === 0) {
+                    page.poolSize = elements.length;
                 }
-                var numElems = pageInclude[i].poolSize - elemInclude.length;
-                pageInclude[i].audioElements = elemInclude.concat(randomSubArray(elemPool, numElems));
+                elements = pickSubPool(elements, page.poolSize);
             }
-            storage.createTestPageStore(pageInclude[i]);
-            audioEngineContext.loadPageData(pageInclude[i]);
-        }
+            if (page.randomiseOrder) {
+                elements = randomiseOrder(elements);
+            }
+            page.audioElements = elements;
+            storage.createTestPageStore(page);
+            audioEngineContext.loadPageData(page);
+        }, this);
 
-        if (specification.preTest != null) {
+        if (specification.preTest !== null) {
             this.preTestSurvey = specification.preTest;
         }
-        if (specification.postTest != null) {
+        if (specification.postTest !== null) {
             this.postTestSurvey = specification.postTest;
         }
 
         if (this.stateMap.length > 0) {
-            if (this.stateIndex != null) {
+            if (this.stateIndex !== null) {
                 console.log('NOTE - State already initialise');
             }
             this.stateIndex = -2;
@@ -1262,7 +1286,7 @@
         }
     };
     this.advanceState = function () {
-        if (this.stateIndex == null) {
+        if (this.stateIndex === null) {
             this.initialise();
         }
         if (this.stateIndex > -2) {
@@ -1270,7 +1294,7 @@
         }
         if (this.stateIndex == -2) {
             this.stateIndex++;
-            if (this.preTestSurvey != null) {
+            if (this.preTestSurvey !== undefined) {
                 popup.initState(this.preTestSurvey, storage.globalPreTest);
             } else {
                 this.advanceState();
@@ -1290,7 +1314,7 @@
             // All test pages complete, post test
             console.log('Ending test ...');
             this.stateIndex++;
-            if (this.postTestSurvey == null) {
+            if (this.postTestSurvey === undefined) {
                 this.advanceState();
             } else {
                 popup.initState(this.postTestSurvey, storage.globalPostTest);
@@ -1299,18 +1323,19 @@
             createProjectSave(specification.projectReturn);
         } else {
             popup.hidePopup();
-            if (this.currentStateMap == null) {
+            if (this.currentStateMap === null) {
                 this.currentStateMap = this.stateMap[this.stateIndex];
                 // Find and extract the outside reference
                 var elements = [],
                     ref = [];
-                var elem;
-                while (elem = this.currentStateMap.audioElements.pop()) {
+                var elem = this.currentStateMap.audioElements.pop();
+                while (elem) {
                     if (elem.type == "outside-reference") {
                         ref.push(elem);
                     } else {
                         elements.push(elem);
                     }
+                    elem = this.currentStateMap.audioElements.pop();
                 }
                 elements = elements.reverse();
                 if (this.currentStateMap.randomiseOrder) {
@@ -1319,7 +1344,7 @@
                 this.currentStateMap.audioElements = elements.concat(ref);
 
                 this.currentStore = storage.testPages[this.stateIndex];
-                if (this.currentStateMap.preTest != null) {
+                if (this.currentStateMap.preTest !== undefined) {
                     this.currentStatePosition = 'pre';
                     popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest);
                 } else {
@@ -1336,7 +1361,7 @@
                     this.currentStatePosition = 'post';
                     // Save the data
                     this.testPageCompleted();
-                    if (this.currentStateMap.postTest == null) {
+                    if (this.currentStateMap.postTest === undefined) {
                         this.advanceState();
                         return;
                     } else {
@@ -1348,7 +1373,7 @@
                     this.currentStateMap = null;
                     this.advanceState();
                     break;
-            };
+            }
         }
     };
 
@@ -1366,12 +1391,12 @@
         }
 
         var audioObjects = audioEngineContext.audioObjects;
-        for (var ao of audioEngineContext.audioObjects) {
+        audioEngineContext.audioObjects.forEach(function (ao) {
             ao.exportXMLDOM();
-        }
-        for (var element of interfaceContext.commentQuestions) {
+        });
+        interfaceContext.commentQuestions.forEach(function (element) {
             element.exportXMLDOM(storePoint);
-        }
+        });
         pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
         storePoint.complete();
     };
@@ -1382,14 +1407,14 @@
         } else {
             return null;
         }
-    }
+    };
     this.getCurrentTestPageStore = function () {
         if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
             return this.currentStore;
         } else {
             return null;
         }
-    }
+    };
 }
 
 function AudioEngine(specification) {
@@ -1444,7 +1469,7 @@
             }
             for (var i = 0; i < this.users.length; i++) {
                 this.users[i].state = 1;
-                if (this.users[i].interfaceDOM != null) {
+                if (this.users[i].interfaceDOM !== null) {
                     this.users[i].bufferLoaded(this);
                 }
             }
@@ -1472,7 +1497,7 @@
                 }
             }
             return false;
-        }
+        };
         this.getMedia = function () {
             var self = this;
             var currentUrlIndex = 0;
@@ -1516,7 +1541,7 @@
                     return true;
                 }, function (e) {
                     var waveObj = new WAVE();
-                    if (waveObj.open(response) == 0) {
+                    if (waveObj.open(response) === 0) {
                         self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate);
                         for (var c = 0; c < waveObj.num_channels; c++) {
                             var buffer_ptr = self.buffer.getChannelData(c);
@@ -1524,14 +1549,13 @@
                                 buffer_ptr[n] = waveObj.decoded_data[c][n];
                             }
                         }
-
-                        delete waveObj;
                     }
-                    if (self.buffer != undefined) {
+                    if (self.buffer !== undefined) {
                         self.status = 2;
                         calculateLoudness(self, "I");
                         return true;
                     }
+                    waveObj = undefined;
                     return false;
                 });
             }
@@ -1541,7 +1565,7 @@
                 this.status = -1;
                 for (var i = 0; i < this.users.length; i++) {
                     this.users[i].state = -1;
-                    if (this.users[i].interfaceDOM != null) {
+                    if (this.users[i].interfaceDOM !== null) {
                         this.users[i].bufferLoaded(this);
                     }
                 }
@@ -1552,14 +1576,14 @@
                 if (event.lengthComputable) {
                     this.progress = event.loaded / event.total;
                     for (var i = 0; i < this.users.length; i++) {
-                        if (this.users[i].interfaceDOM != null) {
+                        if (this.users[i].interfaceDOM !== null) {
                             if (typeof this.users[i].interfaceDOM.updateLoading === "function") {
                                 this.users[i].interfaceDOM.updateLoading(this.progress * 100);
                             }
                         }
                     }
                 }
-            };
+            }
 
             this.progress = 0;
             this.status = 1;
@@ -1570,11 +1594,11 @@
         this.registerAudioObject = function (audioObject) {
             // Called by an audioObject to register to the buffer for use
             // First check if already in the register pool
-            for (var objects of this.users) {
-                if (audioObject.id == objects.id) {
+            this.users.forEach(function (object) {
+                if (audioObject.id == object.id) {
                     return 0;
                 }
-            }
+            });
             this.users.push(audioObject);
             if (this.status == 3 || this.status == -1) {
                 // The buffer is already ready, trigger bufferLoaded
@@ -1584,23 +1608,24 @@
 
         this.copyBuffer = function (preSilenceTime, postSilenceTime) {
             // Copies the entire bufferObj.
-            if (preSilenceTime == undefined) {
+            if (preSilenceTime === undefined) {
                 preSilenceTime = 0;
             }
-            if (postSilenceTime == undefined) {
+            if (postSilenceTime === undefined) {
                 postSilenceTime = 0;
             }
             var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate);
             var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate);
             var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples;
             var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
+            var c;
             // Now we can use some efficient background copy schemes if we are just padding the end
-            if (preSilenceSamples == 0 && typeof copybuffer.copyToChannel == "function") {
-                for (var c = 0; c < this.buffer.numberOfChannels; c++) {
+            if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") {
+                for (c = 0; c < this.buffer.numberOfChannels; c++) {
                     copybuffer.copyToChannel(this.buffer.getChannelData(c), c);
                 }
             } else {
-                for (var c = 0; c < this.buffer.numberOfChannels; c++) {
+                for (c = 0; c < this.buffer.numberOfChannels; c++) {
                     var src = this.buffer.getChannelData(c);
                     var dst = copybuffer.getChannelData(c);
                     for (var n = 0; n < src.length; n++)
@@ -1611,7 +1636,7 @@
             copybuffer.lufs = this.buffer.lufs;
             copybuffer.playbackGain = this.buffer.playbackGain;
             return copybuffer;
-        }
+        };
 
         this.cropBuffer = function (startTime, stopTime) {
             // Copy and return the cropped buffer
@@ -1632,21 +1657,17 @@
                 }
             }
             return copybuffer;
-        }
+        };
     };
 
     this.loadPageData = function (page) {
         // Load the URL from pages
-        for (var element of page.audioElements) {
+        function loadAudioElementData(element) {
             var URL = page.hostURL + element.url;
-            var buffer = null;
-            for (var buffObj of this.buffers) {
-                if (buffObj.hasUrl(URL)) {
-                    buffer = buffObj;
-                    break;
-                }
-            }
-            if (buffer == null) {
+            var buffer = this.buffers.find(function (buffObj) {
+                return buffObj.hasUrl(URL);
+            });
+            if (buffer === undefined) {
                 buffer = new this.bufferObj();
                 var urls = [{
                     url: URL,
@@ -1663,44 +1684,56 @@
                 this.buffers.push(buffer);
             }
         }
+        page.audioElements.forEach(loadAudioElementData, this);
     };
 
+    function playNormal(id) {
+        var playTime = audioContext.currentTime + 0.1;
+        var stopTime = playTime + specification.crossFade;
+        this.audioObjects.forEach(function (ao) {
+            if (ao.id === id) {
+                ao.play(playTime);
+            } else {
+                ao.stop(stopTime);
+            }
+        });
+    }
+
+    function playLoopSync(id) {
+        var playTime = audioContext.currentTime + 0.1;
+        var stopTime = playTime + specification.crossFade;
+        this.audioObjects.forEach(function (ao) {
+            ao.play(playTime);
+            if (ao.id === id) {
+                ao.loopStart(playTime);
+            } else {
+                ao.loopStop(stopTime);
+            }
+        });
+    }
+
     this.play = function (id) {
         // Start the timer and set the audioEngine state to playing (1)
-        if (this.status == 0) {
-            // Check if all audioObjects are ready
-            this.bufferReady(id);
-        } else {
-            this.status = 1;
+        if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) {
+            throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
         }
-        if (this.status == 1) {
+        var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays;
+        if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) {
+            interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times");
+            return;
+        }
+        if (this.status === 1) {
             this.timer.startTest();
-            if (id == undefined) {
-                id = -1;
-                console.error('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
-                return;
-            } else {
-                interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
-            }
-            var setTime = audioContext.currentTime;
+            interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
             if (this.synchPlayback && this.loopPlayback) {
                 // Traditional looped playback
-                for (var i = 0; i < this.audioObjects.length; i++) {
-                    this.audioObjects[i].play(audioContext.currentTime);
-                    if (id == i) {
-                        this.audioObjects[i].loopStart(setTime);
-                    } else {
-                        this.audioObjects[i].loopStop(setTime + specification.crossFade);
-                    }
+                playLoopSync.call(this, id);
+            } else {
+                if (this.bufferReady(id) === false) {
+                    console.log("Cannot play. Buffer not ready");
+                    return;
                 }
-            } else {
-                for (var i = 0; i < this.audioObjects.length; i++) {
-                    if (i != id) {
-                        this.audioObjects[i].stop(setTime + specification.crossFade);
-                    } else if (i == id) {
-                        this.audioObjects[id].play(setTime);
-                    }
-                }
+                playNormal.call(this, id);
             }
             interfaceContext.playhead.start();
         }
@@ -1710,9 +1743,9 @@
         // Send stop and reset command to all playback buffers
         if (this.status == 1) {
             var setTime = audioContext.currentTime + 0.1;
-            for (var i = 0; i < this.audioObjects.length; i++) {
-                this.audioObjects[i].stop(setTime);
-            }
+            this.audioObjects.forEach(function (a) {
+                a.stop(setTime);
+            });
             interfaceContext.playhead.stop();
         }
     };
@@ -1722,19 +1755,15 @@
         // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
 
         // Create the audioObject with ID of the new track length;
-        audioObjectId = this.audioObjects.length;
+        var audioObjectId = this.audioObjects.length;
         this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
 
         // Check if audioObject buffer is currently stored by full URL
         var URL = testState.currentStateMap.hostURL + element.url;
-        var buffer = null;
-        for (var i = 0; i < this.buffers.length; i++) {
-            if (this.buffers[i].hasUrl(URL)) {
-                buffer = this.buffers[i];
-                break;
-            }
-        }
-        if (buffer == null) {
+        var buffer = this.buffers.find(function (buffObj) {
+            return buffObj.hasUrl(URL);
+        });
+        if (buffer === undefined) {
             console.log("[WARN]: Buffer was not loaded in pre-test! " + URL);
             buffer = new this.bufferObj();
             this.buffers.push(buffer);
@@ -1760,9 +1789,9 @@
         this.status = 0;
         this.audioObjectsReady = false;
         this.metric.reset();
-        for (var i = 0; i < this.buffers.length; i++) {
-            this.buffers[i].users = [];
-        }
+        this.buffers.forEach(function (buffer) {
+            buffer.users = [];
+        });
         this.audioObjects = [];
         this.timer = new timer();
         this.loopPlayback = audioHolderObject.loop;
@@ -1770,9 +1799,9 @@
     };
 
     this.checkAllPlayed = function () {
-        arr = [];
+        var arr = [];
         for (var id = 0; id < this.audioObjects.length; id++) {
-            if (this.audioObjects[id].metric.wasListenedTo == false) {
+            if (this.audioObjects[id].metric.wasListenedTo === false) {
                 arr.push(this.audioObjects[id].id);
             }
         }
@@ -1782,11 +1811,11 @@
     this.checkAllReady = function () {
         var ready = true;
         for (var i = 0; i < this.audioObjects.length; i++) {
-            if (this.audioObjects[i].state == 0) {
+            if (this.audioObjects[i].state === 0) {
                 // Track not ready
                 console.log('WAIT -- audioObject ' + i + ' not ready yet!');
                 ready = false;
-            };
+            }
         }
         return ready;
     };
@@ -1803,11 +1832,11 @@
             }
         }
         // Extract the audio and zero-pad
-        for (var ao of this.audioObjects) {
+        this.audioObjects.forEach(function (ao) {
             if (ao.buffer.buffer.duration !== duration) {
                 ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration);
             }
-        }
+        });
     };
 
     this.bufferReady = function (id) {
@@ -1819,10 +1848,6 @@
             return true;
         }
         return false;
-    }
-
-    this.exportXML = function () {
-
     };
 
 }
@@ -1830,7 +1855,9 @@
 function audioObject(id) {
     // The main buffer object with common control nodes to the AudioEngine
 
-    this.specification;
+    var playCounter = 0;
+
+    this.specification = undefined;
     this.id = id;
     this.state = 0; // 0 - no data, 1 - ready
     this.url = null; // Hold the URL given for the output back to the results.
@@ -1853,7 +1880,7 @@
 
     // the audiobuffer is not designed for multi-start playback
     // When stopeed, the buffer node is deleted and recreated with the stored buffer.
-    this.buffer;
+    this.buffer = undefined;
 
     this.bufferLoaded = function (callee) {
         // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
@@ -1861,7 +1888,7 @@
         if (callee.status == -1) {
             // ERROR
             this.state = -1;
-            if (this.interfaceDOM != null) {
+            if (this.interfaceDOM !== null) {
                 this.interfaceDOM.error();
             }
             this.buffer = callee;
@@ -1875,7 +1902,7 @@
         var copybuffer = new callee.constructor();
 
         copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration);
-        if (preSilenceTime != 0 || postSilenceTime != 0) {
+        if (preSilenceTime !== 0 || postSilenceTime !== 0) {
             copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime);
         }
 
@@ -1888,7 +1915,7 @@
         } else {
             this.buffer.buffer.playbackGain = 1.0;
         }
-        if (this.interfaceDOM != null) {
+        if (this.interfaceDOM !== null) {
             this.interfaceDOM.enable();
         }
         this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0);
@@ -1917,7 +1944,7 @@
     };
 
     this.loopStop = function (setTime) {
-        if (this.outputGain.gain.value != 0.0) {
+        if (this.outputGain.gain.value !== 0.0) {
             this.outputGain.gain.linearRampToValueAtTime(0.0, setTime);
             this.metric.stopListening(audioEngineContext.timer.getTestTime());
         }
@@ -1925,7 +1952,8 @@
     };
 
     this.play = function (startTime) {
-        if (this.bufferNode == undefined && this.buffer.buffer != undefined) {
+        if (this.bufferNode === undefined && this.buffer.buffer !== undefined) {
+            playCounter++;
             this.bufferNode = audioContext.createBufferSource();
             this.bufferNode.owner = this;
             this.bufferNode.connect(this.outputGain);
@@ -1934,7 +1962,7 @@
             this.bufferNode.onended = function (event) {
                 // Safari does not like using 'this' to reference the calling object!
                 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
-                if (event.currentTarget != null) {
+                if (event.currentTarget !== null) {
                     event.currentTarget.owner.stop(audioContext.currentTime + 1);
                 }
             };
@@ -1959,7 +1987,7 @@
 
     this.stop = function (stopTime) {
         this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
-        if (this.bufferNode != undefined) {
+        if (this.bufferNode !== undefined) {
             this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
             this.bufferNode.stop(stopTime);
             this.bufferNode = undefined;
@@ -1970,7 +1998,7 @@
 
     this.getCurrentPosition = function () {
         var time = audioEngineContext.timer.getTestTime();
-        if (this.bufferNode != undefined) {
+        if (this.bufferNode !== undefined) {
             var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
             if (isNaN(position)) {
                 return 0;
@@ -1990,8 +2018,8 @@
         this.storeDOM.appendChild(file);
         if (this.specification.type != 'outside-reference') {
             var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
-            if (interfaceXML != null) {
-                if (interfaceXML.length == undefined) {
+            if (interfaceXML !== null) {
+                if (interfaceXML.length === undefined) {
                     this.storeDOM.appendChild(interfaceXML);
                 } else {
                     for (var i = 0; i < interfaceXML.length; i++) {
@@ -1999,16 +2027,23 @@
                     }
                 }
             }
-            if (this.commentDOM != null) {
+            if (this.commentDOM !== null) {
                 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
             }
         }
-        var nodes = this.metric.exportXMLDOM();
-        var mroot = this.storeDOM.getElementsByTagName('metric')[0];
-        for (var i = 0; i < nodes.length; i++) {
-            mroot.appendChild(nodes[i]);
+        this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]);
+    };
+
+    Object.defineProperties(this, {
+        "numberOfPlays": {
+            'get': function () {
+                return playCounter;
+            },
+            'set': function () {
+                return playCounter;
+            }
         }
-    };
+    });
 }
 
 function timer() {
@@ -2020,7 +2055,7 @@
     this.testDuration = 0;
     this.minimumTestTime = 0; // No minimum test time
     this.startTest = function () {
-        if (this.testStarted == false) {
+        if (this.testStarted === false) {
             this.testStartTime = audioContext.currentTime;
             this.testStarted = true;
             this.updateTestTime();
@@ -2128,7 +2163,7 @@
     };
 
     this.startListening = function (time) {
-        if (this.listenHold == false) {
+        if (this.listenHold === false) {
             this.wasListenedTo = true;
             this.listenStart = time;
             this.listenHold = true;
@@ -2147,7 +2182,7 @@
     };
 
     this.stopListening = function (time, bufferStopTime) {
-        if (this.listenHold == true) {
+        if (this.listenHold === true) {
             var diff = time - this.listenStart;
             this.listenedTimer += (diff);
             this.listenStart = 0;
@@ -2157,7 +2192,7 @@
             var testTime = evnt.getElementsByTagName('testTime')[0];
             var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
             testTime.setAttribute('stop', time);
-            if (bufferStopTime == undefined) {
+            if (bufferStopTime === undefined) {
                 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
             } else {
                 bufferTime.setAttribute('stop', bufferStopTime);
@@ -2166,64 +2201,99 @@
         }
     };
 
-    this.exportXMLDOM = function () {
-        var storeDOM = [];
+    function exportElementTimer(parentElement) {
+        var mElementTimer = storage.document.createElement('metricresult');
+        mElementTimer.setAttribute('name', 'enableElementTimer');
+        mElementTimer.textContent = this.listenedTimer;
+        parentElement.appendChild(mElementTimer);
+        return mElementTimer;
+    }
+
+    function exportElementTrack(parentElement) {
+        var elementTrackerFull = storage.document.createElement('metricresult');
+        elementTrackerFull.setAttribute('name', 'elementTrackerFull');
+        for (var k = 0; k < this.movementTracker.length; k++) {
+            var timePos = storage.document.createElement('movement');
+            timePos.setAttribute("time", this.movementTracker[k][0]);
+            timePos.setAttribute("value", this.movementTracker[k][1]);
+            elementTrackerFull.appendChild(timePos);
+        }
+        parentElement.appendChild(elementTrackerFull);
+        return elementTrackerFull;
+    }
+
+    function exportElementListenTracker(parentElement) {
+        var elementListenTracker = storage.document.createElement('metricresult');
+        elementListenTracker.setAttribute('name', 'elementListenTracker');
+        for (var k = 0; k < this.listenTracker.length; k++) {
+            elementListenTracker.appendChild(this.listenTracker[k]);
+        }
+        parentElement.appendChild(elementListenTracker);
+        return elementListenTracker;
+    }
+
+    function exportElementInitialPosition(parentElement) {
+        var elementInitial = storage.document.createElement('metricresult');
+        elementInitial.setAttribute('name', 'elementInitialPosition');
+        elementInitial.textContent = this.initialPosition;
+        parentElement.appendChild(elementInitial);
+        return elementInitial;
+    }
+
+    function exportFlagListenedTo(parentElement) {
+        var flagListenedTo = storage.document.createElement('metricresult');
+        flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
+        flagListenedTo.textContent = this.wasListenedTo;
+        parentElement.appendChild(flagListenedTo);
+        return flagListenedTo;
+    }
+
+    function exportFlagMoved(parentElement) {
+        var flagMoved = storage.document.createElement('metricresult');
+        flagMoved.setAttribute('name', 'elementFlagMoved');
+        flagMoved.textContent = this.wasMoved;
+        parentElement.appendChild(flagMoved);
+        return flagMoved;
+    }
+
+    function exportFlagComments(parentElement) {
+        var flagComments = storage.document.createElement('metricresult');
+        flagComments.setAttribute('name', 'elementFlagComments');
+        if (this.parent.commentDOM === null) {
+            flagComments.textContent = 'false';
+        } else if (this.parent.commentDOM.textContent.length === 0) {
+            flagComments.textContent = 'false';
+        } else {
+            flagComments.textContet = 'true';
+        }
+        parentElement.appendChild(flagComments);
+        return flagComments;
+    }
+
+    this.exportXMLDOM = function (parentElement) {
+        var elems = [];
         if (audioEngineContext.metric.enableElementTimer) {
-            var mElementTimer = storage.document.createElement('metricresult');
-            mElementTimer.setAttribute('name', 'enableElementTimer');
-            mElementTimer.textContent = this.listenedTimer;
-            storeDOM.push(mElementTimer);
+            elems.push(exportElementTimer.call(this, parentElement));
         }
         if (audioEngineContext.metric.enableElementTracker) {
-            var elementTrackerFull = storage.document.createElement('metricresult');
-            elementTrackerFull.setAttribute('name', 'elementTrackerFull');
-            for (var k = 0; k < this.movementTracker.length; k++) {
-                var timePos = storage.document.createElement('movement');
-                timePos.setAttribute("time", this.movementTracker[k][0]);
-                timePos.setAttribute("value", this.movementTracker[k][1]);
-                elementTrackerFull.appendChild(timePos);
-            }
-            storeDOM.push(elementTrackerFull);
+            elems.push(exportElementTrack.call(this, parentElement));
         }
         if (audioEngineContext.metric.enableElementListenTracker) {
-            var elementListenTracker = storage.document.createElement('metricresult');
-            elementListenTracker.setAttribute('name', 'elementListenTracker');
-            for (var k = 0; k < this.listenTracker.length; k++) {
-                elementListenTracker.appendChild(this.listenTracker[k]);
-            }
-            storeDOM.push(elementListenTracker);
+            elems.push(exportElementListenTracker.call(this, parentElement));
         }
         if (audioEngineContext.metric.enableElementInitialPosition) {
-            var elementInitial = storage.document.createElement('metricresult');
-            elementInitial.setAttribute('name', 'elementInitialPosition');
-            elementInitial.textContent = this.initialPosition;
-            storeDOM.push(elementInitial);
+            elems.push(exportElementInitialPosition.call(this, parentElement));
         }
         if (audioEngineContext.metric.enableFlagListenedTo) {
-            var flagListenedTo = storage.document.createElement('metricresult');
-            flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
-            flagListenedTo.textContent = this.wasListenedTo;
-            storeDOM.push(flagListenedTo);
+            elems.push(exportFlagListenedTo.call(this, parentElement));
         }
         if (audioEngineContext.metric.enableFlagMoved) {
-            var flagMoved = storage.document.createElement('metricresult');
-            flagMoved.setAttribute('name', 'elementFlagMoved');
-            flagMoved.textContent = this.wasMoved;
-            storeDOM.push(flagMoved);
+            elems.push(exportFlagMoved.call(this, parentElement));
         }
         if (audioEngineContext.metric.enableFlagComments) {
-            var flagComments = storage.document.createElement('metricresult');
-            flagComments.setAttribute('name', 'elementFlagComments');
-            if (this.parent.commentDOM == null) {
-                flag.textContent = 'false';
-            } else if (this.parent.commentDOM.textContent.length == 0) {
-                flag.textContent = 'false';
-            } else {
-                flag.textContet = 'true';
-            }
-            storeDOM.push(flagComments);
+            elems.push(exportFlagComments.call(this, parentElement));
         }
-        return storeDOM;
+        return elems;
     };
 }
 
@@ -2249,12 +2319,12 @@
         popup.resize(event);
         this.volume.resize();
         this.lightbox.resize();
-        for (var i = 0; i < this.commentBoxes.length; i++) {
-            this.commentBoxes[i].resize();
-        }
-        for (var i = 0; i < this.commentQuestions.length; i++) {
-            this.commentQuestions[i].resize();
-        }
+        this.commentBoxes.boxes.forEach(function (elem) {
+            elem.resize();
+        });
+        this.commentQuestions.forEach(function (elem) {
+            elem.resize();
+        });
         try {
             resizeWindow(event);
         } catch (err) {
@@ -2303,7 +2373,7 @@
         hold.appendChild(time);
         return hold;
 
-    }
+    };
 
     this.lightbox = {
         parent: this,
@@ -2345,7 +2415,7 @@
         resize: function (event) {
             this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
         }
-    }
+    };
 
     this.lightbox.root.appendChild(this.lightbox.content);
     this.lightbox.root.appendChild(this.lightbox.accept);
@@ -2361,10 +2431,11 @@
     document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
     document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
 
-    this.commentBoxes = new function () {
-        this.boxes = [];
-        this.injectPoint = null;
-        this.elementCommentBox = function (audioObject) {
+    this.commentBoxes = (function () {
+        var commentBoxes = {};
+        commentBoxes.boxes = [];
+        commentBoxes.injectPoint = null;
+        commentBoxes.elementCommentBox = function (audioObject) {
             var element = audioObject.specification;
             this.audioObject = audioObject;
             this.id = audioObject.id;
@@ -2410,39 +2481,60 @@
                 this.trackCommentBox.style.width = boxwidth - 6 + "px";
             };
             this.resize();
+            this.highlight = function (state) {
+                if (state === true) {
+                    $(this.trackComment).addClass("comment-box-playing");
+                } else {
+                    $(this.trackComment).removeClass("comment-box-playing");
+                }
+            };
         };
-        this.createCommentBox = function (audioObject) {
+        commentBoxes.createCommentBox = function (audioObject) {
             var node = new this.elementCommentBox(audioObject);
             this.boxes.push(node);
             audioObject.commentDOM = node;
             return node;
         };
-        this.sortCommentBoxes = function () {
+        commentBoxes.sortCommentBoxes = function () {
             this.boxes.sort(function (a, b) {
                 return a.id - b.id;
             });
         };
 
-        this.showCommentBoxes = function (inject, sort) {
+        commentBoxes.showCommentBoxes = function (inject, sort) {
             this.injectPoint = inject;
             if (sort) {
                 this.sortCommentBoxes();
             }
-            for (var box of this.boxes) {
+            this.boxes.forEach(function (box) {
                 inject.appendChild(box.trackComment);
-            }
+            });
         };
 
-        this.deleteCommentBoxes = function () {
-            if (this.injectPoint != null) {
-                for (var box of this.boxes) {
+        commentBoxes.deleteCommentBoxes = function () {
+            if (this.injectPoint !== null) {
+                this.boxes.forEach(function (box) {
                     this.injectPoint.removeChild(box.trackComment);
-                }
+                }, this);
                 this.injectPoint = null;
             }
             this.boxes = [];
         };
-    }
+        commentBoxes.highlightById = function (id) {
+            if (id === undefined || typeof id !== "number" || id >= this.boxes.length) {
+                console.log("Error - Invalid id");
+                id = -1;
+            }
+            this.boxes.forEach(function (a) {
+                if (a.id === id) {
+                    a.highlight(true);
+                } else {
+                    a.highlight(false);
+                }
+            });
+        };
+        return commentBoxes;
+    })();
 
     this.commentQuestions = [];
 
@@ -2501,47 +2593,32 @@
         // Create a string next to each comment asking for a comment
         this.string = document.createElement('span');
         this.string.innerHTML = commentQuestion.statement;
-        var br = document.createElement('br');
         // Add to the holder.
         this.holder.appendChild(this.string);
-        this.holder.appendChild(br);
         this.options = [];
         this.inputs = document.createElement('div');
-        this.span = document.createElement('div');
-        this.inputs.align = 'center';
-        this.inputs.style.marginLeft = '12px';
-        this.inputs.className = "comment-radio-inputs-holder";
-        this.span.style.marginLeft = '12px';
-        this.span.align = 'center';
-        this.span.style.marginTop = '15px';
-        this.span.className = "comment-radio-span-holder";
+        this.inputs.className = "comment-checkbox-inputs-holder";
 
         var optCount = commentQuestion.options.length;
-        for (var optNode of commentQuestion.options) {
+        for (var i = 0; i < optCount; i++) {
             var div = document.createElement('div');
-            div.style.width = '80px';
-            div.style.float = 'left';
+            div.className = "comment-checkbox-inputs-flex";
+
+            var span = document.createElement('span');
+            span.textContent = commentQuestion.options[i].text;
+            span.className = 'comment-radio-span';
+            div.appendChild(span);
+
             var input = document.createElement('input');
             input.type = 'radio';
             input.name = commentQuestion.id;
-            input.setAttribute('setvalue', optNode.name);
+            input.setAttribute('setvalue', commentQuestion.options[i].name);
             input.className = 'comment-radio';
             div.appendChild(input);
+
             this.inputs.appendChild(div);
-
-
-            div = document.createElement('div');
-            div.style.width = '80px';
-            div.style.float = 'left';
-            div.align = 'center';
-            var span = document.createElement('span');
-            span.textContent = optNode.text;
-            span.className = 'comment-radio-span';
-            div.appendChild(span);
-            this.span.appendChild(div);
             this.options.push(input);
         }
-        this.holder.appendChild(this.span);
         this.holder.appendChild(this.inputs);
 
         this.exportXMLDOM = function (storePoint) {
@@ -2552,7 +2629,7 @@
             question.textContent = this.string.textContent;
             var response = document.createElement('response');
             var i = 0;
-            while (this.options[i].checked == false) {
+            while (this.options[i].checked === false) {
                 i++;
                 if (i >= this.options.length) {
                     break;
@@ -2579,24 +2656,6 @@
                 boxwidth = 400;
             }
             this.holder.style.width = boxwidth + "px";
-            var text = this.holder.getElementsByClassName("comment-radio-span-holder")[0];
-            var options = this.holder.getElementsByClassName("comment-radio-inputs-holder")[0];
-            var optCount = options.childElementCount;
-            var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px';
-            var options = options.firstChild;
-            var text = text.firstChild;
-            options.style.marginRight = spanMargin;
-            options.style.marginLeft = spanMargin;
-            text.style.marginRight = spanMargin;
-            text.style.marginLeft = spanMargin;
-            while (options.nextSibling != undefined) {
-                options = options.nextSibling;
-                text = text.nextSibling;
-                options.style.marginRight = spanMargin;
-                options.style.marginLeft = spanMargin;
-                text.style.marginRight = spanMargin;
-                text.style.marginLeft = spanMargin;
-            }
         };
         this.resize();
     };
@@ -2609,47 +2668,32 @@
         // Create a string next to each comment asking for a comment
         this.string = document.createElement('span');
         this.string.innerHTML = commentQuestion.statement;
-        var br = document.createElement('br');
         // Add to the holder.
         this.holder.appendChild(this.string);
-        this.holder.appendChild(br);
         this.options = [];
         this.inputs = document.createElement('div');
-        this.span = document.createElement('div');
-        this.inputs.align = 'center';
-        this.inputs.style.marginLeft = '12px';
         this.inputs.className = "comment-checkbox-inputs-holder";
-        this.span.style.marginLeft = '12px';
-        this.span.align = 'center';
-        this.span.style.marginTop = '15px';
-        this.span.className = "comment-checkbox-span-holder";
 
         var optCount = commentQuestion.options.length;
         for (var i = 0; i < optCount; i++) {
             var div = document.createElement('div');
-            div.style.width = '80px';
-            div.style.float = 'left';
+            div.className = "comment-checkbox-inputs-flex";
+
+            var span = document.createElement('span');
+            span.textContent = commentQuestion.options[i].text;
+            span.className = 'comment-radio-span';
+            div.appendChild(span);
+
             var input = document.createElement('input');
             input.type = 'checkbox';
             input.name = commentQuestion.id;
             input.setAttribute('setvalue', commentQuestion.options[i].name);
             input.className = 'comment-radio';
             div.appendChild(input);
+
             this.inputs.appendChild(div);
-
-
-            div = document.createElement('div');
-            div.style.width = '80px';
-            div.style.float = 'left';
-            div.align = 'center';
-            var span = document.createElement('span');
-            span.textContent = commentQuestion.options[i].text;
-            span.className = 'comment-radio-span';
-            div.appendChild(span);
-            this.span.appendChild(div);
             this.options.push(input);
         }
-        this.holder.appendChild(this.span);
         this.holder.appendChild(this.inputs);
 
         this.exportXMLDOM = function (storePoint) {
@@ -2678,24 +2722,6 @@
                 boxwidth = 400;
             }
             this.holder.style.width = boxwidth + "px";
-            var text = this.holder.getElementsByClassName("comment-checkbox-span-holder")[0];
-            var options = this.holder.getElementsByClassName("comment-checkbox-inputs-holder")[0];
-            var optCount = options.childElementCount;
-            var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px';
-            var options = options.firstChild;
-            var text = text.firstChild;
-            options.style.marginRight = spanMargin;
-            options.style.marginLeft = spanMargin;
-            text.style.marginRight = spanMargin;
-            text.style.marginLeft = spanMargin;
-            while (options.nextSibling != undefined) {
-                options = options.nextSibling;
-                text = text.nextSibling;
-                options.style.marginRight = spanMargin;
-                options.style.marginLeft = spanMargin;
-                text.style.marginRight = spanMargin;
-                text.style.marginLeft = spanMargin;
-            }
         };
         this.resize();
     };
@@ -2783,10 +2809,10 @@
         this.outsideReferenceHolder.setAttribute('track-id', index);
         this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
         this.outsideReferenceHolder.disabled = true;
-
-        this.outsideReferenceHolder.onclick = function (event) {
-            audioEngineContext.play(event.currentTarget.getAttribute('track-id'));
+        this.handleEvent = function (event) {
+            audioEngineContext.play(this.parent.id);
         };
+        this.outsideReferenceHolder.addEventListener("click", this);
         inject.appendChild(this.outsideReferenceHolder);
         this.enable = function () {
             if (this.parent.state == 1) {
@@ -2828,33 +2854,34 @@
             // audioObject has an error!!
             this.outsideReferenceHolder.textContent = "Error";
             this.outsideReferenceHolder.style.backgroundColor = "#F00";
-        }
-    }
+        };
+    };
 
-    this.playhead = new function () {
-        this.object = document.createElement('div');
-        this.object.className = 'playhead';
-        this.object.align = 'left';
+    this.playhead = (function () {
+        var playhead = {};
+        playhead.object = document.createElement('div');
+        playhead.object.className = 'playhead';
+        playhead.object.align = 'left';
         var curTime = document.createElement('div');
         curTime.style.width = '50px';
-        this.curTimeSpan = document.createElement('span');
-        this.curTimeSpan.textContent = '00:00';
-        curTime.appendChild(this.curTimeSpan);
-        this.object.appendChild(curTime);
-        this.scrubberTrack = document.createElement('div');
-        this.scrubberTrack.className = 'playhead-scrub-track';
+        playhead.curTimeSpan = document.createElement('span');
+        playhead.curTimeSpan.textContent = '00:00';
+        curTime.appendChild(playhead.curTimeSpan);
+        playhead.object.appendChild(curTime);
+        playhead.scrubberTrack = document.createElement('div');
+        playhead.scrubberTrack.className = 'playhead-scrub-track';
 
-        this.scrubberHead = document.createElement('div');
-        this.scrubberHead.id = 'playhead-scrubber';
-        this.scrubberTrack.appendChild(this.scrubberHead);
-        this.object.appendChild(this.scrubberTrack);
+        playhead.scrubberHead = document.createElement('div');
+        playhead.scrubberHead.id = 'playhead-scrubber';
+        playhead.scrubberTrack.appendChild(playhead.scrubberHead);
+        playhead.object.appendChild(playhead.scrubberTrack);
 
-        this.timePerPixel = 0;
-        this.maxTime = 0;
+        playhead.timePerPixel = 0;
+        playhead.maxTime = 0;
 
-        this.playbackObject;
+        playhead.playbackObject = undefined;
 
-        this.setTimePerPixel = function (audioObject) {
+        playhead.setTimePerPixel = function (audioObject) {
             //maxTime must be in seconds
             this.playbackObject = audioObject;
             this.maxTime = audioObject.buffer.buffer.duration;
@@ -2867,7 +2894,7 @@
             }
         };
 
-        this.update = function () {
+        playhead.update = function () {
             // Update the playhead position, startPlay must be called
             if (this.timePerPixel > 0) {
                 var time = this.playbackObject.getCurrentPosition();
@@ -2895,66 +2922,65 @@
                     }
                 }
             }
+            if (this.playbackObject !== undefined && this.interval === undefined) {
+                window.requestAnimationFrame(this.update.bind(this));
+            }
         };
 
-        this.interval = undefined;
+        playhead.interval = undefined;
 
-        this.start = function () {
-            if (this.playbackObject != undefined && this.interval == undefined) {
-                if (this.maxTime < 60) {
-                    this.interval = setInterval(function () {
-                        interfaceContext.playhead.update();
-                    }, 10);
-                } else {
-                    this.interval = setInterval(function () {
-                        interfaceContext.playhead.update();
-                    }, 100);
-                }
+        playhead.start = function () {
+            if (this.playbackObject !== undefined && this.interval === undefined) {
+                window.requestAnimationFrame(this.update.bind(this));
             }
         };
-        this.stop = function () {
-            clearInterval(this.interval);
-            this.interval = undefined;
-            this.scrubberHead.style.left = '0px';
-            if (this.maxTime < 60) {
-                this.curTimeSpan.textContent = '0.00';
-            } else {
-                this.curTimeSpan.textContent = '00:00';
-            }
+        playhead.stop = function () {
+            this.timePerPixel = 0;
         };
-    };
+        return playhead;
+    })();
 
-    this.volume = new function () {
+    this.volume = (function () {
         // An in-built volume module which can be viewed on page
         // Includes trackers on page-by-page data
         // Volume does NOT reset to 0dB on each page load
-        this.valueLin = 1.0;
-        this.valueDB = 0.0;
-        this.root = document.createElement('div');
-        this.root.id = 'master-volume-root';
-        this.object = document.createElement('div');
-        this.object.className = 'master-volume-holder-float';
-        this.object.appendChild(this.root);
-        this.slider = document.createElement('input');
-        this.slider.id = 'master-volume-control';
-        this.slider.type = 'range';
-        this.valueText = document.createElement('span');
-        this.valueText.id = 'master-volume-feedback';
-        this.valueText.textContent = '0dB';
+        var volume = {};
+        volume.valueLin = 1.0;
+        volume.valueDB = 0.0;
+        volume.root = document.createElement('div');
+        volume.root.id = 'master-volume-root';
+        volume.object = document.createElement('div');
+        volume.object.className = 'master-volume-holder-float';
+        volume.object.appendChild(volume.root);
+        volume.slider = document.createElement('input');
+        volume.slider.id = 'master-volume-control';
+        volume.slider.type = 'range';
+        volume.valueText = document.createElement('span');
+        volume.valueText.id = 'master-volume-feedback';
+        volume.valueText.textContent = '0dB';
 
-        this.slider.min = -60;
-        this.slider.max = 12;
-        this.slider.value = 0;
-        this.slider.step = 1;
-        this.slider.onmousemove = function (event) {
-            interfaceContext.volume.valueDB = event.currentTarget.value;
-            interfaceContext.volume.valueLin = decibelToLinear(interfaceContext.volume.valueDB);
-            interfaceContext.volume.valueText.textContent = interfaceContext.volume.valueDB + 'dB';
-            audioEngineContext.outputGain.gain.value = interfaceContext.volume.valueLin;
-        }
-        this.slider.onmouseup = function (event) {
+        volume.slider.min = -60;
+        volume.slider.max = 12;
+        volume.slider.value = 0;
+        volume.slider.step = 1;
+        volume.handleEvent = function (event) {
+            if (event.type == "mousemove") {
+                this.valueDB = Number(this.slider.value);
+                this.valueLin = decibelToLinear(this.valueDB);
+                this.valueText.textContent = this.valueDB + 'dB';
+                audioEngineContext.outputGain.gain.value = this.valueLin;
+            } else if (event.type == "mouseup") {
+                this.onmouseup();
+            }
+            this.slider.value = this.valueDB;
+
+            if (event.stopPropagation) {
+                event.stopPropagation();
+            }
+        };
+        volume.onmouseup = function () {
             var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
-            if (storePoint.length == 0) {
+            if (storePoint.length === 0) {
                 storePoint = storage.document.createElement('metricresult');
                 storePoint.setAttribute('name', 'volumeTracker');
                 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
@@ -2963,29 +2989,48 @@
             }
             var node = storage.document.createElement('movement');
             node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
-            node.setAttribute('volume', interfaceContext.volume.valueDB);
+            node.setAttribute('volume', this.valueDB);
             node.setAttribute('format', 'dBFS');
             storePoint.appendChild(node);
-        }
+        };
+        volume.slider.addEventListener("mousemove", volume);
+        volume.root.addEventListener("mouseup", volume);
 
         var title = document.createElement('div');
         title.innerHTML = '<span>Master Volume Control</span>';
         title.style.fontSize = '0.75em';
         title.style.width = "100%";
         title.align = 'center';
-        this.root.appendChild(title);
+        volume.root.appendChild(title);
 
-        this.root.appendChild(this.slider);
-        this.root.appendChild(this.valueText);
+        volume.root.appendChild(volume.slider);
+        volume.root.appendChild(volume.valueText);
 
-        this.resize = function (event) {
+        volume.resize = function (event) {
             if (window.innerWidth < 1000) {
-                this.object.className = "master-volume-holder-inline"
+                this.object.className = "master-volume-holder-inline";
             } else {
                 this.object.className = 'master-volume-holder-float';
             }
-        }
-    }
+        };
+        return volume;
+    })();
+
+    this.imageHolder = (function () {
+        var imageController = {};
+        imageController.root = document.createElement("div");
+        imageController.root.id = "imageController";
+        imageController.img = document.createElement("img");
+        imageController.root.appendChild(imageController.img);
+        imageController.setImage = function (src) {
+            imageController.img.src = "";
+            if (typeof src !== "string" || src.length === undefined) {
+                return;
+            }
+            imageController.img.src = src;
+        };
+        return imageController;
+    })();
 
     this.calibrationModuleObject = null;
     this.calibrationModule = function () {
@@ -3001,6 +3046,7 @@
             this.holder.className = "calibration-holder";
             this.calibrationNodes = [];
             while (f0 < 20000) {
+                /* jshint loopfunc: true */
                 var obj = {
                     root: document.createElement("div"),
                     input: document.createElement("input"),
@@ -3025,7 +3071,7 @@
                                     audioEngineContext.outputGain.gain.value = value;
                                     interfaceContext.volume.slider.value = this.input.value;
                                 } else {
-                                    this.gain.gain.value = value
+                                    this.gain.gain.value = value;
                                 }
                                 break;
                         }
@@ -3033,7 +3079,7 @@
                     disconnect: function () {
                         this.gain.disconnect();
                     }
-                }
+                };
                 obj.root.className = "calibration-slider";
                 obj.root.appendChild(obj.input);
                 obj.oscillator.connect(obj.gain);
@@ -3061,53 +3107,63 @@
                 f0 *= 2;
             }
             inject.appendChild(this.holder);
-        }
+        };
         this.collect = function () {
-            for (var obj of this.calibrationNodes) {
+            this.calibrationNodes.forEach(function (obj) {
                 var node = storage.document.createElement("calibrationresult");
                 node.setAttribute("frequency", obj.f);
                 node.setAttribute("range-min", obj.input.min);
                 node.setAttribute("range-max", obj.input.max);
                 node.setAttribute("gain-lin", obj.gain.gain.value);
                 this.storeDOM.appendChild(node);
-            }
-        }
-    }
+            }, this);
+        };
+    };
 
 
     // Global Checkers
     // These functions will help enforce the checkers
-    this.checkHiddenAnchor = function () {
-        for (var ao of audioEngineContext.audioObjects) {
-            if (ao.specification.type == "anchor") {
-                if (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0) {
-                    // Anchor is not set below
-                    console.log('Anchor node not below marker value');
-                    interfaceContext.lightbox.post("Message", 'Please keep listening');
-                    this.storeErrorNode('Anchor node not below marker value');
-                    return false;
-                }
+    this.checkHiddenAnchor = function (message) {
+        var anchors = audioEngineContext.audioObjects.filter(function (ao) {
+            return ao.specification.type === "anchor";
+        });
+        var state = anchors.some(function (ao) {
+            return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0);
+        });
+        if (state) {
+            console.log('Anchor node not below marker value');
+            if (message) {
+                interfaceContext.lightbox.post("Message", message);
+            } else {
+                interfaceContext.lightbox.post("Message", 'Please keep listening');
             }
+            this.storeErrorNode('Anchor node not below marker value');
+            return false;
         }
         return true;
     };
 
-    this.checkHiddenReference = function () {
-        for (var ao of audioEngineContext.audioObjects) {
-            if (ao.specification.type == "reference") {
-                if (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0) {
-                    // Anchor is not set below
-                    console.log('Reference node not above marker value');
-                    this.storeErrorNode('Reference node not above marker value');
-                    interfaceContext.lightbox.post("Message", 'Please keep listening');
-                    return false;
-                }
+    this.checkHiddenReference = function (message) {
+        var references = audioEngineContext.audioObjects.filter(function (ao) {
+            return ao.specification.type === "reference";
+        });
+        var state = references.some(function (ao) {
+            return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0);
+        });
+        if (state) {
+            console.log('Reference node not below marker value');
+            if (message) {
+                interfaceContext.lightbox.post("Message", message);
+            } else {
+                interfaceContext.lightbox.post("Message", 'Please keep listening');
             }
+            this.storeErrorNode('Reference node not below marker value');
+            return false;
         }
         return true;
     };
 
-    this.checkFragmentsFullyPlayed = function () {
+    this.checkFragmentsFullyPlayed = function (message) {
         // Checks the entire file has been played back
         // NOTE ! This will return true IF playback is Looped!!!
         if (audioEngineContext.loopPlayback) {
@@ -3115,8 +3171,9 @@
             return true;
         }
         var check_pass = true;
-        var error_obj = [];
-        for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
+        var error_obj = [],
+            i;
+        for (i = 0; i < audioEngineContext.audioObjects.length; i++) {
             var object = audioEngineContext.audioObjects[i];
             var time = object.buffer.buffer.duration;
             var metric = object.metric;
@@ -3131,37 +3188,40 @@
                     break;
                 }
             }
-            if (passed == false) {
+            if (passed === false) {
                 check_pass = false;
                 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
                 error_obj.push(object.interfaceDOM.getPresentedId());
             }
         }
-        if (check_pass == false) {
+        if (check_pass === false) {
             var str_start = "You have not completely listened to fragments ";
-            for (var i = 0; i < error_obj.length; i++) {
+            for (i = 0; i < error_obj.length; i++) {
                 str_start += error_obj[i];
                 if (i != error_obj.length - 1) {
                     str_start += ', ';
                 }
             }
             str_start += ". Please keep listening";
-            console.log("[ALERT]: " + str_start);
-            this.storeErrorNode("[ALERT]: " + str_start);
+            console.log(str_start);
+            this.storeErrorNode(str_start);
+            if (message) {
+                str_start = message;
+            }
             interfaceContext.lightbox.post("Error", str_start);
             return false;
         }
         return true;
     };
-    this.checkAllMoved = function () {
+    this.checkAllMoved = function (message) {
         var str = "You have not moved ";
         var failed = [];
-        for (var ao of audioEngineContext.audioObjects) {
-            if (ao.metric.wasMoved == false && ao.interfaceDOM.canMove() == true) {
+        audioEngineContext.audioObjects.forEach(function (ao) {
+            if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) {
                 failed.push(ao.interfaceDOM.getPresentedId());
             }
-        }
-        if (failed.length == 0) {
+        }, this);
+        if (failed.length === 0) {
             return true;
         } else if (failed.length == 1) {
             str += 'track ' + failed[0];
@@ -3173,20 +3233,23 @@
             str += 'and ' + failed[i];
         }
         str += '.';
-        interfaceContext.lightbox.post("Error", str);
         console.log(str);
         this.storeErrorNode(str);
+        if (message) {
+            str = message;
+        }
+        interfaceContext.lightbox.post("Error", str);
         return false;
     };
-    this.checkAllPlayed = function () {
+    this.checkAllPlayed = function (message) {
         var str = "You have not played ";
         var failed = [];
-        for (var ao of audioEngineContext.audioObjects) {
-            if (ao.metric.wasListenedTo == false) {
+        audioEngineContext.audioObjects.forEach(function (ao) {
+            if (ao.metric.wasListenedTo === false) {
                 failed.push(ao.interfaceDOM.getPresentedId());
             }
-        }
-        if (failed.length == 0) {
+        }, this);
+        if (failed.length === 0) {
             return true;
         } else if (failed.length == 1) {
             str += 'track ' + failed[0];
@@ -3198,12 +3261,15 @@
             str += 'and ' + failed[i];
         }
         str += '.';
-        interfaceContext.lightbox.post("Error", str);
         console.log(str);
         this.storeErrorNode(str);
+        if (message) {
+            str = message;
+        }
+        interfaceContext.lightbox.post("Error", str);
         return false;
     };
-    this.checkAllCommented = function () {
+    this.checkAllCommented = function (message) {
         var str = "You have not commented on all the fragments.";
         var cont = true,
             boxes = this.commentBoxes.boxes,
@@ -3211,45 +3277,102 @@
             i;
         for (i = 0; i < numBoxes; i++) {
             if (boxes[i].trackCommentBox.value === "") {
-                interfaceContext.lightbox.post("Error", str);
                 console.log(str);
                 this.storeErrorNode(str);
+                if (message) {
+                    str = message;
+                }
+                interfaceContext.lightbox.post("Error", str);
                 return false;
             }
         }
         return true;
-    }
-    this.checkScaleRange = function (min, max) {
+    };
+    this.checkScaleRange = function (message) {
         var page = testState.getCurrentTestPage();
-        var audioObjects = audioEngineContext.audioObjects;
+        var interfaceObject = page.interfaces;
         var state = true;
         var str = "Please keep listening. ";
-        var minRanking = Infinity;
-        var maxRanking = -Infinity;
-        for (var ao of audioObjects) {
-            var rank = ao.interfaceDOM.getValue();
-            if (rank < minRanking) {
-                minRanking = rank;
-            }
-            if (rank > maxRanking) {
-                maxRanking = rank;
-            }
+        if (interfaceObject === undefined) {
+            return true;
         }
-        if (minRanking * 100 > min) {
-            str += "At least one fragment must be below the " + min + " mark.";
+        interfaceObject = interfaceObject[0];
+        var scales = (function () {
+            var scaleRange = interfaceObject.options.find(function (a) {
+                return a.name == "scalerange";
+            });
+            return {
+                min: scaleRange.min,
+                max: scaleRange.max
+            };
+        })();
+        var range = audioEngineContext.audioObjects.reduce(function (a, b) {
+            var v = b.interfaceDOM.getValue() * 100.0;
+            return {
+                min: Math.min(a.min, v),
+                max: Math.max(a.max, v)
+            };
+        }, {
+            min: 100,
+            max: 0
+        });
+        if (range.min > scales.min) {
+            str += "At least one fragment must be below the " + scales.min + " mark.";
+            state = false;
+        } else if (range.max < scales.max) {
+            str += "At least one fragment must be above the " + scales.max + " mark.";
             state = false;
         }
-        if (maxRanking * 100 < max) {
-            str += "At least one fragment must be above the " + max + " mark."
-            state = false;
-        }
-        if (!state) {
+        if (state === false) {
             console.log(str);
             this.storeErrorNode(str);
+            if (message) {
+                str = message;
+            }
             interfaceContext.lightbox.post("Error", str);
         }
         return state;
-    }
+    };
+    this.checkFragmentMinPlays = function () {
+        var failedObjects = audioEngineContext.audioObjects.filter(function (a) {
+            var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays;
+            if (minPlays === undefined || a.numberOfPlays >= minPlays) {
+                return false;
+            }
+            return true;
+        });
+        if (failedObjects.length === 0) {
+            return true;
+        }
+        var failedString = [];
+        failedObjects.forEach(function (a) {
+            failedString.push(a.interfaceDOM.getPresentedId());
+        });
+        var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening";
+        interfaceContext.lightbox.post("Message", str);
+        this.storeErrorNode(str);
+        return false;
+    };
+
+
+    this.sortFragmentsByScore = function () {
+        var elements = audioEngineContext.audioObjects.filter(function (elem) {
+            return elem.specification.type !== "outside-reference";
+        });
+        var indexes = [];
+        var i = 0;
+        while (indexes.push(i++) < elements.length);
+        return indexes.sort(function (x, y) {
+            var a = elements[x].interfaceDOM.getValue();
+            var b = elements[y].interfaceDOM.getValue();
+            if (a > b) {
+                return 1;
+            } else if (a < b) {
+                return -1;
+            }
+            return 0;
+        }, elements[0].interfaceDOM.getValue());
+    };
 
     this.storeErrorNode = function (errorMessage) {
         var time = audioEngineContext.timer.getTestTime();
@@ -3274,13 +3397,12 @@
                 case "capital":
                     return String.fromCharCode((index + offset) % 26 + 65);
                 case "samediff":
-                    if (index == 0) {
+                    if (index === 0) {
                         return "Same";
                     } else if (index == 1) {
                         return "Difference";
-                    } else {
-                        return "";
                     }
+                    return "";
                 case "number":
                     return String(index + offset);
                 default:
@@ -3288,7 +3410,7 @@
             }
         }
 
-        if (typeof labelStart !== "string" || labelStart.length == 0) {
+        if (typeof labelStart !== "string" || labelStart.length === 0) {
             labelStart = String.fromCharCode(0);
         }
 
@@ -3313,7 +3435,6 @@
                     labelStart = 1;
                 }
                 break;
-            case "none":
             default:
                 labelStart = 0;
         }
@@ -3330,7 +3451,7 @@
         } else {
             throw ("Invalid arguments");
         }
-    }
+    };
 
     this.getCombinedInterfaces = function (page) {
         // Combine the interfaces with the global interface nodes
@@ -3355,7 +3476,7 @@
             }
         });
         return local;
-    }
+    };
 }
 
 function Storage() {
@@ -3366,16 +3487,17 @@
     this.document = null;
     this.root = null;
     this.state = 0;
+    var pFilenamePrefix = "save";
 
     this.initialise = function (existingStore) {
-        if (existingStore == undefined) {
+        if (existingStore === undefined) {
             // We need to get the sessionKey
             this.SessionKey.requestKey();
             this.document = document.implementation.createDocument(null, "waetresult", null);
             this.root = this.document.childNodes[0];
             var projectDocument = specification.projectXML;
-            projectDocument.setAttribute('file-name', url);
-            projectDocument.setAttribute('url', qualifyURL(url));
+            projectDocument.setAttribute('file-name', specification.url);
+            projectDocument.setAttribute('url', qualifyURL(specification.url));
             this.root.appendChild(projectDocument);
             this.root.appendChild(interfaceContext.returnDateNode());
             this.root.appendChild(interfaceContext.returnNavigator());
@@ -3384,10 +3506,10 @@
             this.root = existingStore.firstChild;
             this.SessionKey.key = this.root.getAttribute("key");
         }
-        if (specification.preTest != undefined) {
+        if (specification.preTest !== undefined) {
             this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
         }
-        if (specification.postTest != undefined) {
+        if (specification.postTest !== undefined) {
             this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
         }
     };
@@ -3399,7 +3521,7 @@
         handleEvent: function () {
             var parse = new DOMParser();
             var xml = parse.parseFromString(this.request.response, "text/xml");
-            if (this.request.response.length == 0) {
+            if (this.request.response.length === 0) {
                 console.error("An unspecified error occured, no server key could be generated");
                 return;
             }
@@ -3432,7 +3554,7 @@
             this.request.send();
         },
         update: function () {
-            if (this.key == null) {
+            if (this.key === null) {
                 console.log("Cannot save as key == null");
                 return;
             }
@@ -3444,7 +3566,7 @@
                     returnURL = specification.projectReturn;
                 }
             }
-            xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key);
+            xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix);
             xmlhttp.setRequestHeader('Content-Type', 'text/xml');
             xmlhttp.onerror = function () {
                 console.log('Error updating file to server!');
@@ -3467,10 +3589,51 @@
                         console.log("Intermediate save: Error! " + message.textContent);
                     }
                 }
+            };
+            xmlhttp.send([hold.innerHTML]);
+        },
+        finish: function () {
+            // Final upload to complete the test
+            this.parent.finish();
+            var hold = document.createElement("div");
+            var clone = this.parent.root.cloneNode(true);
+            hold.appendChild(clone);
+            var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=";
+            if (this.parent.filenamePrefix.length === 0) {
+                saveURL += "save";
+            } else {
+                saveURL += this.parent.filenamePrefix;
             }
-            xmlhttp.send([hold.innerHTML]);
+            return new Promise(function (resolve, reject) {
+                var xmlhttp = new XMLHttpRequest();
+                xmlhttp.open("POST", saveURL);
+                xmlhttp.setRequestHeader('Content-Type', 'text/xml');
+                xmlhttp.onerror = function () {
+                    console.log('Error updating file to server!');
+                    createProjectSave("local");
+                };
+                xmlhttp.onload = function () {
+                    if (this.status >= 300) {
+                        console.log("WARNING - Could not update at this time");
+                        createProjectSave("local");
+                    } else {
+                        var parser = new DOMParser();
+                        var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
+                        var response = xmlDoc.getElementsByTagName('response')[0];
+                        if (response.getAttribute("state") == "OK") {
+                            var file = response.getElementsByTagName("file")[0];
+                            console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
+                            resolve(response);
+                        } else {
+                            var message = response.getElementsByTagName("message");
+                            reject(message);
+                        }
+                    }
+                };
+                xmlhttp.send([hold.innerHTML]);
+            });
         }
-    }
+    };
 
     this.createTestPageStore = function (specification) {
         var store = new this.pageNode(this, specification);
@@ -3485,55 +3648,67 @@
         this.XMLDOM = this.parent.document.createElement('survey');
         this.XMLDOM.setAttribute('location', this.specification.location);
         this.XMLDOM.setAttribute("state", this.state);
-        for (var optNode of this.specification.options) {
+        this.specification.options.forEach(function (optNode) {
             if (optNode.type != 'statement') {
                 var node = this.parent.document.createElement('surveyresult');
                 node.setAttribute("ref", optNode.id);
                 node.setAttribute('type', optNode.type);
                 this.XMLDOM.appendChild(node);
             }
-        }
+        }, this);
         root.appendChild(this.XMLDOM);
 
         this.postResult = function (node) {
+            function postNumber(doc, value) {
+                var child = doc.createElement("response");
+                child.textContent = value;
+                return child;
+            }
+
+            function postRadio(doc, node) {
+                var child = doc.createElement('response');
+                if (node.response !== null) {
+                    child.setAttribute('name', node.response.name);
+                    child.textContent = node.response.text;
+                }
+                return child;
+            }
+
+            function postCheckbox(doc, node) {
+                var checkNode = doc.createElement('response');
+                checkNode.setAttribute('name', node.name);
+                checkNode.setAttribute('checked', node.checked);
+                return checkNode;
+            }
             // From popup: node is the popupOption node containing both spec. and results
             // ID is the position
             if (node.specification.type == 'statement') {
                 return;
             }
             var surveyresult = this.XMLDOM.firstChild;
-            while (surveyresult != null) {
+            while (surveyresult !== null) {
                 if (surveyresult.getAttribute("ref") == node.specification.id) {
                     break;
                 }
                 surveyresult = surveyresult.nextElementSibling;
             }
+            surveyresult.setAttribute("duration", node.elapsedTime);
             switch (node.specification.type) {
                 case "number":
                 case "question":
                 case "slider":
-                    var child = this.parent.document.createElement('response');
-                    child.textContent = node.response;
-                    surveyresult.appendChild(child);
+                    surveyresult.appendChild(postNumber(this.parent.document, node.response));
                     break;
                 case "radio":
-                    var child = this.parent.document.createElement('response');
-                    if (node.response !== null) {
-                        child.setAttribute('name', node.response.name);
-                        child.textContent = node.response.text;
-                    }
-                    surveyresult.appendChild(child);
+                    surveyresult.appendChild(postRadio(this.parent.document, node));
                     break;
                 case "checkbox":
-                    if (node.response == undefined) {
+                    if (node.response === undefined) {
                         surveyresult.appendChild(this.parent.document.createElement('response'));
                         break;
                     }
                     for (var i = 0; i < node.response.length; i++) {
-                        var checkNode = this.parent.document.createElement('response');
-                        checkNode.setAttribute('name', node.response[i].name);
-                        checkNode.setAttribute('checked', node.response[i].checked);
-                        surveyresult.appendChild(checkNode);
+                        surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i]));
                     }
                     break;
             }
@@ -3541,7 +3716,7 @@
         this.complete = function () {
             this.state = "complete";
             this.XMLDOM.setAttribute("state", this.state);
-        }
+        };
     };
 
     this.pageNode = function (parent, specification) {
@@ -3553,10 +3728,10 @@
         this.XMLDOM.setAttribute('ref', specification.id);
         this.XMLDOM.setAttribute('presentedId', specification.presentedId);
         this.XMLDOM.setAttribute("state", this.state);
-        if (specification.preTest != undefined) {
+        if (specification.preTest !== undefined) {
             this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
         }
-        if (specification.postTest != undefined) {
+        if (specification.postTest !== undefined) {
             this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
         }
 
@@ -3565,12 +3740,12 @@
         this.XMLDOM.appendChild(page_metric);
 
         // Add the audioelement
-        for (var element of this.specification.audioElements) {
+        this.specification.audioElements.forEach(function (element) {
             var aeNode = this.parent.document.createElement('audioelement');
             aeNode.setAttribute('ref', element.id);
-            if (element.name != undefined) {
-                aeNode.setAttribute('name', element.name)
-            };
+            if (element.name !== undefined) {
+                aeNode.setAttribute('name', element.name);
+            }
             aeNode.setAttribute('type', element.type);
             aeNode.setAttribute('url', element.url);
             aeNode.setAttribute('fqurl', qualifyURL(element.url));
@@ -3583,26 +3758,38 @@
             var ae_metric = this.parent.document.createElement('metric');
             aeNode.appendChild(ae_metric);
             this.XMLDOM.appendChild(aeNode);
-        }
+        }, this);
 
         this.parent.root.appendChild(this.XMLDOM);
 
         this.complete = function () {
             this.state = "complete";
             this.XMLDOM.setAttribute("state", "complete");
-        }
+        };
     };
     this.update = function () {
         this.SessionKey.update();
-    }
+    };
     this.finish = function () {
-        if (this.state == 0) {
-            this.update();
-        }
         this.state = 1;
         this.root.setAttribute("state", "complete");
         return this.root;
     };
+
+    Object.defineProperties(this, {
+        'filenamePrefix': {
+            'get': function () {
+                return pFilenamePrefix;
+            },
+            'set': function (value) {
+                if (typeof value !== "string") {
+                    value = String(value);
+                }
+                pFilenamePrefix = value;
+                return value;
+            }
+        }
+    });
 }
 
 var window_depedancy_callback;
--- a/js/loader.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/js/loader.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,8 +1,8 @@
 // Script to load the relevant JS files if the system supports it
-
+/*globals window, document */
 window.onload = function () {
     // First check if the Web Audio API is supported
-    if (window.AudioContext == undefined && window.webkitAudioContext == undefined) {
+    if (window.AudioContext === undefined && window.webkitAudioContext === undefined) {
         // Display unsuported error message
         var body = document.getElementsByTagName("body")[0];
         body.innerHTML = "<h1>Sorry! Your browser is not supported :(</h1><p>Your browser does not support the HTML5 Web Audio API. Please use one of the following supported browsers instead.<p>";
@@ -26,4 +26,4 @@
             head.appendChild(script);
         }
     }
-}
+};
--- a/js/loudness.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/js/loudness.js	Fri Jul 14 15:39:24 2017 +0100
@@ -5,7 +5,7 @@
  * 	return gain values to correct for a target loudness or match loudness between
  *  multiple objects
  */
-
+/* globals webkitOfflineAudioContext, navigator, audioContext, Float32Array */
 var interval_cal_loudness_event = null;
 
 if (typeof OfflineAudioContext == "undefined") {
@@ -21,16 +21,16 @@
     if (navigator.platform == 'iPad' || navigator.platform == 'iPhone') {
         buffer.ready();
     }
-    if (buffer == undefined) {
+    if (buffer === undefined) {
         return 0;
     }
-    if (timescale == undefined) {
+    if (timescale === undefined) {
         timescale = "I";
     }
-    if (target == undefined) {
+    if (target === undefined) {
         target = -23;
     }
-    if (offlineContext == undefined) {
+    if (offlineContext === undefined) {
         offlineContext = new OfflineAudioContext(audioContext.destination.channelCount, buffer.buffer.duration * audioContext.sampleRate, audioContext.sampleRate);
     }
     // Create the required filters
@@ -77,18 +77,23 @@
 }
 
 function calculateMeanSquared(buffer, frame_dur, frame_overlap) {
-    frame_size = Math.floor(buffer.sampleRate * frame_dur);
-    step_size = Math.floor(frame_size * (1.0 - frame_overlap));
-    num_frames = Math.floor((buffer.length - frame_size) / step_size);
+    var frame_size = Math.floor(buffer.sampleRate * frame_dur);
+    var step_size = Math.floor(frame_size * (1.0 - frame_overlap));
+    var num_frames = Math.floor((buffer.length - frame_size) / step_size);
+    num_frames = Math.max(num_frames, 0);
 
-    MS = Array(buffer.numberOfChannels);
+    var MS = Array(buffer.numberOfChannels);
     for (var c = 0; c < buffer.numberOfChannels; c++) {
         MS[c] = new Float32Array(num_frames);
         var data = buffer.getChannelData(c);
         for (var no = 0; no < num_frames; no++) {
             MS[c][no] = 0.0;
             for (var ptr = 0; ptr < frame_size; ptr++) {
-                var sample = data[no * step_size + ptr];
+                var i = no * step_size + ptr;
+                if (i >= buffer.length) {
+                    break;
+                }
+                var sample = data[i];
                 MS[c][no] += sample * sample;
             }
             MS[c][no] /= frame_size;
@@ -119,13 +124,14 @@
     var num_frames = source[0].length;
     var num_channels = source.length;
     var LK = Array(num_channels);
-    for (var c = 0; c < num_channels; c++) {
+    var n, c;
+    for (c = 0; c < num_channels; c++) {
         LK[c] = [];
     }
 
-    for (var n = 0; n < num_frames; n++) {
+    for (n = 0; n < num_frames; n++) {
         if (blocks[n] > threshold) {
-            for (var c = 0; c < num_channels; c++) {
+            for (c = 0; c < num_channels; c++) {
                 LK[c].push(source[c][n]);
             }
         }
--- a/js/specification.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/js/specification.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,45 +1,58 @@
+/* globals document, console, DOMParser */
 function Specification() {
+    var schemaRoot;
+    var schemaString;
     // Handles the decoding of the project specification XML into a simple JavaScript Object.
 
     // <setup> attributes
-    this.interface = null;
-    this.projectReturn = null;
-    this.returnURL = null;
-    this.randomiseOrder = null;
-    this.poolSize = null;
-    this.loudness = null;
-    this.sampleRate = null;
-    this.calibration = null;
-    this.crossFade = null;
-    this.preSilence = null;
-    this.postSilence = null;
-    this.playOne = null;
+    this.interface = undefined;
+    this.projectReturn = undefined;
+    this.returnURL = undefined;
+    this.randomiseOrder = undefined;
+    this.poolSize = undefined;
+    this.loudness = undefined;
+    this.sampleRate = undefined;
+    this.calibration = undefined;
+    this.crossFade = undefined;
+    this.preSilence = undefined;
+    this.postSilence = undefined;
+    this.playOne = undefined;
+    this.minNumberPlays = undefined;
+    this.maxNumberPlays = undefined;
 
     // nodes
-    this.metrics = null;
-    this.preTest = undefined;
-    this.postTest = undefined;
+    this.metrics = new metricNode();
+    this.preTest = new surveyNode(this);
+    this.postTest = new surveyNode(this);
+    this.preTest.location = "pre";
+    this.postTest.location = "post";
     this.pages = [];
-    this.interfaces = null;
+    this.interfaces = new interfaceNode(this);
     this.errors = [];
-    this.schema = null;
     this.exitText = "Thank you.";
 
-    this.processAttribute = function (attribute, schema, schemaRoot) {
+    // Creators
+    this.createNewPage = function () {
+        var newpage = new page(this);
+        this.pages.push(newpage);
+        return newpage;
+    };
+
+    var processAttribute = function (attribute, schema) {
         // attribute is the string returned from getAttribute on the XML
         // schema is the <xs:attribute> node
-        if (schema.getAttribute('name') == undefined && schema.getAttribute('ref') != undefined) {
-            schema = schemaRoot.getAllElementsByName(schema.getAttribute('ref'))[0];
+        if (schema.getAttribute('name') === null && schema.getAttribute('ref') !== undefined) {
+            schema = schemaRoot.querySelector("[name=" + schema.getAttribute('ref') + "]");
         }
         var defaultOpt = schema.getAttribute('default');
-        if (attribute == null) {
+        if (attribute === null) {
             attribute = defaultOpt;
         }
         var dataType = schema.getAttribute('type');
         if (typeof dataType == "string") {
             dataType = dataType.substr(3);
         } else {
-            var rest = schema.getAllElementsByTagName("xs:restriction").concat(schema.getAllElementsByTagName("xs:enumeration"));
+            var rest = schema.querySelectorAll("restriction,enumeration");
             if (rest.length > 0) {
                 dataType = rest[0].getAttribute("base");
                 if (typeof dataType == "string") {
@@ -51,7 +64,7 @@
                 dataType = "string";
             }
         }
-        if (attribute == null) {
+        if (attribute === null) {
             return attribute;
         }
         switch (dataType) {
@@ -71,7 +84,6 @@
             case "short":
                 attribute = Number(attribute);
                 break;
-            case "string":
             default:
                 attribute = String(attribute);
                 break;
@@ -79,26 +91,44 @@
         return attribute;
     };
 
+    this.processSchema = function (schemaXSD) {
+        if (schemaRoot === undefined) {
+            schemaString = schemaXSD;
+            var parse = new DOMParser();
+            schemaRoot = parse.parseFromString(schemaString, 'text/xml');
+            Object.defineProperties(this, {
+                'schema': {
+                    'value': schemaRoot
+                },
+                'schemaString': {
+                    'value': schemaString
+                }
+            });
+        }
+    };
+    this.getSchema = function () {
+        return schemaRoot;
+    };
+    this.getSchemaString = function () {
+        return schemaString;
+    };
+
     this.decode = function (projectXML) {
+        schemaRoot = this.schema;
         this.errors = [];
         // projectXML - DOM Parsed document
         this.projectXML = projectXML.childNodes[0];
         var setupNode = projectXML.getElementsByTagName('setup')[0];
-        var schemaSetup = this.schema.getAllElementsByName('setup')[0];
+        var schemaSetup = schemaRoot.querySelector('[name=setup]');
         // First decode the attributes
-        var attributes = schemaSetup.getAllElementsByTagName('xs:attribute');
-        for (var i = 0; i < attributes.length; i++) {
+        var attributes = schemaSetup.querySelectorAll('attribute');
+        var i;
+        for (i = 0; i < attributes.length; i++) {
             var attributeName = attributes[i].getAttribute('name') || attributes[i].getAttribute('ref');
             var projectAttr = setupNode.getAttribute(attributeName);
-            projectAttr = this.processAttribute(projectAttr, attributes[i], this.schema);
-            switch (typeof projectAttr) {
-                case "number":
-                case "boolean":
-                    eval('this.' + attributeName + ' = ' + projectAttr);
-                    break;
-                case "string":
-                    eval('this.' + attributeName + ' = "' + projectAttr + '"');
-                    break;
+            projectAttr = processAttribute(projectAttr, attributes[i]);
+            if (projectAttr !== null) {
+                this[attributeName] = projectAttr;
             }
 
         }
@@ -108,23 +138,19 @@
             this.exitText = exitTextNode[0].textContent;
         }
 
-        this.metrics = new this.metricNode();
-
         this.metrics.decode(this, setupNode.getElementsByTagName('metric')[0]);
 
         // Now process the survey node options
         var survey = setupNode.getElementsByTagName('survey');
-        for (var i = 0; i < survey.length; i++) {
+        for (i = 0; i < survey.length; i++) {
             var location = survey[i].getAttribute('location');
             switch (location) {
                 case 'pre':
                 case 'before':
-                    this.preTest = new this.surveyNode(this);
                     this.preTest.decode(this, survey[i]);
                     break;
                 case 'post':
                 case 'after':
-                    this.postTest = new this.surveyNode(this);
                     this.postTest.decode(this, survey[i]);
                     break;
             }
@@ -134,17 +160,17 @@
         if (interfaceNode.length > 1) {
             this.errors.push("Only one <interface> node in the <setup> node allowed! Others except first ingnored!");
         }
-        this.interfaces = new this.interfaceNode(this);
-        if (interfaceNode.length != 0) {
+
+        if (interfaceNode.length !== 0) {
             interfaceNode = interfaceNode[0];
-            this.interfaces.decode(this, interfaceNode, this.schema.getAllElementsByName('interface')[1]);
+            this.interfaces.decode(this, interfaceNode, this.schema.querySelectorAll('[name=interface]')[1]);
         }
 
         // Page tags
         var pageTags = projectXML.getElementsByTagName('page');
-        var pageSchema = this.schema.getAllElementsByName('page')[0];
-        for (var i = 0; i < pageTags.length; i++) {
-            var node = new this.page(this);
+        var pageSchema = this.schema.querySelector('[name=page]');
+        for (i = 0; i < pageTags.length; i++) {
+            var node = new page(this);
             node.decode(this, pageTags[i], pageSchema);
             this.pages.push(node);
         }
@@ -157,21 +183,21 @@
         root.setAttribute("xsi:noNamespaceSchemaLocation", "test-schema.xsd");
         // Build setup node
         var setup = RootDocument.createElement("setup");
-        var schemaSetup = this.schema.getAllElementsByName('setup')[0];
+        var schemaSetup = schemaRoot.querySelector('[name=setup]');
         // First decode the attributes
-        var attributes = schemaSetup.getAllElementsByTagName('xs:attribute');
+        var attributes = schemaSetup.querySelectorAll('attribute');
         for (var i = 0; i < attributes.length; i++) {
             var name = attributes[i].getAttribute("name");
-            if (name == undefined) {
+            if (name === null) {
                 name = attributes[i].getAttribute("ref");
             }
-            if (eval("this." + name + " != undefined") || attributes[i].getAttribute("use") == "required") {
-                eval("setup.setAttribute('" + name + "',this." + name + ")");
+            if (this[name] !== undefined || attributes[i].getAttribute("use") == "required") {
+                setup.setAttribute(name, this[name]);
             }
         }
         root.appendChild(setup);
         // Survey node
-        if (this.exitText != null) {
+        if (this.exitText !== null) {
             var exitTextNode = RootDocument.createElement('exitText');
             exitTextNode.textContent = this.exitText;
             setup.appendChild(exitTextNode);
@@ -180,19 +206,24 @@
         setup.appendChild(this.postTest.encode(RootDocument));
         setup.appendChild(this.metrics.encode(RootDocument));
         setup.appendChild(this.interfaces.encode(RootDocument));
-        for (var page of this.pages) {
+        this.pages.forEach(function (page) {
             root.appendChild(page.encode(RootDocument));
-        }
+        });
         return RootDocument;
     };
 
-    this.surveyNode = function (specification) {
-        this.location = null;
+    function surveyNode(specification) {
+        this.location = undefined;
         this.options = [];
-        this.parent = null;
-        this.schema = specification.schema.getAllElementsByName('survey')[0];
+        this.parent = undefined;
         this.specification = specification;
 
+        this.addOption = function () {
+            var node = new this.OptionNode(this.specification);
+            this.options.push(node);
+            return node;
+        };
+
         this.OptionNode = function (specification) {
             this.type = undefined;
             this.schema = undefined;
@@ -204,27 +235,23 @@
             this.options = [];
             this.min = undefined;
             this.max = undefined;
+            this.minWait = undefined;
             this.step = undefined;
             this.conditions = [];
 
             this.decode = function (parent, child) {
-                this.schema = specification.schema.getAllElementsByName(child.nodeName)[0];
-                var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
-                for (var i in attributeMap) {
-                    if (isNaN(Number(i)) == true) {
+                this.schema = schemaRoot.querySelector("[name=" + child.nodeName + "]");
+                var attributeMap = this.schema.querySelectorAll('attribute');
+                var i;
+                for (i in attributeMap) {
+                    if (isNaN(Number(i)) === true) {
                         break;
                     }
                     var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
                     var projectAttr = child.getAttribute(attributeName);
-                    projectAttr = parent.processAttribute(projectAttr, attributeMap[i], parent.schema);
-                    switch (typeof projectAttr) {
-                        case "number":
-                        case "boolean":
-                            eval('this.' + attributeName + ' = ' + projectAttr);
-                            break;
-                        case "string":
-                            eval('this.' + attributeName + ' = "' + projectAttr + '"');
-                            break;
+                    projectAttr = processAttribute(projectAttr, attributeMap[i]);
+                    if (projectAttr !== null) {
+                        this[attributeName] = projectAttr;
                     }
                 }
                 if (child.nodeName == 'surveyentry') {
@@ -239,13 +266,13 @@
                 this.statement = child.getElementsByTagName('statement')[0].textContent;
                 if (this.type == "checkbox" || this.type == "radio") {
                     var children = child.getElementsByTagName('option');
-                    if (children.length == null) {
+                    if (children.length === null) {
                         console.log('Malformed' + child.nodeName + 'entry');
                         this.statement = 'Malformed' + child.nodeName + 'entry';
                         this.type = 'statement';
                     } else {
                         this.options = [];
-                        for (var i = 0; i < children.length; i++) {
+                        for (i = 0; i < children.length; i++) {
                             this.options.push({
                                 name: children[i].getAttribute('name'),
                                 text: children[i].textContent
@@ -265,14 +292,14 @@
                     }
                 }
                 var conditionElements = child.getElementsByTagName("conditional");
-                for (var i = 0; i < conditionElements.length; i++) {
+                for (i = 0; i < conditionElements.length; i++) {
                     var condition = conditionElements[i];
                     var obj = {
                         check: condition.getAttribute("check"),
                         value: condition.getAttribute("value"),
                         jumpToOnPass: condition.getAttribute("jumpToOnPass"),
                         jumpToOnFail: condition.getAttribute("jumpToOnFail")
-                    }
+                    };
                     this.conditions.push(obj);
                 }
             };
@@ -283,28 +310,28 @@
                 statement.textContent = this.statement;
                 node.appendChild(statement);
                 node.id = this.id;
-                if (this.name != undefined) {
+                if (this.name !== undefined) {
                     node.setAttribute("name", this.name);
                 }
-                if (this.mandatory != undefined) {
+                if (this.mandatory !== undefined) {
                     node.setAttribute("mandatory", this.mandatory);
                 }
                 node.id = this.id;
-                if (this.name != undefined) {
+                if (this.name !== undefined) {
                     node.setAttribute("name", this.name);
                 }
                 switch (this.type) {
                     case "checkbox":
-                        if (this.min != undefined) {
+                        if (this.min !== undefined) {
                             node.setAttribute("min", this.min);
                         } else {
                             node.setAttribute("min", "0");
                         }
-                        if (this.max != undefined) {
+                        if (this.max !== undefined) {
                             node.setAttribute("max", this.max);
                         } else {
                             node.setAttribute("max", "undefined");
-                        }
+                        } /* falls through */
                     case "radio":
                         for (var i = 0; i < this.options.length; i++) {
                             var option = this.options[i];
@@ -315,27 +342,27 @@
                         }
                         break;
                     case "number":
-                        if (this.min != undefined) {
+                        if (this.min !== undefined) {
                             node.setAttribute("min", this.min);
                         }
-                        if (this.max != undefined) {
+                        if (this.max !== undefined) {
                             node.setAttribute("max", this.max);
                         }
                         break;
                     case "question":
-                        if (this.boxsize != undefined) {
+                        if (this.boxsize !== undefined) {
                             node.setAttribute("boxsize", this.boxsize);
                         }
-                        if (this.mandatory != undefined) {
+                        if (this.mandatory !== undefined) {
                             node.setAttribute("mandatory", this.mandatory);
                         }
                         break;
                     case "video":
-                        if (this.mandatory != undefined) {
+                        if (this.mandatory !== undefined) {
                             node.setAttribute("mandatory", this.mandatory);
-                        }
+                        } /* falls through */
                     case "youtube":
-                        if (this.url != undefined) {
+                        if (this.url !== undefined) {
                             node.setAttribute("url", this.url);
                         }
                         break;
@@ -352,21 +379,23 @@
                             maxText.textContent = this.rightText;
                             node.appendChild(maxText);
                         }
+                        break;
                     default:
                         break;
                 }
-                for (var condition of this.conditions) {
+                this.conditions.forEach(function (condition) {
                     var conditionDOM = doc.createElement("conditional");
                     conditionDOM.setAttribute("check", condition.check);
                     conditionDOM.setAttribute("value", condition.value);
                     conditionDOM.setAttribute("jumpToOnPass", condition.jumpToOnPass);
                     conditionDOM.setAttribute("jumpToOnFail", condition.jumpToOnFail);
                     node.appendChild(conditionDOM);
-                }
+                });
                 return node;
             };
         };
         this.decode = function (parent, xml) {
+            this.schema = schemaRoot.querySelector('[name=survey]');
             this.parent = parent;
             this.location = xml.getAttribute('location');
             if (this.location == 'before') {
@@ -374,14 +403,14 @@
             } else if (this.location == 'after') {
                 this.location = 'post';
             }
-            var child = xml.firstElementChild
+            var child = xml.firstElementChild;
             while (child) {
                 var node = new this.OptionNode(this.specification);
                 node.decode(parent, child);
                 this.options.push(node);
                 child = child.nextElementSibling;
             }
-            if (this.options.length == 0) {
+            if (this.options.length === 0) {
                 console.log("Empty survey node");
                 console.log(this);
             }
@@ -394,16 +423,18 @@
             }
             return node;
         };
-    };
+    }
 
-    this.interfaceNode = function (specification) {
-        this.title = null;
-        this.name = null;
+    function interfaceNode(specification) {
+        this.title = undefined;
+        this.name = undefined;
+        this.image = undefined;
         this.options = [];
         this.scales = [];
-        this.schema = specification.schema.getAllElementsByName('interface')[1];
+        this.schema = undefined;
 
         this.decode = function (parent, xml) {
+            this.schema = schemaRoot.querySelectorAll('[name=interface]')[1];
             this.name = xml.getAttribute('name');
             var titleNode = xml.getElementsByTagName('title');
             if (titleNode.length == 1) {
@@ -411,38 +442,36 @@
             }
             var interfaceOptionNodes = xml.getElementsByTagName('interfaceoption');
             // Extract interfaceoption node schema
-            var interfaceOptionNodeSchema = this.schema.getAllElementsByName('interfaceoption')[0];
-            var attributeMap = interfaceOptionNodeSchema.getAllElementsByTagName('xs:attribute');
-            for (var i = 0; i < interfaceOptionNodes.length; i++) {
+            var interfaceOptionNodeSchema = this.schema.querySelector('[name=interfaceoption]');
+            var attributeMap = interfaceOptionNodeSchema.querySelectorAll('attribute');
+            var i, j;
+            for (i = 0; i < interfaceOptionNodes.length; i++) {
                 var ioNode = interfaceOptionNodes[i];
                 var option = {};
-                for (var j = 0; j < attributeMap.length; j++) {
+                for (j = 0; j < attributeMap.length; j++) {
                     var attributeName = attributeMap[j].getAttribute('name') || attributeMap[j].getAttribute('ref');
                     var projectAttr = ioNode.getAttribute(attributeName);
-                    if (parent.processAttribute) {
-                        parent.processAttribute(projectAttr, attributeMap[j], parent.schema)
-                    } else {
-                        parent.parent.processAttribute(projectAttr, attributeMap[j], parent.parent.schema)
+                    projectAttr = processAttribute(projectAttr, attributeMap[j]);
+                    if (projectAttr !== null) {
+                        option[attributeName] = projectAttr;
                     }
-                    switch (typeof projectAttr) {
-                        case "number":
-                        case "boolean":
-                            eval('option.' + attributeName + ' = ' + projectAttr);
-                            break;
-                        case "string":
-                            eval('option.' + attributeName + ' = "' + projectAttr + '"');
-                            break;
-                    }
+                }
+                if (option.type == "check" && ioNode.firstElementChild) {
+                    option.errorMessage = ioNode.firstElementChild.textContent;
                 }
                 this.options.push(option);
             }
-
+            // Get the image node
+            var imageNode = xml.getElementsByTagName("image");
+            if (imageNode.length == 1) {
+                this.image = imageNode[0].getAttribute("src");
+            }
             // Now the scales nodes
             var scaleParent = xml.getElementsByTagName('scales');
             if (scaleParent.length == 1) {
                 scaleParent = scaleParent[0];
-                var scalelabels = scaleParent.getAllElementsByTagName('scalelabel');
-                for (var i = 0; i < scalelabels.length; i++) {
+                var scalelabels = scaleParent.querySelectorAll('scalelabel');
+                for (i = 0; i < scalelabels.length; i++) {
                     this.scales.push({
                         text: scalelabels[i].textContent,
                         position: Number(scalelabels[i].getAttribute('position'))
@@ -453,48 +482,58 @@
 
         this.encode = function (doc) {
             var node = doc.createElement("interface");
-            if (typeof name == "string" && name.length > 0)
+            if (typeof this.name == "string" && this.name.length > 0)
                 node.setAttribute("name", this.name);
             if (typeof this.title == "string") {
                 var titleNode = doc.createElement("title");
                 titleNode.textContent = this.title;
                 node.appendChild(titleNode);
             }
-            for (var option of this.options) {
+            this.options.forEach(function (option) {
                 var child = doc.createElement("interfaceoption");
                 child.setAttribute("type", option.type);
                 child.setAttribute("name", option.name);
+                if (option.type == "check" && option.errorMessage !== undefined) {
+                    var errorMessage = doc.createElement("errormessage");
+                    errorMessage.textContent = option.errorMessage;
+                    child.appendChild(errorMessage);
+                }
                 node.appendChild(child);
+            });
+            if (typeof this.image == "string" && this.image.length !== 0) {
+                var imgNode = doc.createElement("image");
+                imgNode.setAttribute("src", this.image);
+                node.appendChild(imgNode);
             }
-            if (this.scales.length != 0) {
+            if (this.scales.length !== 0) {
                 var scales = doc.createElement("scales");
-                for (var scale of this.scales) {
+                this.scales.forEach(function (scale) {
                     var child = doc.createElement("scalelabel");
                     child.setAttribute("position", scale.position);
                     child.textContent = scale.text;
                     scales.appendChild(child);
-                }
+                });
                 node.appendChild(scales);
             }
             return node;
         };
-    };
+    }
 
-    this.metricNode = function () {
+    function metricNode() {
         this.enabled = [];
         this.decode = function (parent, xml) {
             var children = xml.getElementsByTagName('metricenable');
             for (var i in children) {
-                if (isNaN(Number(i)) == true) {
+                if (isNaN(Number(i)) === true) {
                     break;
                 }
                 this.enabled.push(children[i].textContent);
             }
-        }
+        };
         this.encode = function (doc) {
             var node = doc.createElement('metric');
             for (var i in this.enabled) {
-                if (isNaN(Number(i)) == true) {
+                if (isNaN(Number(i)) === true) {
                     break;
                 }
                 var child = doc.createElement('metricenable');
@@ -502,87 +541,101 @@
                 node.appendChild(child);
             }
             return node;
-        }
+        };
     }
 
-    this.page = function (specification) {
+    function page(specification) {
         this.presentedId = undefined;
         this.id = undefined;
         this.title = undefined;
         this.hostURL = undefined;
         this.randomiseOrder = undefined;
         this.loop = undefined;
-        this.outsideReference = null;
-        this.loudness = null;
-        this.label = null;
-        this.labelStart = "";
-        this.preTest = null;
-        this.postTest = null;
+        this.outsideReference = undefined;
+        this.loudness = undefined;
+        this.label = undefined;
+        this.labelStart = undefined;
+        this.preTest = new surveyNode(specification);
+        this.postTest = new surveyNode(specification);
+        this.preTest.location = "pre";
+        this.postTest.location = "post";
         this.interfaces = [];
-        this.playOne = null;
-        this.restrictMovement = null;
+        this.playOne = undefined;
+        this.restrictMovement = undefined;
+        this.position = undefined;
         this.commentBoxPrefix = "Comment on track";
+        this.minNumberPlays = undefined;
+        this.maxNumberPlays = undefined;
         this.audioElements = [];
         this.commentQuestions = [];
-        this.schema = specification.schema.getAllElementsByName("page")[0];
+        this.schema = schemaRoot.querySelector("[name=page]");
         this.specification = specification;
-        this.parent = null;
+        this.parent = undefined;
+
+        this.addInterface = function () {
+            var node = new interfaceNode(specification);
+            this.interfaces.push(node);
+            return node;
+        };
+        this.addCommentQuestion = function () {
+            var node = new commentQuestionNode(specification);
+            this.commentQuestions.push(node);
+            return node;
+        };
+        this.addAudioElement = function () {
+            var node = new audioElementNode(specification);
+            this.audioElements.push(node);
+            return node;
+        };
 
         this.decode = function (parent, xml) {
             this.parent = parent;
-            var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
-            for (var i = 0; i < attributeMap.length; i++) {
+            var attributeMap = this.schema.querySelectorAll('attribute');
+            var i, node;
+            for (i = 0; i < attributeMap.length; i++) {
                 var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
                 var projectAttr = xml.getAttribute(attributeName);
-                projectAttr = parent.processAttribute(projectAttr, attributeMap[i], parent.schema);
-                switch (typeof projectAttr) {
-                    case "number":
-                    case "boolean":
-                        eval('this.' + attributeName + ' = ' + projectAttr);
-                        break;
-                    case "string":
-                        eval('this.' + attributeName + ' = "' + projectAttr + '"');
-                        break;
+                projectAttr = processAttribute(projectAttr, attributeMap[i]);
+                if (projectAttr !== null) {
+                    this[attributeName] = projectAttr;
                 }
             }
 
             // Get the title
             var title = xml.getElementsByTagName('title');
-            if (title.length != 0 && title[0].parentElement == xml) {
+            if (title.length !== 0 && title[0].parentElement == xml) {
                 this.title = title[0].textContent;
             }
 
             // Get the Comment Box Prefix
             var CBP = xml.getElementsByTagName('commentboxprefix');
-            if (CBP.length != 0 && CBP[0].parentElement == xml) {
+            if (CBP.length !== 0 && CBP[0].parentElement == xml) {
                 this.commentBoxPrefix = CBP[0].textContent;
             }
 
             // Now decode the interfaces
-            var interfaceNode = xml.getElementsByTagName('interface');
-            for (var i = 0; i < interfaceNode.length; i++) {
-                var node = new parent.interfaceNode(this.specification);
-                node.decode(this, interfaceNode[i], parent.schema.getAllElementsByName('interface')[1]);
+            var interfaceNodes = xml.getElementsByTagName('interface');
+            for (i = 0; i < interfaceNodes.length; i++) {
+                node = new interfaceNode(this.specification);
+                node.decode(this, interfaceNodes[i], parent.schema.querySelectorAll('[name=interface]')[1]);
                 this.interfaces.push(node);
             }
 
             // Now process the survey node options
             var survey = xml.getElementsByTagName('survey');
-            var surveySchema = parent.schema.getAllElementsByName('survey')[0];
-            for (var i = 0; i < survey.length; i++) {
+            var surveySchema = parent.schema.querySelector('[name=survey]');
+            for (i = 0; i < survey.length; i++) {
                 var location = survey[i].getAttribute('location');
                 if (location == 'pre' || location == 'before') {
-                    if (this.preTest != null) {
+                    if (this.preTest.options.length !== 0) {
                         this.errors.push("Already a pre/before test survey defined! Ignoring second!!");
                     } else {
-                        this.preTest = new parent.surveyNode(this.specification);
                         this.preTest.decode(parent, survey[i], surveySchema);
                     }
                 } else if (location == 'post' || location == 'after') {
-                    if (this.postTest != null) {
+                    if (this.postTest.options.length !== 0) {
                         this.errors.push("Already a post/after test survey defined! Ignoring second!!");
                     } else {
-                        this.postTest = new parent.surveyNode(this.specification);
                         this.postTest.decode(parent, survey[i], surveySchema);
                     }
                 }
@@ -590,19 +643,19 @@
 
             // Now process the audioelement tags
             var audioElements = xml.getElementsByTagName('audioelement');
-            for (var i = 0; i < audioElements.length; i++) {
-                var node = new this.audioElementNode(this.specification);
-                node.decode(this, audioElements[i]);
-                this.audioElements.push(node);
+            for (i = 0; i < audioElements.length; i++) {
+                var audioNode = new audioElementNode(this.specification);
+                audioNode.decode(this, audioElements[i]);
+                this.audioElements.push(audioNode);
             }
 
             // Now decode the commentquestions
             var cqNode = xml.getElementsByTagName('commentquestions');
-            if (cqNode.length != 0) {
+            if (cqNode.length !== 0) {
                 cqNode = cqNode[0];
                 var commentQuestion = cqNode.firstElementChild;
                 while (commentQuestion) {
-                    var node = new this.commentQuestionNode(this.specification);
+                    node = new commentQuestionNode(this.specification);
                     node.decode(parent, commentQuestion);
                     this.commentQuestions.push(node);
                     commentQuestion = commentQuestion.nextElementSibling;
@@ -613,33 +666,31 @@
         this.encode = function (root) {
             var AHNode = root.createElement("page");
             // First decode the attributes
-            var attributes = this.schema.getAllElementsByTagName('xs:attribute');
-            for (var i = 0; i < attributes.length; i++) {
+            var attributes = this.schema.querySelectorAll('attribute');
+            var i;
+            for (i = 0; i < attributes.length; i++) {
                 var name = attributes[i].getAttribute("name");
-                if (name == undefined) {
+                if (name === null) {
                     name = attributes[i].getAttribute("ref");
                 }
-                if (eval("this." + name + " != undefined") || attributes[i].getAttribute("use") == "required") {
-                    eval("AHNode.setAttribute('" + name + "',this." + name + ")");
+                if (this[name] !== undefined || attributes[i].getAttribute("use") == "required") {
+                    AHNode.setAttribute(name, this[name]);
                 }
             }
-            if (this.loudness != null) {
-                AHNode.setAttribute("loudness", this.loudness);
-            }
             // <commentboxprefix>
             var commentboxprefix = root.createElement("commentboxprefix");
             commentboxprefix.textContent = this.commentBoxPrefix;
             AHNode.appendChild(commentboxprefix);
 
-            for (var i = 0; i < this.interfaces.length; i++) {
+            for (i = 0; i < this.interfaces.length; i++) {
                 AHNode.appendChild(this.interfaces[i].encode(root));
             }
 
-            for (var i = 0; i < this.audioElements.length; i++) {
+            for (i = 0; i < this.audioElements.length; i++) {
                 AHNode.appendChild(this.audioElements[i].encode(root));
             }
             // Create <CommentQuestion>
-            for (var i = 0; i < this.commentQuestions.length; i++) {
+            for (i = 0; i < this.commentQuestions.length; i++) {
                 AHNode.appendChild(this.commentQuestions[i].encode(root));
             }
 
@@ -648,15 +699,18 @@
             return AHNode;
         };
 
-        this.commentQuestionNode = function (specification) {
-            this.id = null;
+        function commentQuestionNode(specification) {
+            this.id = undefined;
             this.name = undefined;
             this.type = undefined;
             this.statement = undefined;
-            this.schema = specification.schema.getAllElementsByName('commentquestion')[0];
+            this.schema = schemaRoot.querySelector('[name=commentquestion]');
             this.decode = function (parent, xml) {
                 this.id = xml.id;
                 this.name = xml.getAttribute('name');
+                if (this.name === null) {
+                    this.name = undefined;
+                }
                 switch (xml.nodeName) {
                     case "commentradio":
                         this.type = "radio";
@@ -673,9 +727,10 @@
                         this.step = undefined;
                         break;
                     case "commentquestion":
-                    default:
                         this.type = "question";
                         break;
+                    default:
+                        throw ("Unknown comment type " + xml.nodeName);
                 }
                 this.statement = xml.getElementsByTagName('statement')[0].textContent;
                 if (this.type == "radio" || this.type == "checkbox") {
@@ -692,12 +747,12 @@
                     this.min = Number(xml.getAttribute("min"));
                     this.max = Number(xml.getAttribute("max"));
                     this.step = Number(xml.getAttribute("step"));
-                    if (this.step == undefined) {
+                    if (this.step === undefined) {
                         this.step = 1;
                     }
                     this.value = Number(xml.getAttribute("value"));
-                    if (this.value == undefined) {
-                        this.value = min;
+                    if (this.value === undefined) {
+                        this.value = this.min;
                     }
                     this.leftText = xml.getElementsByTagName("minText");
                     if (this.leftText && this.leftText.length > 0) {
@@ -727,25 +782,26 @@
                         node = root.createElement("commentslider");
                         break;
                     case "question":
-                    default:
                         node = root.createElement("commentquestion");
                         break;
+                    default:
+                        throw ("Unknown type " + this.type);
                 }
                 node.id = this.id;
                 node.setAttribute("type", this.type);
-                if (this.name != undefined) {
+                if (this.name !== undefined) {
                     node.setAttribute("name", this.name);
                 }
                 var statement = root.createElement("statement");
                 statement.textContent = this.statement;
                 node.appendChild(statement);
                 if (this.type == "radio" || this.type == "checkbox") {
-                    for (var option of this.options) {
+                    this.options.forEach(function (option) {
                         var child = root.createElement("option");
                         child.setAttribute("name", option.name);
                         child.textContent = option.text;
                         node.appendChild(child);
-                    }
+                    });
                 }
                 if (this.type == "slider") {
                     node.setAttribute("min", this.min);
@@ -769,39 +825,36 @@
                 }
                 return node;
             };
-        };
+        }
 
-        this.audioElementNode = function (specification) {
-            this.url = null;
-            this.id = null;
-            this.name = null;
-            this.parent = null;
-            this.type = null;
-            this.marker = null;
+        function audioElementNode(specification) {
+            this.url = undefined;
+            this.id = undefined;
+            this.name = undefined;
+            this.parent = undefined;
+            this.type = undefined;
+            this.marker = undefined;
             this.enforce = false;
             this.gain = 0.0;
-            this.label = null;
+            this.label = undefined;
             this.startTime = undefined;
             this.stopTime = undefined;
             this.sampleRate = undefined;
+            this.image = undefined;
+            this.minNumberPlays = undefined;
+            this.maxNumberPlays = undefined;
             this.alternatives = [];
-            this.schema = specification.schema.getAllElementsByName('audioelement')[0];;
-            this.parent = null;
+            this.schema = schemaRoot.querySelector('[name=audioelement]');
+            this.parent = undefined;
             this.decode = function (parent, xml) {
                 this.parent = parent;
-                var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
+                var attributeMap = this.schema.querySelectorAll('attribute');
                 for (var i = 0; i < attributeMap.length; i++) {
                     var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
                     var projectAttr = xml.getAttribute(attributeName);
-                    projectAttr = parent.parent.processAttribute(projectAttr, attributeMap[i], parent.parent.schema);
-                    switch (typeof projectAttr) {
-                        case "number":
-                        case "boolean":
-                            eval('this.' + attributeName + ' = ' + projectAttr);
-                            break;
-                        case "string":
-                            eval('this.' + attributeName + ' = "' + projectAttr + '"');
-                            break;
+                    projectAttr = processAttribute(projectAttr, attributeMap[i]);
+                    if (projectAttr !== null) {
+                        this[attributeName] = projectAttr;
                     }
                 }
                 // Get the alternative nodes
@@ -819,14 +872,14 @@
             };
             this.encode = function (root) {
                 var AENode = root.createElement("audioelement");
-                var attributes = this.schema.getAllElementsByTagName('xs:attribute');
+                var attributes = this.schema.querySelectorAll('attribute');
                 for (var i = 0; i < attributes.length; i++) {
                     var name = attributes[i].getAttribute("name");
-                    if (name == undefined) {
+                    if (name === null) {
                         name = attributes[i].getAttribute("ref");
                     }
-                    if (eval("this." + name + " != undefined") || attributes[i].getAttribute("use") == "required") {
-                        eval("AENode.setAttribute('" + name + "',this." + name + ")");
+                    if (this[name] !== undefined || attributes[i].getAttribute("use") == "required") {
+                        AENode.setAttribute(name, this[name]);
                     }
                 }
                 this.alternatives.forEach(function (alt) {
@@ -837,6 +890,6 @@
                 });
                 return AENode;
             };
-        };
-    };
+        }
+    }
 }
--- a/php/requestKey.php	Fri Jul 14 15:37:53 2017 +0100
+++ b/php/requestKey.php	Fri Jul 14 15:39:24 2017 +0100
@@ -58,7 +58,8 @@
 $doc_struct = new DOMDocument;
 $doc_struct->preserveWhiteSpace = false;
 $doc_struct->formatOutput = true;
-$doc_struct->loadXML("<waetresult/>");
+$doc_struct->loadXML("<waetresult />");
+$doc_struct->documentElement->setAttribute("key", $key);
 // Add the root
 if (file_exists($testURL)) {
     $test_proto_doc = new DOMDocument;
--- a/php/save.php	Fri Jul 14 15:37:53 2017 +0100
+++ b/php/save.php	Fri Jul 14 15:39:24 2017 +0100
@@ -28,10 +28,12 @@
 $saveFilenamePrefix = '';
 if (isset($_GET['saveFilenamePrefix'])) {
     $saveFilenamePrefix = $_GET['saveFilenamePrefix'].'-';
+} else {
+    $saveFilenamePrefix = "save-";
 }
 $postText = file_get_contents('php://input');
 $file_key = $_GET['key'];
-$filename = '../saves/'.$saveFilenamePrefix.'save-'.$file_key.".xml";
+$filename = '../saves/'.$saveFilenamePrefix.$file_key.".xml";
 
 if (!file_exists($filename)) {
     die('<response state="error"><message>Could not find save</message></response>');
--- a/python/comment_parser.py	Fri Jul 14 15:37:53 2017 +0100
+++ b/python/comment_parser.py	Fri Jul 14 15:39:24 2017 +0100
@@ -57,7 +57,7 @@
 
                 # for page [page_name], print comments related to fragment [id]
                 for audioelement in audioholder.findall("./audioelement"):
-                    if audioelement is not None: # Check it exists
+                    if audioelement is not None and audioelement.get('type') != "outside-reference":
                         audio_id = str(audioelement.get('ref'))
 
                         csv_name = folder_name +'/' + page_name+'/'+page_name+'-comments-'+audio_id+'.csv'
@@ -73,7 +73,10 @@
                                             delimiter=',', 
                                             dialect="excel",
                                             quoting=csv.QUOTE_ALL)
-                        commentstr = audioelement.find("./comment/response").text
+                        try:
+                            commentstr = audioelement.find("./comment/response").text
+                        except AttributeError:
+                            commentstr = ""
                         valuestr = audioelement.find("./value").text
 
                         if commentstr is None:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/python/commentquestion_parser.py	Fri Jul 14 15:39:24 2017 +0100
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+
+import xml.etree.ElementTree as ET
+import os
+import sys
+import csv
+
+# COMMAND LINE ARGUMENTS
+
+assert len(sys.argv)<3, "commentquestion_parser takes at most 1 command line argument\nUse: python commentquestion_parser.py [rating_folder_location]"
+
+# XML results files location
+if len(sys.argv) == 1:
+    folder_name = "../saves"    # Looks in 'saves/' folder from 'scripts/' folder
+    print("Use: python commentquestion_parser.py [rating_folder_location]")
+    print("Using default path: " + folder_name)
+elif len(sys.argv) == 2:
+    folder_name = sys.argv[1]   # First command line argument is folder
+
+# check if folder_name exists
+if not os.path.exists(folder_name):
+    #the file is not there
+    print("Folder '"+folder_name+"' does not exist.")
+    sys.exit() # terminate script execution
+elif not os.access(os.path.dirname(folder_name), os.W_OK):
+    #the file does exist but write privileges are not given
+    print("No write privileges in folder '"+folder_name+"'.")
+
+# create folder 'ratings' if not yet created
+if not os.path.exists(folder_name + '/comments'):
+    os.makedirs(folder_name + '/comments')
+    
+pagestore = {}
+
+for filename in os.listdir(folder_name):
+    if (filename.endswith(".xml")):
+        tree = ET.parse(folder_name + '/' + filename)
+        root = tree.getroot()
+        
+        subject_id = root.get('key');
+        
+        # get the list of pages
+        for page in root.findall("./page"):
+            pagename = page.get("ref")
+            if pagename is None: # ignore 'empty' audio_holders
+                print("WARNING: " + filename + " contains empty audio holder. (commentquestion_parser.py)")
+                break
+                
+            if page.get('state') != "complete":
+                print("WARNING: " + filename + " contains incomplete page " +pagename+ ". (commentquestion_parser.py)")
+                break
+            try:
+                questionStore = pagestore[pagename]
+            except KeyError:
+                questionStore = {}
+                pagestore[pagename] = questionStore
+            
+            for cq in page.findall("./comment"):
+                cqid = cq.get("id");
+                response = cq.find("./response").text
+                try:
+                    commentStore = questionStore[cqid]
+                except KeyError:
+                    commentStore = [];
+                    questionStore[cqid] = commentStore
+                commentStore.append({"subject": subject_id, "value": response})
+
+for page in pagestore.keys():
+	print page
+	pagedir = folder_name + '/comments/'+page
+	if not os.path.exists(pagedir):
+	    os.makedirs(pagedir)
+	for comment in pagestore[page].keys():
+		with open(pagedir+"/"+comment+".csv", "w") as csvfile:
+			filewriter = csv.writer(csvfile, delimiter=',')
+			filewriter.writerow(("save_id", "value"))
+			for entry in pagestore[page][comment]:
+				filewriter.writerow((entry["subject"], entry["value"]))
\ No newline at end of file
--- a/python/pythonServer.py	Fri Jul 14 15:37:53 2017 +0100
+++ b/python/pythonServer.py	Fri Jul 14 15:39:24 2017 +0100
@@ -137,16 +137,27 @@
     global curFileName
     global curSaveIndex
     options = self.path.rsplit('?')
-    options = options[1].rsplit('=')
-    key = options[1]
+    options = options[1].rsplit('&')
+    for option in options:
+        optionPair = option.rsplit('=')
+        if optionPair[0] == "key":
+            key = optionPair[1]
+        elif optionPair[0] == "saveFilenamePrefix":
+            prefix = optionPair[1]
+    if key == None:
+        self.send_response(404)
+        return
+    if prefix == None:
+        prefix = "save"
     varLen = int(self.headers['Content-Length'])
     postVars = self.rfile.read(varLen)
     print("Saving file key "+key)
-    file = open('../saves/save-'+key+'.xml','wb')
+    filename = prefix+'-'+key+'.xml'
+    file = open('../saves/'+filename,'wb')
     file.write(postVars)
     file.close()
     try:
-        wbytes = os.path.getsize('../saves/save-'+key+'.xml')
+        wbytes = os.path.getsize('../saves/'+filename)
     except OSError:
         self.send_response(200)
         self.send_header("Content-type", "text/xml")
@@ -155,7 +166,7 @@
     self.send_response(200)
     self.send_header("Content-type", "text/xml")
     self.end_headers()
-    reply = '<response state="OK"><message>OK</message><file bytes="'+str(wbytes)+'">"saves/'+curFileName+'"</file></response>'
+    reply = '<response state="OK"><message>OK</message><file bytes="'+str(wbytes)+'">"saves/'+filename+'"</file></response>'
     if sys.version_info[0] == 2:
         self.wfile.write(reply)
     elif sys.version_info[0] == 3:
--- a/python/score_parser.py	Fri Jul 14 15:37:53 2017 +0100
+++ b/python/score_parser.py	Fri Jul 14 15:39:24 2017 +0100
@@ -85,7 +85,7 @@
                 audioElement = page.find("./audioelement/[@ref='"+ fragmentname+ "']") # Get the element
                 for value in audioElement.findall('./value'):
                     axisName = value.get('interface-name')
-                    if axisName == None:
+                    if axisName == None or axisName == "null":
                         axisName = 'default'
                     axisStore = storage[page_name]['axis'][axisName]
                     if hasattr(value, 'text'):
--- a/test.html	Fri Jul 14 15:37:53 2017 +0100
+++ b/test.html	Fri Jul 14 15:39:24 2017 +0100
@@ -37,6 +37,7 @@
         <button id="popup-previous" class="popupButton">Back</button>
     </div>
     <div class="testHalt" style="visibility: hidden"></div>
+    <div id="footer"><a target="_blank" href="https://github.com/BrechtDeMan/WebAudioEvaluationTool">Web Audio Evaluation Toolbox (v1.2.2-pre)</a></div>
 </body>
 
 </html>
--- a/test_create.html	Fri Jul 14 15:37:53 2017 +0100
+++ b/test_create.html	Fri Jul 14 15:39:24 2017 +0100
@@ -1,31 +1,1086 @@
-<html>
-
-<head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8">
-    <!-- This defines the test creator tool for the Web Audio Evaluation Toolbox -->
-    <link rel='stylesheet' type="text/css" href="test_create/style.css" />
-    <link rel='stylesheet' type="text/css" href="test_create/custom.css" />
-    <script type="text/javascript">
-        window.onbeforeunload = function(e) {
-            var message = 'If you leave the page now, any unsaved changes will be lost',
-                e = e || window.event;
-            if (e) {
-                e.returnValue = message;
-            }
-            return message;
-        };
-        // Copy of Specifiation node from Core.js
-
-    </script>
-    <script src="js/jquery-2.1.4.js"></script>
-    <script type="text/javascript" src='js/specification.js'></script>
-    <script type="text/javascript" src="test_create/test_core.js"></script>
-</head>
-
-<body>
-    <div id="popupHolder"></div>
-    <div id="blanket"></div>
-    <div id="content"></div>
-</body>
-
-</html>
+<html>
+
+<head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8">
+    <!-- This defines the test creator tool for the Web Audio Evaluation Toolbox -->
+    <link rel="stylesheet" type="text/css" href="test_create/style.css" />
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+    <script src="js/jquery-2.1.4.js"></script>
+    <script src="js/angular.min.js"></script>
+    <script type="text/javascript" src="js/specification.js"></script>
+    <script type="text/javascript" src="test_create/test_core.js"></script>
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+    <script type="text/javascript" src="js/xmllint.js"></script>
+
+    <title>WAET 1.2.1 Test Creator</title>
+</head>
+
+<body ng-app="creator" ng-controller="view">
+    <div class="container">
+        <div id="pageRoot">
+            <h1>Web Audio Evaluation Tool - Test Creator</h1>
+        </div>
+        <button type="button" class="btn btn-info" ng-click="validate()">Validate</button>
+        <button type="button" class="btn btn-success" ng-click="exportXML()" ng-disabled="validated == false">Export XML</button>
+        <div ng-switch on="validated" ng-show="showValidationMessages">
+            <div class="panel panel-danger" ng-switch-when="false">
+                <div class="panel-heading">
+                    <button type="button" class="close" data-dismiss="alert" aria-label="Close" ng-click="hideValidationMessages"><span aria-hidden="true">&times;</span></button>
+                    <h3 class="panel-title">Invalid Specification!</h3>
+                </div>
+                <div class="panel-body">
+                    <p>Your specification is invalid. Please fix the following issues!</p>
+                    <ul id="validation-error-list">
+                        <li>Errors</li>
+                    </ul>
+                </div>
+            </div>
+            <div class="alert alert-success" role="alert" ng-switch-when="true">
+                <button type="button" class="close" data-dismiss="alert" aria-label="Close" ng-click="hideValidationMessages"><span aria-hidden="true">&times;</span></button>
+                <strong>Validates!</strong><span>Well done, you can export this specification!</span>
+            </div>
+        </div>
+        <div id="setupNode" class="node" ng-controller="setup">
+            <h2>Setup</h2>
+            <div class="attributes">
+                <div class="attribute">
+                    <span>Interface: </span>
+                    <input type="text" ng-model="specification.interface" required/>
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="If you would like to save to a server other than your hosting server, you can place the full WAET URL here">
+                    <span>Save URL: </span>
+                    <input type="text" ng-model="specification.projectReturn" placeholder="save.php" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Once the test is completed and save confirmed, the browser will redirect to this page, if not defined.">
+                    <span>Exit URL: </span>
+                    <input type="text" ng-model="specification.returnURL" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Randomise the page order">
+                    <span>Randomise Page Order: </span>
+                    <input type="checkbox" ng-model="specification.randomiseOrder" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Set the number of pages to present to the user. This includes repeated pages. Set to '0' or blank to ignore. Randomise page order must be selected.">
+                    <span>Page Pool Size: </span>
+                    <input type="number" ng-model="specification.poolSize" min="0" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Automatically analyse and normalsie audio to this target LUFS. If unsure, use -25LUFS. 0 or blank disables normalisation">
+                    <span>Loudness Normalisation (LUFS): </span>
+                    <input type="number" ng-model="specification.loudness" max="0" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Only perform the test if the browser reported sampling rate matches this.">
+                    <span>Fixed Sampling Rate: </span>
+                    <input type="number" ng-model="specification.sampleRate" min="0" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Show a 'method of adjustment' audio calibration before testing.">
+                    <span>Pre-Test audio calibration: </span>
+                    <input type="checkbox" ng-model="specification.calibration" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Default cross-fade time when switching between elements. Can be over-ridden on each page">
+                    <span>Global Cross-fade time: </span>
+                    <input type="number" ng-model="specification.crossFade" min="0" step="0.1" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Default pre-play element silence. Can be over-ridden on each page and element">
+                    <span>Global Fragment Pre-Silence: </span>
+                    <input type="number" ng-model="specification.preSilence" min="0" step="0.1" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Default post-play element silence. Can be over-ridden on each page and element">
+                    <span>Global Fragment Post-Silence: </span>
+                    <input type="number" ng-model="specification.preSilence" min="0" step="0.1" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Disable switching of audio elements">
+                    <span>Play audio one-at-a-time: </span>
+                    <input type="checkbox" ng-model="specification.playOne" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Minimum number of times an audio fragment must be played">
+                    <span>Minimum number of fragment plays</span>
+                    <input type="number" ng-model="specification.minNumberPlays" min="0" max="{{specification.maxNumberPlays}}" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Maximum number of times an audio fragment can be played">
+                    <span>Maximum number of fragment plays</span>
+                    <input type="number" ng-model="specification.maxNumberPlays" min="{{specification.minNumberPlays || 0}}" />
+                </div>
+            </div>
+            <div class="node">
+                <h2>Test Completed Message</h2>
+                <textarea ng-model="specification.exitText" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Once the test is completed, you can show a message to the user. Markdown syntax is supported for formatting."></textarea>
+            </div>
+            <div id="metricsNode" class="node">
+                <h3>Session Metrics</h3>
+                <div class="attributes">
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Report the total test participation time">
+                        <span>Collect Total Test Time: </span>
+                        <input type="checkbox" value="testTimer" ng-click="enableMetric($event)" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Collect the accumulative listening time for each fragment">
+                        <span>Collect Fragment Listen Time: </span>
+                        <input type="checkbox" value="elementTimer" ng-click="enableMetric($event)" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Store the initial position of the fragment">
+                        <span>Collect Fragment Initial Position: </span>
+                        <input type="checkbox" value="elementInitialPosition" ng-click="enableMetric($event)" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Store each movement / value change of each fragment with page-relative timestamps.">
+                        <span>Collect Fragment Movements: </span>
+                        <input type="checkbox" value="elementTracker" ng-click="enableMetric($event)" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Store boolean reporting if a fragment has been played">
+                        <span>Collect Fragment Listened To Flag: </span>
+                        <input type="checkbox" value="elementFlagListenedTo" ng-click="enableMetric($event)" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Store boolean reporting if a fragment has been moved">
+                        <span>Collect Fragment Moved Flag: </span>
+                        <input type="checkbox" value="elementFlagMoved" ng-click="enableMetric($event)" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Store each time a fragment starts and stops playback with page relative timestamps. Also holds fragment relative timestamps.">
+                        <span>Collect Fragment Listened Flag: </span>
+                        <input type="checkbox" value="elementListenTracker" ng-click="enableMetric($event)" />
+                    </div>
+                </div>
+            </div>
+            <div id="globalpresurvey" class="node" ng-controller="survey" ng-init="survey = specification.preTest">
+                <h2>Pre Test Survey</h2>
+                <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
+                <div class="node" ng-repeat="opt in survey.options" ng-controller="surveyOption">
+                    <h3>Survey Entry</h3>
+                    <button type="button" class="btn btn-danger" ng-click="removeSurveyEntry(opt);">Delete Entry</button>
+                    <div class="attributes">
+                        <div class="attribute">
+                            <span>Survey Type: </span>
+                            <select ng-model="opt.type">
+                                <option value="question">Question</option>
+                                <option value="radio">Radio</option>
+                                <option value="checkbox">Checkbox</option>
+                                <option value="statement">Statement</option>
+                                <option value="number">Number</option>
+                                <option value="slider">Slider</option>
+                                <option value="video">Video</option>
+                                <option value="youtube">YouTube</option>
+                            </select>
+                        </div>
+                        <div class="attribute">
+                            <span>Unique Survey Entry ID:</span>
+                            <input type="text" ng-model="opt.id" required/>
+                        </div>
+                        <div class="attribute">
+                            <span>Entry Name:</span>
+                            <input type="text" ng-model="opt.name" />
+                        </div>
+                        <div class="attribute" ng-show="['question', 'checkbox', 'radio', 'number'].indexOf(opt.type) >= 0">
+                            <span>Mandatory:</span>
+                            <input type="checkbox" ng-model="opt.mandatory" />
+                        </div>
+                        <div class="attribute">
+                            <span>Minimum Wait Time (s):</span>
+                            <input type="number" ng-model="opt.minWait" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="opt.type == 'question'">
+                            <span>Box Size:</span>
+                            <select ng-model="opt.boxsize">
+                                <option value="small">Small</option>
+                                <option value="normal">Normal</option>
+                                <option value="large">Large</option>
+                                <option value="huge">Huge</option>
+                            </select>
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Minimum Selected:</span>
+                            <input type="number" ng-model="opt.min" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Maximum Selected:</span>
+                            <input type="number" ng-model="opt.max" max="{{opt.options.length}}" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Minimum Value:</span>
+                            <input type="number" ng-model="opt.min" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Maximum Value:</span>
+                            <input type="number" ng-model="opt.max" />
+                        </div>
+                        <div class="attribute" ng-show="['video', 'youtube'].indexOf(opt.type) >= 0">
+                            <span>Video URL:</span>
+                            <input type="text" ng-model="opt.url" />
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Statement</h4>
+                        <textarea ng-model="opt.statement"></textarea>
+                    </div>
+                    <div class="node" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                        <h4>Options</h4>
+                        <div>
+                            <button type="button" class="btn btn-default" ng-click="addOption();">Add Option</button>
+                        </div>
+                        <div class="node" ng-repeat="option in opt.options">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-default" ng-click="removeOption(option);">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Name: </span>
+                                    <input type="text" ng-model="option.name" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Displayed Text: </span>
+                                    <input type="text" ng-model="option.text" required/>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Conditionals</h4>
+                        <button type="button" class="btn btn-default" ng-click="addCondition()">Add Condition</button>
+                        <div class="node" ng-repeat="condition in opt.conditions">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-danger" ng-click="removeCondition(condition)">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Check Type:</span>
+                                    <select ng-model="condition.check">
+                                        <option value="equals">Equal To</option>
+                                        <option value="lessThan">Less Than</option>
+                                        <option value="greaterThan">Greater Than</option>
+                                        <option value="stringContains">String Contains</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Value: </span>
+                                    <input type="text" ng-model="condition.value" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Pass: </span>
+                                    <select ng-model="condition.jumpToOnPass">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Fail: </span>
+                                    <select ng-model="condition.jumpToOnFail">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div id="globalpostsurvey" class="node" ng-controller="survey" ng-init="survey = specification.postTest">
+                <h2>Post Test Survey</h2>
+                <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
+                <div class="node" ng-repeat="opt in survey.options" ng-controller="surveyOption">
+                    <h3>Survey Entry</h3>
+                    <button type="button" class="btn btn-danger" ng-click="removeSurveyEntry(opt);">Delete Entry</button>
+                    <div class="attributes">
+                        <div class="attribute">
+                            <span>Survey Type: </span>
+                            <select ng-model="opt.type">
+                                <option value="question">Question</option>
+                                <option value="radio">Radio</option>
+                                <option value="checkbox">Checkbox</option>
+                                <option value="statement">Statement</option>
+                                <option value="number">Number</option>
+                                <option value="slider">Slider</option>
+                                <option value="video">Video</option>
+                                <option value="youtube">YouTube</option>
+                            </select>
+                        </div>
+                        <div class="attribute">
+                            <span>Unique Survey Entry ID:</span>
+                            <input type="text" ng-model="opt.id" required/>
+                        </div>
+                        <div class="attribute">
+                            <span>Entry Name:</span>
+                            <input type="text" ng-model="opt.name" />
+                        </div>
+                        <div class="attribute" ng-show="['question', 'checkbox', 'radio', 'number'].indexOf(opt.type) >= 0">
+                            <span>Mandatory:</span>
+                            <input type="checkbox" ng-model="opt.mandatory" />
+                        </div>
+                        <div class="attribute">
+                            <span>Minimum Wait Time (s):</span>
+                            <input type="number" ng-model="opt.minWait" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="opt.type == 'question'">
+                            <span>Box Size:</span>
+                            <select ng-model="opt.boxsize">
+                                <option value="small">Small</option>
+                                <option value="normal">Normal</option>
+                                <option value="large">Large</option>
+                                <option value="huge">Huge</option>
+                            </select>
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Minimum Selected:</span>
+                            <input type="number" ng-model="opt.min" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Maximum Selected:</span>
+                            <input type="number" ng-model="opt.max" max="{{opt.options.length}}" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Minimum Value:</span>
+                            <input type="number" ng-model="opt.min" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Maximum Value:</span>
+                            <input type="number" ng-model="opt.max" />
+                        </div>
+                        <div class="attribute" ng-show="['video', 'youtube'].indexOf(opt.type) >= 0">
+                            <span>Video URL:</span>
+                            <input type="text" ng-model="opt.url" />
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Statement</h4>
+                        <textarea ng-model="opt.statement"></textarea>
+                    </div>
+                    <div class="node" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                        <h4>Options</h4>
+                        <div>
+                            <button type="button" class="btn btn-default" ng-click="addOption();">Add Option</button>
+                        </div>
+                        <div class="node" ng-repeat="option in opt.options">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-default" ng-click="removeOption(option);">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Name: </span>
+                                    <input type="text" ng-model="option.name" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Displayed Text: </span>
+                                    <input type="text" ng-model="option.text" required/>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Conditionals</h4>
+                        <button type="button" class="btn btn-default" ng-click="addCondition()">Add Condition</button>
+                        <div class="node" ng-repeat="condition in opt.conditions">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-danger" ng-click="removeCondition(condition)">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Check Type:</span>
+                                    <select ng-model="condition.check">
+                                        <option value="equals">Equal To</option>
+                                        <option value="lessThan">Less Than</option>
+                                        <option value="greaterThan">Greater Than</option>
+                                        <option value="stringContains">String Contains</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Value: </span>
+                                    <input type="text" ng-model="condition.value" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Pass: </span>
+                                    <select ng-model="condition.jumpToOnPass">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Fail: </span>
+                                    <select ng-model="condition.jumpToOnFail">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div id="globalinterface" class="node" ng-controller="interfaceNode" ng-init="interface = specification.interfaces">
+                <h2>Interface (Globals)</h2>
+                <div class="node interfaceOptions">
+                    <div class="attributes">
+                        <div class="attribute" name="fragmentPlayed" type="check">
+                            <span>Check all fragments played: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="fragmentFullPlayback" type="check">
+                            <span>Check all fragments fully played: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="fragmentMoved" type="check">
+                            <span>Check all fragments have been moved: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="fragmentComments" type="check">
+                            <span>Check all fragments have comments: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="scalerange" type="check">
+                            <span>Enforce a scale usage: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                            <span>Minimum:</span>
+                            <input type="number" min="0" max="100" name="min" />
+                            <span>Maximum:</span>
+                            <input type="number" min="0" max="100" name="max" />
+                        </div>
+                        <div class="attribute" name="volume" type="show">
+                            <span>Show master volume control: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="playhead" type="show">
+                            <span>Show playhead: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="page-count" type="show">
+                            <span>Show Page Count: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="comments" type="show">
+                            <span>Show Fragment Comments: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div style="text-align: center;">
+            <button type="button" class="btn btn-success" ng-click="addPage()">Add Page</button>
+        </div>
+        <div class="node pageNode" ng-controller="page" ng-repeat="page in specification.pages">
+            <h2>Page</h2>
+            <button type="button" class="btn btn-danger" ng-click="removePage(page)">Remove Page</button>
+            <div class="attributes">
+                <div class="attribute">
+                    <span>Unique ID: </span>
+                    <input type="text" ng-model="page.id" required/>
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Define the common root of each fragment URL. For example if every fragment URL starts with 'http://example.org/media/' then this can be placed here and the remainder for each fragment in their respective URL boxed.">
+                    <span>Fragment common-root URL: </span>
+                    <input type="text" ng-model="page.hostURL" />
+                </div>
+                <div class="attribute">
+                    <span>Randomise Fragment Order: </span>
+                    <input type="checkbox" ng-model="page.randomiseOrder" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Specify if this page should be repeated and how many times. Please note, that if page-pooling is also selected then it 'may' not repeat as many times.">
+                    <span>Repeat Page N-times: </span>
+                    <input type="number" ng-model="page.repeatCount" value="0" step="1" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Loop audio playback until manually stopped or the page submit button is pressed">
+                    <span>Loop audio: </span>
+                    <input type="checkbox" ng-model="page.loop" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Synchronise playback of each fragment in this page. If all fragments have the same audio (such as mix evaluations) then this can enable users to seemlessly transition. Otherwise audio will start from the beginning of each fragment">
+                    <span>Synchronous audio playback: </span>
+                    <input type="checkbox" ng-model="page.synchronous" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global loudness normalisation">
+                    <span>Loudness (page): </span>
+                    <input type="number" ng-model="page.loudness" max="0" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Label type to display on the fragments.">
+                    <span>Label type: </span>
+                    <select ng-model="page.label">
+                        <option value="default">Default</option>
+                        <option value="none">None</option>
+                        <option value="number">[1, 2, 3...]</option>
+                        <option value="letter">[a, b, c...]</option>
+                        <option value="capital">[A, B, C...]</option>
+                        <option value="samediff" ng-show="specification.interface == 'AB'">[Same, Different]</option>
+                    </select>
+                </div>
+                <div class="attribute" ng-show="page.label != 'default' && page.label != 'none'">
+                    <span>Label Start: </span>
+                    <input type="text" ng-model="page.labelStart" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Select a subgroup of the given audio fragments to display. 0 or blank means all fragments will be displayed.">
+                    <span>Fragment pool size: </span>
+                    <input type="number" ng-model="page.poolSize" min="0" max="page.audioElements.length" />
+                </div>
+                <div class="attribute" ng-show="specification.poolSize > 0" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Always display this page, even after sub-pooling of pages">
+                    <span>Always include page: </span>
+                    <input type="checkbox" ng-model="page.alwaysInclude" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Always show this page in this position. Useful for training pages to ensure they are always positioned first.">
+                    <span>Fixed Page Position: </span>
+                    <input type="number" ng-model="page.position" min="0" max="{{specification.pages.length}}" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global pre-silence">
+                    <span>Fragment pre-silence: </span>
+                    <input type="number" ng-model="page.preSilence" min="0" step="0.1" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global post-silence">
+                    <span>Fragment post-silence: </span>
+                    <input type="number" ng-model="page.postSilence" min="0" step="0.1" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Disable switching of audio">
+                    <span>Cannot interupt audio: </span>
+                    <input type="checkbox" ng-model="page.playOne" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Only allow playing fragments' interface handle to be manipulated during playback.">
+                    <span>Only move playing audio: </span>
+                    <input type="checkbox" ng-model="page.restrictMovement" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global minimum number of fragment plays">
+                    <span>Minimum number of fragment plays</span>
+                    <input type="number" ng-model="page.minNumberPlays" min="0" max="{{page.maxNumberPlays || specification.maxNumberPlays}}" />
+                </div>
+                <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global maximum number of fragment plays">
+                    <span>Maximum number of fragment plays</span>
+                    <input type="number" ng-model="page.maxNumberPlays" min="{{page.minNumberPlays || specification.minNumberPlays || 0}}" />
+                </div>
+            </div>
+            <div class="node" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Set the title of the page">
+                <h3>Page Title</h3>
+                <textarea ng-model="page.title"></textarea>
+            </div>
+            <div class="node">
+                <h3>Comment box text prefix</h3>
+                <textarea ng-model="page.commentboxprefix"></textarea>
+                <p>Example:
+                    <span style="font-weight:600">{{page.commentboxprefix}} A</span>
+                </p>
+            </div>
+            <div class="node" ng-controller="survey" ng-init="survey = page.preTest">
+                <h2>Pre Page Survey</h2>
+                <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
+                <div class="node" ng-repeat="opt in survey.options" ng-controller="surveyOption">
+                    <h3>Survey Entry</h3>
+                    <button type="button" class="btn btn-danger" ng-click="removeSurveyEntry(opt);">Delete Entry</button>
+                    <div class="attributes">
+                        <div class="attribute">
+                            <span>Survey Type: </span>
+                            <select ng-model="opt.type">
+                                <option value="question">Question</option>
+                                <option value="radio">Radio</option>
+                                <option value="checkbox">Checkbox</option>
+                                <option value="statement">Statement</option>
+                                <option value="number">Number</option>
+                                <option value="slider">Slider</option>
+                                <option value="video">Video</option>
+                                <option value="youtube">YouTube</option>
+                            </select>
+                        </div>
+                        <div class="attribute">
+                            <span>Unique Survey Entry ID:</span>
+                            <input type="text" ng-model="opt.id" required/>
+                        </div>
+                        <div class="attribute">
+                            <span>Entry Name:</span>
+                            <input type="text" ng-model="opt.name" />
+                        </div>
+                        <div class="attribute" ng-show="['question', 'checkbox', 'radio', 'number'].indexOf(opt.type) >= 0">
+                            <span>Mandatory:</span>
+                            <input type="checkbox" ng-model="opt.mandatory" />
+                        </div>
+                        <div class="attribute">
+                            <span>Minimum Wait Time (s):</span>
+                            <input type="number" ng-model="opt.minWait" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="opt.type == 'question'">
+                            <span>Box Size:</span>
+                            <select ng-model="opt.boxsize">
+                                <option value="small">Small</option>
+                                <option value="normal">Normal</option>
+                                <option value="large">Large</option>
+                                <option value="huge">Huge</option>
+                            </select>
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Minimum Selected:</span>
+                            <input type="number" ng-model="opt.min" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Maximum Selected:</span>
+                            <input type="number" ng-model="opt.max" max="{{opt.options.length}}" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Minimum Value:</span>
+                            <input type="number" ng-model="opt.min" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Maximum Value:</span>
+                            <input type="number" ng-model="opt.max" />
+                        </div>
+                        <div class="attribute" ng-show="['video', 'youtube'].indexOf(opt.type) >= 0">
+                            <span>Video URL:</span>
+                            <input type="text" ng-model="opt.url" />
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Statement</h4>
+                        <textarea ng-model="opt.statement"></textarea>
+                    </div>
+                    <div class="node" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                        <h4>Options</h4>
+                        <div>
+                            <button type="button" class="btn btn-default" ng-click="addOption();">Add Option</button>
+                        </div>
+                        <div class="node" ng-repeat="option in opt.options">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-default" ng-click="removeOption(option);">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Name: </span>
+                                    <input type="text" ng-model="option.name" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Displayed Text: </span>
+                                    <input type="text" ng-model="option.text" required/>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Conditionals</h4>
+                        <button type="button" class="btn btn-default" ng-click="addCondition()">Add Condition</button>
+                        <div class="node" ng-repeat="condition in opt.conditions">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-danger" ng-click="removeCondition(condition)">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Check Type:</span>
+                                    <select ng-model="condition.check">
+                                        <option value="equals">Equal To</option>
+                                        <option value="lessThan">Less Than</option>
+                                        <option value="greaterThan">Greater Than</option>
+                                        <option value="stringContains">String Contains</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Value: </span>
+                                    <input type="text" ng-model="condition.value" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Pass: </span>
+                                    <select ng-model="condition.jumpToOnPass">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Fail: </span>
+                                    <select ng-model="condition.jumpToOnFail">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="node" ng-controller="survey" ng-init="survey = page.postTest">
+                <h2>Post Page Survey</h2>
+                <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
+                <div class="node" ng-repeat="opt in survey.options" ng-controller="surveyOption">
+                    <h3>Survey Entry</h3>
+                    <button type="button" class="btn btn-danger" ng-click="removeSurveyEntry(opt);">Delete Entry</button>
+                    <div class="attributes">
+                        <div class="attribute">
+                            <span>Survey Type: </span>
+                            <select ng-model="opt.type">
+                                <option value="question">Question</option>
+                                <option value="radio">Radio</option>
+                                <option value="checkbox">Checkbox</option>
+                                <option value="statement">Statement</option>
+                                <option value="number">Number</option>
+                                <option value="slider">Slider</option>
+                                <option value="video">Video</option>
+                                <option value="youtube">YouTube</option>
+                            </select>
+                        </div>
+                        <div class="attribute">
+                            <span>Unique Survey Entry ID:</span>
+                            <input type="text" ng-model="opt.id" required />
+                        </div>
+                        <div class="attribute">
+                            <span>Entry Name:</span>
+                            <input type="text" ng-model="opt.name" />
+                        </div>
+                        <div class="attribute" ng-show="['question', 'checkbox', 'radio', 'number'].indexOf(opt.type) >= 0">
+                            <span>Mandatory:</span>
+                            <input type="checkbox" ng-model="opt.mandatory" />
+                        </div>
+                        <div class="attribute">
+                            <span>Minimum Wait Time (s):</span>
+                            <input type="number" ng-model="opt.minWait" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="opt.type == 'question'">
+                            <span>Box Size:</span>
+                            <select ng-model="opt.boxsize">
+                                <option value="small">Small</option>
+                                <option value="normal">Normal</option>
+                                <option value="large">Large</option>
+                                <option value="huge">Huge</option>
+                            </select>
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Minimum Selected:</span>
+                            <input type="number" ng-model="opt.min" min="0" />
+                        </div>
+                        <div class="attribute" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                            <span>Maximum Selected:</span>
+                            <input type="number" ng-model="opt.max" max="{{opt.options.length}}" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Minimum Value:</span>
+                            <input type="number" ng-model="opt.min" />
+                        </div>
+                        <div class="attribute" ng-show="['slider', 'number'].indexOf(opt.type) >= 0">
+                            <span>Maximum Value:</span>
+                            <input type="number" ng-model="opt.max" />
+                        </div>
+                        <div class="attribute" ng-show="['video', 'youtube'].indexOf(opt.type) >= 0">
+                            <span>Video URL:</span>
+                            <input type="text" ng-model="opt.url" />
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Statement</h4>
+                        <textarea ng-model="opt.statement"></textarea>
+                    </div>
+                    <div class="node" ng-show="['checkbox', 'radio'].indexOf(opt.type) >= 0">
+                        <h4>Options</h4>
+                        <div>
+                            <button type="button" class="btn btn-default" ng-click="addOption();">Add Option</button>
+                        </div>
+                        <div class="node" ng-repeat="option in opt.options">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-default" ng-click="removeOption(option);">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Name: </span>
+                                    <input type="text" ng-model="option.name" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Displayed Text: </span>
+                                    <input type="text" ng-model="option.text" required/>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Conditionals</h4>
+                        <button type="button" class="btn btn-default" ng-click="addCondition()">Add Condition</button>
+                        <div class="node" ng-repeat="condition in opt.conditions">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-danger" ng-click="removeCondition(condition)">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Check Type:</span>
+                                    <select ng-model="condition.check">
+                                        <option value="equals">Equal To</option>
+                                        <option value="lessThan">Less Than</option>
+                                        <option value="greaterThan">Greater Than</option>
+                                        <option value="stringContains">String Contains</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Value: </span>
+                                    <input type="text" ng-model="condition.value" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Pass: </span>
+                                    <select ng-model="condition.jumpToOnPass">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                                <div class="attribute">
+                                    <span>Jump To On Fail: </span>
+                                    <select ng-model="condition.jumpToOnFail">
+                                        <option value="">None</option>
+                                        <option ng-repeat="entry in survey.options" value="{{entry.id}}">{{entry.id}}</option>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <button type="button" class="btn btn-success" ng-show="specification.interface == 'APE' || page.interfaces.length == 0" ng-click="addInterface()">Add Interface/Axis</button>
+            <div class="node" ng-repeat="interface in page.interfaces" ng-controller="interfaceNode">
+                <h2>Interface</h2>
+                <button type="button" class="btn btn-danger" ng-click="removeInterface(interface)">Remove Interface/Axis</button>
+                <div class="node interfaceOptions">
+                    <div class="attributes">
+                        <div class="attribute" name="fragmentPlayed" type="check">
+                            <span>Check all fragments played: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="fragmentFullPlayback" type="check">
+                            <span>Check all fragments fully played: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="fragmentMoved" type="check">
+                            <span>Check all fragments have been moved: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="fragmentComments" type="check">
+                            <span>Check all fragments have comments: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="scalerange" type="check">
+                            <span>Enforce a scale usage: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                            <span>Minimum:</span>
+                            <input type="number" min="0" max="100" name="min" />
+                            <span>Maximum:</span>
+                            <input type="number" min="0" max="100" name="max" />
+                        </div>
+                        <div class="attribute" name="volume" type="show">
+                            <span>Show master volume control: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="playhead" type="show">
+                            <span>Show playhead: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="page-count" type="show">
+                            <span>Show Page Count: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                        <div class="attribute" name="comments" type="show">
+                            <span>Show Fragment Comments: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
+                    </div>
+                </div>
+                <div class="node">
+                    <h3>Axis Title</h3>
+                    <textarea ng-model="interface.title"></textarea>
+                    <div class="attributes">
+                        <div class="attribute">
+                            <span>Axis name (in saves): </span>
+                            <input type="text" ng-model="interface.name" />
+                        </div>
+                    </div>
+                </div>
+                <div class="node">
+                    <h3>Axis Image</h3>
+                    <textarea ng-model="interface.image"></textarea>
+                </div>
+                <div class="node" name="scale-selection">
+                    <h3>Axis Scales</h3>
+                    <button type="button" class="btn btn-success" ng-click="addScale();">Add</button>
+                    <button type="button" class="btn btn-danger" ng-click="clearScales()" ng-show="interface.scales.length > 0">Clear Scales</button>
+                    <div class="dropdown" style="display: inline-block;">
+                        <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                            Scales: {{selectedScale}}
+                            <span class="caret"></span>
+                        </button>
+                        <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
+                            <li ng-repeat="scale in scales" ng-click="useScales(scale)"><a href="#">{{scale.name}}</a></li>
+                        </ul>
+                    </div>
+                    <div class="node" ng-repeat="scale in interface.scales">
+                        <div class="attributes">
+                            <div class="attribute">
+                                <button type="button" class="btn btn-danger" ng-click="removeScale(scale);">Remove</button>
+                            </div>
+                            <div class="attribute">
+                                <span>Position: </span>
+                                <input type="number" min="0" max="100" ng-model="scale.position" required/>
+                            </div>
+                            <div class="attribute">
+                                <span>Text: </span>
+                                <input type="text" ng-model="scale.text" required/>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="node">
+                <h3>Comment Questions</h3>
+                <button type="button" class="btn btn-success" ng-click="addCommentQuestion()">Add Comment Question</button>
+                <div class="node" ng-repeat="cq in page.commentQuestions">
+                    <button type="button" class="btn btn-danger" ng-click="removeCommentQuestion(cq)">Remove Comment Question</button>
+                    <div class="attributes">
+                        <div class="attribute">
+                            <span>Unique ID:</span>
+                            <input type="text" ng-model="cq.id" required/>
+                        </div>
+                        <div class="attribute">
+                            <span>Common Name:</span>
+                            <input type="text" ng-model="cq.name" />
+                        </div>
+                        <div class="attribute" ng-show="cq.type == 'slider'">
+                            <span>Minimum:</span>
+                            <input type="number" ng-model="cq.min" />
+                        </div>
+                        <div class="attribute" ng-show="cq.type == 'slider'">
+                            <span>Maximum:</span>
+                            <input type="number" ng-model="cq.max" />
+                        </div>
+                        <div class="attribute" ng-show="cq.type == 'slider'">
+                            <span>Step size:</span>
+                            <input type="number" ng-model="cq.step" />
+                        </div>
+                        <div class="attribute" ng-show="cq.type == 'slider'">
+                            <span>Initial Value:</span>
+                            <input type="number" ng-model="cq.value" />
+                        </div>
+                    </div>
+                    <div class="node">
+                        <h4>Question:</h4>
+                        <textarea ng-model="cq.statement"></textarea>
+                    </div>
+                    <div class="node" ng-show="['radio', 'checkbox'].indexOf(cq.type) >= 0">
+                        <h4>Options</h4>
+                        <div class="node" ng-repeat="option in cq.options">
+                            <div class="attributes">
+                                <div class="attribute">
+                                    <button type="button" class="btn btn-danger" ng-click="removeCommentQuestionOption(cq,option)">Remove</button>
+                                </div>
+                                <div class="attribute">
+                                    <span>Name: </span>
+                                    <input type="text" ng-model="option.name" required/>
+                                </div>
+                                <div class="attribute">
+                                    <span>Display Text: </span>
+                                    <input type="text" ng-model="option.text" required/>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <button type="button" class="btn btn-success" ng-click="addAudioElement()">Add Fragment</button>
+            <div class="node" ng-repeat="fragment in page.audioElements">
+                <h3>Audio Fragment</h3>
+                <button type="button" class="btn btn-danger" ng-click="removeAudioElement(fragment)">Remove Fragment</button>
+                <div class="attributes">
+                    <div class="attribute">
+                        <span>Unique ID: </span>
+                        <input type="text" ng-model="fragment.id" required/>
+                    </div>
+                    <div class="attribute">
+                        <span>URL: </span>
+                        <input type="text" ng-model="fragment.url" required/>
+                        <span>Full URL: </span><span style="font-weight=600">{{page.hostURL}}{{fragment.url}}</span>
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Set the gain of this fragment. This is applied after any normalisation">
+                        <span>Fragment Gain (dB): </span>
+                        <input type="number" ng-model="fragment.gain" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Manually set the label">
+                        <span>Fragment Label: </span>
+                        <input type="text" ng-model="fragment.label" />
+                    </div>
+                    <div class="attribute">
+                        <span>Fragment Common name: </span>
+                        <input type="text" ng-model="fragment.name" />
+                    </div>
+                    <div class="attribute">
+                        <span>Fragment Type: </span>
+                        <select ng-model="fragment.type">
+                            <option value="normal">Normal</option>
+                            <option value="anchor">Hidden Anchor</option>
+                            <option value="reference">Hidden Reference</option>
+                            <option value="outside-reference">Outside Reference</option>
+                        </select>
+                    </div>
+                    <div class="attribute" ng-show="fragment.type == 'anchor'">
+                        <span>Anchor must be below: </span>
+                        <input type="number" ng-model="fragment.marker" min="0" max="100" />
+                    </div>
+                    <div class="attribute" ng-show="fragment.type == 'reference'">
+                        <span>Reference must be above: </span>
+                        <input type="number" ng-model="fragment.marker" min="0" max="100" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global and page loudness">
+                        <span>Loudness: </span>
+                        <input type="number" ng-model="fragment.loudness" max="0" />
+                    </div>
+                    <div class="attribute" ng-show="page.poolSize > 0" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Always include this fragment after any sub-pooling">
+                        <span>Always include fragment: </span>
+                        <input type="checkbox" ng-model="fragment.alwaysInclude" max="0" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global / page pre-silence">
+                        <span>Fragment Pre-Silence: </span>
+                        <input type="number" ng-model="fragment.preSilence" max="0" step="0.1" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Over-ride global / page post-silence">
+                        <span>Fragment Post-Silence: </span>
+                        <input type="number" ng-model="fragment.postSilence" max="0" step="0.1" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="By default the fragment will start playback at the beginning.">
+                        <span>Fragment playback start position (s): </span>
+                        <input type="number" ng-model="fragment.startTime" min="0" max="{{fragment.stopTime}}" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="By default the fragment will play until the end">
+                        <span>Fragment playback stop position (s): </span>
+                        <input type="number" ng-model="fragment.stopTime" min="{{fragment.startTime}}" />
+                    </div>
+                    <div class="attribute">
+                        <span>Fragment sampling rate: </span>
+                        <input type="number" ng-model="fragment.sampleRate" min="1" />
+                    </div>
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Associate an image with this fragment">
+                        <span>Fragment Image (URL): </span>
+                        <input type="text" ng-model="fragment.image" />
+                    </div>
+                    <div class="attribute">
+                        <span>Minimum number of plays</span>
+                        <input type="number" ng-model="fragment.minNumberPlays" min="0" max="{{fragment.maxNumberPlays || page.maxNumberPlays || specification.maxNumberPlays}}" />
+                    </div>
+                    <div class="attribute">
+                        <span>Maximum number of plays</span>
+                        <input type="number" ng-model="fragment.maxNumberPlays" min="{{fragment.minNumberPlays || page.minNumberPlays || specification.minNumberPlays || 0}}" />
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div id="popupHolder" ng-show="popupVisible">
+        <div ng-controller="introduction" class="popup" ng-show="popupVisible">
+            <div class="popupTitle" ng-switch="state">
+                <span ng-switch-when="0">Test Creator</span>
+                <span ng-switch-when="1">Create New Test</span>
+            </div>
+            <div class="popupContent container-fluid" ng-switch="state">
+                <div ng-switch-when="0">
+                    <div>
+                        <span>Welcome to the WAET test creator tool. This will allow you to create a new test from scratch to suit your testing needs. If you wish to update a test file, please drag and drop the XML document into the area below for processing, otherwise press 'Next' to start a new test. This tool generates files for the WAET 1.2.1 version.</span>
+                    </div>
+                    <div>
+                        <input type="file" id="files" ng-model="files" onchange="handleFiles(event)" />
+                    </div>
+                </div>
+                <div ng-switch-when="1">
+                    <div>
+                        <span>Please select the interface you would like to use below. Selecting an interface will give a brief description of the interface type.</span>
+                    </div>
+                    <div class="row">
+                        <div class="col-md-6" style="overflow-y: scroll;height: 333px;">
+                            <div class="new-test" ng-repeat="i in testSpecifications.interfaces" ng-mouseover="mouseover(i.name)" ng-click="initialise(i.name)">
+                                <label style="cursor:pointer">
+                                    <input type="radio" name="new-test" value="{{i.name}}" id="i.name" style="cursor:pointer" /> {{i.name}}
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-md-6">
+                            <span>{{description}}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="popupButtons">
+                <button id="popupBack" type="button" class="btn btn-default" ng-show="state>0" ng-click="back()">Back</button>
+                <button id="popupNext" type="button" class="btn btn-default" ng-click="next()">Next</button>
+            </div>
+        </div>
+    </div>
+    <div id="screenblank" ng-show="popupVisible"></div>
+</body>
+
+</html>
--- a/test_create/attributes.json	Fri Jul 14 15:37:53 2017 +0100
+++ b/test_create/attributes.json	Fri Jul 14 15:39:24 2017 +0100
@@ -1,35 +1,1 @@
-{
-    "id": "ID",
-    "mandatory": "Mandatory",
-    "name": "Name",
-    "interface": "Interface Module",
-    "projectReturn": "Save Return URL",
-    "returnURL": "On complete redirect URL",
-    "randomiseOrder": "Randomise Order",
-    "testPages": "Test Pages",
-    "loudness": "Target Loudness (LUFS)",
-    "sampleRate": "Required Sample Rate",
-    "hostURL": "Element URL Prefix",
-    "repeatCount": "Repeat Count",
-    "loop": "Loop playback",
-    "synchronous": "Synchronous playback",
-    "type": "Type",
-    "min": "Minimum",
-    "max": "Maximum",
-    "position": "Position",
-    "url": "URL",
-    "gain": "Gain (dB)",
-    "marker": "Marker",
-    "boxsize": "Box Size",
-    "label": "Label",
-    "calibration": "Perform Calibration",
-    "preSilence": "Pre Silence",
-    "postSilence": "Post Silence",
-    "poolSize": "Pool Size",
-    "alwaysInclude": "Always Include",
-    "crossFade": "Cross Fade",
-    "check": "Check",
-    "value": "Value",
-    "jumpToOnPass": "Jump To ID On Pass",
-    "jumpToOnFail": "Jump To ID On Fail"
-}
+
--- a/test_create/custom.css	Fri Jul 14 15:37:53 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-div#content > div.node {
-    background-color: rgb(200, 228, 151);
-}
-div#content > div#setup {
-    background-color: coral;
-}
-input:disabled+span {
-    text-decoration: line-through;
-}
-div.attribute {
-    float: none;
-}
-div.attribute input {
-    max-width: 100%;
-    width: 300px;
-}
-div.attribute input[type=radio],
-div.attribute input[type=checkbox] {
-    width: 10px;
-}
--- a/test_create/interface-specs.xml	Fri Jul 14 15:37:53 2017 +0100
+++ b/test_create/interface-specs.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -500,7 +500,7 @@
             </test>
             <test name="AB" interface="AB">
                 <descriptions>
-                    <description lang="en">Each page has only two audio fragments. The user must select one of the two fragments to proceed. There can be one hidden reference.</description>
+                    <description lang="en">A page contains a number of audio fragments. The user must select one of the fragments to proceed. There can be a hidden reference.</description>
                 </descriptions>
                 <checks>
                     <entry name="fragmentPlayed" support="none" />
@@ -520,7 +520,7 @@
             </test>
             <test name="ABX" interface="ABX">
                 <descriptions>
-                    <description lang="en">Each page has two audio fragments presented as A and B. The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B, is closest to X.</description>
+                    <description lang="en">Each page has a number of audio fragments presented as A and B (and C, ...). The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B (or C, ...), is closest to X.</description>
                 </descriptions>
             </test>
             <test name="timeline" interface="timeline">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test_create/interfaces/specifications.json	Fri Jul 14 15:39:24 2017 +0100
@@ -0,0 +1,573 @@
+{
+    "interfaces": [
+        {
+            "name": "Audio Perceptual Evaluation (APE)",
+            "interface": "APE",
+            "description": {
+                "en": "Audio Perceptual Evaluation. A multi-stimulus test where each audio fragment is shown on one continuous slider. Fragments are randomnly positioned along the slider. The user clicks a fragment to play and drags to move."
+            },
+            "checks": [],
+            "show": [],
+            "elements": []
+        }, {
+            "name": "MUSHRA",
+            "interface": "MUSHRA",
+            "description": {
+                "en": "Multi-stimulus with hidden reference and anchor. Each fragment is shown on its own vertical slider. One fragment must be labelled as a reference and another labelled as an anchor. One external reference must also be shown."
+            },
+            "scales": ["ACR"],
+            "checks": [{
+                "name": "fragmentMoved",
+                "support": "none"
+            }, {
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }, {
+                "name": "scalerange",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }],
+            "elements": [{
+                "anchor": {
+                    "min": 1,
+                    "max": "undefined"
+                },
+                "reference": {
+                    "min": 1,
+                    "max": "undefined"
+                },
+                "outsidereference": {
+                    "min": 1,
+                    "max": "undefined"
+                }
+            }]
+        }, {
+            "name": "Vertical Sliders",
+            "interface": "MUSHRA",
+            "description": {
+                "en": "Each element is given its own vertical slider with user defined scale markers."
+            }
+        }, {
+            "name": "Horizontal Sliders",
+            "interface": "horizontal",
+            "description": {
+                "en": "Each element is given its own horizontal slider with user defined scale markers."
+            }
+        }, {
+            "name": "Discrete",
+            "interface": "discrete",
+            "description": {
+                "en": "Each element is given a horizontal scale broken into a number of discrete choices. The number of choices is defined by the scale markers."
+            }
+        }, {
+            "name": "Rank",
+            "interface": "ordinal",
+            "description": {
+                "en": "Each stimulus is placed on a discrete scale equalling the number of fragments. The fragments are then ranked based on the question posed. Only one element can occupy a rank position"
+            }
+        }, {
+            "name": "Likert",
+            "interface": "discrete",
+            "description": {
+                "en": "Each stimulus is placed on a discrete scale. The scale is fixed to the Likert scale options of 'Strongly Disagree', 'Disagree', 'Neutral', 'Agree' and 'Strongly Agree'"
+            },
+            "scales": ["Likert"],
+            "checks": [{
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }]
+        }, {
+            "name": "ABC/HR",
+            "interface": "MUSHRA",
+            "description": {
+                "en": "Each stimulus is placed on a vertical slider. The scale is fixed with the labels 'Imperceptible' to 'Very Annoying'"
+            },
+            "scales": ["ABC"],
+            "checks": [{
+                "name": "fragmentMoved",
+                "support": "none"
+            }, {
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }]
+        }, {
+            "name": "Bipolar",
+            "interface": "horizontal",
+            "description": {
+                "en": "Each stimulus is placed on a horizontal slider and initialised to the value '0'. The scale operates from -50 to +5-. In the results this is normalised, like all other interfaces, from 0 (-50) to 1 (+50)"
+            },
+            "scales": ["Bipolar"],
+            "checks": [{
+                "name": "fragmentMoved",
+                "support": "none"
+            }, {
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }]
+        }, {
+            "name": "Absolute Category Rating",
+            "interface": "discrete",
+            "description": {
+                "en": "Each element is on a discrete scale of 'Bad', 'Poor', 'Fair', 'Good' and 'Excellent'. Each element must be given a rating."
+            },
+            "scales": ["ACR"],
+            "checks": [{
+                "name": "fragmentMoved",
+                "support": "mandatory"
+            }, {
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }]
+        }, {
+            "name": "Discrete Category Rating",
+            "interface": "discrete",
+            "description": {
+                "en": ""
+            },
+            "scales": ["DCR"],
+            "checks": [{
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }]
+        }, {
+            "name": "Hedonic Cat. Rating",
+            "interface": "MUSHRA",
+            "description": {
+                "en": ""
+            },
+            "scales": ["Hedonic Category Rating Scale"],
+            "checks": [{
+                "name": "fragmentMoved",
+                "support": "mandatory"
+            }, {
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }],
+            "elements": {
+                "outsidereference": {
+                    "min": 1,
+                    "max": 1
+                }
+            }
+        }, {
+            "name": "ITUR5PCIS",
+            "interface": "MUSHRA",
+            "description": {
+                "en": ""
+            },
+            "scales": ["ABC"],
+            "checks": [{
+                "name": "fragmentMoved",
+                "support": "none"
+            }, {
+                "name": "fragmentPlayed",
+                "support": "none"
+            }, {
+                "name": "fragmentFullPlayback",
+                "support": "none"
+            }, {
+                "name": "fragmentComments",
+                "support": "none"
+            }],
+            "show": [{
+                "name": "volume",
+                "support": "none"
+            }, {
+                "name": "page-count",
+                "support": "none"
+            }, {
+                "name": "playhead",
+                "support": "none"
+            }, {
+                "name": "comments",
+                "support": "none"
+            }],
+            "elements": {
+                "outsidereference": {
+                    "min": 1,
+                    "max": 1
+                }
+            }
+        }, {
+            "name": "Pairwise",
+            "interface": "AB",
+            "description": {
+                "en": "A discrete interface where each page holds each fragment. The user must select one fragment. All other fragments are not selected"
+            },
+            "hasScales": "false",
+            "elements": {
+                "number": {
+                    "min": 2,
+                    "max": "undefined"
+                }
+            }
+        }, {
+            "name": "AB",
+            "interface": "AB",
+            "description": {
+                "en": "Each page contains two audio fragments. The user must select one of the fragments to proceed. There can be an outside reference."
+            },
+            "hasScales": "false",
+            "checks": [{
+                "name": "fragmentPlayed",
+                "support": "mandatory"
+            }],
+            "elements": {
+                "number": {
+                    "min": 2,
+                    "max": 2
+                },
+                "outsidereference": {
+                    "min": 0,
+                    "max": 1
+                }
+            }
+        }, {
+            "name": "ABX",
+            "interface": "ABX",
+            "description": {
+                "en": "Each page has two audio fragments presented as A and B. The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B, is closest to X."
+            },
+            "hasScales": "false",
+            "checks": [{
+                "name": "fragmentPlayed",
+                "support": "mandatory"
+            }],
+            "elements": {
+                "number": {
+                    "min": 2,
+                    "max": 2
+                },
+                "outsidereference": {
+                    "min": 0,
+                    "max": 1
+                }
+            }
+        }, {
+            "name": "Timeline",
+            "interface": "timeline",
+            "description": {
+                "en": "Each fragment is displayed with a clickable waveform of itself. The user must click on the waveform at the location that a specific event occured. Users can then enter in information about this event. This test is unit-/value-less."
+            }
+        }
+    ],
+    "scales": [
+        {
+            "name": "Likert",
+            "scales": [
+                {
+                    "text": "Strongly Disagree",
+                    "position": 0
+                },
+                {
+                    "text": "Disagree",
+                    "position": 25
+                },
+                {
+                    "text": "Neutral",
+                    "position": 50
+                },
+                {
+                    "text": "Agree",
+                    "position": 75
+                },
+                {
+                    "text": "Strongly Agree",
+                    "position": 100
+                }
+            ]
+        }, {
+            "name": "ABC",
+            "scales": [
+                {
+                    "text": "Very annoying",
+                    "position": 0
+                },
+                {
+                    "text": "Annoying",
+                    "position": 25
+                },
+                {
+                    "text": "Slightly annoying",
+                    "position": 50
+                },
+                {
+                    "text": "Perceptible but not annoying",
+                    "position": 75
+                },
+                {
+                    "text": "Imperceptible",
+                    "position": 100
+                }
+            ]
+        }, {
+            "name": "Bipolar",
+            "scales": [
+                {
+                    "text": "-50",
+                    "position": 0
+                },
+                {
+                    "text": "0",
+                    "position": 50
+                },
+                {
+                    "text": "50",
+                    "position": 100
+                }
+            ]
+        }, {
+            "name": "ACR",
+            "scales": [
+                {
+                    "text": "Bad",
+                    "position": 0
+                },
+                {
+                    "text": "Poor",
+                    "position": 25
+                },
+                {
+                    "text": "Fair",
+                    "position": 50
+                },
+                {
+                    "text": "Good",
+                    "position": 75
+                },
+                {
+                    "text": "Excellent",
+                    "position": 100
+                }
+            ]
+        }, {
+            "name": "DCR",
+            "scales": [
+                {
+                    "text": "(1) Very Annoying",
+                    "position": 0
+                },
+                {
+                    "text": "(2) Annoying",
+                    "position": 25
+                },
+                {
+                    "text": "(3) Slightly Annoying",
+                    "position": 50
+                },
+                {
+                    "text": "(4) Audible but not Annoying",
+                    "position": 75
+                },
+                {
+                    "text": "(5) Inaudible",
+                    "position": 100
+                }
+            ]
+        }, {
+            "name": "CCR",
+            "scales": [
+                {
+                    "text": "Much Worse",
+                    "position": 12
+                },
+                {
+                    "text": "Worse",
+                    "position": 25
+                },
+                {
+                    "text": "Slightly Worse",
+                    "position": 38
+                },
+                {
+                    "text": "About the same",
+                    "position": 50
+                },
+                {
+                    "text": "Slightly Better",
+                    "position": 62
+                },
+                {
+                    "text": "Better",
+                    "position": 75
+                },
+                {
+                    "text": "Much Better",
+                    "position": 88
+                }
+            ]
+        }, {
+            "name": "Hedonic Category Rating Scale",
+            "scales": [
+                {
+                    "text": "Dislike Extremeley",
+                    "position": 10
+                },
+                {
+                    "text": "Dislike Very Much",
+                    "position": 20
+                },
+                {
+                    "text": "Dislike Moderate",
+                    "position": 30
+                },
+                {
+                    "text": "Dislike Slightly",
+                    "position": 40
+                },
+                {
+                    "text": "Neither like nor dislike",
+                    "position": 50
+                },
+                {
+                    "text": "Like Slightly",
+                    "position": 60
+                },
+                {
+                    "text": "Like Moderate",
+                    "position": 70
+                },
+                {
+                    "text": "Like Very Much",
+                    "position": 80
+                },
+                {
+                    "text": "Like Extremely",
+                    "position": 90
+                }
+            ]
+        }
+    ]
+}
--- a/test_create/style.css	Fri Jul 14 15:37:53 2017 +0100
+++ b/test_create/style.css	Fri Jul 14 15:39:24 2017 +0100
@@ -1,131 +1,95 @@
-div#blanket {
-    z-index: 2;
-    background-color: rgba(0, 0, 0, 0.5);
+#screenblank {
+    z-index: 1;
     width: 100%;
     height: 100%;
     position: fixed;
+    top: 0px;
     left: 0px;
+    background-color: rgba(0, 0, 0, 0.75);
+}
+#popupHolder {
+    text-align: center;
+    width: 100%;
+    height: 100%;
+    position: fixed;
     top: 0px;
+    z-index: 2;
 }
-div#popupHolder {
+.popup {
+    position: relative;
     z-index: 3;
     background-color: rgba(255, 255, 255, 1);
     width: 730px;
     height: 480px;
-    position: fixed;
+    display: inline-block;
+    text-align: left;
     border-radius: 10px;
     box-shadow: 0px 0px 50px #000;
     padding: 10px;
+    margin-top: 20px;
 }
-div#popup-title-holder {
+.popupTitle {
     width: 100%;
     height: 50px;
     font-size: 2em;
+    text-align: center;
 }
-button.popup-button {
-    width: 60px;
-    height: 27px;
-    padding: 5px;
+.popupButtons {
     position: absolute;
-    bottom: 10px;
+    bottom: 5px;
+    width: 90%;
+    margin-left: 30px;
+    display: block;
+    align-self: center;
 }
-button#popup-proceed {
-    right: 10px;
+#popupBack {
+    float: left;
 }
-button#popup-back {
-    left: 10px;
+#popupNext {
+    float: right;
 }
-div.drag-area {
-    border: 3px black dashed;
+#introdragdrop {
+    width: 100%;
+    height: 100px;
+    border: 2px dashed black;
+    font-size: 1.5em;
+    text-align: center;
+    padding-top: 30px;
+    color: grey;
 }
-div.drag-over {
-    background-color: aquamarine;
+.new-test {
+    cursor: pointer;
 }
-div.drag-dropped {
-    background-color: aqua;
+.new-test:hover {
+    font-style: italic;
 }
-div.drag-error {
-    background-color: coral
+.node {
+    padding: 10px 20px;
+    border: 2px solid black;
+    margin: 20px;
+    border-radius: 20px;
+    background-color: inherit;
 }
-div#project-drop {
-    width: 99%;
-    height: 50px;
-    margin: 10px 0px;
+.node > textarea {
+    width: 80%;
 }
-div.popup-checkbox {
-    padding: 5px;
+.node > h1,
+h2,
+h3,
+h4,
+h5 {
+    text-align: center;
 }
-div.popup-checkbox input {
-    margin: 0px 5px;
+.attribute {
+    display: inline-block;
+    margin: 0px 10px;
+    border-left: 1px solid grey;
+    border-right: 1px solid grey;
+    padding: 5px 5px;
 }
-div.popup-option-entry {
-    padding: 5px 0px;
-    border-bottom: 1px solid;
+#setupNode {
+    background-color: rgba(255, 10, 10, 0.25);
 }
-div.disabled {
-    color: rgb(100, 100, 100);
+.pageNode {
+    background-color: rgba(10, 255, 10, 0.25);
 }
-div#page-holder > div.node {
-    background-color: rgb(200, 228, 151);
-}
-div#content > div#setup {
-    background-color: coral;
-}
-div.node {
-    float: left;
-    padding: 10px;
-    border: black 2px solid;
-    border-radius: 10px;
-    margin: 10px;
-    min-width: 92%;
-    background-color: rgba(255, 255, 255, 0.5);
-}
-div.node-title {
-    float: left;
-    width: 100%;
-    font-size: 2em;
-    margin: 5px 0px;
-}
-div.node-attributes {
-    min-width: 92%;
-    float: none;
-    padding: 10px;
-}
-div.attribute {
-    float: left;
-    margin-right: 10px;
-}
-div.node-children {
-    float: left;
-    min-width: 92%;
-}
-div.node-buttons {
-    float: left;
-    min-width: 92%;
-}
-div.attribute input {
-    max-width: 100%;
-    width: 300px;
-    margin-right: 10px;
-}
-div.attribute input[type=number] {
-    width: 80px;
-}
-div.attribute input[type=radio],
-div.attribute input[type=checkbox] {
-    width: 10px;
-}
-input:disabled+label {
-    text-decoration: line-through;
-}
-div.survey-entry-attribute {
-    margin: 10px 0px;
-    border: 1px gray solid;
-    border-radius: 5px;
-    height: 40px;
-    line-height: 40px;
-    padding: 0px 10px;
-}
-div.survey-entry-attribute span {
-    margin-right: 10px;
-}
--- a/test_create/test_core.js	Fri Jul 14 15:37:53 2017 +0100
+++ b/test_create/test_core.js	Fri Jul 14 15:39:24 2017 +0100
@@ -1,2339 +1,453 @@
-var interfaceSpecs;
-var xmlHttp;
-var popupObject;
-var popupStateNodes;
-var specification;
-var convert;
-var attributeText;
-var page_lang = "en";
+/* globals document, angular, window, Promise, XMLHttpRequest, Specification, XMLSerializer, Blob, DOMParser, FileReader, $*/
+function get(url) {
+    // Return a new promise.
+    return new Promise(function (resolve, reject) {
+        // Do the usual XHR stuff
+        var req = new XMLHttpRequest();
+        req.open('GET', url);
 
-// Firefox does not have an XMLDocument.prototype.getElementsByName
-// and there is no searchAll style command, this custom function will
-// search all children recusrively for the name. Used for XSD where all
-// element nodes must have a name and therefore can pull the schema node
-XMLDocument.prototype.getAllElementsByName = function (name) {
-    name = String(name);
-    var selected = this.documentElement.getAllElementsByName(name);
-    return selected;
+        req.onload = function () {
+            // This is called even on 404 etc
+            // so check the status
+            if (req.status == 200) {
+                // Resolve the promise with the response text
+                resolve(req.response);
+            } else {
+                // Otherwise reject with the status text
+                // which will hopefully be a meaningful error
+                reject(Error(req.statusText));
+            }
+        };
+
+        // Handle network errors
+        req.onerror = function () {
+            reject(Error("Network Error"));
+        };
+
+        // Make the request
+        req.send();
+    });
 }
 
-Element.prototype.getAllElementsByName = function (name) {
-    name = String(name);
-    var selected = [];
-    var node = this.firstElementChild;
-    while (node != null) {
-        if (node.getAttribute('name') == name) {
-            selected.push(node);
-        }
-        if (node.childElementCount > 0) {
-            selected = selected.concat(node.getAllElementsByName(name));
-        }
-        node = node.nextElementSibling;
-    }
-    return selected;
+var AngularInterface = angular.module("creator", []);
+
+var specification = new Specification();
+
+window.onload = function () {
+    // Get the test interface specifications
+    $(function () {
+        $('[data-toggle="popover"]').popover();
+    });
+};
+
+function handleFiles(event) {
+    var s = angular.element(event.currentTarget).scope();
+    s.handleFiles(event);
+    s.$apply();
 }
 
-XMLDocument.prototype.getAllElementsByTagName = function (name) {
-    name = String(name);
-    var selected = this.documentElement.getAllElementsByTagName(name);
-    return selected;
-}
+AngularInterface.controller("view", ['$scope', '$element', '$window', function ($s, $e, $w) {
+    $s.popupVisible = true;
+    $s.testSpecifications = {};
 
-Element.prototype.getAllElementsByTagName = function (name) {
-    name = String(name);
-    var selected = [];
-    var node = this.firstElementChild;
-    while (node != null) {
-        if (node.nodeName == name) {
-            selected.push(node);
+    (function () {
+        new Promise(function (resolve, reject) {
+            var xml = new XMLHttpRequest();
+            xml.open("GET", "test_create/interfaces/specifications.json");
+            xml.onload = function () {
+                if (xml.status === 200) {
+                    resolve(xml.responseText);
+                    return;
+                }
+                reject(xml.status);
+            };
+            xml.onerror = function () {
+                reject(new Error("Network Error"));
+            };
+            xml.send();
+        }).then(JSON.parse).then(function (data) {
+            $s.testSpecifications = data;
+            $s.$apply();
+        });
+    })();
+
+    $s.showPopup = function () {
+        $s.popupVisible = true;
+    };
+    $s.hidePopup = function () {
+        $s.popupVisible = false;
+    };
+    $s.globalSchema = undefined;
+    get("xml/test-schema.xsd").then(function (text) {
+        specification.processSchema(text);
+        $s.globalSchema = specification.getSchema();
+    });
+    $s.specification = specification;
+    $s.selectedTestPrototype = undefined;
+    $s.setTestPrototype = function (obj) {
+        $s.selectedTestPrototype = obj;
+        $w.specification.interface = obj.interface;
+    }
+
+    $s.addPage = function () {
+        $s.specification.createNewPage();
+    };
+
+    $s.removePage = function (page) {
+        var index = $s.specification.pages.findIndex(function (a) {
+            return a == page;
+        });
+        if (index === -1) {
+            throw ("Invalid Page");
         }
-        if (node.childElementCount > 0) {
-            selected = selected.concat(node.getAllElementsByTagName(name));
+        $s.specification.pages.splice(index, 1);
+    };
+
+    $s.exportXML = function () {
+        var s = new XMLSerializer();
+        var doc = specification.encode();
+        var xmlstr = s.serializeToString(doc);
+        var bb = new Blob([s.serializeToString(doc)], {
+            type: 'application/xml'
+        });
+        var dnlk = window.URL.createObjectURL(bb);
+        var a = document.createElement("a");
+        a.href = dnlk;
+        a.download = "test.xml";
+        a.click();
+        window.URL.revokeObjectURL(dnlk);
+    };
+    $s.validated = false;
+    $s.showValidationMessages = false;
+    $s.validate = function () {
+        var s = new XMLSerializer();
+        var Module = {
+            xml: s.serializeToString(specification.encode()),
+            schema: specification.getSchemaString(),
+            arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml']
+        };
+        var xmllint = validateXML(Module);
+        console.log(xmllint);
+        if (xmllint != 'document.xml validates\n') {
+            $s.validated = false;
+            var list = $e[0].querySelector("#validation-error-list");
+            while (list.firstChild) {
+                list.removeChild(list.firstChild);
+            }
+            var errors = xmllint.split('\n');
+            errors = errors.slice(0, errors.length - 2);
+            errors.forEach(function (str) {
+                var li = document.createElement("li");
+                li.textContent = str;
+                list.appendChild(li);
+            });
+        } else {
+            $s.validated = true;
         }
-        node = node.nextElementSibling;
+        $s.showValidationMessages = true;
     }
-    return selected;
-}
+    $s.hideValidationMessages = function () {
+        $s.showValidationMessages = false;
+    }
+}]);
 
-// Firefox does not have an XMLDocument.prototype.getElementsByName
-if (typeof XMLDocument.prototype.getElementsByName != "function") {
-    XMLDocument.prototype.getElementsByName = function (name) {
-        name = String(name);
-        var node = this.documentElement.firstElementChild;
-        var selected = [];
-        while (node != null) {
-            if (node.getAttribute('name') == name) {
-                selected.push(node);
+AngularInterface.controller("introduction", ['$scope', '$element', '$window', function ($s, $e, $w) {
+    $s.state = 0;
+    $s.next = function () {
+        $s.state++;
+        if ($s.state > 1 || $s.file) {
+            $s.hidePopup();
+        }
+    };
+    $s.back = function () {
+        $s.state--;
+    };
+    $s.mouseover = function (name) {
+        var obj = $s.testSpecifications.interfaces.find(function (i) {
+            return i.name == name;
+        });
+        if (obj) {
+            $s.description = obj.description.en;
+        }
+    };
+    $s.initialise = function (name) {
+        var obj = $s.testSpecifications.interfaces.find(function (i) {
+            return i.name == name;
+        });
+        if (obj === undefined) {
+            throw ("Cannot find specification");
+        }
+        $s.setTestPrototype(obj);
+    };
+    // Get the test interface specifications
+    $s.file = undefined;
+    $s.description = "";
+
+    $s.handleFiles = function ($event) {
+        $s.file = $event.currentTarget.files[0];
+        var r = new FileReader();
+        r.onload = function () {
+            var p = new DOMParser();
+            specification.decode(p.parseFromString(r.result, "text/xml"));
+            $s.$apply();
+        };
+        r.readAsText($s.file);
+    };
+}]);
+
+AngularInterface.controller("setup", ['$scope', '$element', '$window', function ($s, $e, $w) {
+    function initialise() {
+        if ($s.globalSchema) {
+            $s.schema = $s.globalSchema.querySelector("[name=setup]");
+        }
+    }
+    $s.schema = undefined;
+    $s.attributes = [];
+
+    $s.$watch("globalSchema", initialise);
+    $s.$watch("specification.metrics.enabled.length", function () {
+        var metricsNode = document.getElementById("metricsNode");
+        if (!$s.specification.metrics) {
+            return;
+        }
+        metricsNode.querySelectorAll("input").forEach(function (DOM) {
+            DOM.checked = false;
+        });
+        $s.specification.metrics.enabled.forEach(function (metric) {
+            var DOM = metricsNode.querySelector("[value=" + metric + "]");
+            if (DOM) {
+                DOM.checked = true;
             }
-            node = node.nextElementSibling;
-        }
-        return selected;
-    }
-}
+        });
+    });
 
-window.onload = function () {
-    specification = new Specification();
-    convert = new SpecificationToHTML();
-    xmlHttp = new XMLHttpRequest();
-    xmlHttp.open("GET", "test_create/interface-specs.xml", true);
-    xmlHttp.onload = function () {
-        var parse = new DOMParser();
-        interfaceSpecs = parse.parseFromString(xmlHttp.response, 'text/xml');
-        buildPage();
-        popupObject.postNode(popupStateNodes.state[0])
-    }
-    xmlHttp.send();
-
-    var xsdGet = new XMLHttpRequest();
-    xsdGet.open("GET", "xml/test-schema.xsd", true);
-    xsdGet.onload = function () {
-        var parse = new DOMParser();
-        specification.schema = parse.parseFromString(xsdGet.response, 'text/xml');;
-    }
-    xsdGet.send();
-
-    var jsonAttribute = new XMLHttpRequest();
-    jsonAttribute.open("GET", "test_create/attributes.json", true);
-    jsonAttribute.onload = function () {
-        attributeText = JSON.parse(jsonAttribute.response)
-    }
-    jsonAttribute.send();
-}
-
-function buildPage() {
-    popupObject = new function () {
-        this.object = document.getElementById("popupHolder");
-        this.blanket = document.getElementById("blanket");
-
-        this.popupTitle = document.createElement("div");
-        this.popupTitle.id = "popup-title-holder";
-        this.popupTitle.align = "center";
-        this.titleDOM = document.createElement("span");
-        this.titleDOM.id = "popup-title";
-        this.popupTitle.appendChild(this.titleDOM);
-        this.object.appendChild(this.popupTitle);
-
-        this.popupContent = document.createElement("div");
-        this.popupContent.id = "popup-content";
-        this.object.appendChild(this.popupContent);
-
-        this.proceedButton = document.createElement("button");
-        this.proceedButton.id = "popup-proceed";
-        this.proceedButton.className = "popup-button";
-        this.proceedButton.textContent = "Next";
-        this.proceedButton.onclick = function () {
-            popupObject.popupContent.innerHTML = null;
-            if (typeof popupObject.shownObject.continue == "function") {
-                popupObject.shownObject.continue();
-            } else {
-                popupObject.hide();
+    $s.enableMetric = function ($event) {
+        var metric = $event.currentTarget.value;
+        var index = specification.metrics.enabled.findIndex(function (a) {
+            return a == metric;
+        });
+        if ($event.currentTarget.checked) {
+            if (index == -1) {
+                specification.metrics.enabled.push(metric);
             }
-        };
-        this.object.appendChild(this.proceedButton);
-
-        this.backButton = document.createElement("button");
-        this.backButton.id = "popup-back";
-        this.backButton.className = "popup-button";
-        this.backButton.textContent = "Back";
-        this.backButton.onclick = function () {
-            popupObject.popupContent.innerHTML = null;
-            popupObject.shownObject.back();
-        };
-        this.object.appendChild(this.backButton);
-
-        this.shownObject;
-
-        this.resize = function () {
-            var w = window.innerWidth;
-            var h = window.innerHeight;
-            this.object.style.left = Math.floor((w - 750) / 2) + 'px';
-            this.object.style.top = Math.floor((h - 500) / 2) + 'px';
-        }
-
-        this.show = function () {
-            this.object.style.visibility = "visible";
-            this.blanket.style.visibility = "visible";
-            if (typeof this.shownObject.back == "function") {
-                this.backButton.style.visibility = "visible";
-            } else {
-                this.backButton.style.visibility = "hidden";
+        } else {
+            if (index >= 0) {
+                specification.metrics.enabled.splice(index, 1);
             }
         }
-
-        this.hide = function () {
-            this.object.style.visibility = "hidden";
-            this.blanket.style.visibility = "hidden";
-            this.backButton.style.visibility = "hidden";
-        }
-
-        this.postNode = function (postObject) {
-            //Passed object must have the following:
-            // Title: text to show in the title
-            // Content: HTML DOM to show on the page
-            // On complete this HTML DOM is destroyed so make sure it is referenced elsewhere for processing
-            this.titleDOM.textContent = postObject.title;
-            this.popupContent.appendChild(postObject.content);
-            this.shownObject = postObject;
-            if (typeof this.shownObject.back == "function") {
-                this.backButton.style.visibility = "visible";
-            } else {
-                this.backButton.style.visibility = "hidden";
-            }
-            if (typeof this.shownObject.continue == "function") {
-                this.proceedButton.textContent = "Next";
-            } else {
-                this.proceedButton.textContent = "Finish";
-            }
-            this.show();
-        }
-
-        this.resize();
-        this.hide();
     };
 
-    popupStateNodes = new function () {
-        // This defines the several popup states wanted
-        this.state = [];
-        this.state[0] = new function () {
-            this.title = "Welcome";
-            this.content = document.createElement("div");
-            this.content.id = "state-0";
-            var span = document.createElement("span");
-            span.textContent = "Welcome to the WAET test creator tool. This will allow you to create a new test from scratch to suit your testing needs. If you wish to update a test file, please drag and drop the XML document into the area below for processing, otherwise press 'Next' to start a new test. This tool generates files for the WAET 1.2.0 version."
-            this.content.appendChild(span);
-            this.dragArea = document.createElement("div");
-            this.dragArea.className = "drag-area";
-            this.dragArea.id = "project-drop";
-            this.content.appendChild(this.dragArea);
+    $s.configure = function () {}
 
-            this.dragArea.addEventListener('dragover', function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-                e.dataTransfer.dropEffect = 'copy';
-                e.currentTarget.className = "drag-area drag-over";
+    $s.$watch("selectedTestPrototype", $s.configure);
+}]);
+
+AngularInterface.controller("survey", ['$scope', '$element', '$window', function ($s, $e, $w) {
+    $s.addSurveyEntry = function () {
+        $s.survey.addOption();
+    };
+    $s.removeSurveyEntry = function (entry) {
+        var index = $s.survey.options.findIndex(function (a) {
+            return a == entry;
+        });
+        if (index === -1) {
+            throw ("Invalid Entry");
+        }
+        $s.survey.options.splice(index, 1);
+    };
+}]);
+
+AngularInterface.controller("surveyOption", ['$scope', '$element', '$window', function ($s, $e, $w) {
+
+    $s.removeOption = function (option) {
+        var index = $s.opt.options.findIndex(function (a) {
+            return a == option;
+        });
+        if (index === -1) {
+            throw ("Invalid option");
+        }
+        $s.opt.options.splice(index, 1);
+    };
+    $s.addOption = function () {
+        $s.opt.options.push({
+            name: "",
+            text: ""
+        });
+    };
+
+    $s.addCondition = function () {
+        $s.opt.conditions.push({
+            check: "equals",
+            value: "",
+            jumpToOnPass: undefined,
+            jumpToOnFail: undefined
+        });
+    };
+
+    $s.removeCondition = function (condition) {
+        var index = $s.opt.conditions.findIndex(function (c) {
+            return c == condition;
+        });
+        if (index === -1) {
+            throw ("Invalid Condition");
+        }
+        $s.opt.conditions.splice(index, 1);
+    };
+}]);
+
+AngularInterface.controller("interfaceNode", ['$scope', '$element', '$window', function ($s, $e, $w) {
+    $s.$watch("interface.options.length", function () {
+        if (!$s.interface || !$s.interface.options) {
+            return;
+        }
+        var options = $e[0].querySelector(".interfaceOptions").querySelectorAll(".attribute");
+        options.forEach(function (option) {
+            var name = option.getAttribute("name");
+            var index = $s.interface.options.findIndex(function (io) {
+                return io.name == name;
             });
+            option.querySelector("input").checked = (index >= 0);
+            if (name == "scalerange" && index >= 0) {
+                option.querySelector("[name=min]").value = $s.interface.options[index].min;
+                option.querySelector("[name=max]").value = $s.interface.options[index].max;
+            }
+        });
+    });
+    $s.enableInterfaceOption = function ($event) {
+        var name = $event.currentTarget.parentElement.getAttribute("name");
+        var type = $event.currentTarget.parentElement.getAttribute("type");
+        var index = $s.interface.options.findIndex(function (io) {
+            return io.name == name;
+        });
+        if (index == -1 && $event.currentTarget.checked) {
+            var obj = $s.interface.options.push({
+                name: name,
+                type: type
+            });
+            if (name == "scalerange") {
+                obj.min = $event.currentTarget.parentElement.querySelector("[name=min]").value;
+                obj.max = $event.currentTarget.parentElement.querySelector("[name=max]").value;
+            }
+        } else if (index >= 0 && !$event.currentTarget.checked) {
+            $s.interface.options.splice(index, 1);
+        }
+    };
+    $s.scales = [];
+    $s.removeScale = function (scale) {
+        var index = $s.interface.scales.findIndex(function (s) {
+            return s == scale;
+        });
+        if (index >= 0) {
+            $s.interface.scales.splice(index, 1);
+        }
+    };
+    $s.addScale = function () {
+        $s.interface.scales.push({
+            position: undefined,
+            text: undefined
+        });
+    };
+    $s.clearScales = function () {
+        $s.interface.scales = [];
+    };
+    $s.useScales = function (scale) {
+        $s.clearScales();
+        scale.scales.forEach(function (s) {
+            $s.interface.scales.push(s);
+        });
+        $s.selectedScale = scale.name;
+    };
+    $s.selectedScale = undefined;
 
-            this.dragArea.addEventListener('dragexit', function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-                e.dataTransfer.dropEffect = 'copy';
-                e.currentTarget.className = "drag-area";
+    $s.configure = function () {
+        if ($s.selectedTestPrototype === undefined) {
+            return;
+        }
+        if ($s.selectedTestPrototype.checks && $s.selectedTestPrototype.checks.length >= 1) {
+            $s.selectedTestPrototype.checks.forEach(function (entry) {
+                var dom = $e[0].querySelector("[name=\"" + entry.name + "\"] input");
+                if (entry.support == "none") {
+                    dom.checked = false;
+                    dom.disabled = true;
+                }
             });
-
-            this.dragArea.addEventListener('drop', function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-                e.currentTarget.className = "drag-area drag-dropped";
-                var files = e.dataTransfer.files[0];
-                var reader = new FileReader();
-                reader.onload = function (decoded) {
-                    var parse = new DOMParser();
-                    specification.decode(parse.parseFromString(decoded.target.result, 'text/xml'));
-                    popupObject.hide();
-                    popupObject.popupContent.innerHTML = null;
-                    convert.convert(document.getElementById('content'));
+        }
+        if ($s.selectedTestPrototype.show && $s.selectedTestPrototype.show.length >= 1) {
+            $s.selectedTestPrototype.show.forEach(function (entry) {
+                var dom = $e[0].querySelector("[name=\"" + entry.name + "\"] input");
+                if (entry.support == "none") {
+                    dom.checked = false;
+                    dom.disabled = true;
                 }
-                reader.readAsText(files);
             });
-
-
-            this.continue = function () {
-                popupObject.postNode(popupStateNodes.state[1]);
+        }
+        if ($s.interface !== specification.interfaces) {
+            // Page specific interface nodes
+            if ($s.selectedTestPrototype.hasScales !== undefined && ($s.selectedTestPrototype.hasScales == "false" || $s.selectedTestPrototype.hasScales == false)) {
+                var elem = $e[0].querySelector("[name=\"scale-selection\"]")
+                elem.style.visibility = "hidden";
+                elem.style.height = "0px";
+            }
+            if ($s.selectedTestPrototype.scales && $s.selectedTestPrototype.show.length >= 1) {
+                $s.scales = [];
+                $s.selectedTestPrototype.scales.forEach(function (scalename) {
+                    var obj = $s.testSpecifications.scales.find(function (a) {
+                        return a.name == scalename;
+                    });
+                    $s.scales.push(obj);
+                });
+                if ($s.selectedTestPrototype.scales.includes($s.selectedScale) == false) {
+                    $s.clearScales();
+                }
+                if ($s.scales.length == 1) {
+                    $s.clearScales();
+                    $s.useScales($s.scales[0]);
+                }
+            } else {
+                $s.scales = $s.testSpecifications.scales;
             }
         }
-        this.state[1] = new function () {
-            this.title = "Select your interface";
-            this.content = document.createElement("div");
-            this.content.id = "state-1";
-            var spnH = document.createElement('div');
-            var span = document.createElement("span");
-            span.textContent = "Please select your interface from the list shown below. This will define the various options which are available. This can later be changed.";
-            spnH.appendChild(span);
-            this.content.appendChild(spnH);
-            this.select = document.createElement("select");
-            this.content.appendChild(this.select);
-            this.description = document.createElement("p");
-            this.content.appendChild(this.description);
-            this.testsXML = interfaceSpecs.getElementsByTagName('tests')[0].getElementsByTagName('test');
-            for (var i = 0; i < this.testsXML.length; i++) {
-                var option = document.createElement('option');
-                option.value = this.testsXML[i].getAttribute('name');
-                option.textContent = this.testsXML[i].getAttribute('name');
-                this.select.appendChild(option);
-            }
-            this.handleEvent = function (event) {
-                var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(this.select.value)[0];
-                var descriptors = testXML.getAllElementsByTagName("description");
-                this.description.textContent = "";
-                for (var i = 0; i < descriptors.length; i++) {
-                    if (descriptors[i].getAttribute("lang") == page_lang) {
-                        this.description.textContent = descriptors[i].textContent;
-                    }
-                }
-            }
-            this.select.addEventListener("change", this);
-            this.handleEvent();
-            this.continue = function () {
-                var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(this.select.value)[0];
-                specification.interface = testXML.getAttribute("interface");
-                if (specification.interfaces == null) {
-                    specification.interfaces = new specification.interfaceNode(specification);
-                }
-                if (specification.metrics == null) {
-                    specification.metrics = new specification.metricNode();
-                }
-                popupStateNodes.state[2].generate();
-                popupObject.postNode(popupStateNodes.state[2]);
-            }
-            this.back = function () {
-                popupObject.postNode(popupStateNodes.state[0]);
-            }
+    };
+
+    $s.$watch("selectedTestPrototype", $s.configure);
+    $s.configure();
+}]);
+AngularInterface.controller("page", ['$scope', '$element', '$window', function ($s, $e, $w) {
+    $s.addInterface = function () {
+        $s.page.addInterface();
+    };
+    $s.removeInterface = function (node) {
+        var index = $s.page.interfaces.findIndex(function (a) {
+            return a == node;
+        });
+        if (index === -1) {
+            throw ("Invalid node");
         }
-        this.state[2] = new function () {
-            this.title = "Test Checks & Restrictions";
-            this.content = document.createElement("div");
-            this.content.id = "state-1";
-            var spnH = document.createElement('div');
-            var span = document.createElement("span");
-            span.textContent = "Select your test checks and restrictions. Greyed out items are fixed by the test/interface and cannot be changed";
-            spnH.appendChild(span);
-            this.content.appendChild(spnH);
-            var holder = document.createElement("div");
-            this.options = [];
-            this.testXML = null;
-            this.interfaceXML = null;
-            this.dynamicContent = document.createElement("div");
-            this.content.appendChild(this.dynamicContent);
-            this.generate = function () {
-                this.options = [];
-                this.dynamicContent.innerHTML = null;
-                var interfaceName = popupStateNodes.state[1].select.value;
-                this.checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("checks")[0];
-                this.testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0];
-                this.interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(this.testXML.getAttribute("interface"))[0].getAllElementsByTagName("checks")[0];
-                this.testXML = this.testXML.getAllElementsByTagName("checks");
-                var interfaceXMLChildren = this.interfaceXML.getElementsByTagName('entry');
-                for (var i = 0; i < interfaceXMLChildren.length; i++) {
-                    var interfaceNode = interfaceXMLChildren[i];
-                    var checkName = interfaceNode.getAttribute('name');
-                    var testNode
-                    if (this.testXML.length > 0) {
-                        testNode = this.testXML[0].getAllElementsByName(checkName);
-                        if (testNode.length != 0) {
-                            testNode = testNode[0];
-                        } else {
-                            testNode = undefined;
-                        }
-                    } else {
-                        testNode = undefined;
-                    }
-                    var obj = {
-                        root: document.createElement("div"),
-                        text: document.createElement("label"),
-                        input: document.createElement("input"),
-                        parent: this,
-                        name: checkName,
-                        handleEvent: function (event) {
-                            if (this.input.checked) {
-                                // Add to specification.interfaces.option
-                                var included = specification.interfaces.options.find(function (element, index, array) {
-                                    if (element.name == this.name) {
-                                        return true;
-                                    } else {
-                                        return false;
-                                    }
-                                }, this);
-                                if (included == null) {
-                                    specification.interfaces.options.push({
-                                        type: "check",
-                                        name: this.name
-                                    });
-                                }
-                            } else {
-                                // Remove from specification.interfaces.option
-                                var position = specification.interfaces.options.findIndex(function (element, index, array) {
-                                    if (element.name == this.name) {
-                                        return true;
-                                    } else {
-                                        return false;
-                                    }
-                                }, this);
-                                if (position >= 0) {
-                                    specification.interfaces.options.splice(position, 1);
-                                }
-                            }
-                        }
-                    }
+        $s.page.interfaces.splice(index, 1);
+    };
 
-                    obj.input.addEventListener("click", obj);
-                    obj.root.className = "popup-checkbox";
-                    obj.input.type = "checkbox";
-                    obj.input.setAttribute('id', checkName);
-                    obj.text.setAttribute("for", checkName);
-                    obj.text.textContent = this.checkText.getAllElementsByName(checkName)[0].textContent;
-                    obj.root.appendChild(obj.input);
-                    obj.root.appendChild(obj.text);
-                    if (testNode != undefined) {
-                        if (testNode.getAttribute('default') == 'on') {
-                            obj.input.checked = true;
-                        }
-                        if (testNode.getAttribute('support') == "none") {
-                            obj.input.disabled = true;
-                            obj.input.checked = false;
-                            obj.root.className = "popup-checkbox disabled";
-                        } else if (interfaceNode.getAttribute('support') == "mandatory") {
-                            obj.input.disabled = true;
-                            obj.input.checked = true;
-                            obj.root.className = "popup-checkbox disabled";
-                        }
-                    } else {
-                        if (interfaceNode.getAttribute('default') == 'on') {
-                            obj.input.checked = true;
-                        }
-                        if (interfaceNode.getAttribute('support') == "none") {
-                            obj.input.disabled = true;
-                            obj.input.checked = false;
-                            obj.root.className = "popup-checkbox disabled";
-                        } else if (interfaceNode.getAttribute('support') == "mandatory") {
-                            obj.input.disabled = true;
-                            obj.input.checked = true;
-                            obj.root.className = "popup-checkbox disabled";
-                        }
-                    }
-                    var included = specification.interfaces.options.find(function (element, index, array) {
-                        if (element.name == this.name) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, obj);
-                    if (included != undefined) {
-                        obj.input.checked = true;
-                    }
-                    obj.handleEvent();
-                    this.options.push(obj);
-                    this.dynamicContent.appendChild(obj.root);
-                }
-            }
-            this.continue = function () {
-                popupStateNodes.state[3].generate();
-                popupObject.postNode(popupStateNodes.state[3]);
-            }
-            this.back = function () {
-                popupObject.postNode(popupStateNodes.state[1]);
-            }
+    $s.addCommentQuestion = function () {
+        $s.page.addCommentQuestion();
+    };
+    $s.removeCommentQuestion = function (node) {
+        var index = $s.page.commentQuestions.findIndex(function (a) {
+            return a == node;
+        });
+        if (index === -1) {
+            throw ("Invalid node");
         }
-        this.state[3] = new function () {
-            this.title = "Test Metrics";
-            this.content = document.createElement("div");
-            this.content.id = "state-1";
-            var spnH = document.createElement('div');
-            var span = document.createElement("span");
-            span.textContent = "Select which data points to include in the exported results XML. Some of this is required for certain post script analysis. See the documentation for further details";
-            spnH.appendChild(span);
-            this.content.appendChild(spnH);
-            this.options = [];
-            this.checkText;
-            this.testXML;
-            this.interfaceXML;
-            this.dynamicContent = document.createElement("div");
-            this.content.appendChild(this.dynamicContent);
-            this.generate = function () {
-                this.options = [];
-                this.dynamicContent.innerHTML = null;
-                var interfaceName = popupStateNodes.state[1].select.value;
-                this.checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("metrics")[0];
-                this.testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0];
-                this.interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(this.testXML.getAttribute("interface"))[0].getAllElementsByTagName("metrics")[0];
-                this.testXML = this.testXML.getAllElementsByTagName("metrics");
-                var interfaceXMLChildren = this.interfaceXML.getElementsByTagName('entry');
-                for (var i = 0; i < interfaceXMLChildren.length; i++) {
-                    var interfaceNode = interfaceXMLChildren[i];
-                    var checkName = interfaceNode.getAttribute('name');
-                    var testNode
-                    if (this.testXML.length > 0) {
-                        testNode = this.testXML[0].getAllElementsByName(checkName);
-                        if (testNode.length != 0) {
-                            testNode = testNode[0];
-                        } else {
-                            testNode = undefined;
-                        }
-                    } else {
-                        testNode = undefined;
-                    }
-                    var obj = {
-                        root: document.createElement("div"),
-                        text: document.createElement("label"),
-                        input: document.createElement("input"),
-                        parent: this,
-                        name: checkName,
-                        handleEvent: function (event) {
-                            if (this.input.checked) {
-                                // Add to specification.interfaces.option
-                                var included = specification.metrics.enabled.find(function (element, index, array) {
-                                    if (element == this.name) {
-                                        return true;
-                                    } else {
-                                        return false;
-                                    }
-                                }, this);
-                                if (included == null) {
-                                    specification.metrics.enabled.push(this.name);
-                                }
-                            } else {
-                                // Remove from specification.interfaces.option
-                                var position = specification.metrics.enabled.findIndex(function (element, index, array) {
-                                    if (element == this.name) {
-                                        return true;
-                                    } else {
-                                        return false;
-                                    }
-                                }, this);
-                                if (position >= 0) {
-                                    specification.metrics.enabled.splice(position, 1);
-                                }
-                            }
-                        }
-                    }
-
-                    obj.input.addEventListener("click", obj);
-                    obj.root.className = "popup-checkbox";
-                    obj.input.type = "checkbox";
-                    obj.input.setAttribute('id', checkName);
-                    obj.text.setAttribute("for", checkName);
-                    obj.text.textContent = this.checkText.getAllElementsByName(checkName)[0].textContent;
-                    obj.root.appendChild(obj.input);
-                    obj.root.appendChild(obj.text);
-                    if (testNode != undefined) {
-                        if (testNode.getAttribute('default') == 'on') {
-                            obj.input.checked = true;
-                        }
-                        if (testNode.getAttribute('support') == "none") {
-                            obj.input.disabled = true;
-                            obj.input.checked = false;
-                            obj.root.className = "popup-checkbox disabled";
-                        } else if (interfaceNode.getAttribute('support') == "mandatory") {
-                            obj.input.disabled = true;
-                            obj.input.checked = true;
-                            obj.root.className = "popup-checkbox disabled";
-                        }
-                    } else {
-                        if (interfaceNode.getAttribute('default') == 'on') {
-                            obj.input.checked = true;
-                        }
-                        if (interfaceNode.getAttribute('support') == "none") {
-                            obj.input.disabled = true;
-                            obj.input.checked = false;
-                            obj.root.className = "popup-checkbox disabled";
-                        } else if (interfaceNode.getAttribute('support') == "mandatory") {
-                            obj.input.disabled = true;
-                            obj.input.checked = true;
-                            obj.root.className = "popup-checkbox disabled";
-                        }
-                    }
-                    var included = specification.metrics.enabled.find(function (element, index, array) {
-                        if (element == this.name) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, obj);
-                    obj.handleEvent();
-                    if (included != undefined) {
-                        obj.input.checked = true;
-                    }
-                    this.options.push(obj);
-                    this.dynamicContent.appendChild(obj.root);
-                }
-            }
-            this.continue = function () {
-                popupStateNodes.state[4].generate();
-                popupObject.postNode(popupStateNodes.state[4]);
-            }
-            this.back = function () {
-                popupObject.postNode(popupStateNodes.state[2]);
-            }
+        $s.page.commentQuestions.splice(index, 1);
+    };
+    $s.addAudioElement = function () {
+        $s.page.addAudioElement();
+    };
+    $s.removeAudioElement = function (element) {
+        var index = $s.page.audioElements.findIndex(function (a) {
+            return a == element;
+        });
+        if (index === -1) {
+            throw ("Invalid node");
         }
-        this.state[4] = new function () {
-            this.title = "Test Visuals";
-            this.content = document.createElement("div");
-            this.content.id = "state-1";
-            var spnH = document.createElement('div');
-            var span = document.createElement("span");
-            span.textContent = "You can display extra visual content with your interface for the test user to interact with. Select from the available options below. Greyed out options are unavailable for your selected interface";
-            spnH.appendChild(span);
-            this.content.appendChild(spnH);
-            this.options = [];
-            this.checkText;
-            this.testXML;
-            this.interfaceXML;
-            this.dynamicContent = document.createElement("div");
-            this.content.appendChild(this.dynamicContent);
-            this.generate = function () {
-                this.options = [];
-                this.dynamicContent.innerHTML = null;
-                var interfaceName = popupStateNodes.state[1].select.value;
-                this.checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("show")[0];
-                this.testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0];
-                this.interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(this.testXML.getAttribute("interface"))[0].getAllElementsByTagName("show")[0];
-                this.testXML = this.testXML.getAllElementsByTagName("show");
-                var interfaceXMLChildren = this.interfaceXML.getElementsByTagName('entry');
-                for (var i = 0; i < interfaceXMLChildren.length; i++) {
-                    var interfaceNode = interfaceXMLChildren[i];
-                    var checkName = interfaceNode.getAttribute('name');
-                    var testNode
-                    if (this.testXML.length > 0) {
-                        testNode = this.testXML[0].getAllElementsByName(checkName);
-                        if (testNode.length != 0) {
-                            testNode = testNode[0];
-                        } else {
-                            testNode = undefined;
-                        }
-                    } else {
-                        testNode = undefined;
-                    }
-                    var obj = {
-                        root: document.createElement("div"),
-                        text: document.createElement("label"),
-                        input: document.createElement("input"),
-                        parent: this,
-                        name: checkName,
-                        handleEvent: function (event) {
-                            if (this.input.checked) {
-                                // Add to specification.interfaces.option
-                                var included = specification.interfaces.options.find(function (element, index, array) {
-                                    if (element.name == this.name) {
-                                        return true;
-                                    } else {
-                                        return false;
-                                    }
-                                }, this);
-                                if (included == null) {
-                                    specification.interfaces.options.push({
-                                        type: "show",
-                                        name: this.name
-                                    });
-                                }
-                            } else {
-                                // Remove from specification.interfaces.option
-                                var position = specification.interfaces.options.findIndex(function (element, index, array) {
-                                    if (element.name == this.name) {
-                                        return true;
-                                    } else {
-                                        return false;
-                                    }
-                                }, this);
-                                if (position >= 0) {
-                                    specification.interfaces.options.splice(position, 1);
-                                }
-                            }
-                        }
-                    }
-
-                    obj.input.addEventListener("click", obj);
-                    obj.root.className = "popup-checkbox";
-                    obj.input.type = "checkbox";
-                    obj.input.setAttribute('id', checkName);
-                    obj.text.setAttribute("for", checkName);
-                    obj.text.textContent = this.checkText.getAllElementsByName(checkName)[0].textContent;
-                    obj.root.appendChild(obj.input);
-                    obj.root.appendChild(obj.text);
-                    if (testNode != undefined) {
-                        if (testNode.getAttribute('default') == 'on') {
-                            obj.input.checked = true;
-                        }
-                        if (testNode.getAttribute('support') == "none") {
-                            obj.input.disabled = true;
-                            obj.input.checked = false;
-                            obj.root.className = "popup-checkbox disabled";
-                        } else if (interfaceNode.getAttribute('support') == "mandatory") {
-                            obj.input.disabled = true;
-                            obj.input.checked = true;
-                            obj.root.className = "popup-checkbox disabled";
-                        }
-                    } else {
-                        if (interfaceNode.getAttribute('default') == 'on') {
-                            obj.input.checked = true;
-                        }
-                        if (interfaceNode.getAttribute('support') == "none") {
-                            obj.input.disabled = true;
-                            obj.input.checked = false;
-                            obj.root.className = "popup-checkbox disabled";
-                        } else if (interfaceNode.getAttribute('support') == "mandatory") {
-                            obj.input.disabled = true;
-                            obj.input.checked = true;
-                            obj.root.className = "popup-checkbox disabled";
-                        }
-                    }
-                    var included = specification.interfaces.options.find(function (element, index, array) {
-                        if (element.name == this.name) {
-                            return true;
-                        } else {
-                            return false;
-                        }
-                    }, obj);
-                    if (included != undefined) {
-                        obj.input.checked = true;
-                    }
-                    obj.handleEvent();
-                    this.options.push(obj);
-                    this.dynamicContent.appendChild(obj.root);
-                }
-            }
-            this.continue = function () {
-                popupObject.hide();
-                convert.convert(document.getElementById('content'));
-            }
-            this.back = function () {
-                popupObject.postNode(popupStateNodes.state[3]);
-            }
-        }
-        this.state[5] = new function () {
-            this.title = "Add/Edit Survey Element";
-            this.content = document.createElement("div");
-            this.content.id = "state-1";
-            var spnH = document.createElement('div');
-            var span = document.createElement("span");
-            span.textContent = "You can configure your survey element here. Press 'Continue' to complete your changes.";
-            spnH.appendChild(span);
-            this.content.appendChild(spnH);
-            this.dynamic = document.createElement("div");
-            this.option = null;
-            this.parent = null;
-            this.optionLists = [];
-            this.select = document.createElement("select");
-            this.select.setAttribute("name", "type");
-            this.select.addEventListener("change", this, false);
-            this.content.appendChild(this.select);
-            this.content.appendChild(this.dynamic);
-            this.generate = function (option, parent) {
-                this.option = option;
-                this.parent = parent;
-                if (this.select.childElementCount == 0) {
-                    var optionList = specification.schema.getAllElementsByName("survey")[0].getAllElementsByName("type")[0].getAllElementsByTagName("xs:enumeration");
-                    for (var i = 0; i < optionList.length; i++) {
-                        var selectOption = document.createElement("option");
-                        selectOption.value = optionList[i].getAttribute("value");
-                        selectOption.textContent = selectOption.value;
-                        this.select.appendChild(selectOption);
-                    }
-                }
-                if (this.option.type != undefined) {
-                    this.select.value = this.option.type
-                } else {
-                    this.select.value = "statement";
-                    this.option.type = "statement";
-                }
-
-                this.dynamic.innerHTML = null;
-                var statement = document.createElement("div");
-                var statementText = document.createElement("span");
-                var statementEntry = document.createElement("input");
-                statement.appendChild(statementText);
-                statement.appendChild(statementEntry);
-                statement.className = "survey-entry-attribute";
-                statementText.textContent = "Statement/Question";
-                statementEntry.style.width = "500px";
-                statementEntry.addEventListener("change", this, false);
-                statementEntry.setAttribute("name", "statement");
-                statementEntry.value = this.option.statement;
-                this.dynamic.appendChild(statement);
-
-                var id = document.createElement("div");
-                var idText = document.createElement("span");
-                var idEntry = document.createElement("input");
-                id.appendChild(idText);
-                id.appendChild(idEntry);
-                id.className = "survey-entry-attribute";
-                idText.textContent = "ID: ";
-                idEntry.addEventListener("change", this, false);
-                idEntry.setAttribute("name", "id");
-                idEntry.value = this.option.id;
-
-                this.dynamic.appendChild(id);
-
-                switch (this.option.type) {
-                    case "statement":
-                        break;
-                    case "question":
-                        var boxsizeSelect = document.createElement("select");
-                        var optionList = specification.schema.getAllElementsByName("survey")[0].getAllElementsByName("boxsize")[0].getAllElementsByTagName("xs:enumeration");
-                        for (var i = 0; i < optionList.length; i++) {
-                            var selectOption = document.createElement("option");
-                            selectOption.value = optionList[i].getAttribute("value");
-                            selectOption.textContent = selectOption.value;
-                            boxsizeSelect.appendChild(selectOption);
-                        }
-                        if (this.option.boxsize != undefined) {
-                            boxsizeSelect.value = this.option.boxsize;
-                        } else {
-                            boxsizeSelect.value = "normal";
-                            this.option.boxsize = "normal";
-                        }
-                        boxsizeSelect.setAttribute("name", "boxsize");
-                        boxsizeSelect.addEventListener("change", this, false);
-                        var boxsize = document.createElement("div");
-                        var boxsizeText = document.createElement("span");
-                        boxsizeText.textContent = "Entry Size: ";
-                        boxsize.appendChild(boxsizeText);
-                        boxsize.appendChild(boxsizeSelect);
-                        boxsize.className = "survey-entry-attribute";
-                        this.dynamic.appendChild(boxsize);
-
-                        var mandatory = document.createElement("div");
-                        var mandatoryInput = document.createElement("input");
-                        var mandatoryText = document.createElement("span");
-                        mandatoryText.textContent = "Mandatory: ";
-                        mandatory.appendChild(mandatoryText);
-                        mandatory.appendChild(mandatoryInput);
-                        mandatory.className = "survey-entry-attribute";
-                        mandatoryInput.type = "checkbox";
-                        if (this.option.mandatory) {
-                            mandatoryInput.checked = true;
-                        } else {
-                            mandatoryInput.checked = false;
-                        }
-                        mandatoryInput.setAttribute("name", "mandatory");
-                        mandatoryInput.addEventListener("change", this, false);
-                        this.dynamic.appendChild(mandatory);
-                        break;
-                    case "number":
-                        this.dynamic.appendChild(id);
-
-                        var mandatory = document.createElement("div");
-                        var mandatoryInput = document.createElement("input");
-                        var mandatoryText = document.createElement("span");
-                        mandatoryText.textContent = "Mandatory: ";
-                        mandatory.appendChild(mandatoryText);
-                        mandatory.appendChild(mandatoryInput);
-                        mandatory.className = "survey-entry-attribute";
-                        mandatoryInput.type = "checkbox";
-                        if (this.option.mandatory) {
-                            mandatoryInput.checked = true;
-                        } else {
-                            mandatoryInput.checked = false;
-                        }
-                        mandatoryInput.setAttribute("name", "mandatory");
-                        mandatoryInput.addEventListener("change", this, false);
-                        this.dynamic.appendChild(mandatory);
-
-                        var minimum = document.createElement("div");
-                        var minimumEntry = document.createElement("input");
-                        var minimumText = document.createElement("span");
-                        minimumText.textContent = "Minimum: ";
-                        minimum.appendChild(minimumText);
-                        minimum.appendChild(minimumEntry);
-                        minimum.className = "survey-entry-attribute";
-                        minimumEntry.type = "number";
-                        minimumEntry.setAttribute("name", "min");
-                        minimumEntry.addEventListener("change", this, false);
-                        minimumEntry.value = this.option.min;
-                        this.dynamic.appendChild(minimum);
-
-                        var maximum = document.createElement("div");
-                        var maximumEntry = document.createElement("input");
-                        var maximumText = document.createElement("span");
-                        maximumText.textContent = "Maximum: ";
-                        maximum.appendChild(maximumText);
-                        maximum.appendChild(maximumEntry);
-                        maximum.className = "survey-entry-attribute";
-                        maximumEntry.type = "number";
-                        maximumEntry.setAttribute("name", "max");
-                        maximumEntry.addEventListener("change", this, false);
-                        maximumEntry.value = this.option.max;
-                        this.dynamic.appendChild(maximum);
-                        break;
-                    case "checkbox":
-                    case "radio":
-                        this.dynamic.appendChild(id);
-                        var optionHolder = document.createElement("div");
-                        optionHolder.className = 'node';
-                        optionHolder.id = 'popup-option-holder';
-                        var optionObject = function (parent, option) {
-                            this.rootDOM = document.createElement("div");
-                            this.rootDOM.className = "popup-option-entry";
-                            this.inputName = document.createElement("input");
-                            this.inputName.setAttribute("name", "name");
-                            this.inputLabel = document.createElement("input");
-                            this.inputLabel.setAttribute("name", "text");
-                            this.specification = option;
-                            this.parent = parent;
-                            this.handleEvent = function () {
-                                var target = event.currentTarget.getAttribute("name");
-                                eval("this.specification." + target + " = event.currentTarget.value");
-                            };
-
-                            var nameText = document.createElement("span");
-                            nameText.textContent = "Name: ";
-                            var labelText = document.createElement("span");
-                            labelText.textContent = "Label: ";
-                            this.rootDOM.appendChild(nameText);
-                            this.rootDOM.appendChild(this.inputName);
-                            this.rootDOM.appendChild(labelText);
-                            this.rootDOM.appendChild(this.inputLabel);
-                            this.inputName.addEventListener("change", this, false);
-                            this.inputLabel.addEventListener("change", this, false);
-                            this.inputName.value = this.specification.name;
-                            this.inputLabel.value = this.specification.text;
-                            this.inputLabel.style.width = "350px";
-
-                            this.deleteEntry = {
-                                root: document.createElement("button"),
-                                parent: this,
-                                handleEvent: function () {
-                                    document.getElementById("popup-option-holder").removeChild(this.parent.rootDOM);
-                                    var index = this.parent.parent.option.options.findIndex(function (element, index, array) {
-                                        if (element == this.parent.specification)
-                                            return true;
-                                        else
-                                            return false;
-                                    }, this);
-                                    var optionList = this.parent.parent.option.options;
-                                    if (index == optionList.length - 1) {
-                                        optionList = optionList.slice(0, index);
-                                    } else {
-                                        optionList = optionList.slice(0, index).concat(optionList.slice(index + 1));
-                                    }
-                                    this.parent.parent.option.options = optionList;
-                                }
-                            };
-                            this.deleteEntry.root.textContent = "Delete Option";
-                            this.deleteEntry.root.addEventListener("click", this.deleteEntry, false);
-                            this.rootDOM.appendChild(this.deleteEntry.root);
-                        }
-                        this.addEntry = {
-                            parent: this,
-                            root: document.createElement("button"),
-                            handleEvent: function () {
-                                var node = {
-                                    name: "name",
-                                    text: "text"
-                                };
-                                var optionsList = this.parent.option.options;
-                                optionsList.push(node);
-                                var obj = new optionObject(this.parent, optionsList[optionsList.length - 1]);
-                                this.parent.optionLists.push(obj);
-                                document.getElementById("popup-option-holder").appendChild(obj.rootDOM);
-                            }
-                        }
-                        this.addEntry.root.textContent = "Add Option";
-                        this.addEntry.root.addEventListener("click", this.addEntry);
-                        this.dynamic.appendChild(this.addEntry.root);
-                        for (var i = 0; i < this.option.options.length; i++) {
-                            var obj = new optionObject(this, this.option.options[i]);
-                            this.optionLists.push(obj);
-                            optionHolder.appendChild(obj.rootDOM);
-                        }
-                        this.dynamic.appendChild(optionHolder);
-                }
-            }
-            this.handleEvent = function (event) {
-                var name = event.currentTarget.getAttribute("name");
-                var nodeName = event.currentTarget.nodeName;
-                if (name == "type" && nodeName == "SELECT") {
-                    // If type has changed, we may need to rebuild the entire state node
-                    if (event.currentTarget.value != this.option.name) {
-                        this.option.type = event.currentTarget.value;
-                        this.generate(this.option, this.parent);
-                    }
-                    return;
-                }
-                switch (event.currentTarget.getAttribute("type")) {
-                    case "checkbox":
-                        eval("this.option." + name + " = event.currentTarget.checked");
-                        break;
-                    default:
-                        eval("this.option." + name + " = event.currentTarget.value");
-                        break;
-                }
-            }
-            this.continue = function () {
-                if (this.parent.type == "surveyNode") {
-                    var newNode = new this.parent.surveyEntryNode(this.parent, this.option);
-                    this.parent.children.push(newNode);
-                    this.parent.childrenDOM.appendChild(newNode.rootDOM);
-                } else if (this.parent.type == "surveyEntryNode") {
-                    this.parent.build();
-                }
-                popupObject.hide();
-            }
-        }
-        this.state[6] = new function () {
-            this.title = "Edit Scale Markers";
-            this.content = document.createElement("div");
-            this.content.id = "state-6";
-            var spnH = document.createElement('div');
-            var span = document.createElement("span");
-            span.textContent = "You can edit your scale markers here for the selected interface.";
-            spnH.appendChild(span);
-            this.scaleRoot;
-            this.parent;
-            this.markerNodes = [];
-            this.preset = {
-                input: document.createElement("select"),
-                parent: this,
-                handleEvent: function (event) {
-                    this.parent.scaleRoot.scales = [];
-                    var protoScale = interfaceSpecs.getAllElementsByTagName('scaledefinitions')[0].getAllElementsByName(event.currentTarget.value)[0];
-                    var protoMarkers = protoScale.getElementsByTagName("scalelabel");
-                    for (var i = 0; i < protoMarkers.length; i++) {
-                        var marker = {
-                            position: protoMarkers[i].getAttribute("position"),
-                            text: protoMarkers[i].textContent
-                        }
-                        this.parent.scaleRoot.scales.push(marker);
-                    }
-                    this.parent.buildMarkerList();
-                }
-            }
-            this.preset.input.addEventListener("change", this.preset);
-            this.content.appendChild(this.preset.input);
-            var optionHolder = document.createElement("div");
-            optionHolder.className = 'node';
-            optionHolder.id = 'popup-option-holder';
-            this.content.appendChild(optionHolder);
-            this.addMarker = {
-                root: document.createElement("button"),
-                parent: this,
-                handleEvent: function () {
-                    var marker = {
-                        position: 0,
-                        text: "text"
-                    };
-                    this.parent.scaleRoot.scales.push(marker);
-                    var markerNode = new this.parent.buildMarkerNode(this.parent, marker);
-                    document.getElementById("popup-option-holder").appendChild(markerNode.root);
-                    this.parent.markerNodes.push(markerNode);
-                }
-            };
-            this.addMarker.root.textContent = "Add Marker";
-            this.addMarker.root.addEventListener("click", this.addMarker);
-            this.generate = function (scaleRoot, parent) {
-                this.scaleRoot = scaleRoot;
-                this.parent = parent;
-
-                // Generate Pre-Set dropdown
-                var protoScales = interfaceSpecs.getAllElementsByTagName('scaledefinitions')[0].getElementsByTagName("scale");
-                this.preset.input.innerHTML = "";
-
-                for (var i = 0; i < protoScales.length; i++) {
-                    var selectOption = document.createElement("option");
-                    var scaleName = protoScales[i].getAttribute("name");
-                    selectOption.setAttribute("name", scaleName);
-                    selectOption.textContent = scaleName;
-                    this.preset.input.appendChild(selectOption);
-                }
-                this.content.appendChild(this.addMarker.root);
-
-                // Create Marker List
-                this.buildMarkerList();
-            }
-            this.buildMarkerList = function () {
-                var markerInject = document.getElementById("popup-option-holder");
-                markerInject.innerHTML = "";
-                this.markerNodes = [];
-                for (var i = 0; i < this.scaleRoot.scales.length; i++) {
-                    var markerNode = new this.buildMarkerNode(this, this.scaleRoot.scales[i]);
-                    markerInject.appendChild(markerNode.root);
-                    this.markerNodes.push(markerNode);
-
-                }
-            }
-
-            this.buildMarkerNode = function (parent, specification) {
-                this.root = document.createElement("div");
-                this.root.className = "popup-option-entry";
-                this.positionInput = document.createElement("input");
-                this.positionInput.min = 0;
-                this.positionInput.max = 100;
-                this.positionInput.value = specification.position;
-                this.positionInput.setAttribute("name", "position");
-                this.textInput = document.createElement("input");
-                this.textInput.setAttribute("name", "text");
-                this.textInput.style.width = "300px";
-                this.textInput.value = specification.text;
-                this.specification = specification;
-                this.parent = parent;
-                this.handleEvent = function (event) {
-                    switch (event.currentTarget.getAttribute("name")) {
-                        case "position":
-                            this.specification.position = Number(event.currentTarget.value);
-                            break;
-                        case "text":
-                            this.specification.text = event.currentTarget.value;
-                            break;
-                    }
-                }
-                this.positionInput.addEventListener("change", this, false);
-                this.textInput.addEventListener("change", this, false);
-
-                var posText = document.createElement("span");
-                posText.textContent = "Position: ";
-                var textText = document.createElement("span");
-                textText.textContent = "Text: ";
-                this.root.appendChild(posText);
-                this.root.appendChild(this.positionInput);
-                this.root.appendChild(textText);
-                this.root.appendChild(this.textInput);
-
-                this.deleteMarker = {
-                    root: document.createElement("button"),
-                    parent: this,
-                    handleEvent: function () {
-                        var index = this.parent.parent.scaleRoot.scales.findIndex(function (element, index, array) {
-                            if (element == this) {
-                                return true;
-                            } else {
-                                return false;
-                            }
-                        }, this.parent.specification)
-                        if (index >= 0) {
-                            this.parent.parent.scaleRoot.scales.splice(index, 1);
-                        }
-                        document.getElementById("popup-option-holder").removeChild(this.parent.root);
-                    }
-                }
-                this.deleteMarker.root.addEventListener("click", this.deleteMarker);
-                this.deleteMarker.root.textContent = "Delete Marker"
-                this.root.appendChild(this.deleteMarker.root);
-            }
-        }
-    }
-}
-
-function SpecificationToHTML() {
-    // This takes the specification node and converts it to an on-page HTML object
-    // Each Specification Node is given its own JS object which listens to the XSD for instant verification
-    // Once generated, it directly binds into the specification object to update with changes
-    // Fixed DOM entries
-    this.injectDOM;
-    this.setupDOM;
-    this.pages = [];
-
-    // Self-contained generators
-    this.createGeneralNodeDOM = function (name, id, parent) {
-        this.type = name;
-        var root = document.createElement('div');
-        root.id = id;
-        root.className = "node";
-
-        var titleDiv = document.createElement('div');
-        titleDiv.className = "node-title";
-        var title = document.createElement('span');
-        title.className = "node-title";
-        title.textContent = name;
-        titleDiv.appendChild(title);
-
-        var attributeDiv = document.createElement('div');
-        attributeDiv.className = "node-attributes";
-
-        var childrenDiv = document.createElement('div');
-        childrenDiv.className = "node-children";
-
-        var buttonsDiv = document.createElement('div');
-        buttonsDiv.className = "node-buttons";
-
-        root.appendChild(titleDiv);
-        root.appendChild(attributeDiv);
-        root.appendChild(childrenDiv);
-        root.appendChild(buttonsDiv);
-
-        var obj = {
-            rootDOM: root,
-            titleDOM: title,
-            attributeDOM: attributeDiv,
-            attributes: [],
-            childrenDOM: childrenDiv,
-            children: [],
-            buttonDOM: buttonsDiv,
-            parent: parent
-        }
-        return obj;
-    }
-
-    this.convertAttributeToDOM = function (node, schema) {
-        // This takes an attribute schema node and returns an object with the input node and any bindings
-        if (schema.getAttribute('name') == undefined && schema.getAttribute('ref') != undefined) {
-            schema = specification.schema.getAllElementsByName(schema.getAttribute('ref'))[0];
-        }
-        var obj = new function () {
-            this.input;
-            this.name;
-            this.owner;
-            this.holder;
-
-            this.name = schema.getAttribute('name');
-            this.default = schema.getAttribute('default');
-            this.dataType = schema.getAttribute('type');
-            if (this.dataType == undefined) {
-                if (schema.childElementCount > 0) {
-                    if (schema.firstElementChild.nodeName == "xs:simpleType") {
-                        this.dataType = schema.getAllElementsByTagName("xs:restriction")[0].getAttribute("base");
-                    }
-                }
-            }
-            if (typeof this.dataType == "string") {
-                this.dataType = this.dataType.substr(3);
-            } else {
-                this.dataType = "string";
-            }
-            var minVar = undefined;
-            var maxVar = undefined;
-            switch (this.dataType) {
-                case "negativeInteger":
-                    maxVar = -1;
-                    break;
-                case "positiveInteger":
-                    minVar = 1;
-                    break;
-                case "nonNegativeInteger":
-                    minVar = 0;
-                    break;
-                case "nonPositiveInteger":
-                    maxVar = 0;
-                    break;
-                case "byte":
-                    minVar = 0;
-                    maxVar = 256;
-                    break;
-                case "short":
-                    minVar = 0;
-                    maxVar = 65536;
-                    break;
-                default:
-                    break;
-            }
-
-            this.enumeration = schema.getAllElementsByTagName("xs:enumeration");
-            if (this.enumeration.length == 0) {
-                this.input = document.createElement('input');
-                switch (this.dataType) {
-                    case "boolean":
-                        this.input.type = "checkbox";
-                        break;
-                    case "negativeInteger":
-                    case "positiveInteger":
-                    case "nonNegativeInteger":
-                    case "nonPositiveInteger":
-                    case "integer":
-                    case "short":
-                    case "byte":
-                        this.input.step = 1;
-                    case "decimal":
-                        this.input.type = "number";
-                        this.input.min = minVar;
-                        this.input.max = maxVar;
-                        break;
-                    default:
-                        break;
-                }
-            } else {
-                this.input = document.createElement("select");
-                for (var i = 0; i < this.enumeration.length; i++) {
-                    var option = document.createElement("option");
-                    var value = this.enumeration[i].getAttribute("value");
-                    option.setAttribute("value", value);
-                    option.textContent = value;
-                    this.input.appendChild(option);
-                }
-            }
-            var value;
-            eval("value = node." + this.name)
-            if (this.default != undefined && value == undefined) {
-                value = this.default;
-            }
-            if (this.input.type == "checkbox") {
-                if (value == "true" || value == "True") {
-                    this.input.checked = false;
-                } else {
-                    this.input.checked = false;
-                }
-            } else {
-                this.input.value = value;
-            }
-            this.handleEvent = function (event) {
-                var value;
-                if (this.input.nodeName == "INPUT") {
-                    switch (this.input.type) {
-                        case "checkbox":
-                            value = event.currentTarget.checked;
-                            break;
-                        case "number":
-                            if (event.currentTarget.value != "") {
-                                value = Number(event.currentTarget.value);
-                            } else {
-                                value = undefined;
-                            }
-                            break;
-                        default:
-                            if (event.currentTarget.value != "") {
-                                value = event.currentTarget.value;
-                            } else {
-                                value = undefined;
-                            }
-                            break;
-                    }
-                } else if (this.input.nodeName == "SELECT") {
-                    value = event.currentTarget.value;
-                }
-                eval("this.owner." + this.name + " = value");
-            }
-            this.holder = document.createElement('div');
-            this.holder.className = "attribute";
-            this.holder.setAttribute('name', this.name);
-            var text = document.createElement('span');
-            eval("text.textContent = attributeText." + this.name + "+': '");
-            this.holder.appendChild(text);
-            this.holder.appendChild(this.input);
-            this.owner = node;
-            this.input.addEventListener("change", this, false);
-        }
-        if (obj.attribute != null) {
-            obj.input.value = obj.attribute;
-        }
-        return obj;
-    }
-
-    this.convert = function (root) {
-        //Performs the actual conversion using the given root DOM as the root
-        this.injectDOM = root;
-
-        // Build the export button
-        var exportButton = document.createElement("button");
-        exportButton.textContent = "Export to XML";
-        exportButton.onclick = function () {
-            var doc = specification.encode();
-            var obj = {};
-            obj.title = "Export";
-            obj.content = document.createElement("div");
-            obj.content.id = "finish";
-            var span = document.createElement("span");
-            span.textContent = "Your XML document is linked below. On most browsers, simply right click on the link and select 'Save As'. Or clicking on the link may download the file directly."
-            obj.content.appendChild(span);
-            span = document.createElement("p");
-            span.textContent = "NOTE FOR SAFARI! You cannot right click on the below link and save it as a file, Safari does not like that at all. Instead click on it to open the XML, the Press Cmd+S to open the save dialogue. Make sure you have 'save as Page Source' selected on the bottom of the window. Currently Safari has no plans to support the HTML 'download' attribute which causes this problem";
-            obj.content.appendChild(span);
-            var link = document.createElement("div");
-            link.appendChild(doc.firstChild);
-            var file = [link.innerHTML];
-            var bb = new Blob(file, {
-                type: 'application/xml'
-            });
-            var dnlk = window.URL.createObjectURL(bb);
-            var a = document.createElement("a");
-            a.hidden = '';
-            a.href = dnlk;
-            a.download = "project-specification.xml";
-            a.textContent = "Save File";
-            obj.content.appendChild(a);
-            popupObject.show();
-            popupObject.postNode(obj);
-        }
-        this.injectDOM.appendChild(exportButton);
-
-        // First perform the setupNode;
-        var setupSchema = specification.schema.getAllElementsByName('setup')[0];
-        this.setupDOM = new this.createGeneralNodeDOM('Global Configuration', 'setup', null);
-        this.injectDOM.appendChild(this.setupDOM.rootDOM);
-        var setupAttributes = setupSchema.getAllElementsByTagName('xs:attribute');
-        for (var i = 0; i < setupAttributes.length; i++) {
-            var attributeName = setupAttributes[i].getAttribute('name');
-            var attrObject = this.convertAttributeToDOM(specification, setupAttributes[i]);
-            this.setupDOM.attributeDOM.appendChild(attrObject.holder);
-            this.setupDOM.attributes.push(attrObject);
-        }
-
-        // Build the exit Text node
-        var exitText = new this.createGeneralNodeDOM("Exit Text", "exit-test", this.setupDOM);
-        exitText.rootDOM.removeChild(exitText.attributeDOM);
-        this.setupDOM.children.push(exitText);
-        this.setupDOM.childrenDOM.appendChild(exitText.rootDOM);
-        var obj = {
-            rootDOM: document.createElement("div"),
-            labelDOM: document.createElement("label"),
-            inputDOM: document.createElement("textarea"),
-            parent: exitText,
-            specification: specification,
-            handleEvent: function (event) {
-                this.specification.exitText = this.inputDOM.value;
-            }
-        }
-        var exitWarning = document.createElement("div");
-        obj.rootDOM.appendChild(exitWarning);
-        exitWarning.textContent = "Only visible when the above 'On complete redirect URL' field is empty.";
-        obj.rootDOM.appendChild(obj.labelDOM);
-        obj.rootDOM.appendChild(obj.inputDOM);
-        obj.labelDOM.textContent = "Text: ";
-        obj.inputDOM.value = obj.specification.exitText;
-        obj.inputDOM.addEventListener("change", obj);
-        exitText.children.push(obj);
-        exitText.childrenDOM.appendChild(obj.rootDOM);
-
-        // Now we must build the interface Node
-        this.interfaceDOM = new this.interfaceNode(this, specification.interfaces);
-        this.interfaceDOM.build("Interface", "setup-interface", this.setupDOM.rootDOM);
-
-        // Now build the Metrics selection node
-        var metric = this.createGeneralNodeDOM("Session Metrics", "setup-metric", this.setupDOM);
-        metric.rootDOM.removeChild(metric.attributeDOM);
-        this.setupDOM.children.push(metric);
-        this.setupDOM.childrenDOM.appendChild(metric.rootDOM);
-        var interfaceName = popupStateNodes.state[1].select.value;
-        var checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("metrics")[0];
-        var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0];
-        var interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(testXML.getAttribute("interface"))[0].getAllElementsByTagName("metrics")[0];
-        testXML = testXML.getAllElementsByTagName("metrics");
-        var interfaceXMLChild = interfaceXML.firstElementChild;
-        while (interfaceXMLChild) {
-            var obj = {
-                input: document.createElement('input'),
-                root: document.createElement('div'),
-                text: document.createElement('span'),
-                specification: specification.metrics.enabled,
-                name: interfaceXMLChild.getAttribute("name"),
-                handleEvent: function () {
-                    for (var i = 0; i < this.specification.length; i++) {
-                        if (this.specification[i] == this.name) {
-                            var options = this.specification;
-                            if (this.input.checked == false) {
-                                if (i == options.length) {
-                                    options = options.slice(0, i);
-                                } else {
-                                    options = options.slice(0, i).concat(options.slice(i + 1));
-                                }
-                            } else {
-                                return;
-                            }
-                            this.specification = options;
-                            break;
-                        }
-                    }
-                    if (this.input.checked) {
-                        this.specification.push(this.name);
-                    }
-                }
-            };
-            obj.root.className = "attribute";
-            obj.input.type = "checkbox";
-            obj.root.appendChild(obj.text);
-            obj.root.appendChild(obj.input);
-            obj.text.textContent = checkText.getAllElementsByName(interfaceXMLChild.getAttribute("name"))[0].textContent;
-            metric.children.push(obj);
-            metric.childrenDOM.appendChild(obj.root);
-            for (var j = 0; j < specification.metrics.enabled.length; j++) {
-                if (specification.metrics.enabled[j] == obj.name) {
-                    obj.input.checked = true;
-                    break;
-                }
-            }
-            interfaceXMLChild = interfaceXMLChild.nextElementSibling;
-        }
-
-        // Now both before and after surveys
-        if (specification.preTest == undefined) {
-            specification.preTest = new specification.surveyNode(specification);
-            specification.preTest.location = "pre";
-        }
-        if (specification.postTest == undefined) {
-            specification.postTest = new specification.surveyNode(specification);
-            specification.postTest.location = "post";
-        }
-        var surveyBefore = new this.surveyNode(this, specification.preTest, "Pre");
-        var surveyAfter = new this.surveyNode(this, specification.postTest, "Post");
-        this.setupDOM.children.push(surveyBefore);
-        this.setupDOM.children.push(surveyAfter);
-        this.setupDOM.childrenDOM.appendChild(surveyBefore.rootDOM);
-        this.setupDOM.childrenDOM.appendChild(surveyAfter.rootDOM);
-
-        // Add in the page creator button
-        this.addPage = {
-            root: document.createElement("button"),
-            parent: this,
-            handleEvent: function () {
-                var pageObj = new specification.page(specification);
-                specification.pages.push(pageObj);
-                var newPage = new this.parent.pageNode(this.parent, pageObj);
-                document.getElementById("page-holder").appendChild(newPage.rootDOM);
-                this.parent.pages.push(newPage);
-            }
-        }
-        this.addPage.root.textContent = "Add Page";
-        this.addPage.root.id = "new-page-button";
-        this.addPage.root.style.float = "left";
-        this.addPage.root.addEventListener("click", this.addPage, false);
-
-        var pageHolder = document.createElement("div");
-        pageHolder.id = "page-holder";
-        this.injectDOM.appendChild(pageHolder);
-
-        // Build each page
-        for (var page of specification.pages) {
-            var newPage = new this.pageNode(this, page);
-            pageHolder.appendChild(newPage.rootDOM);
-            this.pages.push(newPage);
-        }
-
-        this.injectDOM.appendChild(this.addPage.root);
-    }
-
-    this.interfaceNode = function (parent, rootObject) {
-        this.type = "interfaceNode";
-        this.rootDOM;
-        this.titleDOM;
-        this.attributeDOM;
-        this.attributes = [];
-        this.childrenDOM;
-        this.children = [];
-        this.buttonDOM;
-        this.parent = parent;
-        this.HTMLPoint;
-        this.specification = rootObject;
-        this.schema = specification.schema.getAllElementsByName("interface")[1];
-
-        this.createIOasAttr = function (name, specification, parent, type) {
-            this.root = document.createElement('div');
-            this.input = document.createElement("input");
-            this.name = name;
-            this.type = type;
-            this.parent = parent;
-            this.specification = specification;
-            this.handleEvent = function (event) {
-                for (var i = 0; i < this.specification.options.length; i++) {
-                    if (this.specification.options[i].name == this.name) {
-                        var options = this.specification.options;
-                        if (this.input.checked == false) {
-                            if (i == options.length) {
-                                options = options.slice(0, i);
-                            } else {
-                                options = options.slice(0, i).concat(options.slice(i + 1));
-                            }
-                        } else {
-                            return;
-                        }
-                        this.specification.options = options;
-                        break;
-                    }
-                }
-                if (this.input.checked) {
-                    var obj = {
-                        name: this.name,
-                        type: this.type
-                    };
-                    this.specification.options.push(obj);
-                }
-                if (this.parent.HTMLPoint.id == "setup") {
-                    // We've changed a global setting, must update all child 'interfaces' and disable them
-                    for (pages of convert.pages) {
-                        for (interface of pages.interfaces) {
-                            if (this.type == "check") {
-                                for (node of interface.children[0].attributes) {
-                                    if (node.name == this.name) {
-                                        if (this.input.checked) {
-                                            node.input.disabled = true;
-                                            node.input.checked = false;
-                                        } else {
-                                            node.input.disabled = false;
-                                        }
-                                        break;
-                                    }
-                                }
-                            } else if (this.type == "show") {
-                                for (node of interface.children[1].attributes) {
-                                    if (node.name == this.name) {
-                                        if (this.input.checked) {
-                                            node.input.disabled = true;
-                                        } else {
-                                            node.input.disabled = false;
-                                        }
-                                        break;
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            };
-            this.findIndex = function (element, index, array) {
-                if (element.name == this.name)
-                    return true;
-                else
-                    return false;
-            };
-            this.findNode = function (element, index, array) {
-                if (element.name == this.name)
-                    return true;
-                else
-                    return false;
-            };
-            this.input.type = "checkbox";
-            this.input.setAttribute("name", name);
-            this.input.addEventListener("change", this, false);
-            this.root.appendChild(this.input);
-            this.root.className = "attribute";
-            return this;
-        }
-
-        this.build = function (name, id, parent) {
-            var obj = this.parent.createGeneralNodeDOM(name, id, parent);
-
-            this.rootDOM = obj.rootDOM;
-            this.titleDOM = obj.titleDOM;
-            this.attributeDOM = obj.attributeDOM;
-            this.childrenDOM = obj.childrenDOM;
-            this.buttonDOM = obj.buttonsDOM;
-            this.HTMLPoint = parent;
-            this.rootDOM.removeChild(this.attributeDOM);
-            if (parent.id != "setup") {
-                // Put in the <title> node:
-                this.titleNode = {
-                    root: document.createElement("div"),
-                    label: document.createElement("span"),
-                    input: document.createElement("input"),
-                    parent: this,
-                    handleEvent: function (event) {
-                        this.parent.specification.title = event.currentTarget.value;
-                    }
-                }
-                this.titleNode.label.textContent = "Presented Axis Title:";
-                this.titleNode.root.className = "node-children";
-                this.titleNode.root.appendChild(this.titleNode.label);
-                this.titleNode.root.appendChild(this.titleNode.input);
-                this.titleNode.input.addEventListener("change", this.titleNode, false);
-                this.titleNode.input.value = this.specification.title;
-                this.children.push(this.titleNode);
-                this.childrenDOM.appendChild(this.titleNode.root);
-                // Set the interface-name attribute
-                this.axisName = {
-                    root: document.createElement("div"),
-                    label: document.createElement("span"),
-                    input: document.createElement("input"),
-                    parent: this,
-                    handleEvent: function (event) {
-                        this.parent.specification.name = event.currentTarget.value;
-                    }
-                }
-                this.axisName.label.textContent = "Saved Axis Name (no spaces):";
-                this.axisName.root.className = "node-children";
-                this.axisName.root.appendChild(this.axisName.label);
-                this.axisName.root.appendChild(this.axisName.input);
-                this.axisName.input.addEventListener("change", this.axisName, false);
-                this.axisName.input.value = this.specification.name;
-                this.children.push(this.axisName);
-                this.childrenDOM.appendChild(this.axisName.root);
-            }
-
-            // Put in the check / show options as individual children
-            var checks = this.parent.createGeneralNodeDOM("Checks", "setup-interface-checks", this);
-
-            var interfaceName = popupStateNodes.state[1].select.value;
-            var checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("checks")[0];
-            var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0];
-            var interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(testXML.getAttribute("interface"))[0].getAllElementsByTagName("checks")[0];
-            testXML = testXML.getAllElementsByTagName("checks");
-            var interfaceXMLChild = interfaceXML.firstElementChild;
-            while (interfaceXMLChild) {
-                var obj = new this.createIOasAttr(interfaceXMLChild.getAttribute("name"), this.specification, this, "check");
-                for (var option of this.specification.options) {
-                    if (option.name == obj.name) {
-                        obj.input.checked = true;
-                        break;
-                    }
-                }
-                if (parent.id != "setup") {
-                    var node = convert.interfaceDOM.children[0].attributes.find(obj.findNode, obj);
-                    if (node != undefined) {
-                        if (node.input.checked) {
-                            obj.input.checked = false;
-                            obj.input.disabled = true;
-                        }
-                    }
-                }
-                var text = document.createElement('span');
-                text.textContent = checkText.getAllElementsByName(interfaceXMLChild.getAttribute("name"))[0].textContent;
-                obj.root.appendChild(text);
-                checks.attributeDOM.appendChild(obj.root);
-                checks.attributes.push(obj);
-                interfaceXMLChild = interfaceXMLChild.nextElementSibling;
-            }
-            this.children.push(checks);
-            this.childrenDOM.appendChild(checks.rootDOM);
-
-            var show = this.parent.createGeneralNodeDOM("Show", "setup-interface-show", this);
-            interfaceName = popupStateNodes.state[1].select.value;
-            checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("show")[0];
-            testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0];
-            interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(testXML.getAttribute("interface"))[0].getAllElementsByTagName("show")[0];
-            testXML = testXML.getAllElementsByTagName("show");
-            interfaceXMLChild = interfaceXML.firstElementChild;
-            while (interfaceXMLChild) {
-                var obj = new this.createIOasAttr(interfaceXMLChild.getAttribute("name"), this.specification, this, "show");
-                for (var option of this.specification.options) {
-                    if (option.name == obj.name) {
-                        obj.input.checked = true;
-                        break;
-                    }
-                }
-                if (parent.id != "setup") {
-                    var node = convert.interfaceDOM.children[0].attributes.find(obj.findNode, obj);
-                    if (node != undefined) {
-                        if (node.input.checked) {
-                            obj.input.checked = false;
-                            obj.input.disabled = true;
-                        }
-                    }
-                }
-                var text = document.createElement('span');
-                text.textContent = checkText.getAllElementsByName(interfaceXMLChild.getAttribute("name"))[0].textContent;
-                obj.root.appendChild(text);
-                show.attributeDOM.appendChild(obj.root);
-                show.attributes.push(obj);
-                interfaceXMLChild = interfaceXMLChild.nextElementSibling;
-            }
-            this.children.push(show);
-            this.childrenDOM.appendChild(show.rootDOM);
-
-            if (parent.id == "setup") {} else {
-                var nameAttr = this.parent.convertAttributeToDOM(this, specification.schema.getAllElementsByName("name")[0]);
-                this.attributeDOM.appendChild(nameAttr.holder);
-                this.attributes.push(nameAttr);
-                var scales = new this.scalesNode(this, this.specification);
-                this.children.push(scales);
-                this.childrenDOM.appendChild(scales.rootDOM);
-            }
-            if (parent != undefined) {
-                parent.appendChild(this.rootDOM);
-            }
-        }
-
-        this.scalesNode = function (parent, rootObject) {
-            this.type = "scalesNode";
-            this.rootDOM = document.createElement("div");
-            this.titleDOM = document.createElement("span");
-            this.attributeDOM = document.createElement("div");
-            this.attributes = [];
-            this.childrenDOM = document.createElement("div");
-            this.children = [];
-            this.buttonDOM = document.createElement("div");
-            this.parent = parent;
-            this.specification = rootObject;
-            this.schema = specification.schema.getAllElementsByName("page")[0];
-            this.rootDOM.className = "node";
-
-            var titleDiv = document.createElement('div');
-            titleDiv.className = "node-title";
-            this.titleDOM.className = "node-title";
-            this.titleDOM.textContent = "Interface Scales";
-            titleDiv.appendChild(this.titleDOM);
-
-            this.attributeDOM.className = "node-attributes";
-            this.childrenDOM.className = "node-children";
-            this.buttonDOM.className = "node-buttons";
-
-            this.rootDOM.appendChild(titleDiv);
-            this.rootDOM.appendChild(this.attributeDOM);
-            this.rootDOM.appendChild(this.childrenDOM);
-            this.rootDOM.appendChild(this.buttonDOM);
-
-            this.editButton = {
-                button: document.createElement("button"),
-                parent: this,
-                handleEvent: function (event) {
-                    popupObject.show();
-                    popupObject.postNode(popupStateNodes.state[6]);
-                    popupStateNodes.state[6].generate(this.parent.specification, this.parent);
-                }
-            };
-            this.editButton.button.textContent = "Edit Scales/Markers";
-            this.editButton.button.addEventListener("click", this.editButton, false);
-            this.buttonDOM.appendChild(this.editButton.button);
-        }
-    }
-
-    this.surveyNode = function (parent, rootObject, location) {
-        this.type = "surveyNode";
-        this.rootDOM = document.createElement("div");
-        this.titleDOM = document.createElement("span");
-        this.attributeDOM = document.createElement("div");
-        this.attributes = [];
-        this.childrenDOM = document.createElement("div");
-        this.children = [];
-        this.buttonDOM = document.createElement("div");
-        this.parent = parent;
-        this.specification = rootObject;
-        this.schema = specification.schema.getAllElementsByName("survey")[1];
-        this.rootDOM.className = "node";
-
-        var titleDiv = document.createElement('div');
-        titleDiv.className = "node-title";
-        this.titleDOM.className = "node-title";
-        this.titleDOM.textContent = "Survey";
-        titleDiv.appendChild(this.titleDOM);
-
-        this.attributeDOM.className = "node-attributes";
-        var locationAttr = document.createElement("span");
-        this.attributeDOM.appendChild(locationAttr);
-        if (location == "Pre" || location == "pre") {
-            locationAttr.textContent = "Location: Before";
-        } else {
-            locationAttr.textContent = "Location: After";
-        }
-        this.childrenDOM.className = "node-children";
-        this.buttonDOM.className = "node-buttons";
-
-        this.rootDOM.appendChild(titleDiv);
-        this.rootDOM.appendChild(this.attributeDOM);
-        this.rootDOM.appendChild(this.childrenDOM);
-        this.rootDOM.appendChild(this.buttonDOM);
-
-        this.surveyEntryNode = function (parent, rootObject) {
-            this.type = "surveyEntryNode";
-            this.rootDOM = document.createElement("div");
-            this.titleDOM = document.createElement("span");
-            this.attributeDOM = document.createElement("div");
-            this.attributes = [];
-            this.childrenDOM = document.createElement("div");
-            this.children = [];
-            this.buttonDOM = document.createElement("div");
-            this.parent = parent;
-            this.specification = rootObject;
-            this.schema = specification.schema.getAllElementsByName("surveyentry")[1];
-
-            this.rootDOM.className = "node";
-            this.rootDOM.style.minWidth = "50%";
-
-            var titleDiv = document.createElement('div');
-            titleDiv.className = "node-title";
-            this.titleDOM.className = "node-title";
-            titleDiv.appendChild(this.titleDOM);
-
-            this.attributeDOM.className = "node-attributes";
-            this.childrenDOM.className = "node-children";
-            this.buttonDOM.className = "node-buttons";
-
-            this.rootDOM.appendChild(titleDiv);
-            this.rootDOM.appendChild(this.attributeDOM);
-            this.rootDOM.appendChild(this.childrenDOM);
-            this.rootDOM.appendChild(this.buttonDOM);
-
-            this.build = function () {
-                this.attributeDOM.innerHTML = null;
-                this.childrenDOM.innerHTML = null;
-                var statementRoot = document.createElement("div");
-                var statement = document.createElement("span");
-                statement.textContent = "Statement / Question: " + this.specification.statement;
-                statementRoot.appendChild(statement);
-                this.children.push(statementRoot);
-                this.childrenDOM.appendChild(statementRoot);
-                switch (this.specification.type) {
-                    case "statement":
-                        this.titleDOM.textContent = "Statement";
-                        break;
-                    case "question":
-                        this.titleDOM.textContent = "Question";
-                        var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]);
-                        var mandatory = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("mandatory")[0]);
-                        var boxsize = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("boxsize")[0]);
-                        this.attributeDOM.appendChild(id.holder);
-                        this.attributes.push(id);
-                        this.attributeDOM.appendChild(mandatory.holder);
-                        this.attributes.push(mandatory);
-                        this.attributeDOM.appendChild(boxsize.holder);
-                        this.attributes.push(boxsize);
-                        break;
-                    case "number":
-                        this.titleDOM.textContent = "Number";
-                        var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]);
-                        var mandatory = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("mandatory")[0]);
-                        var min = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("min")[0]);
-                        var max = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("max")[0]);
-                        this.attributeDOM.appendChild(id.holder);
-                        this.attributes.push(id);
-                        this.attributeDOM.appendChild(min.holder);
-                        this.attributes.push(min);
-                        this.attributeDOM.appendChild(max.holder);
-                        this.attributes.push(max);
-                        break;
-                    case "checkbox":
-                        this.titleDOM.textContent = "Checkbox";
-                        var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]);
-                        this.attributeDOM.appendChild(id.holder);
-                        this.attributes.push(id);
-                        break;
-                    case "radio":
-                        this.titleDOM.textContent = "Radio";
-                        var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]);
-                        this.attributeDOM.appendChild(id.holder);
-                        this.attributes.push(id);
-                        break;
-                }
-            }
-            this.build();
-
-            this.editNode = {
-                root: document.createElement("button"),
-                parent: this,
-                handleEvent: function () {
-                    popupObject.show();
-                    popupStateNodes.state[5].generate(this.parent.specification, this.parent);
-                    popupObject.postNode(popupStateNodes.state[5]);
-                }
-            }
-            this.editNode.root.textContent = "Edit Entry";
-            this.editNode.root.addEventListener("click", this.editNode, false);
-            this.buttonDOM.appendChild(this.editNode.root);
-
-            this.deleteNode = {
-                root: document.createElement("button"),
-                parent: this,
-                handleEvent: function () {
-                    var optionList = this.parent.parent.specification.options;
-                    var childList = this.parent.parent.children;
-                    for (var i = 0; i < this.parent.parent.specification.options.length; i++) {
-                        var option = this.parent.parent.specification.options[i];
-                        if (option == this.parent.specification) {
-                            this.parent.parent.childrenDOM.removeChild(this.parent.rootDOM);
-                            if (i == this.parent.parent.specification.options.length - 1) {
-                                optionList = optionList.slice(0, i);
-                                childList = childList.slice(0, i);
-                            } else {
-                                optionList = optionList.slice(0, i).concat(optionList.slice(i + 1));
-                                childList = childList.slice(0, i).concat(childList.slice(i + 1));
-                            }
-                            this.parent.parent.specification.options = optionList;
-                            this.parent.parent.children = childList;
-                        }
-                    }
-                }
-            }
-            this.deleteNode.root.textContent = "Delete Entry";
-            this.deleteNode.root.addEventListener("click", this.deleteNode, false);
-            this.buttonDOM.appendChild(this.deleteNode.root);
-
-            this.moveToPosition = function (new_index) {
-                new_index = Math.min(new_index, this.parent.children.length);
-                var curr_index = this.parent.children.findIndex(function (elem) {
-                    if (elem == this) {
-                        return true;
-                    } else {
-                        return false;
-                    }
-                }, this);
-                // Split at the current location to remove the node and shift all the children up
-                var tail = this.parent.children.splice(curr_index + 1);
-                this.parent.children.pop();
-                this.parent.children = this.parent.children.concat(tail);
-
-                //Split at the new location and insert the node
-                tail = this.parent.children.splice(new_index);
-                this.parent.children.push(this);
-                this.parent.children = this.parent.children.concat(tail);
-
-                // Re-build the specification
-                this.parent.specification.options = [];
-                this.parent.childrenDOM.innerHTML = "";
-                for (var obj of this.parent.children) {
-                    this.parent.specification.options.push(obj.specification);
-                    this.parent.childrenDOM.appendChild(obj.rootDOM);
-                }
-                this.parent.children.forEach(function (obj, index) {
-                    obj.moveButtons.disable(index);
-                });
-            }
-
-            this.moveButtons = {
-                root_up: document.createElement("button"),
-                root_down: document.createElement("button"),
-                parent: this,
-                handleEvent: function (event) {
-                    var index = this.parent.parent.children.indexOf(this.parent);
-                    if (event.currentTarget.getAttribute("direction") == "up") {
-                        index = Math.max(index - 1, 0);
-                    } else if (event.currentTarget.getAttribute("direction") == "down") {
-                        index = Math.min(index + 1, this.parent.parent.children.length - 1);
-                    }
-                    this.parent.moveToPosition(index);
-                    this.disable(index);
-                },
-                disable: function (index) {
-                    if (index == 0) {
-                        this.root_up.disabled = true;
-                    } else {
-                        this.root_up.disabled = false;
-                    }
-                    if (index == this.parent.parent.children.length - 1) {
-                        this.root_down.disabled = true;
-                    } else {
-                        this.root_down.disabled = false;
-                    }
-                }
-            }
-            this.moveButtons.root_up.setAttribute("direction", "up");
-            this.moveButtons.root_down.setAttribute("direction", "down");
-            this.moveButtons.root_up.addEventListener("click", this.moveButtons, false);
-            this.moveButtons.root_down.addEventListener("click", this.moveButtons, false);
-            this.moveButtons.root_up.textContent = "Move Up";
-            this.moveButtons.root_down.textContent = "Move Down";
-            this.buttonDOM.appendChild(this.moveButtons.root_up);
-            this.buttonDOM.appendChild(this.moveButtons.root_down);
-        }
-        this.addNode = {
-            root: document.createElement("button"),
-            parent: this,
-            handleEvent: function () {
-                var newNode = new this.parent.specification.OptionNode(this.parent.specification);
-                this.parent.specification.options.push(newNode);
-                popupObject.show();
-                popupStateNodes.state[5].generate(newNode, this.parent);
-                popupObject.postNode(popupStateNodes.state[5]);
-            }
-        }
-        this.addNode.root.textContent = "Add Survey Entry";
-        this.addNode.root.addEventListener("click", this.addNode, false);
-        this.buttonDOM.appendChild(this.addNode.root);
-
-        for (var option of this.specification.options) {
-            var newNode = new this.surveyEntryNode(this, option);
-            this.children.push(newNode);
-            this.childrenDOM.appendChild(newNode.rootDOM);
-        }
-
-        this.children.forEach(function (obj, index) {
-            obj.moveButtons.disable(index);
-        });
-    }
-
-    this.pageNode = function (parent, rootObject) {
-        this.type = "pageNode";
-        this.rootDOM = document.createElement("div");
-        this.titleDOM = document.createElement("span");
-        this.attributeDOM = document.createElement("div");
-        this.attributes = [];
-        this.childrenDOM = document.createElement("div");
-        this.children = [];
-        this.buttonDOM = document.createElement("div");
-        this.parent = parent;
-        this.specification = rootObject;
-        this.schema = specification.schema.getAllElementsByName("page")[0];
-        this.rootDOM.className = "node";
-
-        var titleDiv = document.createElement('div');
-        titleDiv.className = "node-title";
-        this.titleDOM.className = "node-title";
-        this.titleDOM.textContent = "Test Page";
-        titleDiv.appendChild(this.titleDOM);
-
-        this.attributeDOM.className = "node-attributes";
-        this.childrenDOM.className = "node-children";
-        this.buttonDOM.className = "node-buttons";
-
-        this.rootDOM.appendChild(titleDiv);
-        this.rootDOM.appendChild(this.attributeDOM);
-        this.rootDOM.appendChild(this.childrenDOM);
-        this.rootDOM.appendChild(this.buttonDOM);
-
-        // Do the comment prefix node
-        var cpn = this.parent.createGeneralNodeDOM("Comment Prefix", "" + this.specification.id + "-commentprefix", this.parent);
-        cpn.rootDOM.removeChild(cpn.attributeDOM);
-        var obj = {
-            root: document.createElement("div"),
-            input: document.createElement("input"),
-            parent: this,
-            handleEvent: function () {
-                this.parent.specification.commentBoxPrefix = event.currentTarget.value;
-            }
-        }
-        cpn.children.push(obj);
-        cpn.childrenDOM.appendChild(obj.root);
-        obj.root.appendChild(obj.input);
-        obj.input.addEventListener("change", obj, false);
-        obj.input.value = this.specification.commentBoxPrefix;
-        this.childrenDOM.appendChild(cpn.rootDOM);
-        this.children.push(cpn);
-
-        // Now both before and after surveys
-        if (this.specification.preTest == undefined) {
-            this.specification.preTest = new specification.surveyNode(specification);
-            this.specification.preTest.location = "pre";
-        }
-        if (this.specification.postTest == undefined) {
-            this.specification.postTest = new specification.surveyNode(specification);
-            this.specification.postTest.location = "post";
-        }
-        var surveyBefore = new this.parent.surveyNode(this, this.specification.preTest, "Pre");
-        var surveyAfter = new this.parent.surveyNode(this, this.specification.postTest, "Post");
-        this.children.push(surveyBefore);
-        this.children.push(surveyAfter);
-        this.childrenDOM.appendChild(surveyBefore.rootDOM);
-        this.childrenDOM.appendChild(surveyAfter.rootDOM);
-
-        // Build the attributes
-        var attributeList = this.schema.getAllElementsByTagName("xs:attribute");
-        for (var i = 0; i < attributeList.length; i++) {
-            var attributeName = attributeList[i].getAttribute('name');
-            var attrObject = this.parent.convertAttributeToDOM(rootObject, attributeList[i]);
-            this.attributeDOM.appendChild(attrObject.holder);
-            this.attributes.push(attrObject);
-        }
-
-        this.interfaces = [];
-
-        this.getAudioElements = function () {
-            var array = [];
-            for (var i = 0; i < this.children.length; i++) {
-                if (this.children[i].type == "audioElementNode") {
-                    array[array.length] = this.children[i];
-                }
-            }
-            return array;
-        }
-
-        this.redrawChildren = function () {
-            this.childrenDOM.innerHTML = "";
-            for (var child of this.children) {
-                this.childrenDOM.appendChild(child.rootDOM);
-            }
-        }
-
-        this.audioElementNode = function (parent, rootObject) {
-            this.type = "audioElementNode";
-            this.rootDOM = document.createElement("div");
-            this.titleDOM = document.createElement("span");
-            this.attributeDOM = document.createElement("div");
-            this.attributes = [];
-            this.childrenDOM = document.createElement("div");
-            this.children = [];
-            this.buttonDOM = document.createElement("div");
-            this.parent = parent;
-            this.specification = rootObject;
-            this.schema = specification.schema.getAllElementsByName("audioelement")[0];
-            this.rootDOM.className = "node";
-
-            var titleDiv = document.createElement('div');
-            titleDiv.className = "node-title";
-            this.titleDOM.className = "node-title";
-            this.titleDOM.textContent = "Audio Element";
-            titleDiv.appendChild(this.titleDOM);
-
-            this.attributeDOM.className = "node-attributes";
-            this.childrenDOM.className = "node-children";
-            this.buttonDOM.className = "node-buttons";
-
-            this.rootDOM.appendChild(titleDiv);
-            this.rootDOM.appendChild(this.attributeDOM);
-            this.rootDOM.appendChild(this.childrenDOM);
-            this.rootDOM.appendChild(this.buttonDOM);
-
-            // Build the attributes
-            var attributeList = this.schema.getAllElementsByTagName("xs:attribute");
-            for (var i = 0; i < attributeList.length; i++) {
-                var attributeName = attributeList[i].getAttribute('name');
-                var attrObject = this.parent.parent.convertAttributeToDOM(rootObject, attributeList[i]);
-                this.attributeDOM.appendChild(attrObject.holder);
-                this.attributes.push(attrObject);
-            }
-
-            this.deleteNode = {
-                root: document.createElement("button"),
-                parent: this,
-                handleEvent: function () {
-                    var i = this.parent.parent.specification.audioElements.findIndex(this.findNode, this);
-                    if (i >= 0) {
-                        var aeList = this.parent.parent.specification.audioElements;
-                        if (i < aeList.length - 1) {
-                            aeList = aeList.slice(0, i).concat(aeList.slice(i + 1));
-                        } else {
-                            aeList = aeList.slice(0, i);
-                        }
-                    }
-                    i = this.parent.parent.children.findIndex(function (element, index, array) {
-                        if (element == this.parent)
-                            return true;
-                        else
-                            return false;
-                    }, this);
-                    if (i >= 0) {
-                        var childList = this.parent.children;
-                        if (i < aeList.length - 1) {
-                            childList = childList.slice(0, i).concat(childList.slice(i + 1));
-                        } else {
-                            childList = childList.slice(0, i);
-                        }
-                        this.parent.parent.childrenDOM.removeChild(this.parent.rootDOM);
-                    }
-                },
-                findNode: function (element, index, array) {
-                    if (element == this.parent.specification)
-                        return true;
-                    else
-                        return false;
-                }
-            }
-            this.deleteNode.root.textContent = "Delete Entry";
-            this.deleteNode.root.addEventListener("click", this.deleteNode, false);
-            this.buttonDOM.appendChild(this.deleteNode.root);
-
-            this.moveButtons = {
-                root_up: document.createElement("button"),
-                root_down: document.createElement("button"),
-                parent: this,
-                handleEvent: function (event) {
-                    var index = this.parent.parent.getAudioElements().indexOf(this.parent);
-                    if (event.currentTarget.getAttribute("direction") == "up") {
-                        index = Math.max(index - 1, 0);
-                    } else if (event.currentTarget.getAttribute("direction") == "down") {
-                        index = Math.min(index + 1, this.parent.parent.getAudioElements().length - 1);
-                    }
-                    this.parent.moveToPosition(index);
-                    this.disable(index);
-                },
-                disable: function (index) {
-                    if (index == 0) {
-                        this.root_up.disabled = true;
-                    } else {
-                        this.root_up.disabled = false;
-                    }
-                    if (index == this.parent.parent.getAudioElements().length - 1) {
-                        this.root_down.disabled = true;
-                    } else {
-                        this.root_down.disabled = false;
-                    }
-                }
-            }
-            this.moveButtons.root_up.setAttribute("direction", "up");
-            this.moveButtons.root_down.setAttribute("direction", "down");
-            this.moveButtons.root_up.addEventListener("click", this.moveButtons, false);
-            this.moveButtons.root_down.addEventListener("click", this.moveButtons, false);
-            this.moveButtons.root_up.textContent = "Move Up";
-            this.moveButtons.root_down.textContent = "Move Down";
-            this.buttonDOM.appendChild(this.moveButtons.root_up);
-            this.buttonDOM.appendChild(this.moveButtons.root_down);
-
-            this.moveToPosition = function (new_index) {
-
-                // Get the zero-th Object
-                var zero_object = this.parent.getAudioElements()[0];
-                var parent_children_root_index = this.parent.children.indexOf(zero_object);
-                // splice out the array for processing
-                var process_array = this.parent.children.splice(parent_children_root_index);
-
-
-                new_index = Math.min(new_index, process_array.length);
-                var curr_index = process_array.findIndex(function (elem) {
-                    if (elem == this) {
-                        return true;
-                    } else {
-                        return false;
-                    }
-                }, this);
-
-                // Split at the current location to remove the node and shift all the children up
-                var tail = process_array.splice(curr_index + 1);
-                process_array.pop();
-                process_array = process_array.concat(tail);
-
-                //Split at the new location and insert the node
-                tail = process_array.splice(new_index);
-                process_array.push(this);
-                process_array = process_array.concat(tail);
-
-                // Re-attach to the parent.children
-                this.parent.children = this.parent.children.concat(process_array);
-
-                // Re-build the specification
-                this.parent.specification.audioElements = [];
-                for (var obj of process_array) {
-                    this.parent.specification.audioElements.push(obj.specification);
-                }
-                this.parent.redrawChildren();
-
-                process_array.forEach(function (obj, index) {
-                    obj.moveButtons.disable(index);
-                });
-
-            }
-        }
-
-        this.commentQuestionNode = function (parent, rootObject) {
-            this.type = "commentQuestionNode";
-            this.rootDOM = document.createElement("div");
-            this.titleDOM = document.createElement("span");
-            this.attributeDOM = document.createElement("div");
-            this.attributes = [];
-            this.childrenDOM = document.createElement("div");
-            this.children = [];
-            this.buttonDOM = document.createElement("div");
-            this.parent = parent;
-            this.specification = rootObject;
-            this.schema = specification.schema.getAllElementsByName("page")[0];
-            this.rootDOM.className = "node audio-element";
-
-            var titleDiv = document.createElement('div');
-            titleDiv.className = "node-title";
-            this.titleDOM.className = "node-title";
-            this.titleDOM.textContent = "Test Page";
-            titleDiv.appendChild(this.titleDOM);
-
-            this.attributeDOM.className = "node-attributes";
-            this.childrenDOM.className = "node-children";
-            this.buttonDOM.className = "node-buttons";
-
-            this.rootDOM.appendChild(titleDiv);
-            this.rootDOM.appendChild(this.attributeDOM);
-            this.rootDOM.appendChild(this.childrenDOM);
-            this.rootDOM.appendChild(this.buttonDOM);
-
-        }
-
-        // Build the components
-        if (this.specification.interfaces.length == 0) {
-            this.specification.interfaces.push(new specification.interfaceNode(specification));
-        }
-        for (var interfaceObj of this.specification.interfaces) {
-            var newInterface = new this.parent.interfaceNode(this.parent, interfaceObj);
-            newInterface.build("Interface", "" + this.specification.id + "-interface", this.childrenDOM);
-            this.children.push(newInterface);
-            this.interfaces.push(newInterface);
-        }
-
-        for (var elements of this.specification.audioElements) {
-            var audioElementDOM = new this.audioElementNode(this, elements);
-            this.children.push(audioElementDOM);
-            this.childrenDOM.appendChild(audioElementDOM.rootDOM);
-        }
-
-        this.getAudioElements().forEach(function (elem) {
-            elem.moveButtons.disable();
-        });
-
-        this.addInterface = {
-            root: document.createElement("button"),
-            parent: this,
-            handleEvent: function () {
-                var InterfaceObj = new specification.interfaceNode(specification);
-                var newInterface = new this.parent.parent.interfaceNode(this.parent.parent, InterfaceObj);
-                newInterface.build("Interface", "" + this.parent.specification.id + "-interface", this.parent.childrenDOM);
-                this.parent.children.push(newInterface);
-                this.parent.specification.interfaces.push(InterfaceObj);
-                this.parent.interfaces.push(newInterface);
-            }
-        }
-        this.addInterface.root.textContent = "Add Interface";
-        this.addInterface.root.addEventListener("click", this.addInterface, false);
-        this.buttonDOM.appendChild(this.addInterface.root);
-
-        this.addAudioElement = {
-            root: document.createElement("button"),
-            parent: this,
-            handleEvent: function () {
-                var audioElementObject = new this.parent.specification.audioElementNode(specification);
-                var audioElementDOM = new this.parent.audioElementNode(this.parent, audioElementObject);
-                this.parent.specification.audioElements.push(audioElementObject);
-                this.parent.children.push(audioElementDOM);
-                this.parent.childrenDOM.appendChild(audioElementDOM.rootDOM);
-            }
-        }
-        this.addAudioElement.root.textContent = "Add Audio Element";
-        this.addAudioElement.root.addEventListener("click", this.addAudioElement, false);
-        this.buttonDOM.appendChild(this.addAudioElement.root);
-    }
-}
+        $s.page.audioElements.splice(index, 1);
+    };
+}]);
--- a/tests/examples/ABX_example.xml	Fri Jul 14 15:37:53 2017 +0100
+++ b/tests/examples/ABX_example.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -58,8 +58,8 @@
             <interface>
                 <title>Depth</title>
             </interface>
-            <audioelement url="0.wav" id="track-0" />
-            <audioelement url="1.wav" id="track-1" />
+            <audioelement url="0.wav" id="track-0" image="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
+            <audioelement url="1.wav" id="track-1" image="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
             <survey location="before">
                 <surveyentry type="statement" id="test-0-intro">
                     <statement>A two way comparison using randomised element order, automatic loudness and synchronised looping.</statement>
--- a/tests/examples/AB_example.xml	Fri Jul 14 15:37:53 2017 +0100
+++ b/tests/examples/AB_example.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -1,39 +1,39 @@
 <?xml version="1.0" encoding="utf-8"?>
     <waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd">
-        <setup interface="AB" projectReturn="save.php" randomiseOrder='true' poolSize="2" loudness="-23" playOne="true">
+        <setup interface="AB" projectReturn="save.php" randomiseOrder="false" poolSize="2" loudness="-23" playOne="true">
             <survey location="before">
-                <surveyentry type="question" id="sessionId" mandatory="true">
+                <surveyquestion id="sessionId" mandatory="true">
                     <statement>Please enter your name.</statement>
-                </surveyentry>
-                <surveyentry type="checkbox" id="checkboxtest" mandatory="true">
+                </surveyquestion>
+                <surveycheckbox id="checkboxtest" mandatory="true">
                     <statement>Please select with which activities you have any experience (example checkbox question)</statement>
                     <option name="musician">Playing a musical instrument</option>
                     <option name="soundengineer">Recording or mixing audio</option>
                     <option name="developer">Developing audio software</option>
                     <option name="hwdesigner">Designing or building audio hardware</option>
                     <option name="researcher">Research in the field of audio</option>
-                </surveyentry>
-                <surveyentry type="statement" id="test-intro">
+                </surveycheckbox>
+                <surveystatement id="test-intro">
                     <statement>This is an example of an 'AB'-style test, with two pages, using the test stimuli in 'example_eval/'. The 'playOne' configuration option means a fragment has to be finished playing before another fragment can be auditioned. </statement>
-                </surveyentry>
+                </surveystatement>
             </survey>
             <survey location="after">
-                <surveyentry type="question" id="location" mandatory="true" boxsize="large">
+                <surveyquestion id="location" mandatory="true" boxsize="large">
                     <statement>Please enter your location. (example mandatory text question)</statement>
-                </surveyentry>
-                <surveyentry type="number" id="age" min="0">
+                </surveyquestion>
+                <surveynumber id="age" min="0">
                     <statement>Please enter your age (example non-mandatory number question)</statement>
-                </surveyentry>
-                <surveyentry type="radio" id="rating">
+                </surveynumber>
+                <surveyradio id="rating">
                     <statement>Please rate this interface (example radio button question)</statement>
                     <option name="bad">Bad</option>
                     <option name="poor">Poor</option>
                     <option name="good">Good</option>
                     <option name="great">Great</option>
-                </surveyentry>
-                <surveyentry type="statement" id="test-thank-you">
+                </surveyradio>
+                <surveystatement id="test-thank-you">
                     <statement>Thank you for taking this listening test. Please click 'submit' and your results will appear in the 'saves/' folder.</statement>
-                </surveyentry>
+                </surveystatement>
             </survey>
             <metric>
                 <metricenable>testTimer</metricenable>
@@ -46,7 +46,9 @@
             </metric>
             <interface>
                 <interfaceoption type="check" name="fragmentMoved" />
-                <interfaceoption type="check" name="scalerange" min="25" max="75" />
+                <interfaceoption type="check" name="scalerange" min="25" max="75">
+                    <errormessage>Test Error Message</errormessage>
+                </interfaceoption>
                 <interfaceoption type="show" name='playhead' />
                 <interfaceoption type="show" name="page-count" />
                 <interfaceoption type="show" name='volume' />
@@ -56,19 +58,20 @@
         <page id='test-0' hostURL="media/example/" randomiseOrder='true' repeatCount='0' loop='false' loudness="-12">
             <commentboxprefix>Comment on fragment</commentboxprefix>
             <interface>
-                <title>Depth</title>
+                <title>Which sounds most like a drum?</title>
+                <image src="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
             </interface>
             <audioelement url="0.wav" id="track-0" />
             <audioelement url="1.wav" id="track-1" />
             <survey location="before">
-                <surveyentry type="statement" id="test-0-intro">
-                    <statement>A two way comparison using randomised element order, automatic loudness and synchronised looping.</statement>
-                </surveyentry>
+                <surveystatement id="test-0-intro">
+                    <statement>A two way comparison using randomised element order, automatic loudness and synchronised looping. Also an embedded image</statement>
+                </surveystatement>
             </survey>
             <survey location="after">
-                <surveyentry type="question" id="genre-0" mandatory="true">
+                <surveyquestion id="genre-0" mandatory="true">
                     <statement>Please enter the genre.</statement>
-                </surveyentry>
+                </surveyquestion>
             </survey>
         </page>
         <page id='test-1' hostURL="media/example/" randomiseOrder='true' repeatCount='0' loop='false' loudness="-12">
@@ -84,14 +87,14 @@
             <audioelement url="5.wav" id="track-7" />
             <audioelement url="6.wav" id="track-8" />
             <survey location="before">
-                <surveyentry type="statement" id="test-1-intro">
+                <surveystatement id="test-1-intro">
                     <statement>A 7 way comparison using randomised element order and synchronised looping.</statement>
-                </surveyentry>
+                </surveystatement>
             </survey>
             <survey location="after">
-                <surveyentry type="question" id="genre-1" mandatory="true">
+                <surveyquestion id="genre-1" mandatory="true">
                     <statement>Please enter the genre.</statement>
-                </surveyentry>
+                </surveyquestion>
             </survey>
         </page>
     </waet>
--- a/tests/examples/mushra_example.xml	Fri Jul 14 15:37:53 2017 +0100
+++ b/tests/examples/mushra_example.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -48,6 +48,7 @@
             <interface>
                 <interfaceoption type="check" name="fragmentMoved" />
                 <interfaceoption type="check" name="scalerange" min="25" max="75" />
+                <interfaceoption type="show" name="fragmentSort" />
                 <interfaceoption type="show" name='playhead' />
                 <interfaceoption type="show" name="page-count" />
                 <interfaceoption type="show" name="volume" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/examples/ordinal_example.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+    <waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd">
+        <setup interface="ordinal" projectReturn="save.php" randomiseOrder="true">
+            <metric>
+                <metricenable>testTimer</metricenable>
+                <metricenable>elementTimer</metricenable>
+                <metricenable>elementInitialPosition</metricenable>
+                <metricenable>elementTracker</metricenable>
+                <metricenable>elementFlagListenedTo</metricenable>
+                <metricenable>elementFlagMoved</metricenable>
+                <metricenable>elementListenTracker</metricenable>
+            </metric>
+            <interface>
+                <interfaceoption type="check" name="fragmentComment" />
+                <interfaceoption type="show" name='playhead' />
+                <interfaceoption type="show" name="page-count" />
+                <interfaceoption type="show" name="volume" />
+                <interfaceoption type="show" name="comments" />
+            </interface>
+        </setup>
+        <page id='test-0' hostURL="media/example/" randomiseOrder='true' repeatCount='0' loop='true' loudness="-23">
+            <title>Ordinal Evaluation</title>
+            <interface></interface>
+            <audioelement url="0.wav" id="track-1" />
+            <audioelement url="1.wav" id="track-2" />
+            <audioelement url="2.wav" id="track-3" />
+            <audioelement url="3.wav" id="track-4" />
+            <audioelement url="4.wav" id="track-5" />
+            <audioelement url="5.wav" id="track-6" />
+            <audioelement url="6.wav" id="track-7" />
+            <audioelement url="7.wav" id="track-8" />
+        </page>
+    </waet>
--- a/tests/examples/timeline.xml	Fri Jul 14 15:37:53 2017 +0100
+++ b/tests/examples/timeline.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -30,8 +30,8 @@
                     <scalelabel position="100">(5) Inaudible</scalelabel>
                 </scales>
             </interface>
-            <audioelement url="0.wav" id="track-1" />
-            <audioelement url="1.wav" id="track-2" />
+            <audioelement url="0.wav" id="track-1" image="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
+            <audioelement url="1.wav" id="track-2" image="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
             <commentquestions>
                 <commentradio id="preference">
                     <statement>Please enter your overall preference</statement>
@@ -61,8 +61,8 @@
                     <scalelabel position="100">(5) Inaudible</scalelabel>
                 </scales>
             </interface>
-            <audioelement url="0.wav" id="track-3" />
-            <audioelement url="1.wav" id="track-4" />
+            <audioelement url="0.wav" id="track-3" image="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
+            <audioelement url="1.wav" id="track-4" image="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
             <commentquestions>
                 <commentradio id="preference1">
                     <statement>Please enter your overall preference</statement>
--- a/tests/pool.xml	Fri Jul 14 15:37:53 2017 +0100
+++ b/tests/pool.xml	Fri Jul 14 15:39:24 2017 +0100
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
     <waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd">
-        <setup interface="likert" projectReturn="save.php" crossFade="3.0" poolSize="3">
+        <setup interface="discrete" projectReturn="save.php" crossFade="0.1" poolSize="3">
             <metric>
                 <metricenable>testTimer</metricenable>
                 <metricenable>elementTimer</metricenable>
--- a/xml/test-schema.xsd	Fri Jul 14 15:37:53 2017 +0100
+++ b/xml/test-schema.xsd	Fri Jul 14 15:39:24 2017 +0100
@@ -27,8 +27,13 @@
             </xs:simpleType>
         </xs:attribute>
 
+        <xs:attribute name="minNumberPlays" type="xs:nonNegativeInteger" default="0" />
+        <xs:attribute name="maxNumberPlays" type="xs:nonNegativeInteger" />
+
         <xs:attribute name="playOne" type="xs:boolean" default="false" />
 
+        <xs:attribute name="minWait" type="xs:nonNegativeInteger" default="0" />
+
         <!-- define complex elements-->
         <xs:element name="waet">
             <xs:complexType>
@@ -65,6 +70,8 @@
                 <xs:attribute ref="preSilence" />
                 <xs:attribute ref="postSilence" />
                 <xs:attribute ref="playOne" />
+                <xs:attribute ref="minNumberPlays" use="optional" />
+                <xs:attribute ref="maxNumberPlays" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -100,10 +107,13 @@
                 <xs:attribute name="labelStart" type="xs:string" use="optional" default="" />
                 <xs:attribute ref="poolSize" />
                 <xs:attribute ref="alwaysInclude" />
+                <xs:attribute name="position" use="optional" type="xs:nonNegativeInteger" />
                 <xs:attribute ref="preSilence" />
                 <xs:attribute ref="postSilence" />
                 <xs:attribute ref="playOne" />
                 <xs:attribute name="restrictMovement" type="xs:boolean" default="false" use="optional" />
+                <xs:attribute ref="minNumberPlays" use="optional" />
+                <xs:attribute ref="maxNumberPlays" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -119,8 +129,16 @@
             <xs:complexType>
                 <xs:sequence>
                     <xs:element ref="title" minOccurs="0" maxOccurs="1" />
+                    <xs:element name="image" minOccurs="0" maxOccurs="1">
+                        <xs:complexType>
+                            <xs:attribute name="src" type="xs:anyURI" use="required" />
+                        </xs:complexType>
+                    </xs:element>
                     <xs:element name="interfaceoption" minOccurs="0" maxOccurs="unbounded">
                         <xs:complexType>
+                            <xs:sequence>
+                                <xs:element name="errormessage" type="xs:string" minOccurs="0" maxOccurs="1" />
+                            </xs:sequence>
                             <xs:attribute name="type" use="required">
                                 <xs:simpleType>
                                     <xs:restriction base="xs:string">
@@ -226,6 +244,9 @@
                         </xs:restriction>
                     </xs:simpleType>
                 </xs:attribute>
+                <xs:attribute name="image" type="xs:anyURI" use="optional" />
+                <xs:attribute ref="minNumberPlays" use="optional" />
+                <xs:attribute ref="maxNumberPlays" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -331,6 +352,7 @@
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
                 <xs:attribute ref="mandatory" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="boxsize" default="normal">
                     <xs:simpleType>
                         <xs:restriction base="xs:string">
@@ -364,6 +386,7 @@
                 <xs:attribute ref="mandatory" />
                 <xs:attribute name="min" type="xs:decimal" />
                 <xs:attribute name="max" type="xs:decimal" />
+                <xs:attribute ref="minWait" />
             </xs:complexType>
         </xs:element>
 
@@ -385,6 +408,7 @@
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
                 <xs:attribute ref="mandatory" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="min" type="xs:decimal" />
                 <xs:attribute name="max" type="xs:decimal" />
             </xs:complexType>
@@ -395,6 +419,7 @@
                 <xs:sequence>
                     <xs:element ref="statement" minOccurs="1" maxOccurs="1" />
                 </xs:sequence>
+                <xs:attribute ref="minWait" />
                 <xs:attribute ref="id" use="required" />
             </xs:complexType>
         </xs:element>
@@ -408,6 +433,7 @@
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
                 <xs:attribute ref="mandatory" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="min" type="xs:decimal" />
                 <xs:attribute name="max" type="xs:decimal" />
             </xs:complexType>
@@ -423,6 +449,7 @@
                 </xs:sequence>
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="min" use="required" type="xs:decimal" />
                 <xs:attribute name="max" use="required" type="xs:decimal" />
             </xs:complexType>
@@ -434,6 +461,7 @@
                     <xs:element ref="statement" minOccurs="1" maxOccurs="1" />
                 </xs:sequence>
                 <xs:attribute ref="id" use="required" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="url" use="required" type="xs:string" />
             </xs:complexType>
         </xs:element>
@@ -444,13 +472,14 @@
                     <xs:element ref="statement" minOccurs="1" maxOccurs="1" />
                 </xs:sequence>
                 <xs:attribute ref="id" use="required" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="url" use="required" type="xs:string" />
             </xs:complexType>
         </xs:element>
 
         <xs:element name="survey">
             <xs:complexType>
-                <xs:choice maxOccurs="unbounded">
+                <xs:choice maxOccurs="unbounded" minOccurs="0">
                     <xs:element name="surveyentry" maxOccurs="unbounded">
                         <xs:complexType>
                             <xs:sequence>