changeset 3083:2b550d4c7fde

Merge branch 'vnext' into Dev_main # Conflicts: # tests/examples/APE_example.xml # tests/examples/horizontal_example.xml # tests/examples/mushra_example.xml # tests/examples/radio_example.xml
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Wed, 22 Nov 2017 10:10:44 +0000
parents d25e09e3b8fe (diff) 396b9aac3bb9 (current diff)
children 1ff68ea1a4cf
files tests/examples/AB_example.xml tests/examples/APE_example.xml tests/examples/horizontal_example.xml tests/examples/mushra_example.xml tests/examples/radio_example.xml
diffstat 30 files changed, 1132 insertions(+), 916 deletions(-) [+]
line wrap: on
line diff
--- a/.gitignore	Wed Nov 22 10:08:26 2017 +0000
+++ b/.gitignore	Wed Nov 22 10:10:44 2017 +0000
@@ -11,3 +11,6 @@
 *.DS_STORE
 *.swp
 *.swo
+saves/ratings/*
+saves/timelines/*
+saves/timelines_movement/*
--- a/css/core.css	Wed Nov 22 10:08:26 2017 +0000
+++ b/css/core.css	Wed Nov 22 10:10:44 2017 +0000
@@ -199,6 +199,7 @@
 div.master-volume-holder-inline {
     width: 100%;
     padding: 5px;
+    float: left;
 }
 div.master-volume-holder-float {
     position: absolute;
--- a/index.html	Wed Nov 22 10:08:26 2017 +0000
+++ b/index.html	Wed Nov 22 10:10:44 2017 +0000
@@ -51,7 +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)</a></div>
+    <div id="footer"><a target="_blank" href="https://github.com/BrechtDeMan/WebAudioEvaluationTool">Web Audio Evaluation Toolbox (v1.2.3)</a></div>
 </body>
 
 </html>
--- a/interfaces/AB.css	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/AB.css	Wed Nov 22 10:10:44 2017 +0000
@@ -24,6 +24,10 @@
     height: 40px;
     font-size: 1.2em;
 }
+div#submit-holder {
+    width: 100%;
+    text-align: center;
+}
 div.interface-buttons {
     height: 40px;
 }
--- a/interfaces/AB.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/AB.js	Wed Nov 22 10:10:44 2017 +0000
@@ -75,13 +75,14 @@
     var boxes = document.createElement('div');
     boxes.id = "box-holders";
 
+    var submitHolder = document.createElement("div");
+    submitHolder.id = "submit-holder"
     var submit = document.createElement('button');
     submit.id = "submit";
     submit.onclick = buttonSubmitClick;
     submit.className = "big-button";
-    submit.textContent = "submit";
-    submit.style.position = "relative";
-    submit.style.left = (window.innerWidth - 250) / 2 + 'px';
+    submit.textContent = "Submit";
+    submitHolder.appendChild(submit);
 
     feedbackHolder.appendChild(boxes);
 
@@ -95,7 +96,7 @@
     testContent.appendChild(interfaceButtons);
     testContent.appendChild(outsideRef);
     testContent.appendChild(feedbackHolder);
-    testContent.appendChild(submit);
+    testContent.appendChild(submitHolder);
     testContent.appendChild(comments);
     interfaceContext.insertPoint.appendChild(testContent);
 
@@ -395,10 +396,13 @@
 function buttonSubmitClick() {
     var checks = testState.currentStateMap.interfaces[0].options,
         canContinue = true;
-    
+
     if (interfaceContext.checkFragmentMinPlays() === false) {
-    return;
-}
+        return;
+    }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
 
     for (var i = 0; i < checks.length; i++) {
         if (checks[i].type == 'check') {
--- a/interfaces/ABX.css	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/ABX.css	Wed Nov 22 10:10:44 2017 +0000
@@ -20,9 +20,12 @@
     height: 40px;
     font-size: 1.2em;
 }
+div#submit-holder {
+    width: 100%;
+    text-align: center;
+}
 div#box-holders {
     width: 100%;
-    text-align: center;
 }
 div#playback-holder {
     float: none;
--- a/interfaces/ABX.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/ABX.js	Wed Nov 22 10:10:44 2017 +0000
@@ -77,15 +77,15 @@
     var boxes = document.createElement('div');
     boxes.align = "center";
     boxes.id = "box-holders";
-    boxes.style.float = "left";
 
+    var submitHolder = document.createElement("div");
+    submitHolder.id = "submit-holder"
     var submit = document.createElement('button');
     submit.id = "submit";
     submit.onclick = buttonSubmitClick;
     submit.className = "big-button";
     submit.textContent = "submit";
-    submit.style.position = "relative";
-    submit.style.left = (window.innerWidth - 250) / 2 + 'px';
+    submitHolder.appendChild(submit);
 
     feedbackHolder.appendChild(boxes);
 
@@ -98,7 +98,7 @@
     testContent.appendChild(pagetitle);
     testContent.appendChild(interfaceButtons);
     testContent.appendChild(feedbackHolder);
-    testContent.appendChild(submit);
+    testContent.appendChild(submitHolder);
     testContent.appendChild(comments);
     interfaceContext.insertPoint.appendChild(testContent);
 
@@ -424,9 +424,6 @@
         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';
 }
 
 function buttonSubmitClick() {
@@ -436,6 +433,9 @@
     if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
 
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
--- a/interfaces/ape.css	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/ape.css	Wed Nov 22 10:10:44 2017 +0000
@@ -26,7 +26,6 @@
 }
 div.slider {
     /* Specify any structure for the slider holder interface */
-    background-color: #eee;
     height: 150px;
     margin: 5px 50px;
     -moz-user-select: -moz-none;
@@ -61,6 +60,12 @@
     -webkit-user-select: none;
     border: 1px solid black;
 }
+canvas.tick-canvas {
+    z-index: -1;
+    position: absolute;
+    height: 150px;
+    background-color: #eee;
+}
 div#outside-reference-holder {
     display: flex;
     align-content: center;
--- a/interfaces/ape.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/ape.js	Wed Nov 22 10:10:44 2017 +0000
@@ -21,136 +21,15 @@
     testContent.id = 'testContent';
 
     // Bindings for interfaceContext
-    interfaceContext.checkAllPlayed = function () {
-        var hasBeenPlayed = audioEngineContext.checkAllPlayed();
-        if (hasBeenPlayed.length > 0) // if a fragment has not been played yet
-        {
-            var str = "";
-            if (hasBeenPlayed.length > 1) {
-                for (var i = 0; i < hasBeenPlayed.length; i++) {
-                    var ao_id = audioEngineContext.audioObjects[hasBeenPlayed[i]].interfaceDOM.getPresentedId();
-                    str = str + ao_id; // start from 1
-                    if (i < hasBeenPlayed.length - 2) {
-                        str += ", ";
-                    } else if (i == hasBeenPlayed.length - 2) {
-                        str += " or ";
-                    }
-                }
-                str = 'You have not played fragments ' + str + ' yet. Please listen, rate and comment all samples before submitting.';
-            } else {
-                str = 'You have not played fragment ' + (audioEngineContext.audioObjects[hasBeenPlayed[0]].interfaceDOM.getPresentedId()) + ' yet. Please listen, rate and comment all samples before submitting.';
-            }
-            this.storeErrorNode(str);
-            interfaceContext.lightbox.post("Message", str);
-            return false;
-        }
-        return true;
-    };
 
     interfaceContext.checkAllMoved = function () {
-        var state = true;
-        var str = 'You have not moved the following sliders. ';
-        for (var i = 0; i < this.interfaceSliders.length; i++) {
-            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()) {
-                    state = false;
-                    interfaceTID.push(j);
-                }
-            }
-            if (interfaceTID.length !== 0) {
-                var interfaceName = this.interfaceSliders[i].interfaceObject.title;
-                if (interfaceName === undefined) {
-                    str += 'On axis ' + String(i + 1) + ' you must move ';
-                } else {
-                    str += 'On axis "' + interfaceName + '" you must move ';
-                }
-                if (interfaceTID.length == 1) {
-                    str += 'slider ' + (audioEngineContext.audioObjects[interfaceTID[0]].interfaceDOM.getPresentedId()) + '. '; // start from 1
-                } else {
-                    str += 'sliders ';
-                    for (var k = 0; k < interfaceTID.length - 1; k++) {
-                        str += (audioEngineContext.audioObjects[interfaceTID[k]].interfaceDOM.getPresentedId()) + ', '; // start from 1
-                    }
-                    str += (audioEngineContext.audioObjects[interfaceTID[interfaceTID.length - 1]].interfaceDOM.getPresentedId()) + '. ';
-                }
-            }
-        }
-        if (state !== true) {
-            this.storeErrorNode(str);
-            interfaceContext.lightbox.post("Message", str);
-            console.log(str);
-        }
-        return state;
+        return module.checkAllMoved();
     };
 
     interfaceContext.checkScaleRange = function () {
-        var audioObjs = audioEngineContext.audioObjects;
-        var audioHolder = testState.stateMap[testState.stateIndex];
-        var interfaceObject = this.interfaceSliders[0].interfaceObject;
-        var state = true;
-        var str = '';
-        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. ';
-            }
-        });
-        if (state !== true) {
-            this.storeErrorNode(str);
-            interfaceContext.lightbox.post("Message", str);
-            console.log(str);
-        }
-        return state;
+        return module.checkScaleRange();
     };
 
-    Interface.prototype.objectSelected = null;
-    Interface.prototype.objectMoved = false;
-    Interface.prototype.selectObject = function (object) {
-        if (this.objectSelected === null) {
-            this.objectSelected = object;
-            this.objectMoved = false;
-        }
-    };
-    Interface.prototype.moveObject = function () {
-        if (this.objectMoved === false) {
-            this.objectMoved = true;
-        }
-    };
-    Interface.prototype.releaseObject = function () {
-        this.objectSelected = null;
-        this.objectMoved = false;
-    };
-    Interface.prototype.getSelectedObject = function () {
-        return this.objectSelected;
-    };
-    Interface.prototype.hasSelectedObjectMoved = function () {
-        return this.objectMoved;
-    };
-
-    // Bindings for slider interfaces
-    Interface.prototype.interfaceSliders = [];
-
     // Bindings for audioObjects
 
     // Create the top div for the Title element
@@ -220,12 +99,13 @@
     interfaceContext.insertPoint.appendChild(testContent);
 
     // Load the full interface
+    window.module = new ape();
     testState.initialise();
     testState.advanceState();
-
 }
 
 function loadTest(audioHolderObject) {
+    module.clear();
     var width = window.innerWidth;
     var height = window.innerHeight;
     var id = audioHolderObject.id;
@@ -246,15 +126,10 @@
         document.getElementById("test-title").textContent = audioHolderObject.title;
     }
 
-
     // Delete outside reference
     document.getElementById("outside-reference-holder").innerHTML = "";
 
     var interfaceObj = interfaceContext.getCombinedInterfaces(audioHolderObject);
