changeset 2797:9e060c431ab3

Merge branch 'Dev_main' into vnext
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Sun, 23 Apr 2017 11:32:45 +0100
parents c0cb7c416d8e (current diff) 35037fb7a843 (diff)
children 9e2363f78912
files
diffstat 15 files changed, 335 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
--- a/css/core.css	Sun Apr 23 10:17:33 2017 +0100
+++ b/css/core.css	Sun Apr 23 11:32:45 2017 +0100
@@ -201,7 +201,7 @@
     position: absolute;
     top: 20px;
     left: 50px;
-    width: 250px%;
+    width: 250px;
     padding: 5px;
 }
 div#master-volume-root {
@@ -277,3 +277,11 @@
 div.comment-box-playing {
     background-color: #FFDDDD;
 }
+div#imageController {
+    align-content: center;
+    text-align: center;
+    height: 250px;
+}
+div#imageController img {
+    max-height: 250px;
+}
--- a/interfaces/AB.css	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/AB.css	Sun Apr 23 11:32:45 2017 +0100
@@ -2,6 +2,10 @@
     /* Set the background colour (note US English spelling) to grey*/
     background-color: #fff
 }
+div#feedbackHolder {
+    display: flex;
+    flex-direction: column;
+}
 div.pageTitle {
     width: auto;
     height: 20px;
@@ -34,13 +38,20 @@
     position: inherit;
     margin: 0px 5px;
 }
+div#box-holders {
+    width: 100%;
+    text-align: center;
+}
+div#playback-holder {
+    float: none;
+}
 div.comparator-holder {
     width: 260px;
     height: 300px;
     border: black 1px solid;
-    float: left;
     padding-top: 5px;
     margin: 25px;
+    display: inline-block;
 }
 div.comparator-selector {
     width: 248px;
@@ -49,6 +60,20 @@
     position: relative;
     background-color: #FF0000;
     border-radius: 20px;
+    text-align: center;
+    display: block;
+    margin: auto;
+}
+div.comparator-image {
+    background-color: rgba(255,255,255,0);
+}
+img.comparator-image {
+    width: inherit;
+    height: inherit;
+    z-index: -1;
+    position: absolute;
+    display: inline;
+    right: 0px;
 }
 div.disabled {
     background-color: #AAA;
@@ -72,8 +97,3 @@
     float: left;
     margin: 0px 5px;
 }
-div#master-volume-holder {
-    position: absolute;
-    top: 10px;
-    left: 120px;
-}
--- a/interfaces/AB.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/AB.js	Sun Apr 23 11:32:45 2017 +0100
@@ -73,9 +73,7 @@
 
     // Construct the AB Boxes
     var boxes = document.createElement('div');
-    boxes.align = "center";
     boxes.id = "box-holders";
-    boxes.style.float = "left";
 
     var submit = document.createElement('button');
     submit.id = "submit";
@@ -126,10 +124,15 @@
         document.getElementById("test-title").textContent = audioHolderObject.title;
     }
 
-    if (interfaceObj.title !== null) {
+    if (interfaceObj.title !== undefined) {
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined) {
+        feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("box-holders"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     var interfaceOptions = interfaceObj.options;
     // Clear the interfaceElements
     {
@@ -210,6 +213,13 @@
         this.box.id = 'comparator-' + text;
         this.selector = document.createElement('div');
         this.selector.className = 'comparator-selector disabled';
+        if (audioElement.specification.image) {
+            this.selector.className += " comparator-image";
+            var image = document.createElement("img");
+            image.src = audioElement.specification.image;
+            image.className = "comparator-image";
+            this.selector.appendChild(image);
+        }
         var selectorText = document.createElement('span');
         selectorText.textContent = text;
         this.selector.appendChild(selectorText);
@@ -260,7 +270,7 @@
             } else if (event.currentTarget === this.playback) {
                 this.playbackClicked();
             }
-        }
+        };
         this.playback.addEventListener("click", this);
         this.selector.addEventListener("click", this);
 
@@ -375,9 +385,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';
 
     var outsideRef = document.getElementById('outside-reference');
     if (outsideRef !== null) {
@@ -395,35 +402,35 @@
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    checkState = interfaceContext.checkFragmentsFullyPlayed();
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllMoved();
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     if (checkState === false) {
                         canContinue = false;
                     }
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    checkState = interfaceContext.checkScaleRange();
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     if (checkState === false) {
                         canContinue = false;
                     }
--- a/interfaces/ABX.css	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/ABX.css	Sun Apr 23 11:32:45 2017 +0100
@@ -20,13 +20,20 @@
     height: 40px;
     font-size: 1.2em;
 }
+div#box-holders {
+    width: 100%;
+    text-align: center;
+}
+div#playback-holder {
+    float: none;
+}
 div.comparator-holder {
     width: 260px;
     height: 300px;
     border: black 1px solid;
-    float: left;
     padding-top: 5px;
     margin: 25px;
+    display: inline-block;
 }
 div.comparator-selector {
     width: 248px;
@@ -35,6 +42,9 @@
     position: relative;
     background-color: #FF0000;
     border-radius: 20px;
+    text-align: center;
+    display: block;
+    margin: auto;
 }
 div.disabled {
     background-color: #AAA;
@@ -61,8 +71,3 @@
     float: left;
     margin: 0px 5px;
 }
