changeset 472:23f77550b842 Dev_main

Added new horizontal slider class. Launched using ABC test interface.
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Wed, 13 Jan 2016 12:27:38 +0000
parents 3a9b869ba7f8
children 5bd699bc44d2
files core.js example_eval/horizontal_example.xml interfaces/horizontal-sliders.css interfaces/horizontal-sliders.js interfaces/mushra.js
diffstat 5 files changed, 591 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/core.js	Wed Jan 13 10:31:31 2016 +0000
+++ b/core.js	Wed Jan 13 12:27:38 2016 +0000
@@ -174,6 +174,17 @@
 		css.href = 'interfaces/AB.css';
 		
 		document.getElementsByTagName("head")[0].appendChild(css);
+		case "ABC":
+		// Above enumerate to horizontal sliders
+		interfaceJS.setAttribute("src","interfaces/horizontal-sliders.js");
+		
+		// horizontal-sliders comes with a css file
+		var css = document.createElement('link');
+		css.rel = 'stylesheet';
+		css.type = 'text/css';
+		css.href = 'interfaces/horizontal-sliders.css';
+		
+		document.getElementsByTagName("head")[0].appendChild(css);
 	}
 	document.getElementsByTagName("head")[0].appendChild(interfaceJS);
 	
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example_eval/horizontal_example.xml	Wed Jan 13 12:27:38 2016 +0000
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd">
+	<setup interface="ABC" projectReturn="save.php">
+		<metric>
+			<metricenable>testTimer</metricenable>
+			<metricenable>elementTimer</metricenable>
+			<metricenable>elementInitialPosition</metricenable>
+			<metricenable>elementTracker</metricenable>
+			<metricenable>elementFlagListenedTo</metricenable>
+			<metricenable>elementFlagMoved</metricenable>
+			<metricenable>elementListenTracker</metricenable>
+		</metric>
+		<interface>
+			<interfaceoption type="check" name="fragmentMoved"/>
+			<interfaceoption type="check" name="scalerange" min="25" max="75"/>
+			<interfaceoption type="show" name='playhead'/>
+			<interfaceoption type="show" name="page-count"/>
+		</interface>
+	</setup>
+	<page id='test-0' hostURL="example_eval/" randomiseOrder='true' repeatCount='0' loop='true' showElementComments='true' loudness="-12">
+		<interface>
+			<scales>
+				<scalelabel position="0">-50</scalelabel>
+				<scalelabel position="50">0</scalelabel>
+				<scalelabel position="100">50</scalelabel>
+			</scales>
+		</interface>
+		<audioelement url="1.wav" id="track-1"/>
+		<audioelement url="1.wav" id="track-2"/>
+	</page>
+</waet>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interfaces/horizontal-sliders.css	Wed Jan 13 12:27:38 2016 +0000
@@ -0,0 +1,150 @@
+/*
+ * Hold any style information for MUSHRA interface. Customise if you like to make the interface your own!
+ * 
+ */
+body {
+	/* Set the background colour (note US English spelling) to grey*/
+	background-color: #ddd
+}
+
+div.pageTitle {
+	width: auto;
+	height: 20px;
+	margin: 10px 0px;
+}
+
+div.pageTitle span{
+	font-size: 1.5em;
+}
+
+div.testHalt {
+	/* Specify any colouring during the test halt for pre/post questions */
+	background-color: rgba(0,0,0,0.5);
+	/* Don't mess with this bit */
+	z-index: 2;
+	width: 100%;
+	height: 100%;
+	position: absolute;
+	left: 0px;
+	top: 0px;
+}
+
+button {
+	/* Specify any button structure or style */
+	min-width: 20px;
+	background-color: #ddd
+}
+
+div#slider-holder {
+	height: inherit;
+	position: absolute;
+	left: 0px;
+	z-index: 3;
+	margin-top:25px;
+}
+
+div#scale-holder {
+	height: inherit;
+	position: absolute;
+	left: 0px;
+	z-index: 2;
+}
+
+div#scale-text-holder {
+	position:relative;
+	float: left;
+}
+div.scale-text {
+	position: absolute;
+	font-size: 1.2em;
+}
+
+canvas#scale-canvas {
+	position: relative;
+	float: left;
+}
+
+div.track-slider {
+	float: left;
+	height: 94px;
+	border: solid;
+	border-width: 1px;
+	border-color: black;
+	padding:2px;
+	margin-left: 94px;
+	margin-bottom: 25px;
+}
+
+div.track-slider-title {
+	float: left;
+	padding-top: 40px;
+	width: 100px;
+}
+
+button.track-slider-button {
+	float: left;
+	width: 100px;
+	height: 94px;
+}
+
+
+button.outside-reference {
+	width:120px;
+	height:20px;
+	margin-bottom:5px;
+	position: absolute;
+}
+
+div.track-slider-playing {
+	background-color: #FFDDDD;
+}
+
+input.track-slider-range {
+	float: left;
+	margin: 2px 0px;
+}
+
+input[type=range]
+{
+    height: 94px;
+    padding: 0 10px;
+    color: rgb(255, 144, 144);
+}
+
+input[type=range]::-webkit-slider-runnable-track {
+	width: 8px;
+	cursor: pointer;
+	background: #fff;
+	border-radius: 4px;
+	border: 1px solid #000;
+}
+
+input[type=range]::-moz-range-track {
+	width: 8px;
+	cursor: pointer;
+	background: #fff;
+	border-radius: 4px;
+	border: 1px solid #000;
+}
+
+input.track-slider-not-moved[type=range]::-webkit-slider-runnable-track {
+	background: #aaa;
+}
+
+input.track-slider-not-moved[type=range]::-moz-range-track {
+	background: #aaa;
+}
+
+
+input[type=range]::-moz-range-thumb {
+	margin-left: -7px;
+	cursor: pointer;
+	margin-top: -1px;
+	box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+}
+
+input[type=range]::-webkit-slider-thumb {
+	cursor: pointer;
+	margin-top: -1px;
+	margin-left: -4px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interfaces/horizontal-sliders.js	Wed Jan 13 12:27:38 2016 +0000
@@ -0,0 +1,398 @@
+// Once this is loaded and parsed, begin execution
+loadInterface();
+
+function loadInterface() {
+	// Use this to do any one-time page / element construction. For instance, placing any stationary text objects,
+	// holding div's, or setting up any nodes which are present for the entire test sequence
+	
+	// The injection point into the HTML page
+	interfaceContext.insertPoint = document.getElementById("topLevelBody");
+	var testContent = document.createElement('div');
+	testContent.id = 'testContent';
+	
+	// Create the top div for the Title element
+	var titleAttr = specification.title;
+	var title = document.createElement('div');
+	title.className = "title";
+	title.align = "center";
+	var titleSpan = document.createElement('span');
+	
+	// Set title to that defined in XML, else set to default
+	if (titleAttr != undefined) {
+		titleSpan.textContent = titleAttr;
+	} else {
+		titleSpan.textContent =  'Listening test';
+	}
+	// Insert the titleSpan element into the title div element.
+	title.appendChild(titleSpan);
+	
+	var pagetitle = document.createElement('div');
+	pagetitle.className = "pageTitle";
+	pagetitle.align = "center";
+	var titleSpan = document.createElement('span');
+	titleSpan.id = "pageTitle";
+	pagetitle.appendChild(titleSpan);
+	
+	// Create Interface buttons!
+	var interfaceButtons = document.createElement('div');
+	interfaceButtons.id = 'interface-buttons';
+	interfaceButtons.style.height = '25px';
+	
+	// Create playback start/stop points
+	var playback = document.createElement("button");
+	playback.innerHTML = 'Stop';
+	playback.id = 'playback-button';
+	playback.style.float = 'left';
+	// onclick function. Check if it is playing or not, call the correct function in the
+	// audioEngine, change the button text to reflect the next state.
+	playback.onclick = function() {
+		if (audioEngineContext.status == 1) {
+			audioEngineContext.stop();
+			this.innerHTML = 'Stop';
+            var time = audioEngineContext.timer.getTestTime();
+            console.log('Stopped at ' + time); // DEBUG/SAFETY
+		}
+	};
+	// Create Submit (save) button
+	var submit = document.createElement("button");
+	submit.innerHTML = 'Submit';
+	submit.onclick = buttonSubmitClick;
+	submit.id = 'submit-button';
+	submit.style.float = 'left';
+	// Append the interface buttons into the interfaceButtons object.
+	interfaceButtons.appendChild(playback);
+	interfaceButtons.appendChild(submit);
+	
+	// Create a slider box
+	var sliderBox = document.createElement('div');
+	sliderBox.style.width = "100%";
+	sliderBox.style.height = window.innerHeight - 200+12 + 'px';
+	sliderBox.style.marginBottom = '10px';
+	sliderBox.id = 'slider';
+	var scaleHolder = document.createElement('div');
+	scaleHolder.id = "scale-holder";
+	scaleHolder.style.marginLeft = "107px";
+	sliderBox.appendChild(scaleHolder);
+	var scaleText = document.createElement('div');
+	scaleText.id = "scale-text-holder";
+	scaleText.style.height = "25px";
+	scaleText.style.width = "100%";
+	scaleHolder.appendChild(scaleText);
+	var scaleCanvas = document.createElement('canvas');
+	scaleCanvas.id = "scale-canvas";
+	scaleCanvas.style.marginLeft = "100px";
+	scaleHolder.appendChild(scaleCanvas);
+	var sliderObjectHolder = document.createElement('div');
+	sliderObjectHolder.id = 'slider-holder';
+	sliderObjectHolder.align = "center";
+	sliderBox.appendChild(sliderObjectHolder);
+	
+	// Global parent for the comment boxes on the page
+	var feedbackHolder = document.createElement('div');
+	feedbackHolder.id = 'feedbackHolder';
+	
+	testContent.style.zIndex = 1;
+	interfaceContext.insertPoint.innerHTML = null; // Clear the current schema
+	
+	// Inject into HTML
+	testContent.appendChild(title); // Insert the title
+	testContent.appendChild(pagetitle);
+	testContent.appendChild(interfaceButtons);
+	testContent.appendChild(sliderBox);
+	testContent.appendChild(feedbackHolder);
+	interfaceContext.insertPoint.appendChild(testContent);
+
+	// Load the full interface
+	testState.initialise();
+	testState.advanceState();
+};
+
+function loadTest(page)
+{
+	// Called each time a new test page is to be build. The page specification node is the only item passed in
+	var id = page.id;
+	
+	var feedbackHolder = document.getElementById('feedbackHolder');
+	var interfaceObj = page.interfaces;
+	if (interfaceObj.length > 1)
+	{
+		console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
+	}
+	interfaceObj = interfaceObj[0];
+	if(interfaceObj.title != null)
+	{
+		document.getElementById("pageTitle").textContent = interfaceObj.title;
+	}
+	
+	// Delete outside reference
+	var outsideReferenceHolder = document.getElementById('outside-reference');
+	if (outsideReferenceHolder != null) {
+		document.getElementById('interface-buttons').removeChild(outsideReferenceHolder);
+	}
+	
+	var sliderBox = document.getElementById('slider-holder');
+	feedbackHolder.innerHTML = null;
+	sliderBox.innerHTML = null;
+	
+	var commentBoxPrefix = "Comment on track";
+	if (interfaceObj.commentBoxPrefix != undefined) {
+		commentBoxPrefix = interfaceObj.commentBoxPrefix;
+	}
+	var loopPlayback = page.loop;
+	
+	$(page.commentQuestions).each(function(index,element) {
+		var node = interfaceContext.createCommentQuestion(element);
+		feedbackHolder.appendChild(node.holder);
+	});
+	
+	// Find all the audioElements from the audioHolder
+	var label = 0;
+	$(page.audioElements).each(function(index,element){
+		// Find URL of track
+		// In this jQuery loop, variable 'this' holds the current audioElement.
+		
+		var audioObject = audioEngineContext.newTrack(element);
+		if (element.type == 'outside-reference')
+		{
+			// Construct outside reference;
+			var orNode = new outsideReferenceDOM(audioObject,index,document.getElementById('interface-buttons'));
+			audioObject.bindInterface(orNode);
+		} else {
+			var node = interfaceContext.createCommentBox(audioObject);
+		
+			// Create a slider per track
+			var sliderObj = new sliderObject(audioObject,label);
+			
+			if (typeof page.initialPosition === "number")
+			{
+				// Set the values
+				sliderObj.slider.value = page.initalPosition;
+			} else {
+				// Distribute it randomnly
+				sliderObj.slider.value = Math.random();
+			}
+			sliderBox.appendChild(sliderObj.holder);
+			audioObject.bindInterface(sliderObj);
+			label += 1;
+		}
+        
+	});
+	
+	// Auto-align
+	resizeWindow(null);
+}
+
+function sliderObject(audioObject,label)
+{
+	// An example node, you can make this however you want for each audioElement.
+	// However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following
+	// You attach them by calling audioObject.bindInterface( )
+	this.parent = audioObject;
+	
+	this.holder = document.createElement('div');
+	this.title = document.createElement('div');
+	this.slider = document.createElement('input');
+	this.play = document.createElement('button');
+	
+	this.holder.className = 'track-slider';
+	this.holder.style.width = window.innerWidth-200 + 'px';
+	this.holder.appendChild(this.title);
+	this.holder.appendChild(this.slider);
+	this.holder.appendChild(this.play);
+	this.holder.setAttribute('trackIndex',audioObject.id);
+	this.title.textContent = label;
+	this.title.className = 'track-slider-title';
+	
+	this.slider.type = "range";
+	this.slider.className = "track-slider-range track-slider-not-moved";
+	this.slider.min = "0";
+	this.slider.max = "1";
+	this.slider.step = "0.01";
+	this.slider.style.width = window.innerWidth-420 + 'px';
+	this.slider.onchange = function()
+	{
+		var time = audioEngineContext.timer.getTestTime();
+		var id = Number(this.parentNode.getAttribute('trackIndex'));
+		audioEngineContext.audioObjects[id].metric.moved(time,this.value);
+		console.log('slider '+id+' moved to '+this.value+' ('+time+')');
+		$(this).removeClass('track-slider-not-moved');
+	};
+	
+	this.play.className = 'track-slider-button';
+	this.play.textContent = "Loading...";
+	this.play.value = audioObject.id;
+	this.play.disabled = true;
+	this.play.onclick = function(event)
+	{
+		var id = Number(event.currentTarget.value);
+		//audioEngineContext.metric.sliderPlayed(id);
+		audioEngineContext.play(id);
+		$(".track-slider").removeClass('track-slider-playing');
+		$(event.currentTarget.parentElement).addClass('track-slider-playing');
+		var outsideReference = document.getElementById('outside-reference');
+		if (outsideReference != null) {
+			$(outsideReference).removeClass('track-slider-playing');
+		}
+	};
+	this.enable = function()
+	{
+		// This is used to tell the interface object that playback of this node is ready
+		this.play.disabled = false;
+		this.play.textContent = "Play";
+		$(this.slider).removeClass('track-slider-disabled');
+	};
+	this.updateLoading = function(progress)
+	{
+		// progress is a value from 0 to 100 indicating the current download state of media files
+	};
+	this.getValue = function()
+	{
+		// Return the current value of the object. If there is no value, return 0
+		return this.slider.value;
+	};
+	this.getPresentedId = function()
+	{
+		// Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
+		return this.title.textContent;
+	};
+	this.canMove = function()
+	{
+		// Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale.
+		// These are checked primarily if the interface check option 'fragmentMoved' is enabled.
+		return true;
+	};
+	this.exportXMLDOM = function(audioObject) {
+		// Called by the audioObject holding this element to export the interface <value> node.
+		// If there is no value node (such as outside reference), return null
+		// If there are multiple value nodes (such as multiple scale / 2D scales), return an array of nodes with each value node having an 'interfaceName' attribute
+		// Use storage.document.createElement('value'); to generate the XML node.
+		
+	};
+};
+
+function resizeWindow(event)
+{
+	// Called on every window resize event, use this to scale your page properly
+	
+	var numObj = document.getElementsByClassName('track-slider').length;
+	var totalHeight = (numObj * 125)-25;
+	document.getElementById('scale-holder').style.width = window.innerWidth-220 + 'px';
+	var canvas = document.getElementById('scale-canvas');
+	canvas.width = window.innerWidth-420;
+	canvas.height = totalHeight;
+	drawScale();
+}
+
+function drawScale()
+{
+	var interfaceObj = testState.currentStateMap.interfaces[0];
+	var scales = testState.currentStateMap.interfaces[0].scales;
+	scales = scales.sort(function(a,b) {
+		return a.position - b.position;
+	});
+	var canvas = document.getElementById('scale-canvas');
+	var ctx = canvas.getContext("2d");
+	var height = canvas.height;
+	var width = canvas.width;
+	var textHolder = document.getElementById('scale-text-holder');
+	textHolder.innerHTML = null;
+	ctx.fillStyle = "#000000";
+	ctx.setLineDash([1,4]);
+	for (var scale of scales)
+	{
+		var posPercent = scale.position / 100.0;
+		var posPix = Math.round(width * posPercent);
+		if(posPix<=0){posPix=1;}
+		if(posPix>=width){posPix=width-1;}
+		ctx.moveTo(posPix,0);
+		ctx.lineTo(posPix,height);
+		ctx.stroke();
+		
+		var text = document.createElement('div');
+		text.align = "center";
+		var textC = document.createElement('span');
+		textC.textContent = scale.text;
+		text.appendChild(textC);
+		text.className = "scale-text";
+		textHolder.appendChild(text);
+		text.style.width = Math.ceil($(text).width())+'px';
+		text.style.left = (posPix+100-($(text).width()/2)) +'px';
+	}
+}
+
+function buttonSubmitClick() // TODO: Only when all songs have been played!
+{
+	var checks = [];
+	checks = checks.concat(testState.currentStateMap.interfaces[0].options);
+	checks = checks.concat(specification.interfaces.options);
+	var canContinue = true;
+	
+	// Check that the anchor and reference objects are correctly placed
+	if (interfaceContext.checkHiddenAnchor() == false) {return;}
+	if (interfaceContext.checkHiddenReference() == false) {return;}
+	
+	for (var i=0; i<checks.length; i++) {
+		if (checks[i].type == 'check')
+		{
+			switch(checks[i].name) {
+			case 'fragmentPlayed':
+				// Check if all fragments have been played
+				var checkState = interfaceContext.checkAllPlayed();
+				if (checkState == false) {canContinue = false;}
+				break;
+			case  'fragmentFullPlayback':
+				// Check all fragments have been played to their full length
+				var checkState = interfaceContext.checkAllPlayed();
+				if (checkState == false) {canContinue = false;}
+				console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
+				break;
+			case 'fragmentMoved':
+				// Check all fragment sliders have been moved.
+				var checkState = interfaceContext.checkAllMoved();
+				if (checkState == false) {canContinue = false;}
+				break;
+			case 'fragmentComments':
+				// Check all fragment sliders have been moved.
+				var checkState = interfaceContext.checkAllCommented();
+				if (checkState == false) {canContinue = false;}
+				break;
+			//case 'scalerange':
+				// Check the scale is used to its full width outlined by the node
+				//var checkState = interfaceContext.checkScaleRange();
+				//if (checkState == false) {canContinue = false;}
+			//	break;
+			default:
+				console.log("WARNING - Check option "+checks[i].check+" is not supported on this interface");
+				break;
+			}
+
+		}
+		if (!canContinue) {break;}
+	}
+	
+    if (canContinue) {
+	    if (audioEngineContext.status == 1) {
+	        var playback = document.getElementById('playback-button');
+	        playback.click();
+	    // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
+	    } else
+	    {
+	        if (audioEngineContext.timer.testStarted == false)
+	        {
+	            alert('You have not started the test! Please press start to begin the test!');
+	            return;
+	        }
+	    }
+	    testState.advanceState();
+    } 
+}
+
+function pageXMLSave(store, pageSpecification)
+{
+	// MANDATORY
+	// Saves a specific test page
+	// You can use this space to add any extra nodes to your XML <audioHolder> saves
+	// Get the current <page> information in store (remember to appendChild your data to it)
+	// pageSpecification is the current page node configuration
+	// To create new XML nodes, use storage.document.createElement();
+}
\ No newline at end of file
--- a/interfaces/mushra.js	Wed Jan 13 10:31:31 2016 +0000
+++ b/interfaces/mushra.js	Wed Jan 13 12:27:38 2016 +0000
@@ -202,7 +202,7 @@
 	this.holder.appendChild(this.slider);
 	this.holder.appendChild(this.play);
 	this.holder.align = "center";
-	if (audioObject.id == 0)
+	if (label == 0)
 	{
 		this.holder.style.marginLeft = '0px';
 	}