-    interfaceObj.forEach(function (interfaceObjectInstance) {
-        // Create the div box to center align
-        interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObjectInstance, audioHolderObject));
-    });
     interfaceObj.forEach(function (interface) {
         interface.options.forEach(function (option) {
             if (option.type == "show") {
@@ -298,113 +173,7 @@
 
     var loopPlayback = audioHolderObject.loop;
 
-    var currentTestHolder = document.createElement('audioHolder');
-    currentTestHolder.id = audioHolderObject.id;
-    currentTestHolder.repeatCount = audioHolderObject.repeatCount;
-
-    // Find all the audioElements from the audioHolder
-    $(audioHolderObject.audioElements).each(function (index, element) {
-        // Find URL of track
-        // In this jQuery loop, variable 'this' holds the current audioElement.
-        var audioObject = audioEngineContext.newTrack(element);
-        // Check if an outside reference
-        if (element.type == 'outside-reference') {
-            // Construct outside reference;
-            var orNode = new outsideReferenceDOM(audioObject, index, document.getElementById("outside-reference-holder"));
-            audioObject.bindInterface(orNode);
-        } else {
-            // Create a slider per track
-            var sliderNode = new sliderObject(audioObject, interfaceObj, index);
-            audioObject.bindInterface(sliderNode);
-            interfaceContext.commentBoxes.createCommentBox(audioObject);
-        }
-    });
-
-    // Initialse the interfaceSlider object metrics
-
-    $('.track-slider').mousedown(function (event) {
-        interfaceContext.selectObject($(this)[0]);
-    });
-    $('.track-slider').on('touchstart', null, function (event) {
-        interfaceContext.selectObject($(this)[0]);
-    });
-
-    $('.track-slider').mousemove(function (event) {
-        event.preventDefault();
-    });
-
-    $('.slider').mousemove(function (event) {
-        event.preventDefault();
-        var obj = interfaceContext.getSelectedObject();
-        if (obj === null) {
-            return;
-        }
-        var move = event.clientX - 6;
-        var w = $(event.currentTarget).width();
-        move = Math.max(50, move);
-        move = Math.min(w + 50, move);
-        $(obj).css("left", move + "px");
-        interfaceContext.moveObject();
-    });
-
-    $('.slider').on('touchmove', null, function (event) {
-        event.preventDefault();
-        var obj = interfaceContext.getSelectedObject();
-        if (obj === null) {
-            return;
-        }
-        var move = event.originalEvent.targetTouches[0].clientX - 6;
-        var w = $(event.currentTarget).width();
-        move = Math.max(50, move);
-        move = Math.min(w + 50, move);
-        $(obj).css("left", move + "px");
-        interfaceContext.moveObject();
-    });
-
-    $(document).mouseup(function (event) {
-        event.preventDefault();
-        var obj = interfaceContext.getSelectedObject();
-        if (obj === null) {
-            return;
-        }
-        var interfaceID = obj.parentElement.getAttribute("interfaceid");
-        var trackID = obj.getAttribute("trackindex");
-        var id;
-        if (interfaceContext.hasSelectedObjectMoved() === true) {
-            var l = $(obj).css("left");
-            id = obj.getAttribute('trackIndex');
-            var time = audioEngineContext.timer.getTestTime();
-            var rate = convSliderPosToRate(obj);
-            audioEngineContext.audioObjects[id].metric.moved(time, rate);
-            interfaceContext.interfaceSliders[interfaceID].metrics[trackID].moved(time, rate);
-            console.log("slider " + id + " moved to " + rate + ' (' + time + ')');
-            obj.setAttribute("slider-value", convSliderPosToRate(obj));
-        } else {
-            id = Number(obj.attributes.trackIndex.value);
-            //audioEngineContext.metric.sliderPlayed(id);
-            audioEngineContext.play(id);
-        }
-        interfaceContext.releaseObject();
-    });
-
-    $('.slider').on('touchend', null, function (event) {
-        var obj = interfaceContext.getSelectedObject();
-        if (obj === null) {
-            return;
-        }
-        var interfaceID = obj.parentElement.getAttribute("interfaceid");
-        var trackID = obj.getAttribute("trackindex");
-        if (interfaceContext.hasSelectedObjectMoved() === true) {
-            var l = $(obj).css("left");
-            var id = obj.getAttribute('trackIndex');
-            var time = audioEngineContext.timer.getTestTime();
-            var rate = convSliderPosToRate(obj);
-            audioEngineContext.audioObjects[id].metric.moved(time, rate);
-            interfaceContext.interfaceSliders[interfaceID].metrics[trackID].moved(time, rate);
-            console.log("slider " + id + " moved to " + rate + ' (' + time + ')');
-        }
-        interfaceContext.releaseObject();
-    });
+    module.initialisePage(audioHolderObject);
 
     var interfaceList = audioHolderObject.interfaces.concat(specification.interfaces);
     for (var k = 0; k < interfaceList.length; k++) {
@@ -445,239 +214,562 @@
     });
 
     //testWaitIndicator();
+    module.resize();
 }
 
-function interfaceSliderHolder(interfaceObject, page) {
-    this.sliders = [];
-    this.metrics = [];
-    this.id = document.getElementsByClassName("sliderCanvasDiv").length;
-    this.name = interfaceObject.name;
-    this.interfaceObject = interfaceObject;
-    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) {
+function ape() {
+    var axis = []
+    var DOMRoot = document.getElementById("slider-holder");
+    var AOIs = [];
+    var page = undefined;
+
+    function audioObjectInterface(audioObject, parent) {
+        // The audioObject communicates with this object
+        var playing = false;
+        var sliders = [];
+        this.enable = function () {
+            sliders.forEach(function (s) {
+                s.enable();
+            });
+        }
+
+        this.updateLoading = function (p) {
+            sliders.forEach(function (s) {
+                s.updateLoading(p);
+            });
+        }
+
+        this.startPlayback = function () {
+            playing = true;
+            sliders.forEach(function (s) {
+                s.playing();
+            });
+        }
+
+        this.stopPlayback = function () {
+            playing = false;
+            sliders.forEach(function (s) {
+                s.stopped();
+            });
+        }
+
+        this.getValue = function () {
+            return sliders[0].value;
+        }
+
+        this.getPresentedId = function () {
+            return sliders[0].label;
+        }
+
+        this.canMove = function () {
+            return true;
+        }
+
+        this.exportXMLDOM = function (audioObject) {
+            var elements = [];
+            sliders.forEach(function (s) {
+                elements.push(s.exportXMLDOM());
+            });
+            return elements;
+        }
+
+        this.error = function () {
+            sliders.forEach(function (s) {
+                s.error();
+            });
+        }
+
+        this.addSlider = function (s) {
+            sliders.push(s);
+        }
+
+        this.clicked = function (event) {
+            if (!playing) {
+                audioEngineContext.play(audioObject.id);
+            } else {
+                audioEngineContext.stop();
+            }
+            playing = !playing;
+        }
+
+        this.pageXMLSave = function (store) {
+            var inject = audioObject.storeDOM.getElementsByTagName("metric")[0];
+            sliders.forEach(function (s) {
+                s.pageXMLSave(inject);
+            });
+        }
+
+    }
+
+    function axisObject(interfaceObject, parent) {
+
+        function sliderInterface(AOI, axisInterface) {
+            var trackObj = document.createElement('div');
+            var labelHolder = document.createElement("span");
+            var label = "";
+            var metric = new metricTracker(this);
+            var value = Math.random();
+            trackObj.align = "center";
+            trackObj.className = 'track-slider track-slider-disabled';
+            trackObj.appendChild(labelHolder);
+            axisInterface.sliderRail.appendChild(trackObj);
+            metric.initialise(this.value);
+            this.setLabel = function (s) {
+                label = s;
+            }
+            this.resize = function (event) {
+                var width = $(axisInterface.sliderRail).width();
+                var w = Number(value * width);
+                trackObj.style.left = String(w) + "px";
+            }
+            this.playing = function () {
+                trackObj.classList.add("track-slider-playing");
+            }
+            this.stopped = function () {
+                trackObj.classList.remove("track-slider-playing");
+            }
+            this.enable = function () {
+                trackObj.addEventListener("mousedown", this);
+                trackObj.addEventListener("mouseup", this);
+                trackObj.classList.remove("track-slider-disabled");
+                labelHolder.textContent = label;
+            }
+            this.updateLoading = function (progress) {
+                labelHolder.textContent = progress + "%";
+            }
+            this.exportXMLDOM = function () {
+                var node = storage.document.createElement('value');
+                node.setAttribute("interface-name", axisInterface.name)
+                node.textContent = this.value;
+                return node;
+            }
+            this.error = function () {
+                trackObj.classList.add("error-colour");
+                trackObj.removeEventListener("mousedown");
+                trackObj.removeEventListener("mouseup");
+            }
+            var timing = undefined;
+            this.handleEvent = function (e) {
+                // This is only for the mousedown / touchdown
+                if (e.preventDefault) {
+                    e.preventDefault();
+                }
+                if (e.type == "mousedown") {
+                    axisInterface.mousedown(this);
+                } else if (e.type == "mouseup" || e.type == "touchend" || e.type == "touchcancel") {
+                    axisInterface.mouseup(this);
+                    metric.moved(audioEngineContext.timer.getTestTime(), this.value);
+                    console.log("Slider " + label + " on axis " + axisInterface.name + " moved to " + this.value);
+                }
+            }
+            this.clicked = function (e) {
+                AOI.clicked();
+            }
+            this.pageXMLSave = function (inject) {
+                var nodes = metric.exportXMLDOM(inject);
+                nodes.forEach(function (elem) {
+                    var name = elem.getAttribute("name");
+                    if (name == "elementTracker" || name == "elementTrackerFull" || name == "elementInitialPosition" || name == "elementFlagMoved") {
+                        elem.setAttribute("interface-name", axisInterface.name);
+                    } else {
+                        inject.removeChild(elem);
+                    }
+                });
+            }
+            this.hasMoved = function () {
+                return metric.wasMoved;
+            }
+            Object.defineProperties(this, {
+                "DOM": {
+                    "value": trackObj
+                },
+                "value": {
+                    "get": function () {
+                        return value;
+                    },
+                    "set": function (v) {
+                        if (v >= 0 && v <= 1) {
+                            value = v;
+                        }
+                        this.resize();
+                        return value;
+                    }
+                },
+                "label": {
+                    "get": function () {
+                        return label;
+                    },
+                    "set": function () {}
+                },
+                "metric": {
+                    "value": metric
+                }
+            });
+        }
+
+        function drawTick(position) {
+            var context = tickCanvas.getContext("2d"),
+                w = tickCanvas.width,
+                h = tickCanvas.height;
+            context.beginPath();
+            context.setLineDash([1, 2]);
+            context.moveTo(position * w, 0);
+            context.lineTo(position * w, h);
+            context.closePath();
+            context.stroke();
+        }
+
+        function clearTicks() {
+            var c = tickCanvas.getContext("2d"),
+                w = tickCanvas.width,
+                h = tickCanvas.height;
+            c.clearRect(0, 0, w, h);
+        }
+
+        function createScaleMarkers(interfaceObject, root, w) {
+            var ticks = interfaceObject.options.findIndex(function (a) {
+                return (a.type == "show" && a.name == "ticks");
+            });
+            ticks = (ticks >= 0);
+            clearTicks();
+            interfaceObject.scales.forEach(function (scaleObj) {
+                var position = Number(scaleObj.position) * 0.01;
+                var pixelPosition = (position * w) + 50;
+                var scaleDOM = document.createElement('span');
+                scaleDOM.className = "ape-marker-text";
+                scaleDOM.textContent = scaleObj.text;
+                scaleDOM.setAttribute('value', position);
+                root.appendChild(scaleDOM);
+                scaleDOM.style.left = Math.floor((pixelPosition - ($(scaleDOM).width() / 2))) + 'px';
+                if (ticks) {
+                    drawTick(position);
+                }
+            }, this);
+        }
+        var sliders = [];
+        var UI = {
+            selected: undefined,
+            startTime: undefined
+        }
+        this.name = interfaceObject.name;
+        var DOMRoot = document.createElement("div");
+        parent.getDOMRoot().appendChild(DOMRoot);
+        DOMRoot.className = "sliderCanvasDiv";
+        DOMRoot.id = "sliderCanvasHolder-" + this.name;
+        var sliders = [];
+
+        var axisTitle = document.createElement("div");
+        axisTitle.className = "pageTitle";
+        axisTitle.align = "center";
+        var titleSpan = document.createElement('span');
+        titleSpan.id = "pageTitle-" + this.name;
+        if (interfaceObject.title !== undefined && typeof interfaceObject.title == "string") {
+            titleSpan.textContent = interfaceObject.title;
+        } else {
+            titleSpan.textContent = "Axis " + String(this.id + 1);
+        }
+        axisTitle.appendChild(titleSpan);
+        DOMRoot.appendChild(axisTitle);
+
+        var 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;
+        })();
+        if (interfaceObject.image !== undefined || page.audioElements.some(function (a) {
+                return a.image !== undefined;
+            })) {
+            DOMRoot.appendChild(imageHolder.root);
+            imageHolder.setImage(interfaceObject.image);
+        }
+
+        // Now create the slider box to hold the fragment sliders
+        var sliderRail = document.createElement("div");
+        sliderRail.id = "sliderrail-" + this.name;
+        sliderRail.className = "slider";
+        sliderRail.align = "left";
+        DOMRoot.appendChild(sliderRail);
+
+        // Canvas for the markers
+        var tickCanvas = document.createElement("canvas");
+        tickCanvas.id = "ticks-" + this.name;
+        tickCanvas.className = "tick-canvas";
+        tickCanvas.height = 150;
+        tickCanvas.width = $(sliderRail).width() - 100;
+        tickCanvas.style.width = ($(sliderRail).width() - 100) + "px";
+        sliderRail.appendChild(tickCanvas);
+
+        // Create the div to hold any scale objects
+        var scale = document.createElement("div");
+        scale.className = "sliderScale";
+        scale.id = "slider-scale-holder-" + this.name;
+        scale.slign = "left";
+        DOMRoot.appendChild(scale);
+        createScaleMarkers(interfaceObject, scale, $(sliderRail).width());
+
+        this.resize = function (event) {
+            var w = $(sliderRail).width();
+            var marginsize = 50;
+            sliders.forEach(function (s) {
+                s.resize();
+            });
+            scale.innerHTML = "";
+            tickCanvas.width = $(sliderRail).width();
+            tickCanvas.style.width = tickCanvas.width + "px";
+            createScaleMarkers(interfaceObject, scale, $(sliderRail).width());
+        }
+        this.playing = function (id) {
+            var node = audioEngineContext.audioObjects.find(function (a) {
+                return a.id == id;
+            });
+            if (node === undefined) {
+                this.imageHolder.setImage(interfaceObject.image || "");
                 return;
             }
-            imageController.img.src = src;
-        };
-        return imageController;
-    })();
+            var imgurl = node.specification.image || interfaceObject.image || "";
+            this.imageHolder.setImage(imgurl);
+        }
+        this.stopped = function () {
+            var imgurl = interfaceObject.image || "";
+            this.imageHolder.setImage(imgurl);
+        }
+        this.addSlider = function (aoi) {
+            var node = new sliderInterface(aoi, this);
+            sliders.push(node);
+            return node;
+        }
+        this.mousedown = function (sliderUI) {
+            UI.selected = sliderUI;
+            UI.startTime = new Date();
+        }
+        this.mouseup = function (event) {
+            var delta = new Date() - UI.startTime;
+            if (delta < 200) {
+                UI.selected.clicked();
+            } else if (event.type == "touchend" || event.type == "touchcancel") {
+                UI.selected.handleEvent(event);
+            }
+            UI.selected = undefined;
+            UI.startTime = undefined;
+        }
+        this.handleEvent = function (event) {
+            function getTargetSlider(target) {
+                return sliders.find(function (a) {
+                    return a.DOM == target;
+                });
+            }
+            var time = audioEngineContext.timer.getTestTime();
+            if (event.preventDefault) {
+                event.preventDefault();
+            }
+            if (event.type == "touchstart") {
+                var selected = getTargetSlider(event.target);
+                if (typeof selected != "object") {
+                    return;
+                }
+                UI.startTime = new Date();
+                UI.selected = selected;
+            }
+            if (UI.selected === undefined) {
+                return;
+            }
+            if (event.type == "mousemove") {
+                var move = event.clientX - 6;
+                var w = $(sliderRail).width();
+                move = Math.max(50, move);
+                move = Math.min(w, move);
+                UI.selected.value = (move / w);
+            } else if (event.type == "touchmove") {
+                if (UI.selected == getTargetSlider(event.target)) {
+                    var move;
+                    if (event.targetTouches) {
+                        move = event.targetTouches[0].clientX - 6;
+                    } else if (event.originalEvent.targetTouches) {
+                        move = event.originalEvent.targetTouches[0].clientX - 6;
+                    } else {
+                        return;
+                    }
+                    var w = $(event.currentTarget).width();
+                    move = Math.max(50, move);
+                    move = Math.min(w, move);
+                    UI.selected.value = (move / w);
+                }
+            } else if (event.type == "touchend" || event.type == "touchcancel") {
+                if (UI.selected == getTargetSlider(event.target)) {
+                    this.mouseup(event);
+                }
+            }
+        }
+        this.checkAllMoved = function () {
+            var notMoved = sliders.filter(function (s) {
+                return !s.hasMoved();
+            });
+            if (notMoved.length !== 0) {
+                var ls = [];
+                notMoved.forEach(function (s) {
+                    ls.push(s.label);
+                })
+                var str = "On axis \"" + interfaceObject.title + "\", ";
+                if (ls.length == 1) {
+                    str += "slider " + ls[0];
+                } else {
+                    str += "sliders " + [ls.slice(0, ls.length - 1).join(", ")].concat(ls[ls.length - 1]).join(" and ");
+                }
+                str += ".";
+                return str;
+            } else {
+                return "";
+            }
+        }
+        this.checkScaleRange = function () {
+            var scaleRange = interfaceObject.options.find(function (a) {
+                return a.name == "scalerange";
+            });
+            if (scaleRange === undefined) {
+                return "";
+            }
+            var scales = {
+                min: scaleRange.min,
+                max: scaleRange.max
+            };
+            var maxSlider = sliders.reduce(function (a, b) {
+                return Math.max(a, b.value);
+            }, 0);
+            var minSlider = sliders.reduce(function (a, b) {
+                return Math.min(a, b.value);
+            }, 100);
+            if (minSlider >= scales.min || maxSlider <= scales.max) {
+                return "On axis \"" + interfaceObject.title + "\", you have not used the required width of the scales";
+            }
+            return "";
+        }
+        sliderRail.addEventListener("mousemove", this);
+        sliderRail.addEventListener("touchstart", this);
+        sliderRail.addEventListener("touchmove", this);
+        sliderRail.addEventListener("touchend", this);
+        sliderRail.addEventListener("touchcancel", this);
+        Object.defineProperties(this, {
+            "sliderRail": {
+                "value": sliderRail
+            }
+        });
+    }
+    this.getDOMRoot = function () {
+        return DOMRoot;
+    }
+    this.getPage = function () {
+        return page;
+    }
+    this.clear = function () {
+        page = undefined;
+        axis = [];
+        AOIs = [];
+        DOMRoot.innerHTML = "";
+    }
+    this.initialisePage = function (page_init) {
+        this.clear();
+        page = page_init;
+        var randomiseAxisOrder;
+        if (page.randomiseAxisOrder !== undefined) {
+            randomiseAxisOrder = page.randomiseAxisOrder;
+        } else {
+            randomiseAxisOrder = page.parent.randomiseAxisOrder;
+        }
+        var commentBoxes = false;
+        // Create each of the interface axis
+        if (randomiseAxisOrder) {
+            page.interfaces = randomiseOrder(page.interfaces);
+        }
+        var interfaceObj = interfaceContext.getCombinedInterfaces(page);
+        interfaceObj.forEach(function (i) {
+            var node = new axisObject(i, this);
+            axis.push(node);
+            i.options.forEach(function (o) {
+                if (o.type == "show" && o.name == "comments") {
+                    commentBoxes = true;
+                }
+            });
+        }, this);
 
-    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") {
-        titleSpan.textContent = interfaceObject.title;
-    } else {
-        titleSpan.textContent = "Axis " + String(this.id + 1);
+        // Create the audioObject interface objects for each aO.
+        page.audioElements.forEach(function (element, index) {
+            var audioObject = audioEngineContext.newTrack(element);
+            if (element.type == 'outside-reference') {
+                // Construct outside reference;
+                var orNode = new outsideReferenceDOM(audioObject, index, document.getElementById("outside-reference-holder"));
+                audioObject.bindInterface(orNode);
+            } else {
+                var aoi = new audioObjectInterface(audioObject, this);
+                AOIs.push(aoi);
+                var label = interfaceContext.getLabel(page.label, index, page.labelStart);
+                axis.forEach(function (a) {
+                    var node = a.addSlider(aoi);
+                    node.setLabel(label);
+                    aoi.addSlider(node);
+                });
+                audioObject.bindInterface(aoi);
+                if (commentBoxes) {
+                    interfaceContext.commentBoxes.createCommentBox(audioObject);
+                }
+            }
+        });
     }