-div#master-volume-holder {
-    position: absolute;
-    top: 10px;
-    left: 120px;
-}
--- a/interfaces/ABX.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/ABX.js	Sun Apr 23 11:32:45 2017 +0100
@@ -129,6 +129,11 @@
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined) {
+        feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("box-holders"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     interfaceContext.comparator = new comparator(page);
 
     var interfaceOptions = interfaceObj.options;
@@ -417,16 +422,16 @@
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
 
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    checkState = interfaceContext.checkFragmentsFullyPlayed();
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllMoved();
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
--- a/interfaces/ape.css	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/ape.css	Sun Apr 23 11:32:45 2017 +0100
@@ -89,3 +89,11 @@
     top: 10px;
     left: 120px;
 }
+div.imageController {
+    align-content: center;
+    text-align: center;
+    height: 250px;
+}
+div.imageController img {
+    max-height: 250px;
+}
--- a/interfaces/ape.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/ape.js	Sun Apr 23 11:32:45 2017 +0100
@@ -106,7 +106,7 @@
                 return {
                     min: Math.min(a.min, v),
                     max: Math.max(a.max, v)
-                }
+                };
             }, {
                 min: 100,
                 max: 0
@@ -253,7 +253,7 @@
     var interfaceObj = interfaceContext.getCombinedInterfaces(audioHolderObject);
     interfaceObj.forEach(function (interfaceObjectInstance) {
         // Create the div box to center align
-        interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObjectInstance));
+        interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObjectInstance, audioHolderObject));
     });
     interfaceObj.forEach(function (interface) {
         interface.options.forEach(function (option) {
@@ -447,7 +447,7 @@
     //testWaitIndicator();
 }
 
-function interfaceSliderHolder(interfaceObject) {
+function interfaceSliderHolder(interfaceObject, page) {
     this.sliders = [];
     this.metrics = [];
     this.id = document.getElementsByClassName("sliderCanvasDiv").length;
@@ -456,6 +456,21 @@
     this.sliderDOM = document.createElement('div');
     this.sliderDOM.className = 'sliderCanvasDiv';
     this.sliderDOM.id = 'sliderCanvasHolder-' + this.id;
+    this.imageHolder = (function () {
+        var imageController = {};
+        imageController.root = document.createElement("div");
+        imageController.root.className = "imageController";
+        imageController.img = document.createElement("img");
+        imageController.root.appendChild(imageController.img);
+        imageController.setImage = function (src) {
+            imageController.img.src = "";
+            if (typeof src !== "string" || src.length === undefined) {
+                return;
+            }
+            imageController.img.src = src;
+        };
+        return imageController;
+    })();
 
     var pagetitle = document.createElement('div');
     pagetitle.className = "pageTitle";
@@ -470,6 +485,12 @@
     pagetitle.appendChild(titleSpan);
     this.sliderDOM.appendChild(pagetitle);
 
+    if (interfaceObject.image !== undefined || page.audioElements.some(function (a) {
+            return a.image !== undefined;
+        })) {
+        this.sliderDOM.appendChild(this.imageHolder.root);
+        this.imageHolder.setImage(interfaceObject.image);
+    }
     // Create the slider box to hold the slider elements
     this.canvas = document.createElement('div');
     if (this.name !== undefined)
@@ -555,6 +576,18 @@
             scaleObj.style.left = Math.floor((pixelPosition - ($(scaleObj).width() / 2))) + 'px';
         }
     };
+
+    this.playing = function (id) {
+        var node = audioEngineContext.audioObjects.find(function (a) {
+            return a.id == id;
+        });
+        if (node === undefined) {
+            this.imageHolder.setImage(interfaceObject.image || "");
+            return;
+        }
+        var imgurl = node.specification.image || interfaceObject.image || "";
+        this.imageHolder.setImage(imgurl);
+    }
 }
 
 function sliderObject(audioObject, interfaceObjects, index) {
@@ -598,6 +631,9 @@
             $('.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) {
@@ -714,28 +750,28 @@
     }
 
     for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
         if (checks[i].type == 'check') {
-            var checkState = true;
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    checkState = interfaceContext.checkFragmentsFullyPlayed();
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllMoved();
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale is used to its full width outlined by the node
-                    checkState = interfaceContext.checkScaleRange();
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].name + " is not supported on this interface");
--- a/interfaces/discrete.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/discrete.js	Sun Apr 23 11:32:45 2017 +0100
@@ -137,6 +137,13 @@
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined || audioHolderObject.audioElements.some(function (elem) {
+            return elem.image !== undefined;
+        })) {
+        document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     document.getElementById("outside-reference-holder").innerHTML = "";
 
@@ -339,6 +346,9 @@
             $('.track-slider-button').attr("disabled", "true");
         }
         interfaceContext.commentBoxes.highlightById(audioObject.id);
+        if (audioObject.specification.image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.image);
+        }
     };
     this.stopPlayback = function () {
         // Called by audioObject when playback stops
@@ -354,6 +364,11 @@
             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("");
+            }
         }
     };
 
