view interfaces/timeline.js @ 2524:dde81c372fdc

Merge score_parser2 into score_parser
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Tue, 08 Nov 2016 09:58:31 +0000
parents 3c92c732fb05
children 03a9a90717ed c8e6991951ad
line wrap: on
line source
/**
 * WAET Timeline
 * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing.
 */

// 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

    interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema

    interfaceContext.insertPoint = document.getElementById("topLevelBody");
    var testContent = document.createElement("div");

    // Create the top div and Title element
    var title = document.createElement("div");
    title.className = "title";
    title.align = "center";
    var titleSpan = document.createElement("span");
    titleSpan.id = "test-title";
    titleSpan.textContent = "Listening Test";
    title.appendChild(titleSpan);

    var pagetitle = document.createElement("div");
    pagetitle.className = "pageTitle";
    pagetitle.align = "center";
    titleSpan = document.createElement("span");
    titleSpan.id = "page-title";
    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.onclick = function () {
        if (audioEngineContext.status == 1) {
            audioEngineContext.stop();
            this.innerHTML = "Stop";
            var time = audioEngineContext.timer.getTestTime();
            console.log("Stopped at " + time);
        }
    };
    // Create Submit (save) button
    var submit = document.createElement("button");
    submit.innerHTML = 'Next';
    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 outside reference holder
    var outsideRef = document.createElement("div");
    outsideRef.id = "outside-reference-holder";

    // Create content point
    var content = document.createElement("div");
    content.id = "timeline-test-content";

    //Inject
    testContent.appendChild(title);
    testContent.appendChild(pagetitle);
    testContent.appendChild(interfaceButtons);
    testContent.appendChild(outsideRef);
    testContent.appendChild(content);
    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 content = document.getElementById("timeline-test-content");
    content.innerHTML = "";
    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];

    //Set the page title
    if (typeof page.title == "string" && page.title.length > 0) {
        document.getElementById("test-title").textContent = page.title;
    }

    if (interfaceObj.title != null) {
        document.getElementById("page-title").textContent = interfaceObj.title;
    }

    // Delete outside reference
    var outsideReferenceHolder = document.getElementById("outside-reference-holder");
    outsideReferenceHolder.innerHTML = "";

    var commentBoxPrefix = "Comment on track";
    if (interfaceObj.commentBoxPrefix != undefined) {
        commentBoxPrefix = interfaceObj.commentBoxPrefix;
    }

    $(page.audioElements).each(function (index, element) {
        var audioObject = audioEngineContext.newTrack(element);
        if (page.audioElements.type == 'outside-reference') {
            var refNode = interfaceContext.outsideReferenceDOM(audioObject, index, outsideReferenceHolder);
            audioObject.bindInterface(orNode);
        } else {
            switch (audioObject.specification.parent.label) {
                case "none":
                    label = "";
                    break;
                case "letter":
                    label = String.fromCharCode(97 + index);
                    break;
                case "capital":
                    label = String.fromCharCode(65 + index);
                    break;
                default:
                    label = "" + index;
                    break;
            }
            var node = new interfaceObject(audioObject, label);

            content.appendChild(node.DOM);
            audioObject.bindInterface(node);
        }
    });

    resizeWindow();
}