-    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);
+    this.checkAllMoved = function () {
+        var str = "You have not moved the following sliders. "
+        var cont = true;
+        axis.forEach(function (a) {
+            var msg = a.checkAllMoved();
+            if (msg.length > 0) {
+                cont = false;
+                str += msg;
+            }
+        });
+        if (!cont) {
+            interfaceContext.lightbox.post("Error", str);
+            interfaceContext.storeErrorNode(str);
+            console.log(str);
+        }
+        return cont;
     }
-    // Create the slider box to hold the slider elements
-    this.canvas = document.createElement('div');
-    if (this.name !== undefined)
-        this.canvas.id = 'slider-' + this.name;
-    else
-        this.canvas.id = 'slider-' + this.id;
-    this.canvas.setAttribute("interfaceid", this.id);
-    this.canvas.className = 'slider';
-    this.canvas.align = "left";
-    this.canvas.addEventListener('dragover', function (event) {
-        event.preventDefault();
-        event.dataTransfer.effectAllowed = 'none';
-        event.dataTransfer.dropEffect = 'copy';
-        return false;
-    }, false);
-    this.sliderDOM.appendChild(this.canvas);
-
-    // Create the div to hold any scale objects
-    this.scale = document.createElement('div');
-    this.scale.className = 'sliderScale';
-    this.scale.id = 'sliderScaleHolder-' + this.id;
-    this.scale.align = 'left';
-    this.sliderDOM.appendChild(this.scale);
-    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);
-    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);
-        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');
-        trackObj.align = "center";
-        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) {
-            trackObj.setAttribute('interface-name', this.name);
-        } else {
-            trackObj.setAttribute('interface-name', this.id);
+    this.checkScaleRange = function () {
+        var str = "";
+        var cont = true;
+        axis.forEach(function (a) {
+            var msg = a.checkScaleRange();
+            if (msg.length > 0) {
+                cont = false;
+                str += msg;
+            }
+        });
+        if (!cont) {
+            interfaceContext.lightbox.post("Error", str);
+            interfaceContext.storeErrorNode(str);
+            console.log(str);
         }
-        var offset = 50;
-        // Distribute it randomnly
-        var w = window.innerWidth - (offset + 8) * 2;
-        w = Math.random() * w;
-        w = Math.floor(w + (offset + 8));
-        trackObj.style.left = w + 'px';
-        this.canvas.appendChild(trackObj);
-        this.sliders.push(trackObj);
-        this.metrics.push(new metricTracker(this));
-        var labelHolder = document.createElement("span");
-        labelHolder.textContent = label;
-        trackObj.appendChild(labelHolder);
-        var rate = convSliderPosToRate(trackObj);
-        this.metrics[this.metrics.length - 1].initialise(rate);
-        trackObj.setAttribute("slider-value", rate);
-        return trackObj;
-    };
-
-    this.resize = function (event) {
-        var sliderDiv = this.canvas;
-        var sliderScaleDiv = this.scale;
-        var width = $(sliderDiv).width();
-        var marginsize = 50;
-        // Move sliders into new position
-        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 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);
+        return cont;
     }