@@ -474,24 +489,24 @@
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllMoved();
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
--- a/interfaces/horizontal-sliders.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/horizontal-sliders.js	Sun Apr 23 11:32:45 2017 +0100
@@ -137,6 +137,13 @@
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined || audioHolderObject.audioElements.some(function (elem) {
+            return elem.image !== undefined;
+        })) {
+        document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     document.getElementById("outside-reference-holder").innerHTML = "";
 
@@ -297,6 +304,9 @@
             $(outsideReference).removeClass('track-slider-playing');
         }
         interfaceContext.commentBoxes.highlightById(audioObject.id);
+        if (audioObject.specification.image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.image);
+        }
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
@@ -308,6 +318,11 @@
         if (box) {
             box.highlight(false);
         }
+        if (audioObject.specification.parent.interfaces[0].image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image);
+        } else {
+            interfaceContext.imageHolder.setImage("");
+        }
     };
     this.getValue = function () {
         // Return the current value of the object. If there is no value, return 0
@@ -420,24 +435,24 @@
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllMoved();
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
 
                     break;
                 default:
--- a/interfaces/mushra.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/mushra.js	Sun Apr 23 11:32:45 2017 +0100
@@ -137,6 +137,13 @@
         document.getElementById("pageTitle").textContent = interfaceObj.title;
     }
 
+    if (interfaceObj.image !== undefined || audioHolderObject.audioElements.some(function (elem) {
+            return elem.image !== undefined;
+        })) {
+        document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider"));
+        interfaceContext.imageHolder.setImage(interfaceObj.image);
+    }
+
     // Delete outside reference
     var outsideReferenceHolder = document.getElementById("outside-reference-holder");
     outsideReferenceHolder.innerHTML = "";
@@ -338,6 +345,9 @@
                 });
             }
         }
+        if (audioObject.specification.image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.image);
+        }
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
@@ -354,14 +364,23 @@
         if (box) {
             box.highlight(false);
         }