function interfaceObject(audioObject, labelstr) {
    // Each audio object has a waveform guide and self-generated comments
    this.parent = audioObject;
    this.DOM = document.createElement("div");
    this.DOM.className = "timeline-element";
    this.DOM.id = audioObject.specification.id;

    var root = document.createElement("div");
    root.className = "timeline-element-content";
    this.DOM.appendChild(root);

    var label = document.createElement("div");
    label.style.textAlign = "center";
    var labelSpan = document.createElement("span");
    labelSpan.textContent = "Fragment " + labelstr;
    label.appendChild(labelSpan);
    root.appendChild(label);

    var canvasHolder = document.createElement("div");
    canvasHolder.className = "timeline-element-canvas-holder";
    var buttonHolder = document.createElement("div");
    buttonHolder.className = "timeline-element-button-holder";
    var commentHolder = document.createElement("div");
    commentHolder.className = "timeline-element-comment-holder";

    root.appendChild(canvasHolder);
    root.appendChild(buttonHolder);
    root.appendChild(commentHolder);

    this.comments = {
        parent: this,
        list: [],
        Comment: function (parent, time, str) {
            this.parent = parent;
            this.time = time;
            this.DOM = document.createElement("div");
            this.DOM.className = "comment-entry";
            var titleHolder = document.createElement("div");
            titleHolder.className = "comment-entry-header";
            this.title = document.createElement("span");
            if (str != undefined) {
                this.title.textContent = str;
            } else {
                this.title.textContent = "Time: " + time.toFixed(2) + "s";
            }
            titleHolder.appendChild(this.title);
            this.textarea = document.createElement("textarea");
            this.textarea.className = "comment-entry-text";
            this.DOM.appendChild(titleHolder);
            this.DOM.appendChild(this.textarea);

            this.clear = {
                DOM: document.createElement("button"),
                parent: this,
                handleEvent: function () {
                    this.parent.parent.deleteComment(this.parent);
                }
            }
            this.clear.DOM.textContent = "Delete";
            this.clear.DOM.addEventListener("click", this.clear);
            titleHolder.appendChild(this.clear.DOM);

            this.resize = function () {
                var w = window.innerWidth;
                w = Math.min(w, 800);
                w = Math.max(w, 200);
                var elem_w = w / 2.5;
                elem_w = Math.max(elem_w, 190);
                this.DOM.style.width = elem_w + "px";
                this.textarea.style.width = (elem_w - 5) + "px";
            }
            this.buildXML = function (root) {
                //storage.document.createElement();
                var node = storage.document.createElement("comment");
                var question = storage.document.createElement("question");
                var comment = storage.document.createElement("response");
                node.setAttribute("time", this.time);
                question.textContent = this.title.textContent;
                comment.textContent = this.textarea.value;
                node.appendChild(question);
                node.appendChild(comment);
                root.appendChild(node);
            }
            this.resize();
        },
        newComment: function (time) {
            var node = new this.Comment(this, time);
            this.list.push(node);
            commentHolder.appendChild(node.DOM);
            return node;
        },
        deleteComment: function (comment) {
            var index = this.list.findIndex(function (element, index, array) {
                if (element == comment) {
                    return true;
                }
                return false;
            }, comment);
            if (index == -1) {
                return false;
            }
            var node = this.list.splice(index, 1);
            comment.DOM.remove();
            this.parent.canvas.drawMarkers();
            return true;
        },
        clearList: function () {
            while (this.list.length > 0) {
                this.deleteComment(this.list[0]);
            }
        }
    }

    this.canvas = {
        parent: this,
        comments: this.comments,
        layer1: document.createElement("canvas"),
        layer2: document.createElement("canvas"),
        layer3: document.createElement("canvas"),
        layer4: document.createElement("canvas"),
        resize: function (w) {
            this.layer1.width = w;
            this.layer2.width = w;
            this.layer3.width = w;
            this.layer4.width = w;
            this.layer1.style.width = w + "px";
            this.layer2.style.width = w + "px";
            this.layer3.style.width = w + "px";
            this.layer4.style.width = w + "px";
            this.drawWaveform();
            this.drawMarkers();
        },
        handleEvent: function (event) {
            switch (event.currentTarget) {
                case this.layer1:
                    switch (event.type) {
                        case "mousemove":
                            this.drawMouse(event);
                            break;
                        case "mouseleave":
                            this.clearCanvas(this.layer1);
                            break;
                        case "click":
                            var rect = this.layer1.getBoundingClientRect();
                            var pixX = event.clientX - rect.left;
                            var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width;
                            this.comments.newComment(pixX * tpp);
                            this.drawMarkers();
                            break;
                    }
                    break;
            }
        },
        drawWaveform: function () {
            if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
                return;
            }
            var buffer = this.parent.parent.buffer.buffer;
            var context = this.layer4.getContext("2d");
            context.lineWidth = 1;
            context.strokeStyle = "#888";
            context.clearRect(0, 0, this.layer4.width, this.layer4.height);
            var data = buffer.getChannelData(0);
            var t_per_pixel = buffer.duration / this.layer4.width;
            var s_per_pixel = data.length / this.layer4.width;
            var pixX = 0;
            while (pixX < this.layer4.width) {
                var start = Math.floor(s_per_pixel * pixX);
                var end = Math.min(Math.ceil(s_per_pixel * (pixX + 1)), data.length);
                var frame = data.subarray(start, end);
                var min = frame[0];
                var max = min;
                for (var n = 0; n < frame.length; n++) {
                    if (frame[n] < min) {
                        min = frame[n];
                    }
                    if (frame[n] > max) {
                        max = frame[n];
                    }
                }
                // Assuming min/max normalised between [-1, 1] to map to [150, 0]
                context.beginPath();
                context.moveTo(pixX + 0.5, (min + 1) * -75 + 150);
                context.lineTo(pixX + 0.5, (max + 1) * -75 + 150);
                context.stroke();
                pixX++;
            }
        },
        drawMouse: function (event) {
            var context = this.layer1.getContext("2d");
            context.clearRect(0, 0, this.layer1.width, this.layer1.height);
            var rect = this.layer1.getBoundingClientRect();
            var pixX = event.clientX - rect.left;
            pixX = Math.floor(pixX) - 0.5;
            context.strokeStyle = "#800";
            context.beginPath();
            context.moveTo(pixX, 0);
            context.lineTo(pixX, this.layer1.height);
            context.stroke();
        },
        drawTicker: function () {
            var context = this.layer2.getContext("2d");
            context.clearRect(0, 0, this.layer2.width, this.layer2.height);
            var time = this.parent.parent.getCurrentPosition();
            var ratio = time / this.parent.parent.buffer.buffer.duration;
            var pixX = Math.floor(ratio * this.layer2.width) + 0.5;
            context.strokeStyle = "#080";
            context.beginPath();
            context.moveTo(pixX, 0);
            context.lineTo(pixX, this.layer2.height);
            context.stroke();
        },
        drawMarkers: function () {
            if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
                return;
            }
            var context = this.layer3.getContext("2d");
            context.clearRect(0, 0, this.layer3.width, this.layer3.height);
            context.strokeStyle = "#008";
            var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width;
            for (var i = 0; i < this.comments.list.length; i++) {
                var comment = this.comments.list[i];
                var pixX = Math.floor(comment.time / tpp) + 0.5;
                context.beginPath();
                context.moveTo(pixX, 0);
                context.lineTo(pixX, this.layer3.height);
                context.stroke();
            }
        },
        clearCanvas: function (canvas) {
            var context = canvas.getContext("2d");
            context.clearRect(0, 0, canvas.width, canvas.height);
        }
    }
    this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled";
    this.canvas.layer2.className = "timeline-element-canvas canvas-layer2";
    this.canvas.layer3.className = "timeline-element-canvas canvas-layer3";
    this.canvas.layer4.className = "timeline-element-canvas canvas-layer3";
    this.canvas.layer1.height = "150";
    this.canvas.layer2.height = "150";
    this.canvas.layer3.height = "150";
    this.canvas.layer4.height = "150";
    canvasHolder.appendChild(this.canvas.layer1);
    canvasHolder.appendChild(this.canvas.layer2);
    canvasHolder.appendChild(this.canvas.layer3);
    canvasHolder.appendChild(this.canvas.layer4);
    this.canvas.layer1.addEventListener("mousemove", this.canvas);
    this.canvas.layer1.addEventListener("mouseleave", this.canvas);
    this.canvas.layer1.addEventListener("click", this.canvas);

    var canvasIntervalID = null;

    this.playButton = {
        parent: this,
        DOM: document.createElement("button"),
        handleEvent: function (event) {
            var id = this.parent.parent.id;
            var str = this.DOM.textContent;
            if (str == "Play") {
                audioEngineContext.play(id);
            } else if (str == "Stop") {
                audioEngineContext.stop();
            }
        }
    }
    this.playButton.DOM.addEventListener("click", this.playButton);
    this.playButton.DOM.className = "timeline-button timeline-button-disabled";
    this.playButton.DOM.disabled = true;
    this.playButton.DOM.textContent = "Wait";

    buttonHolder.appendChild(this.playButton.DOM);

    this.resize = function () {
        var w = window.innerWidth;
        w = Math.min(w, 800);
        w = Math.max(w, 200);
        root.style.width = w + "px";
        var c_w = w - 100;
        this.canvas.resize(c_w);
    }

    this.enable = function () {
        // This is used to tell the interface object that playback of this node is ready
        this.canvas.layer1.addEventListener("click", this.canvas);
        this.canvas.layer1.className = "timeline-element-canvas canvas-layer1";
        this.playButton.DOM.className = "timeline-button timeline-button-play";
        this.playButton.DOM.textContent = "Play";
        this.playButton.DOM.disabled = false;

        this.canvas.drawWaveform();
    };
    this.updateLoading = function (progress) {
        // progress is a value from 0 to 100 indicating the current download state of media files
        progress = String(progress);
        progress = progress.substr(0, 5);
        this.playButton.DOM.textContent = "Loading: " + progress + '%';
    };
    this.startPlayback = function () {
        // Called when playback has begun
        canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100);
        this.playButton.DOM.textContent = "Stop";
    };
    this.stopPlayback = function () {
        // Called when playback has stopped. This gets called even if playback never started!
        window.clearInterval(canvasIntervalID);
        this.canvas.clearCanvas(this.canvas.layer2);
        this.playButton.DOM.textContent = "Play";
    };
    this.getValue = function () {
        // Return the current value of the object. If there is no value, return 0
        return 0;
    };
    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 labelSpan.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 false;
    };
    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.
        return null;
    };
    this.error = function () {
        // If there is an error with the audioObject, this will be called to indicate a failure
    }
};