-}
-
-function sliderObject(audioObject, interfaceObjects, index) {
-    // Create a new slider object;
-    this.parent = audioObject;
-    this.trackSliderObjects = [];
-    this.label = interfaceContext.getLabel(audioObject.specification.parent.label, index, audioObject.specification.parent.labelStart);
-    this.playing = false;
-    for (var i = 0; i < interfaceContext.interfaceSliders.length; i++) {
-        var trackObj = interfaceContext.interfaceSliders[i].createSliderObject(audioObject, this.label);
-        this.trackSliderObjects.push(trackObj);
-    }
-
-    // Onclick, switch playback to that track
-
-    this.enable = function () {
-        if (this.parent.state == 1) {
-            $(this.trackSliderObjects).each(function (i, trackObj) {
-                $(trackObj).removeClass('track-slider-disabled');
+    this.pageXMLSave = function (store, pageSpecification) {
+        if (axis.length > 1) {
+            AOIs.forEach(function (ao) {
+                ao.pageXMLSave(store);
             });
         }
-    };
-    this.updateLoading = function (progress) {
-        if (progress != 100) {
-            progress = String(progress);
-            progress = progress.split('.')[0];
-            this.trackSliderObjects[0].children[0].textContent = progress + '%';
-        } else {
-            this.trackSliderObjects[0].children[0].textContent = this.label;
-        }
-    };
-    this.startPlayback = function () {
-        $('.track-slider').removeClass('track-slider-playing');
-        var name = ".track-slider-" + this.parent.id;
-        $(name).addClass('track-slider-playing');
-        interfaceContext.commentBoxes.highlightById(audioObject.id);
-        $('.outside-reference').removeClass('track-slider-playing');
-        this.playing = true;
-
-        if (this.parent.specification.parent.playOne || specification.playOne) {
-            $('.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');
-            $('.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) {
-        // Called by the audioObject holding this element. Must be present
-        var obj = [];
-        $(this.trackSliderObjects).each(function (i, trackObj) {
-            var node = storage.document.createElement('value');
-            if (trackObj.getAttribute("interface-name") !== "null") {
-                node.setAttribute("interface-name", trackObj.getAttribute("interface-name"));
-            }
-            node.textContent = convSliderPosToRate(trackObj);
-            obj.push(node);
+    }
+    this.resize = function (event) {
+        axis.forEach(function (a) {
+            a.resize(event);
         });
-
-        return obj;
-    };
-    this.getValue = function () {
-        return convSliderPosToRate(this.trackSliderObjects[0]);
-    };
-    this.getPresentedId = function () {
-        return this.label;
-    };
-    this.canMove = function () {
-        return true;
-    };
-    this.error = function () {
-        // audioObject has an error!!
-        this.playback.textContent = "Error";
-        $(this.playback).addClass("error-colour");
-    };
+    }
 }
 
 function outsideReferenceDOM(audioObject, index, inject) {
@@ -753,6 +845,9 @@
     if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
 
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
@@ -820,9 +915,7 @@
     // MANDATORY FUNCTION
 
     // Resize the slider objects
-    for (var i = 0; i < interfaceContext.interfaceSliders.length; i++) {
-        interfaceContext.interfaceSliders[i].resize(event);
-    }
+    window.module.resize(event);
 }
 
 function pageXMLSave(store, pageSpecification) {
@@ -833,32 +926,5 @@
     // pageSpecification is the current page node configuration
     // To create new XML nodes, use storage.document.createElement();
 
-    if (interfaceContext.interfaceSliders.length == 1) {
-        // If there is only one axis, there only needs to be one metric return
-        return;
-    }
-    var audioelements = store.getElementsByTagName("audioelement");
-    for (var i = 0; i < audioelements.length; i++) {
-        // Have to append the metric specific nodes
-        if (pageSpecification.outsideReference === undefined || pageSpecification.outsideReference.id != audioelements[i].id) {
-            var inject = audioelements[i].getElementsByTagName("metric");
-            if (inject.length === 0) {
-                inject = storage.document.createElement("metric");
-            } else {
-                inject = inject[0];
-            }
-            for (var k = 0; k < interfaceContext.interfaceSliders.length; k++) {
-                var mrnodes = interfaceContext.interfaceSliders[k].metrics[i].exportXMLDOM(inject);
-                for (var j = 0; j < mrnodes.length; j++) {
-                    var name = mrnodes[j].getAttribute("name");
-                    if (name == "elementTracker" || name == "elementTrackerFull" || name == "elementInitialPosition" || name == "elementFlagMoved") {
-                        if (interfaceContext.interfaceSliders[k].name !== null) {
-                            mrnodes[j].setAttribute("interface-name", interfaceContext.interfaceSliders[k].name);
-                        }
-                        mrnodes[j].setAttribute("interface-id", k);
-                    }
-                }
-            }
-        }
-    }
+    module.pageXMLSave(store, pageSpecification);
 }
--- a/interfaces/discrete.css	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/discrete.css	Wed Nov 22 10:10:44 2017 +0000
@@ -20,79 +20,47 @@
     min-width: 20px;
     background-color: #ddd
 }
-div#slider-holder {
-    height: inherit;
-    position: absolute;
-    left: 0px;
-    z-index: 3;
-    margin-top: 25px;
+div#slider-box {
+    width: 75%;
+    height: auto;
+    margin: auto;
+    padding-bottom: 20px;
 }
-div#scale-holder {
-    position: absolute;
-    left: 0px;
-    z-index: 2;
+div#slider-grid {
+    display: grid;
+    grid-template-columns: 1fr;
+    grid-row-gap: 10px;
 }
 div#scale-text-holder {
-    position: relative;
-    float: left;
+    display: grid;
+    grid-template-rows: 1fr;
+    min-height: 25px;
+    text-align: center;
+}
+div.discrete-row {
+    display: grid;
+    grid-template-rows: 1fr;
+    padding: 10px;
+    border: 1px solid black;
+    height: 50px;
+    line-height: 50px;
+}
+button.discrete-button {
+    width: 100px;
+}
+div.discrete-label {
+    width: 100px;
+    text-align: center;
 }
 div.scale-text {
-    position: absolute;
-    font-size: 1.2em;
-}
-canvas#scale-canvas {
-    position: relative;
-    float: left;
-}
-div.track-slider {
-    float: left;
-    height: 30px;
-    border: solid;
-    border-width: 1px;
-    border-color: black;
-    padding: 2px;
-    margin-left: 94px;
-    margin-bottom: 30px;
-}
-div.track-slider-range {
-    float: left;
-    height: 100%;
-    margin: 0px 50px;
     position: relative;
 }
-div.track-slider-title {
-    float: left;
-    padding-top: 5px;
-    width: 100px;
+div.scale-text > span {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    left: 0;
 }
-button.track-slider-button {
-    float: left;
-    width: 100px;
-    height: 30px;
+div.discrete-row-playing {
+    background-color: rgba(255, 201, 201, 0.5);
 }
-input.track-radio {
-    position: absolute;
-    margin: 9px 0px;
-}
-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;
-}
-div#page-count {
-    float: left;
-    margin: 0px 5px;
-}
-div#master-volume-holder {
-    position: absolute;
-    top: 10px;
-    left: 120px;
-}
--- a/interfaces/discrete.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/discrete.js	Wed Nov 22 10:10:44 2017 +0000
@@ -1,4 +1,8 @@
-/* globals interfaceContext, document, window, $, specification, audioEngineContext, console, window, testState, storage */
+/**
+ * WAET Blank Template
+ * Use this to start building your custom interface
+ */
+
 // Once this is loaded and parsed, begin execution
 loadInterface();
 
@@ -56,44 +60,41 @@
             console.log('Stopped at ' + time); // DEBUG/SAFETY
         }
     };
-
-    // Create outside reference holder
-    var outsideRef = document.createElement("div");
-    outsideRef.id = "outside-reference-holder";
-
     // Create Submit (save) button
     var submit = document.createElement("button");
     submit.innerHTML = 'Next';
     submit.onclick = buttonSubmitClick;
     submit.id = 'submit-button';
     submit.style.float = 'left';
+
+    // Create the sort button
+    var sort = document.createElement("button");
+    sort.id = "sort-fragments";
+    sort.textContent = "Sort";
+    sort.style.display = "inline-block";
+    sort.style.visibility = "hidden";
+    sort.onclick = buttonSortFragmentClick;
+
     // Append the interface buttons into the interfaceButtons object.
     interfaceButtons.appendChild(playback);
     interfaceButtons.appendChild(submit);
+    interfaceButtons.appendChild(sort);
 
-    // Create a slider box
-    var sliderBox = document.createElement('div');
-    sliderBox.style.width = "100%";
-    sliderBox.style.height = window.innerHeight - 200 + 12 + 'px';
-    sliderBox.style.marginBottom = '10px';
-    sliderBox.id = 'slider';
-    var scaleHolder = document.createElement('div');
-    scaleHolder.id = "scale-holder";
-    scaleHolder.style.marginLeft = "107px";
-    sliderBox.appendChild(scaleHolder);
+
+    // Create outside reference holder
+    var outsideRef = document.createElement("div");
+    outsideRef.id = "outside-reference-holder";
+
+    // Create a holder for the slider rows
+    var sliderBox = document.createElement("div");
+    sliderBox.id = 'slider-box';
+    var sliderGrid = document.createElement("div");
+    sliderGrid.id = "slider-grid";
+    sliderBox.appendChild(sliderGrid);
     var scaleText = document.createElement('div');
     scaleText.id = "scale-text-holder";
-    scaleText.style.height = "25px";
-    scaleText.style.width = "100%";
-    scaleHolder.appendChild(scaleText);
-    var scaleCanvas = document.createElement('canvas');
-    scaleCanvas.id = "scale-canvas";
-    scaleCanvas.style.marginLeft = "150px";
-    scaleHolder.appendChild(scaleCanvas);
-    var sliderObjectHolder = document.createElement('div');
-    sliderObjectHolder.id = 'slider-holder';
-    sliderObjectHolder.align = "center";
-    sliderBox.appendChild(sliderObjectHolder);
+    sliderGrid.appendChild(scaleText);
+
 
     // Global parent for the comment boxes on the page
     var feedbackHolder = document.createElement('div');
@@ -114,15 +115,20 @@
     // 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');