+        if (audioObject.specification.parent.interfaces[0].image !== undefined) {
+            interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image);
+        } else {
+            interfaceContext.imageHolder.setImage("");
+        }
     };
     this.getValue = function () {
         return this.slider.value;
     };
 
     this.resize = function (event) {
-        this.holder.style.height = window.innerHeight - 200 + 'px';
-        this.slider.style.height = window.innerHeight - 250 + 'px';
+        var imgHeight = 0;
+        if (document.getElementById("imageController")) {
+            imgHeight = $(interfaceContext.imageHolder.root).height();
+        }
+        this.holder.style.height = window.innerHeight - 200 - imgHeight + 'px';
+        this.slider.style.height = window.innerHeight - 250 - imgHeight + 'px';
     };
     this.updateLoading = function (progress) {
         progress = String(progress);
@@ -389,7 +408,11 @@
     // Function called when the window has been resized.
     // MANDATORY FUNCTION
 
-    var outsideRef = document.getElementById('outside-reference');
+    var outsideRef = document.getElementById('outside-reference'),
+        imageHeight = 0;
+    if (document.getElementById("imageController")) {
+        imageHeight = $(interfaceContext.imageHolder.root).height();
+    }
     if (outsideRef !== null) {
         outsideRef.style.left = (window.innerWidth - 120) / 2 + 'px';
     }
@@ -398,7 +421,7 @@
     var numObj = document.getElementsByClassName('track-slider').length;
     var totalWidth = (numObj - 1) * 150 + 100;
     var diff = (window.innerWidth - totalWidth) / 2;
-    document.getElementById('slider').style.height = window.innerHeight - 180 + 'px';
+    document.getElementById('slider').style.height = window.innerHeight - 180 - imageHeight + 'px';
     if (diff <= 0) {
         diff = 0;
     }
@@ -409,7 +432,7 @@
         }
     }
     document.getElementById('scale-holder').style.marginLeft = (diff - 100) + 'px';
-    document.getElementById('scale-text-holder').style.height = window.innerHeight - 194 + 'px';
+    document.getElementById('scale-text-holder').style.height = window.innerHeight - imageHeight - 194 + 'px';
     // Cheers edge for making me delete a canvas every resize.
     var canvas = document.getElementById('scale-canvas');
     var new_canvas = document.createElement("canvas");
@@ -417,7 +440,7 @@
     canvas.parentElement.appendChild(new_canvas);
     canvas.parentElement.removeChild(canvas);
     new_canvas.width = totalWidth;
-    new_canvas.height = window.innerHeight - 194;
+    new_canvas.height = window.innerHeight - 194 - imageHeight;
     drawScale();
 }
 
@@ -476,24 +499,24 @@
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     // Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     // Check all fragments have been played to their full length
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
                     break;
                 case 'fragmentMoved':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllMoved();
