changeset 3025:c93687862a79

Merge branch 'vnext' into Dev_main
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Tue, 12 Sep 2017 19:24:03 +0100
parents c29ef0cc741f (diff) 9cdcde1dafd8 (current diff)
children b09e9b7ef246
files js/core.js
diffstat 19 files changed, 683 insertions(+), 559 deletions(-) [+]
line wrap: on
line diff
--- a/.gitignore	Tue Sep 12 19:23:56 2017 +0100
+++ b/.gitignore	Tue Sep 12 19:24:03 2017 +0100
@@ -11,3 +11,6 @@
 *.DS_STORE
 *.swp
 *.swo
+saves/ratings/*
+saves/timelines/*
+saves/timelines_movement/*
--- a/interfaces/ape.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/interfaces/ape.js	Tue Sep 12 19:24:03 2017 +0100
@@ -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++) {
@@ -447,237 +216,474 @@
     //testWaitIndicator();
 }
 
-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);
+            trackObj.align = "center";
+            trackObj.className = 'track-slider track-slider-disabled';
+            trackObj.appendChild(labelHolder);
+            trackObj.style.left = (Math.random() * $(sliderRail).width()) + 50 + "px";
+            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 + 50);
+                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.addEventListener("touchstart", 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");
+                trackObj.removeEventListener("touchstart");
+            }
+            var timing = undefined;
+            this.handleEvent = function (e) {
+                // This is only for the mousedown / touchdown
+                if (e.preventDefault) {
+                    e.preventDefault();
+                }
+                if (e.type == "mousedown" || e.type == "touchstart") {
+                    axisInterface.mousedown(this);
+                } else if (e.type == "mouseup") {
+                    axisInterface.mouseup(this);
+                }
+            }
+            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": {
+                    "value": function () {
+                        var maxPix = $(axisInterface.sliderRail).width();
+                        var pix = trackObj.style.left.substr(0, trackObj.style.left.length - 2);
+                        return (pix - 50) / maxPix;
+                    }
+                },
+                "moveToPixel": {
+                    "value": function (pix) {
+                        var t = audioEngineContext.timer.getTestTime();
+                        trackObj.style.left = String(pix) + "px";
+                        metric.moved(t, this.value);
+                    }
+                },
+                "label": {
+                    "get": function () {
+                        return label;
+                    },
+                    "set": function () {}
+                }
+            });
+        }
+
+        function createScaleMarkers(interfaceObject, root, w) {
+            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';
+            }, 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);
+
+        // 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 = "";
+            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 (sliderUI) {
+            var delta = new Date() - UI.startTime;
+            if (delta < 200) {
+                UI.selected.clicked();
+            }
+            UI.selected = undefined;
+            UI.startTime = undefined;
+        }
+        this.handleEvent = function (event) {
+            if (event.preventDefault) {
+                event.preventDefault();
+            }
+            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 + 50, move);
+                UI.selected.moveToPixel(move);
+            } else if (event.type == "touchmove") {
+                var move = event.originalEvent.targetTouches[0].clientX - 6;
+                var w = $(event.currentTarget).width();
+                move = Math.max(50, move);
+                move = Math.min(w + 50, move);
+                UI.selected.moveToPixel(move);
+            }
+        }
+        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("touchmove", 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 interfaceObj = interfaceContext.getCombinedInterfaces(page);
+        var commentBoxes = false;
+        // Create each of the interface axis
+        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);
-        });
-
-        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) {
@@ -833,32 +839,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.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/interfaces/discrete.js	Tue Sep 12 19:24:03 2017 +0100
@@ -67,9 +67,19 @@
     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');
@@ -187,6 +197,10 @@
                 case "comments":
                     interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true);
                     break;
+                case "fragmentSort":
+                    var button = document.getElementById('sort-fragments');
+                    button.style.visibility = "visible";
+                    break;
             }
         }
     });
@@ -539,6 +553,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 pageXMLSave(store, pageSpecification) {
     // MANDATORY
     // Saves a specific test page
--- a/interfaces/horizontal-sliders.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/interfaces/horizontal-sliders.js	Tue Sep 12 19:24:03 2017 +0100
@@ -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;
             }
         }
     });
@@ -419,6 +434,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,
--- a/interfaces/mushra.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/interfaces/mushra.js	Tue Sep 12 19:24:03 2017 +0100
@@ -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;
             }
         }
@@ -513,6 +503,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,
--- a/interfaces/ordinal.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/interfaces/ordinal.js	Tue Sep 12 19:24:03 2017 +0100
@@ -222,6 +222,7 @@
     root.addEventListener('dragleave', this, true);
     root.addEventListener('drop', this, true);
     root.addEventListener('dragend', this, true);
+    this.dragging = false;
     this.handleEvent = function (event) {
         if (event.type == "click") {
             if (playing === false) {
@@ -252,6 +253,7 @@
 
         e.dataTransfer.effectAllowed = 'move';
         e.dataTransfer.setData('text/plain', audioObject.id);
+        this.dragging = "true";
     }
 
     function dragEnter(e) {
@@ -267,13 +269,10 @@
         if (e.preventDefault) {
             e.preventDefault(); // Necessary. Allows us to drop.
         }
-
         e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object.
-
-        var srcid = Number(e.dataTransfer.getData("text/plain"));
         var elements = container.childNodes;
         var srcObject = audioEngineContext.audioObjects.find(function (ao) {
-            return ao.id === srcid;
+            return ao.interfaceDOM.dragging;
         });
         var src = srcObject.interfaceDOM.root;
         if (src !== root) {
@@ -293,8 +292,6 @@
             }
 
         }
-
-        return false;
     }
 
     function drop(e) {
@@ -318,6 +315,7 @@
         // this/e.target is the source node.
         $(".ordinal-element").removeClass("dragging");
         $(".ordinal-element").removeClass("over");
+        this.dragging = false;
     }
 
     this.getElementPosition = function () {
--- a/js/core.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/js/core.js	Tue Sep 12 19:24:03 2017 +0100
@@ -1107,7 +1107,7 @@
         } else {
             this.buttonProceed.textContent = 'Next';
         }
-        if (this.currentIndex > 0)
+        if (this.currentIndex > 0 && this.node.showBackButton)
             this.buttonPrevious.style.visibility = 'visible';
         else
             this.buttonPrevious.style.visibility = 'hidden';
@@ -3608,7 +3608,7 @@
                     returnURL = specification.projectReturn;
                 }
             }
-            xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix);
+            xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix + "&state=update");
             xmlhttp.setRequestHeader('Content-Type', 'text/xml');
             xmlhttp.onerror = function () {
                 console.log('Error updating file to server!');
@@ -3646,6 +3646,7 @@
             } else {
                 saveURL += this.parent.filenamePrefix;
             }
+            saveURL += "&state=finish";
             return new Promise(function (resolve, reject) {
                 var xmlhttp = new XMLHttpRequest();
                 xmlhttp.open("POST", saveURL);
--- a/js/specification.js	Tue Sep 12 19:23:56 2017 +0100
+++ b/js/specification.js	Tue Sep 12 19:24:03 2017 +0100
@@ -216,6 +216,7 @@
         this.location = undefined;
         this.options = [];
         this.parent = undefined;
+        this.showBackButton = true;
         this.specification = specification;
 
         this.addOption = function () {
@@ -403,6 +404,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);
@@ -418,6 +425,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));
             }
--- a/php/save.php	Tue Sep 12 19:23:56 2017 +0100
+++ b/php/save.php	Tue Sep 12 19:24:03 2017 +0100
@@ -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	Tue Sep 12 19:23:56 2017 +0100
+++ b/python/generate_report.py	Tue Sep 12 19:24:03 2017 +0100
@@ -227,17 +227,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	Tue Sep 12 19:23:56 2017 +0100
+++ b/python/pythonServer.py	Tue Sep 12 19:24:03 2017 +0100
@@ -138,12 +138,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
@@ -153,6 +156,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()
@@ -173,6 +178,8 @@
         self.wfile.write(bytes(reply, "utf-8"))
     curSaveIndex += 1
     curFileName = 'test-'+str(curSaveIndex)+'.xml'
+    if update == False:
+        os.remove("../saves/update-"+filename)
     
 def testSave(self):
     self.send_response(200)
@@ -201,7 +208,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/score_parser.py	Tue Sep 12 19:23:56 2017 +0100
+++ b/python/score_parser.py	Tue Sep 12 19:24:03 2017 +0100
@@ -58,17 +58,16 @@
             
             # Check if page in the store
             if storage.get(page_name) == None:
-                storage[page_name] = {'header':[], 'axis':{}} # add to the store
+                storage[page_name] = {'header':[], 'axis':{"default": {}}} # add to the store
             
             # Get the axis names
             pageConfig = root.find('./waet/page/[@id="'+page_name+'"]')
             for interface in pageConfig.findall('./interface'):    # Get the <interface> noeds
                 interfaceName = interface.get("name"); # Get the axis name
-                if interfaceName == None:
+                if interfaceName == None or interfaceName == "null":
                     interfaceName = "default"   # If name not set, make name 'default'
-                if storage[page_name]['axis'].get(interfaceName) == None:
+                if interfaceName not in storage[page_name]['axis'].keys():
                     storage[page_name]['axis'][interfaceName] = {}  # If not in store for page, add empty dict
-                storage[page_name]['axis'][interfaceName][subject_id] = [] # Add the store for the session
                     
             # header: fragment IDs in 'alphabetical' order
             # go to fragment column, or create new column if it doesn't exist yet
@@ -76,7 +75,8 @@
             # get alphabetical array of fragment IDs from this subject's XML
             fragmentnamelist = []    # make empty list
             for audioelement in page.findall("./audioelement"): # iterate over all audioelements
-                fragmentnamelist.append(audioelement.get('ref')) # add to list
+                if audioelement.get("type") != "outside-reference":
+                    fragmentnamelist.append(audioelement.get('ref')) # add to list
             
             fragmentnamelist = sorted(fragmentnamelist);    # Sort the list
             storage[page_name]['header'] = fragmentnamelist;
@@ -87,11 +87,17 @@
                     axisName = value.get('interface-name')
                     if axisName == None or axisName == "null":
                         axisName = 'default'
+                    print(storage[page_name]['axis'])
                     axisStore = storage[page_name]['axis'][axisName]
+                    try:
+                        subjectStore = axisStore[subject_id]
+                    except KeyError:
+                        axisStore[subject_id] = []
+                        subjectStore = axisStore[subject_id]
                     if hasattr(value, 'text'):
-                        axisStore[subject_id].append(value.text)
+                        subjectStore.append(value.text)
                     else:
-                        axisStore[subject_id].append('')
+                        subjectStore.append('')
 
 # Now create the individual files
 for page_name in storage:
--- a/python/timeline_view_movement.py	Tue Sep 12 19:23:56 2017 +0100
+++ b/python/timeline_view_movement.py	Tue Sep 12 19:24:03 2017 +0100
@@ -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_create.html	Tue Sep 12 19:23:56 2017 +0100
+++ b/test_create.html	Tue Sep 12 19:24:03 2017 +0100
@@ -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>
@@ -550,6 +562,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 +699,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>
--- a/tests/examples/AB_example.xml	Tue Sep 12 19:23:56 2017 +0100
+++ b/tests/examples/AB_example.xml	Tue Sep 12 19:24:03 2017 +0100
@@ -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	Tue Sep 12 19:23:56 2017 +0100
+++ b/tests/examples/APE_example.xml	Tue Sep 12 19:24:03 2017 +0100
@@ -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/mushra_example.xml	Tue Sep 12 19:23:56 2017 +0100
+++ b/tests/examples/mushra_example.xml	Tue Sep 12 19:24:03 2017 +0100
@@ -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	Tue Sep 12 19:23:56 2017 +0100
+++ b/tests/examples/radio_example.xml	Tue Sep 12 19:24:03 2017 +0100
@@ -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	Tue Sep 12 19:23:56 2017 +0100
+++ b/xml/test-schema.xsd	Tue Sep 12 19:24:03 2017 +0100
@@ -547,6 +547,7 @@
                         </xs:restriction>
                     </xs:simpleType>
                 </xs:attribute>
+                <xs:attribute name="showBackButton" type="xs:boolean" default="true" />
             </xs:complexType>
         </xs:element>