+    var sliderBox = document.getElementById('slider-box');
+    var sliderGrid = document.getElementById("slider-grid");
+    var scaleTextHolder = document.getElementById("scale-text-holder");
+    var interfaceObj = interfaceContext.getCombinedInterfaces(page);
+    var commentBoxPrefix = "Comment on track";
+    var loopPlayback = page.loop;
     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");
     }
@@ -132,7 +138,7 @@
     if (typeof page.title == "string" && page.title.length > 0) {
         document.getElementById("test-title").textContent = page.title;
     }
-
+    // Set the axis title
     if (interfaceObj.title !== null) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
@@ -147,15 +153,67 @@
     // Delete outside reference
     document.getElementById("outside-reference-holder").innerHTML = "";
 
-    var sliderBox = document.getElementById('slider-holder');
-    sliderBox.innerHTML = "";
-
-    var commentBoxPrefix = "Comment on track";
+    // Get the comment box prefix
     if (interfaceObj.commentBoxPrefix !== undefined) {
         commentBoxPrefix = interfaceObj.commentBoxPrefix;
     }
-    var loopPlayback = page.loop;
 
+    // Populate the comment questions
+    $(page.commentQuestions).each(function (index, element) {
+        var node = interfaceContext.createCommentQuestion(element);
+        feedbackHolder.appendChild(node.holder);
+    });
+
+    // Configure the grid
+    var numRows = page.audioElements.filter(function (a) {
+        return (a.type !== "outside-reference");
+    }).length;
+    var numColumns = page.interfaces[0].scales.length;
+    sliderGrid.style.gridTemplateRows = "50px repeat(" + numRows + ", 72px)";
+    scaleTextHolder.style.gridTemplateColumns = "100px repeat(" + numColumns + ", 1fr) 100px";
+    page.interfaces[0].scales.sort(function (a, b) {
+        if (a.position > b.position) {
+            return 1;
+        } else if (a.position < b.position) {
+            return -1;
+        }
+        return 0;
+    }).forEach(function (a, i) {
+        var h = document.createElement("div");
+        var text = document.createElement("span");
+        h.className = "scale-text";
+        h.style.gridColumn = String(i + 2) + "/" + String(i + 3);
+        text.textContent = a.text;
+        h.appendChild(text);
+        scaleTextHolder.appendChild(h);
+    })
+
+    // Find all the audioElements from the audioHolder
+    var index = 0;
+    var labelType = page.label;
+    if (labelType == "default") {
+        labelType = "number";
+    }
+    $(page.audioElements).each(function (pageIndex, element) {
+        // Find URL of track
+        // In this jQuery loop, variable 'this' holds the current audioElement.
+
+        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 discreteObject(audioObject, label);
+            sliderGrid.appendChild(sliderObj.DOMRoot);
+            audioObject.bindInterface(sliderObj);
+            interfaceContext.commentBoxes.createCommentBox(audioObject);
+            index += 1;
+        }
+
+    });
     interfaceObj.options.forEach(function (option) {
         if (option.type == "show") {
             switch (option.name) {
@@ -187,204 +245,109 @@
                 case "comments":
                     interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true);
                     break;
+                case "fragmentSort":
+                    var button = document.getElementById('sort-fragments');
+                    button.style.visibility = "visible";
+                    break;
             }
         }
     });
-
-    // Find all the audioElements from the audioHolder
-    var index = 0;
-    var interfaceScales = page.interfaces[0].scales;
-    var labelType = page.label;
-    if (labelType == "default") {
-        labelType = "number";
-    }
-    $(page.audioElements).each(function (pageIndex, element) {
-        // Find URL of track
-        // In this jQuery loop, variable 'this' holds the current audioElement.
-
-        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 discreteObject(audioObject, label, interfaceScales);
-            sliderBox.appendChild(sliderObj.holder);
-            audioObject.bindInterface(sliderObj);
-            interfaceContext.commentBoxes.createCommentBox(audioObject);
-            index += 1;
-        }
-
-    });
-
-    $(page.commentQuestions).each(function (index, element) {
-        var node = interfaceContext.createCommentQuestion(element);
-        feedbackHolder.appendChild(node.holder);
-    });
-
     // Auto-align
     resizeWindow(null);
 }
 
-function discreteObject(audioObject, label, interfaceScales) {
+function discreteObject(audioObject, label) {
     // 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) {
-        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!");
-        var numOptions = 5;
-    }
-    this.parent = audioObject;
+    var playing = false;
 
-    this.holder = document.createElement('div');
-    this.title = document.createElement('div');
-    this.discreteHolder = document.createElement('div');
-    this.discretes = [];
-    this.play = document.createElement('button');
-
-    this.holder.className = 'track-slider';
-    this.holder.style.width = window.innerWidth - 200 + 'px';
-    this.holder.appendChild(this.title);
-    this.holder.appendChild(this.discreteHolder);
-    this.holder.appendChild(this.play);
-    this.holder.setAttribute('trackIndex', audioObject.id);
-    this.title.textContent = label;
-    this.title.className = 'track-slider-title';
-
-    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);
+    function buttonClicked(event) {
+        if (!playing) {
+            audioEngineContext.play(audioObject.id);
+        } else {
+            audioEngineContext.stop();
         }
     };
-    for (var i = 0; i < interfaceScales.length; i++) {
-        var node = document.createElement('input');
-        node.setAttribute('type', 'radio');
-        node.className = 'track-radio';
-        node.disabled = true;
-        node.setAttribute('position', interfaceScales[i].position);
-        node.setAttribute('name', audioObject.specification.id);
-        node.setAttribute('id', audioObject.specification.id + '-' + String(i));
-        this.discretes.push(node);
-        this.discreteHolder.appendChild(node);
-        node.addEventListener("click", this);
+
+    function radioSelected(event) {
+        var time = audioEngineContext.timer.getTestTime();
+        audioObject.metric.moved(time, event.currentTarget.value);
+        console.log("slider " + audioObject.id + " moved to " + event.currentTarget.value + "(" + time + ")");
+    };
+
+    var root = document.createElement("div"),
+        labelHolder = document.createElement("div"),
+        button = document.createElement("button");
+    root.className = "discrete-row";
+    labelHolder.className = "discrete-label";
+    button.className = "discrete-button";
+    root.appendChild(labelHolder);
+
+    var labelSpan = document.createElement("span");
+    labelHolder.appendChild(labelSpan);
+    labelSpan.textContent = label;
+    button.textContent = "Listen";
+    button.disabled = "true";
+    button.addEventListener("click", this);
+
+    var numScales = audioObject.specification.parent.interfaces[0].scales.length;
+    root.style.gridTemplateColumns = "100px repeat(" + numScales + ", 1fr) 100px";
+    for (var n = 0; n < numScales; n++) {
+        var input = document.createElement("input");
+        input.type = "radio";
+        input.disabled = "true";
+        input.value = n / (numScales - 1);
+        input.addEventListener("click", this);
+        input.name = audioObject.specification.id;
+        root.appendChild(input);
     }
-
-    this.play.className = 'track-slider-button';
-    this.play.textContent = "Loading...";
-    this.play.value = audioObject.id;
-    this.play.disabled = true;
-    this.play.setAttribute("playstate", "ready");
-    this.play.onclick = function (event) {
-        var id = Number(event.currentTarget.value);
-        //audioEngineContext.metric.sliderPlayed(id);
-        if (event.currentTarget.getAttribute("playstate") == "ready")
-            audioEngineContext.play(id);
-        else if (event.currentTarget.getAttribute("playstate") == "playing")
-            audioEngineContext.stop();
-    };
-    this.resize = function (event) {
-        this.holder.style.width = window.innerWidth - 200 + 'px';
-        this.discreteHolder.style.width = window.innerWidth - 500 + 'px';
-        //text.style.left = (posPix+150-($(text).width()/2)) +'px';
-        for (var i = 0; i < this.discretes.length; i++) {
-            var width = $(this.discreteHolder).width() - 20;
-            var node = this.discretes[i];
-            var nodeW = $(node).width();
-            var position = node.getAttribute('position');
-            var posPix = Math.round(width * (position / 100.0));
-            node.style.left = (posPix + 10 - (nodeW / 2)) + 'px';
+    root.appendChild(button);
+    this.handleEvent = function (event) {
+        if (event.currentTarget === button) {
+            buttonClicked(event);
+        } else if (event.currentTarget.type === "radio") {
+            radioSelected(event);
         }
-    };
+    }
     this.enable = function () {
         // This is used to tell the interface object that playback of this node is ready
-        this.play.disabled = false;
-        this.play.textContent = "Play";
-        $(this.slider).removeClass('track-slider-disabled');
-        this.discretes.forEach(function (elem) {
-            elem.disabled = false;
-        });
+        button.disabled = "";
+        var a = root.querySelectorAll("input[type=\"radio\"]");
+        for (var n = 0; n < a.length; n++) {
+            a[n].disabled = false;
+        }
+        button.textContent = "Listen";
     };
     this.updateLoading = function (progress) {
         // progress is a value from 0 to 100 indicating the current download state of media files
-        if (progress != 100) {
-            progress = String(progress);
-            progress = progress.split('.')[0];
-            this.play.textContent = progress + '%';
-        } else {
-            this.play.textContent = "Play";
-        }
+        button.textContent = progress + "%";
     };
-
     this.startPlayback = function () {
-        // Called by audioObject when playback begins
-        this.play.setAttribute("playstate", "playing");
-        $(".track-slider").removeClass('track-slider-playing');
-        $(this.holder).addClass('track-slider-playing');
-        var outsideReference = document.getElementById('outside-reference');
-        this.play.textContent = "Listening";
-        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);
-        }
+        // Called when playback has begun
+        playing = true;
+        $(root).addClass("discrete-row-playing");
+        button.textContent = "Stop";
     };
     this.stopPlayback = function () {
-        // Called by audioObject when playback stops
-        if (this.play.getAttribute("playstate") == "playing") {
-            this.play.setAttribute("playstate", "ready");
-            $(this.holder).removeClass('track-slider-playing');
-            $('.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("");
+        // Called when playback has stopped. This gets called even if playback never started!
+        playing = false;
+        $(root).removeClass("discrete-row-playing");
+        button.textContent = "Listen";
+    };
+    this.getValue = function () {
+        // Return the current value of the object. If there is no value, return 0
+        var a = root.querySelectorAll("input[type=\"radio\"]");
+        for (var n = 0; n < a.length; n++) {
+            if (a[n].checked) {
+                return Number(a[n].value);
             }
         }
-    };
-
-    this.getValue = function () {
-        // Return the current value of the object. If there is no value, return -1
-        var checkedElement = this.discretes.find(function (elem) {
-            return elem.checked;
-        });
-        if (checkedElement === undefined) {
-            return -1;
-        }
-        return checkedElement.getAttribute("position") / 100.0;
+        return -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 this.title.textContent;
+        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.
@@ -399,75 +362,36 @@
         var node = storage.document.createElement('value');
         node.textContent = this.getValue();
         return node;
+
     };
     this.error = function () {
-        // audioObject has an error!!
-        this.playback.textContent = "Error";
-        $(this.playback).addClass("error-colour");
+        // If there is an error with the audioObject, this will be called to indicate a failure
     };
-}
+    Object.defineProperties(this, {
+        "DOMRoot": {
+            "value": root
+        }
+    });
+};
 
 function resizeWindow(event) {
     // Called on every window resize event, use this to scale your page properly
-    var numObj = document.getElementsByClassName('track-slider').length;
-    var totalHeight = (numObj * 66) - 30;
-    document.getElementById('scale-holder').style.width = window.innerWidth - 220 + 'px';
-    // Cheers edge for making me delete a canvas every resize.
-    var canvas = document.getElementById('scale-canvas');
-    var new_canvas = document.createElement("canvas");
-    new_canvas.id = 'scale-canvas';
-    new_canvas.style.marginLeft = "150px";
-    canvas.parentElement.appendChild(new_canvas);
-    canvas.parentElement.removeChild(canvas);
-    new_canvas.width = window.innerWidth - 520;
-    new_canvas.height = totalHeight;
-    for (var i in audioEngineContext.audioObjects) {
-        if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference') {
-            audioEngineContext.audioObjects[i].interfaceDOM.resize(event);
-        }
-    }
-    document.getElementById('slider-holder').style.height = totalHeight + 'px';
-    document.getElementById('slider').style.height = totalHeight + 70 + 'px';
-    drawScale();
 }
 