+                    checkState = interfaceContext.checkAllMoved(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
                     // Check all fragment sliders have been moved.
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 case 'scalerange':
                     // Check the scale has been used effectively
-                    checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max);
+                    checkState = interfaceContext.checkScaleRange(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
--- a/interfaces/timeline.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/interfaces/timeline.js	Sun Apr 23 11:32:45 2017 +0100
@@ -489,14 +489,14 @@
             switch (checks[i].name) {
                 case 'fragmentPlayed':
                     //Check if all fragments have been played
-                    checkState = interfaceContext.checkAllPlayed();
+                    checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentFullPlayback':
                     //Check if all fragments have played to their full length
-                    checkState = interfaceContext.checkFragmentsFullyPlayed();
+                    checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage);
                     break;
                 case 'fragmentComments':
-                    checkState = interfaceContext.checkAllCommented();
+                    checkState = interfaceContext.checkAllCommented(checks[i].errorMessage);
                     break;
                 default:
                     console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
--- a/js/core.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/js/core.js	Sun Apr 23 11:32:45 2017 +0100
@@ -553,6 +553,7 @@
     this.currentIndex = null;
     this.node = null;
     this.store = null;
+    var lastNodeStart;
     $(window).keypress(function (e) {
         if (e.keyCode == 13 && popup.popup.style.visibility == 'visible') {
             console.log(e);
@@ -1048,6 +1049,7 @@
         var node = this.popupOptions[this.currentIndex],
             converter = new showdown.Converter(),
             p = new DOMParser();
+        lastNodeStart = new Date();
         this.popupResponse.innerHTML = "";
         this.popupTitle.innerHTML = "";
         this.popupTitle.appendChild(p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").getElementsByTagName("body")[0].firstElementChild);
@@ -1110,7 +1112,13 @@
             return;
         }
         var node = this.popupOptions[this.currentIndex],
-            pass = true;
+            pass = true,
+            timeDelta = (new Date() - lastNodeStart) / 1000.0;
+        if (timeDelta < node.specification.minWait) {
+            interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds");
+            return;
+        }
+        node.elapsedTime = timeDelta;
         if (node.specification.type == 'question') {
             // Must extract the question data
             pass = processQuestion.call(this, node);
@@ -3001,6 +3009,22 @@
         return volume;
     })();
 
+    this.imageHolder = (function () {
+        var imageController = {};
+        imageController.root = document.createElement("div");
+        imageController.root.id = "imageController";
+        imageController.img = document.createElement("img");
+        imageController.root.appendChild(imageController.img);
+        imageController.setImage = function (src) {
+            imageController.img.src = "";
+            if (typeof src !== "string" || src.length === undefined) {
+                return;
+            }
+            imageController.img.src = src;
+        };
+        return imageController;
+    })();
+
     this.calibrationModuleObject = null;
     this.calibrationModule = function () {
         // This creates an on-page calibration module
@@ -3092,7 +3116,7 @@
 
     // Global Checkers
     // These functions will help enforce the checkers
-    this.checkHiddenAnchor = function () {
+    this.checkHiddenAnchor = function (message) {
         var anchors = audioEngineContext.audioObjects.filter(function (ao) {
             return ao.specification.type === "anchor";
         });
@@ -3101,14 +3125,18 @@
         });
         if (state) {
             console.log('Anchor node not below marker value');
-            interfaceContext.lightbox.post("Message", 'Please keep listening');
+            if (message) {
+                interfaceContext.lightbox.post("Message", message);
+            } else {
+                interfaceContext.lightbox.post("Message", 'Please keep listening');
+            }
             this.storeErrorNode('Anchor node not below marker value');
             return false;
         }
         return true;
     };
 
-    this.checkHiddenReference = function () {
+    this.checkHiddenReference = function (message) {
         var references = audioEngineContext.audioObjects.filter(function (ao) {
             return ao.specification.type === "reference";
         });
@@ -3117,14 +3145,18 @@
         });
         if (state) {
             console.log('Reference node not below marker value');
-            interfaceContext.lightbox.post("Message", 'Please keep listening');
+            if (message) {
+                interfaceContext.lightbox.post("Message", message);
+            } else {
+                interfaceContext.lightbox.post("Message", 'Please keep listening');
+            }
             this.storeErrorNode('Reference node not below marker value');
             return false;
         }
         return true;
     };
 
-    this.checkFragmentsFullyPlayed = function () {
+    this.checkFragmentsFullyPlayed = function (message) {
         // Checks the entire file has been played back
         // NOTE ! This will return true IF playback is Looped!!!
         if (audioEngineContext.loopPlayback) {
@@ -3164,14 +3196,17 @@
                 }
             }
             str_start += ". Please keep listening";
-            console.log("[ALERT]: " + str_start);
-            this.storeErrorNode("[ALERT]: " + str_start);
+            console.log(str_start);
+            this.storeErrorNode(str_start);
+            if (message) {
+                str_start = message;
+            }
             interfaceContext.lightbox.post("Error", str_start);
             return false;
         }
         return true;
     };
-    this.checkAllMoved = function () {
+    this.checkAllMoved = function (message) {
         var str = "You have not moved ";
         var failed = [];
         audioEngineContext.audioObjects.forEach(function (ao) {
@@ -3191,12 +3226,15 @@
             str += 'and ' + failed[i];
         }
         str += '.';
-        interfaceContext.lightbox.post("Error", str);
         console.log(str);
         this.storeErrorNode(str);
+        if (message) {
+            str = message;
+        }
+        interfaceContext.lightbox.post("Error", str);
         return false;
     };
-    this.checkAllPlayed = function () {
+    this.checkAllPlayed = function (message) {
         var str = "You have not played ";
         var failed = [];
         audioEngineContext.audioObjects.forEach(function (ao) {
@@ -3216,12 +3254,15 @@
             str += 'and ' + failed[i];
         }
         str += '.';
-        interfaceContext.lightbox.post("Error", str);
         console.log(str);
         this.storeErrorNode(str);
+        if (message) {
+            str = message;
+        }
+        interfaceContext.lightbox.post("Error", str);
         return false;
     };
-    this.checkAllCommented = function () {
+    this.checkAllCommented = function (message) {
         var str = "You have not commented on all the fragments.";
         var cont = true,
             boxes = this.commentBoxes.boxes,
@@ -3229,15 +3270,18 @@
             i;
         for (i = 0; i < numBoxes; i++) {
             if (boxes[i].trackCommentBox.value === "") {
-                interfaceContext.lightbox.post("Error", str);
                 console.log(str);
                 this.storeErrorNode(str);
+                if (message) {
+                    str = message;
+                }
+                interfaceContext.lightbox.post("Error", str);
                 return false;
             }
         }
         return true;
     };
-    this.checkScaleRange = function () {
+    this.checkScaleRange = function (message) {
         var page = testState.getCurrentTestPage();
         var interfaceObject = page.interfaces;
         var state = true;
@@ -3275,6 +3319,9 @@
         if (state === false) {
             console.log(str);
             this.storeErrorNode(str);
+            if (message) {
+                str = message;
+            }
             interfaceContext.lightbox.post("Error", str);
         }
         return state;
@@ -3505,7 +3552,7 @@
             var clone = this.parent.root.cloneNode(true);
             hold.appendChild(clone);
             var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=";
-            if (this.parent.filenamePrefix.length == 0) {
+            if (this.parent.filenamePrefix.length === 0) {
                 saveURL += "save";
             } else {
                 saveURL += this.parent.filenamePrefix;
@@ -3598,6 +3645,7 @@
                 }
                 surveyresult = surveyresult.nextElementSibling;
             }
+            surveyresult.setAttribute("duration", node.elapsedTime);
             switch (node.specification.type) {
                 case "number":
                 case "question":
--- a/js/specification.js	Sun Apr 23 10:17:33 2017 +0100
+++ b/js/specification.js	Sun Apr 23 11:32:45 2017 +0100
@@ -223,6 +223,7 @@
             this.options = [];
             this.min = undefined;
             this.max = undefined;
+            this.minWait = undefined;
             this.step = undefined;
             this.conditions = [];
 
@@ -414,6 +415,7 @@
     this.interfaceNode = function (specification) {
         this.title = undefined;
         this.name = undefined;
+        this.image = undefined;
         this.options = [];
         this.scales = [];
         this.schema = schemaRoot.getAllElementsByName('interface')[1];
@@ -440,9 +442,16 @@
                         option[attributeName] = projectAttr;
                     }
                 }
+                if (option.type == "check" && ioNode.firstElementChild) {
+                    option.errorMessage = ioNode.firstElementChild.textContent;
+                }
                 this.options.push(option);
             }
-
+            // Get the image node
+            var imageNode = xml.getElementsByTagName("image");
+            if (imageNode.length == 1) {
+                this.image = imageNode[0].getAttribute("src");
+            }
             // Now the scales nodes
             var scaleParent = xml.getElementsByTagName('scales');
             if (scaleParent.length == 1) {
@@ -470,8 +479,18 @@
                 var child = doc.createElement("interfaceoption");
                 child.setAttribute("type", option.type);
                 child.setAttribute("name", option.name);
+                if (option.type == "check" && option.errorMessage !== undefined) {
+                    var errorMessage = doc.createElement("errormessage");
+                    errorMessage.textContent = option.errorMessage;
+                    child.appendChild(errorMessage);
+                }
                 node.appendChild(child);
             });
+            if (typeof this.image == "string" && this.image.length !== 0) {
+                var imgNode = doc.createElement("image");
+                imgNode.setAttribute("src", this.image);
+                node.appendChild(imgNode);
+            }
             if (this.scales.length !== 0) {
                 var scales = doc.createElement("scales");
                 this.scales.forEach(function (scale) {
@@ -789,6 +808,7 @@
             this.startTime = undefined;
             this.stopTime = undefined;
             this.sampleRate = undefined;
+            this.image = undefined;
             this.alternatives = [];
             this.schema = schemaRoot.getAllElementsByName('audioelement')[0];
             this.parent = undefined;
--- a/tests/examples/AB_example.xml	Sun Apr 23 10:17:33 2017 +0100
+++ b/tests/examples/AB_example.xml	Sun Apr 23 11:32:45 2017 +0100
@@ -1,39 +1,39 @@
 <?xml version="1.0" encoding="utf-8"?>
     <waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd">
-        <setup interface="AB" projectReturn="save.php" randomiseOrder='true' poolSize="2" loudness="-23" playOne="true">
+        <setup interface="AB" projectReturn="save.php" randomiseOrder="false" poolSize="2" loudness="-23" playOne="true">
             <survey location="before">
-                <surveyentry type="question" id="sessionId" mandatory="true">
+                <surveyquestion id="sessionId" mandatory="true">
                     <statement>Please enter your name.</statement>
-                </surveyentry>
-                <surveyentry type="checkbox" id="checkboxtest" mandatory="true">
+                </surveyquestion>
+                <surveycheckbox id="checkboxtest" mandatory="true">
                     <statement>Please select with which activities you have any experience (example checkbox question)</statement>
                     <option name="musician">Playing a musical instrument</option>
                     <option name="soundengineer">Recording or mixing audio</option>
                     <option name="developer">Developing audio software</option>
                     <option name="hwdesigner">Designing or building audio hardware</option>
                     <option name="researcher">Research in the field of audio</option>
-                </surveyentry>
-                <surveyentry type="statement" id="test-intro">
+                </surveycheckbox>
+                <surveystatement id="test-intro">
                     <statement>This is an example of an 'AB'-style test, with two pages, using the test stimuli in 'example_eval/'. The 'playOne' configuration option means a fragment has to be finished playing before another fragment can be auditioned. </statement>
-                </surveyentry>
+                </surveystatement>
             </survey>
             <survey location="after">
-                <surveyentry type="question" id="location" mandatory="true" boxsize="large">
+                <surveyquestion id="location" mandatory="true" boxsize="large">
                     <statement>Please enter your location. (example mandatory text question)</statement>
-                </surveyentry>
-                <surveyentry type="number" id="age" min="0">
+                </surveyquestion>
+                <surveynumber id="age" min="0">
                     <statement>Please enter your age (example non-mandatory number question)</statement>
-                </surveyentry>
-                <surveyentry type="radio" id="rating">
+                </surveynumber>
+                <surveyradio id="rating">
                     <statement>Please rate this interface (example radio button question)</statement>
                     <option name="bad">Bad</option>
                     <option name="poor">Poor</option>
                     <option name="good">Good</option>
                     <option name="great">Great</option>
-                </surveyentry>
-                <surveyentry type="statement" id="test-thank-you">
+                </surveyradio>
+                <surveystatement id="test-thank-you">
                     <statement>Thank you for taking this listening test. Please click 'submit' and your results will appear in the 'saves/' folder.</statement>
-                </surveyentry>
+                </surveystatement>
             </survey>
             <metric>
                 <metricenable>testTimer</metricenable>
@@ -46,7 +46,9 @@
             </metric>
             <interface>
                 <interfaceoption type="check" name="fragmentMoved" />
-                <interfaceoption type="check" name="scalerange" min="25" max="75" />
+                <interfaceoption type="check" name="scalerange" min="25" max="75">
+                    <errormessage>Test Error Message</errormessage>
+                </interfaceoption>
                 <interfaceoption type="show" name='playhead' />
                 <interfaceoption type="show" name="page-count" />
                 <interfaceoption type="show" name='volume' />
@@ -56,19 +58,20 @@
         <page id='test-0' hostURL="media/example/" randomiseOrder='true' repeatCount='0' loop='false' loudness="-12">
             <commentboxprefix>Comment on fragment</commentboxprefix>
             <interface>
-                <title>Depth</title>
+                <title>Which sounds most like a drum?</title>
+                <image src="https://upload.wikimedia.org/wikipedia/commons/0/0a/Drumkit-icon.png" />
             </interface>
             <audioelement url="0.wav" id="track-0" />
             <audioelement url="1.wav" id="track-1" />
             <survey location="before">
-                <surveyentry type="statement" id="test-0-intro">
-                    <statement>A two way comparison using randomised element order, automatic loudness and synchronised looping.</statement>
-                </surveyentry>
+                <surveystatement id="test-0-intro">
+                    <statement>A two way comparison using randomised element order, automatic loudness and synchronised looping. Also an embedded image</statement>
+                </surveystatement>
             </survey>
             <survey location="after">
-                <surveyentry type="question" id="genre-0" mandatory="true">
+                <surveyquestion id="genre-0" mandatory="true">
                     <statement>Please enter the genre.</statement>
-                </surveyentry>
+                </surveyquestion>
             </survey>
         </page>
         <page id='test-1' hostURL="media/example/" randomiseOrder='true' repeatCount='0' loop='false' loudness="-12">
@@ -84,14 +87,14 @@
             <audioelement url="5.wav" id="track-7" />
             <audioelement url="6.wav" id="track-8" />
             <survey location="before">
-                <surveyentry type="statement" id="test-1-intro">
+                <surveystatement id="test-1-intro">
                     <statement>A 7 way comparison using randomised element order and synchronised looping.</statement>
-                </surveyentry>
+                </surveystatement>
             </survey>
             <survey location="after">
-                <surveyentry type="question" id="genre-1" mandatory="true">
+                <surveyquestion id="genre-1" mandatory="true">
                     <statement>Please enter the genre.</statement>
-                </surveyentry>
+                </surveyquestion>
             </survey>
         </page>
     </waet>
--- a/xml/test-schema.xsd	Sun Apr 23 10:17:33 2017 +0100
+++ b/xml/test-schema.xsd	Sun Apr 23 11:32:45 2017 +0100
@@ -29,6 +29,8 @@
 
         <xs:attribute name="playOne" type="xs:boolean" default="false" />
 
+        <xs:attribute name="minWait" type="xs:nonNegativeInteger" default="0" />
+
         <!-- define complex elements-->
         <xs:element name="waet">
             <xs:complexType>
@@ -120,8 +122,16 @@
             <xs:complexType>
                 <xs:sequence>
                     <xs:element ref="title" minOccurs="0" maxOccurs="1" />
+                    <xs:element name="image" minOccurs="0" maxOccurs="1">
+                        <xs:complexType>
+                            <xs:attribute name="src" type="xs:anyURI" use="required" />
+                        </xs:complexType>
+                    </xs:element>
                     <xs:element name="interfaceoption" minOccurs="0" maxOccurs="unbounded">
                         <xs:complexType>
+                            <xs:sequence>
+                                <xs:element name="errormessage" type="xs:string" minOccurs="0" maxOccurs="1" />
+                            </xs:sequence>
                             <xs:attribute name="type" use="required">
                                 <xs:simpleType>
                                     <xs:restriction base="xs:string">
@@ -227,6 +237,7 @@
                         </xs:restriction>
                     </xs:simpleType>
                 </xs:attribute>
+                <xs:attribute name="image" type="xs:anyURI" use="optional" />
             </xs:complexType>
         </xs:element>
 
@@ -332,6 +343,7 @@
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
                 <xs:attribute ref="mandatory" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="boxsize" default="normal">
                     <xs:simpleType>
                         <xs:restriction base="xs:string">
@@ -365,6 +377,7 @@
                 <xs:attribute ref="mandatory" />
                 <xs:attribute name="min" type="xs:decimal" />
                 <xs:attribute name="max" type="xs:decimal" />
+                <xs:attribute ref="minWait" />
             </xs:complexType>
         </xs:element>
 
@@ -386,6 +399,7 @@
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
                 <xs:attribute ref="mandatory" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="min" type="xs:decimal" />
                 <xs:attribute name="max" type="xs:decimal" />
             </xs:complexType>
@@ -409,6 +423,7 @@
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
                 <xs:attribute ref="mandatory" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="min" type="xs:decimal" />
                 <xs:attribute name="max" type="xs:decimal" />
             </xs:complexType>
@@ -424,6 +439,7 @@
                 </xs:sequence>
                 <xs:attribute ref="id" use="required" />
                 <xs:attribute ref="name" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="min" use="required" type="xs:decimal" />
                 <xs:attribute name="max" use="required" type="xs:decimal" />
             </xs:complexType>
@@ -435,6 +451,7 @@
                     <xs:element ref="statement" minOccurs="1" maxOccurs="1" />
                 </xs:sequence>
                 <xs:attribute ref="id" use="required" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="url" use="required" type="xs:string" />
             </xs:complexType>
         </xs:element>
@@ -445,6 +462,7 @@
                     <xs:element ref="statement" minOccurs="1" maxOccurs="1" />
                 </xs:sequence>
                 <xs:attribute ref="id" use="required" />
+                <xs:attribute ref="minWait" />
                 <xs:attribute name="url" use="required" type="xs:string" />
             </xs:complexType>
         </xs:element>