function resizeWindow(event) {
    // Called on every window resize event, use this to scale your page properly
    for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
        audioEngineContext.audioObjects[i].interfaceDOM.resize();
    }
}

function buttonSubmitClick() {
    if (audioEngineContext.timer.testStarted == false) {
        interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!');
        return;
    }
    var checks = [];
    checks = checks.concat(testState.currentStateMap.interfaces[0].options);
    checks = checks.concat(specification.interfaces.options);
    var canContinue = true;
    for (var i = 0; i < checks.length; i++) {
        var checkState = true;
        if (checks[i].type == 'check') {
            switch (checks[i].name) {
                case 'fragmentPlayed':
                    //Check if all fragments have been played
                    checkState = interfaceContext.checkAllPlayed();
                    break;
                case 'fragmentFullPlayback':
                    //Check if all fragments have played to their full length
                    checkState = interfaceContext.checkFragmentsFullyPlayed();
                    break;
                case 'fragmentComments':
                    checkState = interfaceContext.checkAllCommented();
                    break;
                default:
                    console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
                    break;
            }
            if (checkState == false) {
                canContinue == false;
            }
        }
        if (!canContinue) {
            return;
        }
    }

    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
        }
        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();

    for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
        var id = audioEngineContext.audioObjects[i].specification.id;
        var commentsList = audioEngineContext.audioObjects[i].interfaceDOM.comments.list;
        var root = audioEngineContext.audioObjects[i].storeDOM;
        for (var j = 0; j < commentsList.length; j++) {
            commentsList[j].buildXML(root);
        }
    }
}