-function drawScale() {
-    var interfaceObj = testState.currentStateMap.interfaces[0];
-    var scales = testState.currentStateMap.interfaces[0].scales;
-    scales = scales.sort(function (a, b) {
-        return a.position - b.position;
+function buttonSortFragmentClick() {
+    var sortIndex = interfaceContext.sortFragmentsByScore();
+    var sliderBox = document.getElementById("slider-holder");
+    var nodes = audioEngineContext.audioObjects.filter(function (ao) {
+        return ao.specification.type !== "outside-reference";
     });
-    var canvas = document.getElementById('scale-canvas');
-    var ctx = canvas.getContext("2d");
-    var height = canvas.height;
-    var width = canvas.width;
-    var textHolder = document.getElementById('scale-text-holder');
-    textHolder.innerHTML = "";
-    ctx.fillStyle = "#000000";
-    ctx.setLineDash([1, 4]);
-    scales.forEach(function (scale) {
-        var posPercent = scale.position / 100.0;
-        var posPix = Math.round(width * posPercent);
-        if (posPix <= 0) {
-            posPix = 1;
-        }
-        if (posPix >= width) {
-            posPix = width - 1;
-        }
-        ctx.moveTo(posPix, 0);
-        ctx.lineTo(posPix, height);
-        ctx.stroke();
-
-        var text = document.createElement('div');
-        text.align = "center";
-        var textC = document.createElement('span');
-        textC.textContent = scale.text;
-        text.appendChild(textC);
-        text.className = "scale-text";
-        textHolder.appendChild(text);
-        text.style.width = $(text.children[0]).width() + 'px';
-        text.style.left = (posPix + 150 - ($(text).width() / 2)) + 'px';
+    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);
+    }
 }
 
 function buttonSubmitClick() // TODO: Only when all songs have been played!
@@ -485,9 +409,12 @@
     if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
 
     for (var i = 0; i < checks.length; i++) {
-        var checkState;
+        var checkState = true;
         if (checks[i].type == 'check') {
             switch (checks[i].name) {
                 case 'fragmentPlayed':
@@ -510,16 +437,15 @@
                 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;
-            }
         }
-        if (!canContinue) {
+        if (checkState === false) {
+            canContinue = false;
             break;
         }
     }
--- a/interfaces/horizontal-sliders.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/horizontal-sliders.js	Wed Nov 22 10:10:44 2017 +0000
@@ -62,9 +62,20 @@
     submit.onclick = buttonSubmitClick;
     submit.id = 'submit-button';
     submit.style.float = 'left';
+
+    // Create the sort button
+    var sort = document.createElement("button");
+    sort.id = "sort-fragments";
+    sort.textContent = "Sort";
+    sort.style.display = "inline-block";
+    sort.style.visibility = "hidden";
+    sort.onclick = buttonSortFragmentClick;
+
     // Append the interface buttons into the interfaceButtons object.
     interfaceButtons.appendChild(playback);
     interfaceButtons.appendChild(submit);
+    interfaceButtons.appendChild(sort);
+
 
     // Create outside reference holder
     var outsideRef = document.createElement("div");
@@ -226,6 +237,10 @@
                 case "comments":
                     interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true);
                     break;
+                case "fragmentSort":
+                    var button = document.getElementById('sort-fragments');
+                    button.style.visibility = "visible";
+                    break;
             }
         }
     });
@@ -383,6 +398,14 @@
 function drawScale() {
     var interfaceObj = testState.currentStateMap.interfaces[0];
     var scales = testState.currentStateMap.interfaces[0].scales;
+    var ticks = specification.interfaces.options.concat(interfaceObj.options).find(function (a) {
+        return (a.type == "show" && a.name == "ticks");
+    });
+    if (ticks !== undefined) {
+        ticks = true;
+    } else {
+        ticks = false;
+    }
     scales = scales.sort(function (a, b) {
         return a.position - b.position;
     });
@@ -403,9 +426,11 @@
         if (posPix >= width) {
             posPix = width - 1;
         }
-        ctx.moveTo(posPix, 0);
-        ctx.lineTo(posPix, height);
-        ctx.stroke();
+        if (ticks) {
+            ctx.moveTo(posPix, 0);
+            ctx.lineTo(posPix, height);
+            ctx.stroke();
+        }
 
         var text = document.createElement('div');
         text.align = "center";
@@ -419,6 +444,22 @@
     });
 }
 
+function buttonSortFragmentClick() {
+    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);
+    }
+}
+
 function buttonSubmitClick() // TODO: Only when all songs have been played!
 {
     var checks = testState.currentStateMap.interfaces[0].options,
@@ -434,6 +475,9 @@
     if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
 
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
--- a/interfaces/mushra.css	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/mushra.css	Wed Nov 22 10:10:44 2017 +0000
@@ -39,6 +39,8 @@
 }
 div.scale-text {
     position: absolute;
+    text-align: right;
+    min-width: 100px;
 }
 canvas#scale-canvas {
     position: relative;
--- a/interfaces/mushra.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/mushra.js	Wed Nov 22 10:10:44 2017 +0000
@@ -67,9 +67,20 @@
     submit.onclick = buttonSubmitClick;
     submit.id = 'submit-button';
     submit.style.display = 'inline-block';
+
+    // Create the sort button
+    var sort = document.createElement("button");
+    sort.id = "sort-fragments";
+    sort.textContent = "Sort";
+    sort.style.display = "inline-block";
+    sort.style.visibility = "hidden";
+    sort.onclick = buttonSortFragmentClick;
+
     // Append the interface buttons into the interfaceButtons object.
     interfaceButtons.appendChild(playback);
     interfaceButtons.appendChild(submit);
+    interfaceButtons.appendChild(sort);
+
 
     // Create outside reference holder
     var outsideRef = document.createElement("div");
@@ -206,6 +217,8 @@
 
 
     var interfaceOptions = interfaceObj.options;
+    var sortButton = document.getElementById("sort-fragments");
+    sortButton.style.visibility = "hidden";
     interfaceOptions.forEach(function (option) {
         if (option.type == "show") {
             switch (option.name) {
@@ -239,31 +252,8 @@
                     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);
-                            }
-                        };
-                    }
+                    var button = document.getElementById('sort-fragments');
+                    button.style.visibility = "visible";
                     break;
             }
         }
@@ -480,6 +470,14 @@
 function drawScale() {
     var interfaceObj = testState.currentStateMap.interfaces[0];
     var scales = testState.currentStateMap.interfaces[0].scales;
+    var ticks = specification.interfaces.options.concat(interfaceObj.options).find(function (a) {
+        return (a.type == "show" && a.name == "ticks");
+    });
+    if (ticks !== undefined) {
+        ticks = true;
+    } else {
+        ticks = false;
+    }
     scales = scales.sort(function (a, b) {
         return a.position - b.position;
     });
@@ -495,11 +493,13 @@
     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";
-        ctx.setLineDash([1, 2]);
-        ctx.moveTo(0, posPix);
-        ctx.lineTo(width, posPix);
-        ctx.stroke();
+        if (ticks) {
+            ctx.fillStyle = "#000000";
+            ctx.setLineDash([1, 2]);
+            ctx.moveTo(0, posPix);
+            ctx.lineTo(width, posPix);
+            ctx.stroke();
+        }
         var text = document.createElement('div');
         text.align = "right";
         var textC = document.createElement('span');
@@ -508,11 +508,26 @@
         text.className = "scale-text";
         textHolder.appendChild(text);
         text.style.top = (posPix - 9) + 'px';
-        text.style.left = 100 - ($(text).width() + 3) + 'px';
         lastHeight = posPix;
     });
 }
 
+function buttonSortFragmentClick() {
+    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);
+    }
+}
+
 function buttonSubmitClick() // TODO: Only when all songs have been played!
 {
     var checks = testState.currentStateMap.interfaces[0].options,
@@ -528,6 +543,9 @@
     if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
 
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
--- a/interfaces/timeline.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/interfaces/timeline.js	Wed Nov 22 10:10:44 2017 +0000
@@ -505,6 +505,9 @@
     if (interfaceContext.checkFragmentMinPlays() === false) {
         return;
     }
+    if (interfaceContext.checkCommentQuestions() === false) {
+        return;
+    }
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
         if (checks[i].type == 'check') {
--- a/js/core.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/js/core.js	Wed Nov 22 10:10:44 2017 +0000
@@ -2220,10 +2220,19 @@
     };
 
     this.moved = function (time, position) {
+        var last;
         if (time > 0) {
             this.wasMoved = true;
         }
-        this.movementTracker[this.movementTracker.length] = [time, position];
+        // Get the last entry
+        if (this.movementTracker.length > 0) {
+            last = this.movementTracker[this.movementTracker.length - 1];
+        } else {
+            last = -1;
+        }
+        if (position != last[1]) {
+            this.movementTracker[this.movementTracker.length] = [time, position];
+        }
     };
 
     this.startListening = function (time) {
@@ -2711,6 +2720,12 @@
             this.textArea.style.width = boxwidth - 6 + "px";
         };
         this.resize();
+        this.check = function () {
+            if (this.specification.mandatory && this.textArea.value.length == 0) {
+                return false;
+            }
+            return true;
+        }
     };
 
     this.radioBox = function (commentQuestion) {
@@ -2785,6 +2800,15 @@
             }
             this.holder.style.width = boxwidth + "px";
         };
+        this.check = function () {
+            var anyChecked = this.options.some(function (a) {
+                return a.checked;
+            });
+            if (this.specification.mandatory && anyChecked == false) {
+                return false;
+            }
+            return true;
+        }
         this.resize();
     };
 
@@ -2851,6 +2875,15 @@
             }
             this.holder.style.width = boxwidth + "px";
         };
+        this.check = function () {
+            var anyChecked = this.options.some(function (a) {
+                return a.checked;
+            });
+            if (this.specification.mandatory && anyChecked == false) {
+                return false;
+            }
+            return true;
+        };
         this.resize();
     };
 
@@ -2908,6 +2941,9 @@
             this.holder.style.width = boxwidth + "px";
             this.slider.style.width = boxwidth - 24 + "px";
         };
+        this.check = function () {
+            return true;
+        }
         this.resize();
     };
 
@@ -2930,6 +2966,19 @@
         this.commentQuestions = [];
     };
 
