n@656: /** n@656: * ape.js n@656: * Create the APE interface n@656: */ n@656: n@656: /* n@656: * n@656: * WARNING!!! n@656: * n@656: * YOU ARE VIEWING THE DEV VERSION. THERE IS NO GUARANTEE THIS WILL BE FULLY FUNCTIONAL n@656: * n@656: * WARNING!!! n@656: * n@656: */ n@656: n@662: var currentState; // Keep track of the current state (pre/post test, which test, final test? first test?) n@662: // preTest - In preTest state n@662: // testRun-ID - In test running, test Id number at the end 'testRun-2' n@662: // testRunPost-ID - Post test of test ID n@662: // testRunPre-ID - Pre-test of test ID n@662: // postTest - End of test, final submission! n@662: n@656: n@656: // Once this is loaded and parsed, begin execution n@656: loadInterface(projectXML); n@656: n@656: function loadInterface(xmlDoc) { n@656: n@656: // Get the dimensions of the screen available to the page n@656: var width = window.innerWidth; n@656: var height = window.innerHeight; n@656: n@656: // The injection point into the HTML page n@656: var insertPoint = document.getElementById("topLevelBody"); n@656: var testContent = document.createElement('div'); n@656: testContent.id = 'testContent'; n@656: n@656: n@656: // Decode parts of the xmlDoc that are needed n@656: // xmlDoc MUST already be parsed by jQuery! n@656: var xmlSetup = xmlDoc.find('setup'); n@656: // Should put in an error function here incase of malprocessed or malformed XML n@656: n@658: // Extract the different test XML DOM trees n@674: var audioHolders = xmlDoc.find('audioHolder'); n@674: audioHolders.each(function(index,element) { n@674: var repeatN = element.attributes['repeatCount'].value; n@674: for (var r=0; r<=repeatN; r++) { n@674: testXMLSetups[testXMLSetups.length] = element; n@674: } n@674: }); n@668: n@674: // New check if we need to randomise the test order n@674: var randomise = xmlSetup[0].attributes['randomiseOrder']; n@674: if (randomise != undefined) { n@674: randomise = Boolean(randomise.value); n@674: } else { n@674: randomise = false; n@674: } n@674: if (randomise) n@674: { n@674: // TODO: Implement Randomisation!! n@674: } n@668: n@674: // Obtain the metrics enabled n@674: var metricNode = xmlSetup.find('Metric'); n@674: var metricNode = metricNode.find('metricEnable'); n@674: metricNode.each(function(index,node){ n@674: var enabled = node.textContent; n@674: switch(enabled) n@674: { n@674: case 'testTimer': n@674: sessionMetrics.prototype.enableTestTimer = true; n@674: break; n@674: case 'elementTimer': n@674: sessionMetrics.prototype.enableElementTimer = true; n@674: break; n@674: case 'elementTracker': n@674: sessionMetrics.prototype.enableElementTracker = true; n@674: break; n@674: case 'elementInitalPosition': n@674: sessionMetrics.prototype.enableElementInitialPosition = true; n@674: break; n@674: case 'elementFlagListenedTo': n@674: sessionMetrics.prototype.enableFlagListenedTo = true; n@674: break; n@674: case 'elementFlagMoved': n@674: sessionMetrics.prototype.enableFlagMoved = true; n@674: break; n@674: case 'elementFlagComments': n@674: sessionMetrics.prototype.enableFlagComments = true; n@674: break; n@674: } n@674: }); n@658: n@676: // Create APE specific metric functions n@676: audioEngineContext.metric.initialiseTest = function() n@676: { n@676: var sliders = document.getElementsByClassName('track-slider'); n@676: for (var i=0; i= 0) n@676: { n@676: audioEngineContext.audioObjects[this.lastClicked].metric.listening(time); n@676: } n@676: this.lastClicked = id; n@676: audioEngineContext.audioObjects[id].metric.listening(time); n@676: } n@676: }; n@676: n@656: // Create the top div for the Title element n@656: var titleAttr = xmlSetup[0].attributes['title']; n@656: var title = document.createElement('div'); n@656: title.className = "title"; n@656: title.align = "center"; n@656: var titleSpan = document.createElement('span'); n@656: n@656: // Set title to that defined in XML, else set to default n@656: if (titleAttr != undefined) { n@656: titleSpan.innerHTML = titleAttr.value; n@656: } else { n@656: titleSpan.innerHTML = 'APE Tool'; n@656: } n@656: // Insert the titleSpan element into the title div element. n@656: title.appendChild(titleSpan); n@656: n@671: var pagetitle = document.createElement('div'); n@671: pagetitle.className = "pageTitle"; n@671: pagetitle.align = "center"; n@671: var titleSpan = document.createElement('span'); n@671: titleSpan.id = "pageTitle"; n@671: pagetitle.appendChild(titleSpan); n@671: n@656: // Store the return URL path in global projectReturn n@656: projectReturn = xmlSetup[0].attributes['projectReturn'].value; n@656: n@656: // Create Interface buttons! n@656: var interfaceButtons = document.createElement('div'); n@656: interfaceButtons.id = 'interface-buttons'; n@656: n@656: // MANUAL DOWNLOAD POINT n@656: // If project return is null, this MUST be specified as the location to create the download link n@656: var downloadPoint = document.createElement('div'); n@656: downloadPoint.id = 'download-point'; n@656: n@656: // Create playback start/stop points n@656: var playback = document.createElement("button"); n@656: playback.innerHTML = 'Start'; n@673: playback.id = 'playback-button'; n@656: // onclick function. Check if it is playing or not, call the correct function in the n@656: // audioEngine, change the button text to reflect the next state. n@656: playback.onclick = function() { n@656: if (audioEngineContext.status == 0) { n@656: audioEngineContext.play(); n@656: this.innerHTML = 'Stop'; n@656: } else { n@656: audioEngineContext.stop(); n@656: this.innerHTML = 'Start'; n@656: } n@656: }; n@656: // Create Submit (save) button n@656: var submit = document.createElement("button"); n@656: submit.innerHTML = 'Submit'; n@667: submit.onclick = buttonSubmitClick; n@673: submit.id = 'submit-button'; n@656: // Append the interface buttons into the interfaceButtons object. n@656: interfaceButtons.appendChild(playback); n@656: interfaceButtons.appendChild(submit); n@656: interfaceButtons.appendChild(downloadPoint); n@656: n@656: // Now create the slider and HTML5 canvas boxes n@656: n@656: // Create the div box to center align n@656: var sliderBox = document.createElement('div'); n@656: sliderBox.className = 'sliderCanvasDiv'; n@656: sliderBox.id = 'sliderCanvasHolder'; n@656: sliderBox.align = 'center'; n@656: n@656: // Create the slider box to hold the slider elements n@656: var canvas = document.createElement('div'); n@656: canvas.id = 'slider'; n@656: // Must have a known EXACT width, as this is used later to determine the ratings n@656: canvas.style.width = width - 100 +"px"; n@656: canvas.align = "left"; n@656: sliderBox.appendChild(canvas); n@656: n@671: // Create the div to hold any scale objects n@671: var scale = document.createElement('div'); n@671: scale.className = 'sliderScale'; n@671: scale.id = 'sliderScaleHolder'; n@671: scale.align = 'left'; n@671: sliderBox.appendChild(scale); n@671: n@656: // Global parent for the comment boxes on the page n@656: var feedbackHolder = document.createElement('div'); n@658: feedbackHolder.id = 'feedbackHolder'; n@656: n@662: testContent.style.zIndex = 1; n@662: insertPoint.innerHTML = null; // Clear the current schema n@662: n@656: // Create pre and post test questions n@662: var blank = document.createElement('div'); n@662: blank.className = 'testHalt'; n@656: n@662: var popupHolder = document.createElement('div'); n@662: popupHolder.id = 'popupHolder'; n@662: popupHolder.className = 'popupHolder'; n@662: popupHolder.style.position = 'absolute'; n@662: popupHolder.style.left = (window.innerWidth/2)-250 + 'px'; n@662: popupHolder.style.top = (window.innerHeight/2)-125 + 'px'; n@662: insertPoint.appendChild(popupHolder); n@662: insertPoint.appendChild(blank); n@662: hidePopup(); n@656: n@660: var preTest = xmlSetup.find('PreTest'); n@660: var postTest = xmlSetup.find('PostTest'); n@656: preTest = preTest[0]; n@656: postTest = postTest[0]; n@662: n@662: currentState = 'preTest'; n@656: n@656: // Create Pre-Test Box n@656: if (preTest != undefined && preTest.children.length >= 1) n@656: { n@662: showPopup(); n@663: preTestPopupStart(preTest); n@656: } n@662: n@662: // Inject into HTML n@662: testContent.appendChild(title); // Insert the title n@671: testContent.appendChild(pagetitle); n@662: testContent.appendChild(interfaceButtons); n@662: testContent.appendChild(sliderBox); n@662: testContent.appendChild(feedbackHolder); n@662: insertPoint.appendChild(testContent); n@656: n@660: // Load the full interface n@663: n@656: } n@656: n@663: function loadTest(id) n@658: { n@658: // Used to load a specific test page n@663: var textXML = testXMLSetups[id]; n@658: n@658: var feedbackHolder = document.getElementById('feedbackHolder'); n@658: var canvas = document.getElementById('slider'); n@658: feedbackHolder.innerHTML = null; n@658: canvas.innerHTML = null; n@671: n@671: // Setup question title n@671: var interfaceObj = $(textXML).find('interface'); n@671: var titleNode = interfaceObj.find('title'); n@671: if (titleNode[0] != undefined) n@671: { n@671: document.getElementById('pageTitle').textContent = titleNode[0].textContent; n@671: } n@671: var positionScale = canvas.style.width.substr(0,canvas.style.width.length-2); n@671: var offset = 50-8; // Half the offset of the slider (window width -100) minus the body padding of 8 n@671: // TODO: AUTOMATE ABOVE!! n@671: var scale = document.getElementById('sliderScaleHolder'); n@671: scale.innerHTML = null; n@671: interfaceObj.find('scale').each(function(index,scaleObj){ n@671: var position = Number(scaleObj.attributes['position'].value)*0.01; n@671: var pixelPosition = (position*positionScale)+offset; n@671: var scaleDOM = document.createElement('span'); n@671: scaleDOM.textContent = scaleObj.textContent; n@671: scale.appendChild(scaleDOM); n@671: scaleDOM.style.left = Math.floor((pixelPosition-($(scaleDOM).width()/2)))+'px'; n@671: }); n@658: n@658: // Extract the hostURL attribute. If not set, create an empty string. n@658: var hostURL = textXML.attributes['hostURL']; n@658: if (hostURL == undefined) { n@658: hostURL = ""; n@658: } else { n@658: hostURL = hostURL.value; n@658: } n@658: // Extract the sampleRate. If set, convert the string to a Number. n@658: var hostFs = textXML.attributes['sampleRate']; n@658: if (hostFs != undefined) { n@658: hostFs = Number(hostFs.value); n@658: } n@658: n@658: /// CHECK FOR SAMPLE RATE COMPATIBILITY n@658: if (hostFs != undefined) { n@658: if (Number(hostFs) != audioContext.sampleRate) { n@658: var errStr = 'Sample rates do not match! Requested '+Number(hostFs)+', got '+audioContext.sampleRate+'. Please set the sample rate to match before completing this test.'; n@658: alert(errStr); n@658: return; n@658: } n@658: } n@669: n@670: currentTestHolder = document.createElement('audioHolder'); n@670: currentTestHolder.id = textXML.id; n@670: currentTestHolder.repeatCount = textXML.attributes['repeatCount'].value; n@670: var currentPreTestHolder = document.createElement('preTest'); n@670: var currentPostTestHolder = document.createElement('postTest'); n@670: currentTestHolder.appendChild(currentPreTestHolder); n@670: currentTestHolder.appendChild(currentPostTestHolder); n@670: n@669: var randomise = textXML.attributes['randomiseOrder']; n@669: if (randomise != undefined) {randomise = randomise.value;} n@669: else {randomise = false;} n@669: n@658: var audioElements = $(textXML).find('audioElements'); n@658: audioElements.each(function(index,element){ n@669: // Find any blind-repeats n@669: // Not implemented yet, but just incase n@669: currentTrackOrder[index] = element; n@669: }); n@669: if (randomise) { n@669: // TODO: Randomise order n@669: } n@669: n@669: // Find all the audioElements from the audioHolder n@669: $(currentTrackOrder).each(function(index,element){ n@658: // Find URL of track n@658: // In this jQuery loop, variable 'this' holds the current audioElement. n@658: n@658: // Now load each audio sample. First create the new track by passing the full URL n@658: var trackURL = hostURL + this.attributes['url'].value; n@658: audioEngineContext.newTrack(trackURL); n@658: // Create document objects to hold the comment boxes n@658: var trackComment = document.createElement('div'); n@661: trackComment.className = 'comment-div'; n@658: // Create a string next to each comment asking for a comment n@658: var trackString = document.createElement('span'); n@658: trackString.innerHTML = 'Comment on track '+index; n@658: // Create the HTML5 comment box 'textarea' n@658: var trackCommentBox = document.createElement('textarea'); n@658: trackCommentBox.rows = '4'; n@658: trackCommentBox.cols = '100'; n@658: trackCommentBox.name = 'trackComment'+index; n@658: trackCommentBox.className = 'trackComment'; n@658: var br = document.createElement('br'); n@658: // Add to the holder. n@658: trackComment.appendChild(trackString); n@658: trackComment.appendChild(br); n@658: trackComment.appendChild(trackCommentBox); n@658: feedbackHolder.appendChild(trackComment); n@658: n@658: // Create a slider per track n@658: n@658: var trackSliderObj = document.createElement('div'); n@658: trackSliderObj.className = 'track-slider'; n@658: trackSliderObj.id = 'track-slider-'+index; n@658: // Distribute it randomnly n@658: var w = window.innerWidth - 100; n@658: w = Math.random()*w; n@658: trackSliderObj.style.left = Math.floor(w)+50+'px'; n@658: trackSliderObj.innerHTML = ''+index+''; n@658: trackSliderObj.draggable = true; n@658: trackSliderObj.ondragend = dragEnd; n@673: trackSliderObj.ondragstart = function() n@673: { n@673: var id = Number(this.id.substr(13,2)); // Maximum theoretical tracks is 99! n@673: audioEngineContext.metric.sliderMoveStart(id); n@673: }; n@658: n@658: // Onclick, switch playback to that track n@658: trackSliderObj.onclick = function() { n@658: // Get the track ID from the object ID n@658: var id = Number(this.id.substr(13,2)); // Maximum theoretical tracks is 99! n@673: audioEngineContext.metric.sliderPlayed(id); n@658: audioEngineContext.selectedTrack(id); n@658: }; n@658: n@658: canvas.appendChild(trackSliderObj); n@658: }); n@663: n@663: // Now process any pre-test commands n@663: n@668: var preTest = $(testXMLSetups[id]).find('PreTest')[0]; n@663: if (preTest.children.length > 0) n@663: { n@663: currentState = 'testRunPre-'+id; n@663: preTestPopupStart(preTest); n@663: showPopup(); n@663: } else { n@663: currentState = 'testRun-'+id; n@663: } n@663: } n@663: n@663: function preTestPopupStart(preTest) n@663: { n@663: var popupHolder = document.getElementById('popupHolder'); n@663: popupHolder.innerHTML = null; n@663: // Parse the first box n@663: var preTestOption = document.createElement('div'); n@663: preTestOption.id = 'preTest'; n@663: preTestOption.style.marginTop = '25px'; n@663: preTestOption.align = "center"; n@663: var child = preTest.children[0]; n@663: if (child.nodeName == 'statement') n@663: { n@663: preTestOption.innerHTML = ''+child.innerHTML+''; n@663: } else if (child.nodeName == 'question') n@663: { n@663: var questionId = child.attributes['id'].value; n@663: var textHold = document.createElement('span'); n@663: textHold.innerHTML = child.innerHTML; n@663: textHold.id = questionId + 'response'; n@663: var textEnter = document.createElement('textarea'); n@663: preTestOption.appendChild(textHold); n@663: preTestOption.appendChild(textEnter); n@663: } n@663: var nextButton = document.createElement('button'); n@663: nextButton.className = 'popupButton'; n@663: nextButton.value = '0'; n@663: nextButton.innerHTML = 'Next'; n@663: nextButton.onclick = popupButtonClick; n@663: n@663: popupHolder.appendChild(preTestOption); n@663: popupHolder.appendChild(nextButton); n@658: } n@658: n@662: function popupButtonClick() n@662: { n@662: // Global call from the 'Next' button click n@662: if (currentState == 'preTest') n@662: { n@662: // At the start of the preTest routine! n@663: var xmlTree = projectXML.find('setup'); n@663: var preTest = xmlTree.find('PreTest')[0]; n@663: this.value = preTestButtonClick(preTest,this.value); n@663: } else if (currentState.substr(0,10) == 'testRunPre') n@663: { n@663: //Specific test pre-test n@663: var testId = currentState.substr(11,currentState.length-10); n@668: var preTest = $(testXMLSetups[testId]).find('PreTest')[0]; n@663: this.value = preTestButtonClick(preTest,this.value); n@666: } else if (currentState.substr(0,11) == 'testRunPost') n@666: { n@666: // Specific test post-test n@666: var testId = currentState.substr(12,currentState.length-11); n@668: var preTest = $(testXMLSetups[testId]).find('PostTest')[0]; n@666: this.value = preTestButtonClick(preTest,this.value); n@665: } else if (currentState == 'postTest') n@665: { n@665: // At the end of the test, running global post test n@665: var xmlTree = projectXML.find('setup'); n@665: var PostTest = xmlTree.find('PostTest')[0]; n@665: this.value = preTestButtonClick(PostTest,this.value); n@662: } n@662: } n@662: n@663: function preTestButtonClick(preTest,index) n@658: { n@658: // Called on click of pre-test button n@660: // Need to find and parse preTest again! n@660: var preTestOption = document.getElementById('preTest'); n@660: // Check if current state is a question! n@662: if (preTest.children[index].nodeName == 'question') { n@662: var questionId = preTest.children[index].attributes['id'].value; n@660: var questionHold = document.createElement('comment'); n@660: var questionResponse = document.getElementById(questionId + 'response'); n@660: questionHold.id = questionId; n@660: questionHold.innerHTML = questionResponse.value; n@670: postPopupResponse(questionHold); n@660: } n@662: index++; n@662: if (index < preTest.children.length) n@660: { n@660: // More to process n@662: var child = preTest.children[index]; n@660: if (child.nodeName == 'statement') n@660: { n@660: preTestOption.innerHTML = ''+child.innerHTML+''; n@660: } else if (child.nodeName == 'question') n@660: { n@660: var textHold = document.createElement('span'); n@660: textHold.innerHTML = child.innerHTML; n@660: var textEnter = document.createElement('textarea'); n@660: textEnter.id = child.attributes['id'].value + 'response'; n@660: var br = document.createElement('br'); n@660: preTestOption.innerHTML = null; n@660: preTestOption.appendChild(textHold); n@660: preTestOption.appendChild(br); n@660: preTestOption.appendChild(textEnter); n@660: } n@660: } else { n@660: // Time to clear n@661: preTestOption.innerHTML = null; n@667: if (currentState != 'postTest') { n@667: hidePopup(); n@667: // Progress the state! n@667: advanceState(); n@667: } else { n@667: a = createProjectSave(projectReturn); n@667: preTestOption.appendChild(a); n@667: } n@660: } n@662: return index; n@660: } n@660: n@670: function postPopupResponse(response) n@670: { n@670: if (currentState == 'preTest') { n@670: preTestQuestions.appendChild(response); n@670: } else if (currentState == 'postTest') { n@670: postTestQuestions.appendChild(response); n@670: } else { n@670: // Inside a specific test n@670: if (currentState.substr(0,10) == 'testRunPre') { n@670: // Pre Test n@670: var store = $(currentTestHolder).find('preTest'); n@670: } else { n@670: // Post Test n@670: var store = $(currentTestHolder).find('postTest'); n@670: } n@670: store[0].appendChild(response); n@670: } n@670: } n@670: n@660: function showPopup() n@660: { n@661: var popupHolder = document.getElementById('popupHolder'); n@662: popupHolder.style.zIndex = 3; n@661: popupHolder.style.visibility = 'visible'; n@661: var blank = document.getElementsByClassName('testHalt')[0]; n@661: blank.style.zIndex = 2; n@661: blank.style.visibility = 'visible'; n@660: } n@660: n@660: function hidePopup() n@660: { n@661: var popupHolder = document.getElementById('popupHolder'); n@661: popupHolder.style.zIndex = -1; n@661: popupHolder.style.visibility = 'hidden'; n@661: var blank = document.getElementsByClassName('testHalt')[0]; n@661: blank.style.zIndex = -2; n@661: blank.style.visibility = 'hidden'; n@658: } n@658: n@656: function dragEnd(ev) { n@656: // Function call when a div has been dropped n@657: var slider = document.getElementById('slider'); n@675: var w = slider.style.width; n@675: w = Number(w.substr(0,w.length-2)); n@675: var x = ev.x; n@675: if (x >= 42 && x < w+42) { n@675: this.style.left = (x)+'px'; n@656: } else { n@675: if (x<42) { n@675: this.style.left = '42px'; n@656: } else { n@675: this.style.left = (w+42) + 'px'; n@656: } n@656: } n@673: audioEngineContext.metric.sliderMoved(); n@656: } n@656: n@663: function advanceState() n@663: { n@663: console.log(currentState); n@663: if (currentState == 'preTest') n@663: { n@663: // End of pre-test, begin the test n@663: loadTest(0); n@663: } else if (currentState.substr(0,10) == 'testRunPre') n@663: { n@663: // Start the test n@663: var testId = currentState.substr(11,currentState.length-10); n@663: currentState = 'testRun-'+testId; n@666: } else if (currentState.substr(0,11) == 'testRunPost') n@666: { n@668: var testId = currentState.substr(12,currentState.length-11); n@666: testEnded(testId); n@664: } else if (currentState.substr(0,7) == 'testRun') n@664: { n@664: var testId = currentState.substr(8,currentState.length-7); n@664: // Check if we have any post tests to perform n@668: var postXML = $(testXMLSetups[testId]).find('PostTest')[0]; n@664: if (postXML.children.length > 0) n@664: { n@666: currentState = 'testRunPost-'+testId; n@666: showPopup(); n@666: preTestPopupStart(postXML); n@664: } n@666: else { n@664: n@664: n@666: // No post tests, check if we have another test to perform instead n@666: testEnded(testId); n@664: } n@663: } n@663: console.log(currentState); n@663: } n@663: n@666: function testEnded(testId) n@666: { n@669: pageXMLSave(testId); n@666: if (testXMLSetups.length-1 > testId) n@666: { n@666: // Yes we have another test to perform n@668: testId = (Number(testId)+1); n@668: currentState = 'testRun-'+testId; n@668: loadTest(testId); n@666: } else { n@666: console.log('Testing Completed!'); n@666: currentState = 'postTest'; n@666: // Check for any post tests n@666: var xmlSetup = projectXML.find('setup'); n@666: var postTest = xmlSetup.find('PostTest')[0]; n@666: showPopup(); n@666: preTestPopupStart(postTest); n@666: } n@666: } n@666: n@664: function buttonSubmitClick() n@664: { n@673: if (audioEngineContext.status == 1) { n@673: var playback = document.getElementById('playback-button'); n@673: playback.click(); n@664: // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options n@673: } n@664: if (currentState.substr(0,7) == 'testRun') n@664: { n@673: audioEngineContext.timer.stopTest(); n@664: advanceState(); n@664: } n@664: } n@664: n@675: function convSliderPosToRate(id) n@675: { n@675: var w = document.getElementById('slider').style.width; n@675: var maxPix = w.substr(0,w.length-2); n@675: var slider = document.getElementsByClassName('track-slider')[id]; n@675: var pix = slider.style.left; n@675: pix = pix.substr(0,pix.length-2); n@675: var rate = (pix-42)/maxPix; n@675: return rate; n@675: } n@675: n@668: function pageXMLSave(testId) n@668: { n@668: // Saves a specific test page n@670: var xmlDoc = currentTestHolder; n@674: // Check if any session wide metrics are enabled n@674: var metric = document.createElement('metric'); n@674: if (audioEngineContext.metric.enableTestTimer) n@674: { n@674: var testTime = document.createElement('metricResult'); n@674: testTime.id = 'testTime'; n@674: testTime.textContent = audioEngineContext.timer.testDuration; n@674: metric.appendChild(testTime); n@674: } n@674: xmlDoc.appendChild(metric); n@669: var trackSliderObjects = document.getElementsByClassName('track-slider'); n@669: var commentObjects = document.getElementsByClassName('comment-div'); n@669: for (var i=0; i