+    this.checkCommentQuestions = function () {
+        var errored = this.commentQuestions.reduce(function (a, cq) {
+            if (cq.check() == false) {
+                a.push(cq);
+            }
+            return a;
+        }, []);
+        if (errored.length == 0) {
+            return true;
+        }
+        interfaceContext.lightbox.post("Message", "Not all the mandatory comment boxes below have been filled.");
+    }
+
     this.outsideReferenceDOM = function (audioObject, index, inject) {
         this.parent = audioObject;
         this.outsideReferenceHolder = document.createElement('button');
--- a/js/specification.js	Wed Nov 22 10:08:26 2017 +0000
+++ b/js/specification.js	Wed Nov 22 10:10:44 2017 +0000
@@ -19,6 +19,7 @@
     this.playOne = undefined;
     this.minNumberPlays = undefined;
     this.maxNumberPlays = undefined;
+    this.randomiseAxisOrder = undefined;
 
     // nodes
     this.metrics = new metricNode();
@@ -222,6 +223,7 @@
         this.location = undefined;
         this.options = [];
         this.parent = undefined;
+        this.showBackButton = true;
         this.specification = specification;
 
         this.addOption = function () {
@@ -409,6 +411,12 @@
             } else if (this.location == 'after') {
                 this.location = 'post';
             }
+            this.showBackButton = xml.getAttribute("showBackButton");
+            if (this.showBackButton == "false") {
+                this.showBackButton = false;
+            } else {
+                this.showBackButton = true;
+            }
             var child = xml.firstElementChild;
             while (child) {
                 var node = new this.OptionNode(this.specification);
@@ -424,6 +432,7 @@
         this.encode = function (doc) {
             var node = doc.createElement('survey');
             node.setAttribute('location', this.location);
+            node.setAttribute('showBackButton', this.showBackButton);
             for (var i = 0; i < this.options.length; i++) {
                 node.appendChild(this.options[i].exportXML(doc));
             }
@@ -572,6 +581,7 @@
         this.commentBoxPrefix = "Comment on track";
         this.minNumberPlays = undefined;
         this.maxNumberPlays = undefined;
+        this.randomiseAxisOrder = undefined;
         this.audioElements = [];
         this.commentQuestions = [];
         this.schema = schemaRoot.querySelector("[name=page]");
@@ -696,8 +706,12 @@
                 AHNode.appendChild(this.audioElements[i].encode(root));
             }
             // Create <CommentQuestion>
-            for (i = 0; i < this.commentQuestions.length; i++) {
-                AHNode.appendChild(this.commentQuestions[i].encode(root));
+            if (this.commentQuestions.length > 0) {
+                var node = root.createElement("commentquestions");
+                for (i = 0; i < this.commentQuestions.length; i++) {
+                    node.appendChild(this.commentQuestions[i].encode(root));
+                }
+                AHNode.appendChild(node);
             }
 
             AHNode.appendChild(this.preTest.encode(root));
@@ -710,10 +724,12 @@
             this.name = undefined;
             this.type = undefined;
             this.statement = undefined;
+            this.mandatory = undefined;
             this.schema = schemaRoot.querySelector('[name=commentquestion]');
             this.decode = function (parent, xml) {
                 this.id = xml.id;
                 this.name = xml.getAttribute('name');
+                this.mandatory = xml.getAttribute("mandatory") == "true";
                 if (this.name === null) {
                     this.name = undefined;
                 }
@@ -794,6 +810,7 @@
                         throw ("Unknown type " + this.type);
                 }
                 node.id = this.id;
+                node.setAttribute("mandatory", this.mandatory);
                 node.setAttribute("type", this.type);
                 if (this.name !== undefined) {
                     node.setAttribute("name", this.name);
--- a/php/requestKey.php	Wed Nov 22 10:08:26 2017 +0000
+++ b/php/requestKey.php	Wed Nov 22 10:10:44 2017 +0000
@@ -11,6 +11,10 @@
     return $randomString;
 }
 
+if (!file_exists("../saves")) {
+    mkdir("../saves");
+}
+
 // Request a new session key from the server
 header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
 header("Cache-Control: post-check=0, pre-check=0", false);
--- a/php/save.php	Wed Nov 22 10:08:26 2017 +0000
+++ b/php/save.php	Wed Nov 22 10:10:44 2017 +0000
@@ -33,7 +33,17 @@
 }
 $postText = file_get_contents('php://input');
 $file_key = $_GET['key'];
-$filename = '../saves/'.$saveFilenamePrefix.$file_key.".xml";
+
+$update = false;
+if (isset($_GET["update"])) {
+    $update = $_GET["update"] == "update";
+}
+
+if ($update) {
+    $filename = '../saves/update-'.$saveFilenamePrefix.$file_key.".xml";
+} else {
+    $filename = '../saves/'.$saveFilenamePrefix.$file_key.".xml";
+}
 
 if (!file_exists($filename)) {
     die('<response state="error"><message>Could not find save</message></response>');
@@ -132,4 +142,8 @@
 // Return XML confirmation data
 $xml = '<response state="OK"><message>OK</message><file bytes="'.$wbytes.'">"'.$filename.'"</file></response>';
 echo $xml;
+
+if (!$update) {
+    unlink('../saves/update-'.$saveFilenamePrefix.$file_key.".xml");
+}
 ?>
--- a/python/generate_report.py	Wed Nov 22 10:08:26 2017 +0000
+++ b/python/generate_report.py	Wed Nov 22 10:10:44 2017 +0000
@@ -232,17 +232,18 @@
             
             # number of comments (interesting if comments not mandatory)
             for audioelement in audioelements:
-                response = audioelement.find("./comment/response")
-                was_played = audioelement.find("./metric/metricresult/[@name='elementFlagListenedTo']")
-                was_moved = audioelement.find("./metric/metricresult/[@name='elementFlagMoved']")
-                if response is not None and response.text is not None and len(response.text) > 1: 
-                    number_of_comments += 1
-                else: 
-                    number_of_missing_comments += 1
-                if was_played is not None and was_played.text == 'false': 
-                    not_played.append(audioelement.get('name'))
-                if was_moved is not None and was_moved.text == 'false': 
-                    not_moved.append(audioelement.get('name'))
+                if audioelement.get("type") != "outside-reference":
+                    response = audioelement.find("./comment/response")
+                    was_played = audioelement.find("./metric/metricresult/[@name='elementFlagListenedTo']")
+                    was_moved = audioelement.find("./metric/metricresult/[@name='elementFlagMoved']")
+                    if response is not None and response.text is not None and len(response.text) > 1: 
+                        number_of_comments += 1
+                    else: 
+                        number_of_missing_comments += 1
+                    if was_played is not None and was_played.text == 'false': 
+                        not_played.append(audioelement.get('name'))
+                    if was_moved is not None and was_moved.text == 'false': 
+                        not_moved.append(audioelement.get('name'))
             
             # update global counters
             total_empty_comments += number_of_missing_comments
--- a/python/pythonServer.py	Wed Nov 22 10:08:26 2017 +0000
+++ b/python/pythonServer.py	Wed Nov 22 10:10:44 2017 +0000
@@ -13,6 +13,7 @@
 import copy
 import string
 import random
+import errno
 
 if sys.version_info[0] == 2:
     # Version 2.x
@@ -28,6 +29,12 @@
 scriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) # script directory
 os.chdir(scriptdir) # does this work?
 
+try:
+    os.makedirs("../saves")
+except OSError as e:
+    if e.errno != errno.EEXIST:
+        raise
+
 PSEUDO_PATH = '../tests/'
 pseudo_files = []
 pseudo_index = 0
@@ -62,16 +69,17 @@
         st = s.path.rsplit(',')
         lenSt = len(st)
         fmt = st[lenSt-1].rsplit('.')
+        fmt = fmt[len(fmt)-1]
         fpath = "../"+urllib2.unquote(s.path)
         size = os.path.getsize(fpath)
-        fileDump = open(fpath)
+        fileDump = open(fpath, mode='rb')
         s.send_response(200)
 
-        if (fmt[1] == 'html'):
+        if (fmt == 'html'):
             s.send_header("Content-type", 'text/html')
-        elif (fmt[1] == 'css'):
+        elif (fmt == 'css'):
             s.send_header("Content-type", 'text/css')
-        elif (fmt[1] == 'js'):
+        elif (fmt == 'js'):
             s.send_header("Content-type", 'application/javascript')
         else:
             s.send_header("Content-type", 'application/octet-stream')
@@ -146,12 +154,15 @@
     global curSaveIndex
     options = self.path.rsplit('?')
     options = options[1].rsplit('&')
+    update = False
     for option in options:
         optionPair = option.rsplit('=')
         if optionPair[0] == "key":
             key = optionPair[1]
         elif optionPair[0] == "saveFilenamePrefix":
             prefix = optionPair[1]
+        elif optionPair[0] == "state":
+            update = optionPair[1] == "update"
     if key == None:
         self.send_response(404)
         return
@@ -161,6 +172,8 @@
     postVars = self.rfile.read(varLen)
     print("Saving file key "+key)
     filename = prefix+'-'+key+'.xml'
+    if update:
+        filename = "update-"+filename
     file = open('../saves/'+filename,'wb')
     file.write(postVars)
     file.close()
@@ -181,6 +194,9 @@
         self.wfile.write(bytes(reply, "utf-8"))
     curSaveIndex += 1
     curFileName = 'test-'+str(curSaveIndex)+'.xml'
+    if update == False:
+        if(os.path.isfile("../saves/update-"+filename)):
+            os.remove("../saves/update-"+filename)
     
 def testSave(self):
     self.send_response(200)
@@ -209,7 +225,6 @@
         self.wfile.write(message)
     elif sys.version_info[0] == 3:
         self.wfile.write(bytes(message, "utf-8"))
-    
 
 def poolXML(s):
     pool = ET.parse('../tests/pool.xml')
--- a/python/timeline_view_movement.py	Wed Nov 22 10:08:26 2017 +0000
+++ b/python/timeline_view_movement.py	Wed Nov 22 10:10:44 2017 +0000
@@ -74,6 +74,10 @@
             if page_name is None: # ignore 'empty' audio_holders
                 print("Skipping empty page name from "+subject_id+".")
                 break
+            
+            if page.get("state") != "complete":
+                print("Skipping non-completed page "+page_name+" from "+subject_id+".")
+                break
                 
             # subtract total page length from subsequent page event times
             page_time_temp = page.find("./metric/metricresult/[@id='testTime']")
@@ -108,11 +112,20 @@
                 if audioelement is not None: # Check it exists
                     audio_id = str(audioelement.get('ref'))
                     
-                    # break if no initial position or move events registered
+                    # break if outside-reference
+                    if audioelement.get("type") == "outside-reference":
+                        break;
+                    
+                    # break if no initial position....
                     initial_position_temp = audioelement.find("./metric/metricresult/[@name='elementInitialPosition']")
                     if initial_position_temp is None:
                         print("Skipping "+page_name+" from "+subject_id+": does not have initial positions specified.")
                         break
+                    # ... or move events registered
+                    movements = audioelement.find("./metric/metricresult[@name='elementTrackerFull']")
+                    if movements is None:
+                        print("Skipping "+page_name+" from "+subject_id+": does not have trackers.")
+                        break
                     
                     # get move events, initial and eventual position
                     initial_position = float(initial_position_temp.text)
@@ -299,13 +312,20 @@
                 interfaces = page_setup.findall("./interface")
                 interface_title = interfaces[0].find("./title")
                 scales = interfaces[0].findall("./scales") # get first interface by default
-                scalelabels = scales[0].findall("./scalelabel") # get first scale by default
-
+                
                 labelpos = [] # array of scalelabel positions
                 labelstr = [] # array of strings at labels
-                for scalelabel in scalelabels:
-                    labelpos.append(float(scalelabel.get('position'))/100.0)
-                    labelstr.append(scalelabel.text)
+                
+                # No scales given. Use normal floats
+                if len(scales) is 0:
+                    labelpos = [0.0, 1.0]
+                    labelstr = ["0", "100"]
+                else:
+                    scalelabels = scales[0].findall("./scalelabel") # get first scale by default
+                
+                    for scalelabel in scalelabels:
+                        labelpos.append(float(scalelabel.get('position'))/100.0)
+                        labelstr.append(scalelabel.text)
 
                 # use interface name as Y axis label
                 if interface_title is not None:
--- a/test.html	Wed Nov 22 10:08:26 2017 +0000
+++ b/test.html	Wed Nov 22 10:10:44 2017 +0000
@@ -37,7 +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)</a></div>
+    <div id="footer"><a target="_blank" href="https://github.com/BrechtDeMan/WebAudioEvaluationTool">Web Audio Evaluation Toolbox (v1.2.3)</a></div>
 </body>
 
 </html>
--- a/test_create.html	Wed Nov 22 10:08:26 2017 +0000
+++ b/test_create.html	Wed Nov 22 10:10:44 2017 +0000
@@ -142,6 +142,12 @@
             </div>
             <div id="globalpresurvey" class="node" ng-controller="survey" ng-init="survey = specification.preTest">
                 <h2>Pre Test Survey</h2>
+                <div class="attributes">
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Allow users to go both back and forward in the test">
+                        <span>Show back button: </span>
+                        <input type="checkbox" ng-model="survey.showBackButton" />
+                    </div>
+                </div>
                 <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
                 <div class="node surveyentry" ng-repeat="opt in survey.options" ng-controller="surveyOption">
                     <h3>Survey Entry</h3>
@@ -273,6 +279,12 @@
             </div>
             <div id="globalpostsurvey" class="node" ng-controller="survey" ng-init="survey = specification.postTest">
                 <h2>Post Test Survey</h2>
+                <div class="attributes">
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Allow users to go both back and forward in the test">
+                        <span>Show back button: </span>
+                        <input type="checkbox" ng-model="survey.showBackButton" />
+                    </div>
+                </div>
                 <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
                 <div class="node surveyentry" ng-repeat="opt in survey.options" ng-controller="surveyOption">
                     <h3>Survey Entry</h3>
@@ -446,6 +458,10 @@
                             <span>Show Fragment Comments: </span>
                             <input type="checkbox" ng-click="enableInterfaceOption($event)" />
                         </div>
+                        <div class="attribute" name="ticks" type="show" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Show tick marks for each scale label">
+                            <span>Show Scale Ticks: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
                     </div>
                 </div>
             </div>
@@ -550,6 +566,12 @@
             </div>
             <div class="node" ng-controller="survey" ng-init="survey = page.preTest">
                 <h2>Pre Page Survey</h2>
+                <div class="attributes">
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Allow users to go both back and forward in the test">
+                        <span>Show back button: </span>
+                        <input type="checkbox" ng-model="survey.showBackButton" />
+                    </div>
+                </div>
                 <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
                 <div class="node surveyentry" ng-repeat="opt in survey.options" ng-controller="surveyOption">
                     <h3>Survey Entry</h3>
@@ -681,6 +703,12 @@
             </div>
             <div class="node" ng-controller="survey" ng-init="survey = page.postTest">
                 <h2>Post Page Survey</h2>
+                <div class="attributes">
+                    <div class="attribute" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Allow users to go both back and forward in the test">
+                        <span>Show back button: </span>
+                        <input type="checkbox" ng-model="survey.showBackButton" />
+                    </div>
+                </div>
                 <button type="button" class="btn btn-success" ng-click="addSurveyEntry()">Add Entry</button>
                 <div class="node surveyentry" ng-repeat="opt in survey.options" ng-controller="surveyOption">
                     <h3>Survey Entry</h3>
@@ -856,6 +884,10 @@
                             <span>Show Fragment Comments: </span>
                             <input type="checkbox" ng-click="enableInterfaceOption($event)" />
                         </div>
+                        <div class="attribute" name="ticks" type="show" data-container="body" data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="Show tick marks for each scale label">
+                            <span>Show Scale Ticks: </span>
+                            <input type="checkbox" ng-click="enableInterfaceOption($event)" />
+                        </div>
                     </div>
                 </div>
                 <div class="node">
@@ -909,6 +941,15 @@
                     <button type="button" class="btn btn-danger" ng-click="removeCommentQuestion(cq)">Remove Comment Question</button>
                     <div class="attributes">
                         <div class="attribute">
+                            <span>Type:</span>
+                            <select ng-model="cq.type">
+                                <option value="question">Question</option>
+                                <option value="checkbox">Checkbox</option>
+                                <option value="radio">Radio</option>
+                                <option value="slider">Slider</option>
+                            </select>
+                        </div>
+                        <div class="attribute">
                             <span>Unique ID:</span>
                             <input type="text" ng-model="cq.id" required/>
                         </div>
@@ -916,6 +957,10 @@
                             <span>Common Name:</span>
                             <input type="text" ng-model="cq.name" />
                         </div>
+                        <div class="attribute">
+                            <span>Mandatory:</span>
+                            <input type="checkbox" ng-model="cq.mandatory" />
+                        </div>
                         <div class="attribute" ng-show="cq.type == 'slider'">
                             <span>Minimum:</span>
                             <input type="number" ng-model="cq.min" />
--- a/tests/examples/AB_example.xml	Wed Nov 22 10:08:26 2017 +0000
+++ b/tests/examples/AB_example.xml	Wed Nov 22 10:10:44 2017 +0000
@@ -45,10 +45,6 @@
                 <metricenable>elementListenTracker</metricenable>
             </metric>
             <interface>
-                <interfaceoption type="check" name="fragmentMoved" />
-                <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' />
--- a/tests/examples/APE_example.xml	Wed Nov 22 10:08:26 2017 +0000
+++ b/tests/examples/APE_example.xml	Wed Nov 22 10:10:44 2017 +0000
@@ -123,7 +123,7 @@
                 <option name="Good"></option>
                 <option name="Great">Great</option>
             </commentradio>
-            <commentcheckbox id="character" type="checkbox">
+            <commentcheckbox id="character">
                 <statement>Please describe the overall character</statement>
                 <option name="funky">Funky</option>
                 <option name="mellow">Mellow</option>
--- a/tests/examples/horizontal_example.xml	Wed Nov 22 10:08:26 2017 +0000
+++ b/tests/examples/horizontal_example.xml	Wed Nov 22 10:10:44 2017 +0000
@@ -17,6 +17,7 @@
             <interfaceoption type="show" name="page-count" />
             <interfaceoption type="show" name="volume" />
             <interfaceoption type="show" name="comments" />
+            <interfaceoption type="show" name="ticks" />
         </interface>
     </setup>
     <page id='test-0' hostURL="media/example/" randomiseOrder='true' repeatCount='0' loop='true' loudness="-12">
--- a/tests/examples/mushra_example.xml	Wed Nov 22 10:08:26 2017 +0000
+++ b/tests/examples/mushra_example.xml	Wed Nov 22 10:10:44 2017 +0000
@@ -112,24 +112,24 @@
         <audioelement url="5.wav" gain="0.0" id="track-10" />
         <audioelement url="1.wav" gain="0.0" id="track-11" type="outside-reference" />
         <commentquestions>
-            <commentquestion id='mixingExperience' type="question">
+            <commentquestion id='mixingExperience'>
                 <statement>What is your general experience with numbers?</statement>
             </commentquestion>
-            <commentquestion id="preference" type="radio">
+            <commentradio id="preference">
                 <statement>Please enter your overall preference</statement>
                 <option name="worst">Very Bad</option>
                 <option name="bad"></option>
                 <option name="OK">OK</option>
                 <option name="Good"></option>
                 <option name="Great">Great</option>
-            </commentquestion>
-            <commentquestion id="character" type="checkbox">
+            </commentradio>
+            <commentcheckbox id="character">
                 <statement>Please describe the overall character</statement>
                 <option name="funky">Funky</option>
                 <option name="mellow">Mellow</option>
                 <option name="laidback">Laid back</option>
                 <option name="heavy">Heavy</option>
-            </commentquestion>
+            </commentcheckbox>
         </commentquestions>
         <survey location="before">
             <surveyentry type="statement" id="test-1-intro">
--- a/tests/examples/radio_example.xml	Wed Nov 22 10:08:26 2017 +0000
+++ b/tests/examples/radio_example.xml	Wed Nov 22 10:10:44 2017 +0000
@@ -29,9 +29,9 @@
                 <scalelabel position="100">(5) Inaudible</scalelabel>
             </scales>
         </interface>
-        <audioelement url="0.wav" id="track-1" alwaysInclude="true" />
-        <audioelement url="1.wav" id="track-2" />
-        <audioelement url="3.wav" id="track-4" />
-        <audioelement url="3.wav" id="track-5" />
+        <audioelement url="0.wav" id="track-0" alwaysInclude="true" />
+        <audioelement url="1.wav" id="track-1" />
+        <audioelement url="2.wav" id="track-2" />
+        <audioelement url="3.wav" id="track-3" />
     </page>
 </waet>
--- a/xml/test-schema.xsd	Wed Nov 22 10:08:26 2017 +0000
+++ b/xml/test-schema.xsd	Wed Nov 22 10:10:44 2017 +0000
@@ -67,6 +67,7 @@
                         </xs:restriction>
                     </xs:simpleType>
                 </xs:attribute>
+                <xs:attribute name="randomiseAxisOrder" type="xs:boolean" default="false" />
                 <xs:attribute ref="preSilence" />
                 <xs:attribute ref="postSilence" />
                 <xs:attribute ref="playOne" />
@@ -105,6 +106,7 @@
                     </xs:simpleType>
                 </xs:attribute>
                 <xs:attribute name="labelStart" type="xs:string" use="optional" default="" />
+                <xs:attribute name="randomiseAxisOrder" type="xs:boolean" use="optional" />
                 <xs:attribute ref="poolSize" />
                 <xs:attribute ref="alwaysInclude" />
                 <xs:attribute name="position" use="optional" type="xs:nonNegativeInteger" />
@@ -277,6 +279,7 @@
                 </xs:sequence>
                 <xs:attribute ref="id" use="optional" />
                 <xs:attribute ref="name" use="optional" />
+                <xs:attribute ref="mandatory" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -296,6 +299,7 @@
                 </xs:sequence>
                 <xs:attribute ref="id" use="optional" />
                 <xs:attribute ref="name" use="optional" />
+                <xs:attribute ref="mandatory" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -306,6 +310,7 @@
                 </xs:sequence>
                 <xs:attribute ref="id" use="optional" />
                 <xs:attribute ref="name" use="optional" />
+                <xs:attribute ref="mandatory" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -322,6 +327,7 @@
                 <xs:attribute name="max" type="xs:decimal" use="required" />
                 <xs:attribute name="step" type="xs:decimal" use="optional" default="1" />
                 <xs:attribute name="value" type="xs:decimal" use="optional" />
+                <xs:attribute ref="mandatory" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -547,6 +553,7 @@
                         </xs:restriction>
                     </xs:simpleType>
                 </xs:attribute>
+                <xs:attribute name="showBackButton" type="xs:boolean" default="true" />
             </xs:complexType>
         </xs:element>