Source: controller/GlobalController.js

/**
 * @file Class GlobalController
 * @version March 26, 2017
 *
 * @author Olivier Pirson --- http://www.opimedia.be/
 * @license GPLv3 --- Copyright (C) 2017 Olivier Pirson
 */

/**
 * Controller for global controls of all matchings and views.
 */
class GlobalController {
    /**
     * Construct a controller for the global view
     * and link to two controllers of matching.
     *
     * @param {GlobalView} globalView
     * @param {MatchingController} leftMatchingController
     * @param {MatchingController} rightMatchingController
     */
    constructor(globalView, leftMatchingController, rightMatchingController) {
        assert(globalView instanceof GlobalView, globalView);
        assert(leftMatchingController instanceof MatchingController, leftMatchingController);
        assert(rightMatchingController instanceof MatchingController, rightMatchingController);

        this._globalView = globalView;
        this._leftController = leftMatchingController;
        this._rightController = rightMatchingController;


        // Set values from HTML elements
        this._changeAction();
        this._changeVerticalHorizontal();


        this._setEventListeners();
    }



    /**
     * Returns the global view.
     */
    get globalView() { return this._globalView; }


    /**
     * Returns the left controller.
     */
    get leftController() { return this._leftController; }


    /**
     * Returns the right controller.
     */
    get rightController() { return this._rightController; }



    /**
     * Fill the list of matchings with all possible perfect matchings
     * with the set of points of the left matching.
     */
    _buildPerfectMatchings() {
        const thisGlobalController = this;

        this._runWithWaitMessage(function () {
            thisGlobalController._leftController.matching.setPerfectMatchings();
            thisGlobalController._globalView.update();
        });
    }


    /**
     * Fill the list of matchings with a shortest transformation
     * between left and right matchings.
     */
    _buildTransformation() {
        const thisGlobalController = this;

        this._runWithWaitMessage(function () {
            const found = thisGlobalController._leftController.matching.setTransformation();
            thisGlobalController._globalView.update();

            if (!found) {
                thisGlobalController._warningMessage("Not found!");
            }
        });
    }


    /**
     * Set action property for two controllers of matching.
     */
    _changeAction() {
        this._leftController.changeAction();
        this._rightController.changeAction();
    }


    /**
     * Set drawSegment property for two controllers of matching.
     */
    _changeDrawSegments() {
        this._leftController.changeDrawSegments();
        this._rightController.changeDrawSegments();
        this._globalView.update(null, false);
    }


    /**
     * Set vertical-horizontal property for two controllers of matching.
     */
    _changeVerticalHorizontal() {
        this._leftController.changeVerticalHorizontal();
        this._rightController.changeVerticalHorizontal();
    }


    /**
     * Reset the two controllers.
     */
    _clear() {
        this._leftController.matching.clearIntermediaryLinkedMatchings();

        this._leftController.clear();
        this._rightController.clear();
    }

    /**
     * Reset the list of matchings.
     */
    _clearTransformation() {
        this._leftController.matching.clearIntermediaryLinkedMatchings();
        this._globalView.update();
    }


    /**
     * Load matchings from a text format
     * (in the format such that returns by Matching.matchingsToString())
     * and update the global view.
     *
     * @param {String} text
     */
    _load(text) {
        assert(typeof text === "string", text);

        this._clear();

        const lines = text.split(/\D+/);  // split for each number (ignore other stuffs)


        // Read points
        const numberPoints = parseInt(lines.shift(), 10);

        for (let i = 0; i < numberPoints; ++i) {
            const x = parseInt(lines.shift(), 10);
            const y = parseInt(lines.shift(), 10);

            this._leftController.matching.pointAdd(new Point(x, y));
        }


        // Read matchings
        const numberMatchings = parseInt(lines.shift(), 10);

        const linkedMatchings = [this._leftController.matching];

        for (let i = 1; i < numberMatchings - 1; ++i) {
            linkedMatchings.push(new Matching());
        }
        linkedMatchings.push(this._rightController.matching);

        for (let i = 0; i < numberMatchings; ++i) {
            // Read segments
            const numberSegments = parseInt(lines.shift(), 10);

            for (let j = 0; j < numberSegments; ++j) {
                const indexA = parseInt(lines.shift(), 10);
                const indexB = parseInt(lines.shift(), 10);

                linkedMatchings[i].segmentAdd(new Segment(this._leftController.matching.points[indexA], this._leftController.matching.points[indexB]));
            }
        }

        for (let linkedMatching of linkedMatchings) {
            linkedMatching._linkedMatchings = linkedMatchings;
        }

        this._globalView.update();
    }


    /**
     * Load matchings from a local text file
     * (in the format such that returns by Matching.matchingsToString())
     * and update the global view.
     *
     * @param {Event} event
     */
    _loadFile(event) {
        assert(event instanceof Event, event);

        const file = document.getElementById("button-load").files[0];
        const fileReader = new FileReader();

        const controller = this;

        fileReader.onload = function(event) {
            controller._load(event.target.result.trim());
        };

        fileReader.readAsText(file);
    }


    /**
     * Load matchings from a text file
     * (in the format such that returns by Matching.matchingsToString())
     * and update the global view.
     *
     * @param {String} url
     */
    _loadUrl(url) {
        assert(typeof url === "string", url);

        var xhr = new XMLHttpRequest();

        xhr.open("GET", url, true);
        xhr.setRequestHeader("Content-Type", "plain/text");

        const controller = this;

        xhr.onload = function() {
            if ((xhr.readyState !== 4) || (xhr.status !== 200)) {
                return;
            }

            controller._load(xhr.responseText);

            location.hash = "2-Experiment-by-yourself-1-Interactive-application";;
        }

        xhr.send(null);
    }


    /**
     * Display a waiting message,
     * run the function
     * and then remove the waiting message.
     *
     * @param {function} func
     */
    _runWithWaitMessage(func) {
        assert(typeof func === "function", func);

        this._waitShow();

        const thisGlobalController = this;

        window.setTimeout(function () {
            func();
            window.setTimeout(thisGlobalController._waitHide, 100);
        }, 100);
    }


    /**
     * Save all matchings in a text file named "matchings.txt"
     * (in the format returns by Matching.matchingsToString()).
     *
     * Warning! Only integers are valid.
     */
    _saveToFile() {
        // Create content
        const text = this._leftController.matching.matchingsToString();
        const blob = new Blob([text], {type:"text/plain"});

        // Create a link to content
        const itemA = document.createElement("a");

        itemA.download = "matchings.txt";
        itemA.href = window.URL.createObjectURL(blob);
        itemA.onclick = function (event) { document.body.removeChild(event.target); };
        itemA.style.display = "none";

        document.body.appendChild(itemA);

        // Click to the link
        itemA.click();
    }


    /**
     * Swap segments between the left and right matchings.
     */
    _swap() {
        this._leftController.matching.clearIntermediaryLinkedMatchings();

        const tmp = this._leftController.matching._segments;

        this._leftController.matching._segments = this._rightController.matching._segments;
        this._rightController.matching._segments = tmp;

        this._globalView.update();
    }


    /**
     * Set listeners on the view.
     */
    _setEventListeners() {
        const controller = this;


        // Set canonical to left matching
        const canonicalLeft = function (event) {
            controller._leftController.matching.clearIntermediaryLinkedMatchings();
            controller._leftController.matching.setCanonical();
            controller._globalView.update();
        };

        document.getElementById("button-canonical-left").addEventListener("click", canonicalLeft, false);

        // Set canonical to right matching
        const canonicalRight = function (event) {
            controller._leftController.matching.clearIntermediaryLinkedMatchings();
            controller._rightController.matching.setCanonical();
            controller._globalView.update();
        };

        document.getElementById("button-canonical-right").addEventListener("click", canonicalRight, false);


        // Shuffle segments in left matching
        const shuffleLeft = function (event) {
            controller._leftController.matching.clearIntermediaryLinkedMatchings();
            controller._leftController.matching.shuffleSegments();
            controller._globalView.update();
        };

        document.getElementById("button-shuffle-left").addEventListener("click", shuffleLeft, false);

        // Shuffle segments in right matching
        const shuffleRight = function (event) {
            controller._leftController.matching.clearIntermediaryLinkedMatchings();
            controller._rightController.matching.shuffleSegments();
            controller._globalView.update();
        };

        document.getElementById("button-shuffle-right").addEventListener("click", shuffleRight, false);


        // Build list perfect matchings
        document.getElementById("button-build-list-perfect-matchings").addEventListener("click",
                                                                                        function () { controller._buildPerfectMatchings(); },
                                                                                        false);

        // Build list transformation
        document.getElementById("button-build-list-transformation").addEventListener("click",
                                                                                     function () { controller._buildTransformation(); },
                                                                                     false);

        // Clear list matchings
        document.getElementById("button-clear-list-matchings").addEventListener("click",
                                                                                function () { controller._clearTransformation(); },
                                                                                false);


        // Clear
        document.getElementById("button-clear").addEventListener("click",
                                                                 function () { controller._clear(); },
                                                                 false);


        // Load from file
        document.getElementById("button-load").addEventListener("change",
                                                                function (event) { controller._loadFile(event); },
                                                                false);

        this._setEventListenersLoadExample();

        // Save to file
        document.getElementById("button-save").addEventListener("click",
                                                                function () { controller._saveToFile(); },
                                                                false);

        // Swap
        document.getElementById("button-swap").addEventListener("click",
                                                                function () { controller._swap(); },
                                                                false);


        // Change action mode
        document.getElementById("radio-action-point").addEventListener("change",
                                                                       function () { controller._changeAction(); },
                                                                       false);
        document.getElementById("radio-action-segment").addEventListener("change",
                                                                         function () { controller._changeAction(); },
                                                                         false);


        // Change draw segments mode
        document.getElementById("radio-draw-segments-only").addEventListener("change",
                                                                             function () { controller._changeDrawSegments(); },
                                                                             false);

        document.getElementById("radio-draw-segments-consecutive").addEventListener("change",
                                                                                    function () { controller._changeDrawSegments(); }, false);

        document.getElementById("radio-draw-segments-all").addEventListener("change",
                                                                            function () { controller._changeDrawSegments(); },
                                                                            false);


        // Change vertical-horizontal mode
        document.getElementById("checkbox-vertical-horizontal").addEventListener("change",
                                                                                 function () { controller._changeVerticalHorizontal(); },
                                                                                 false);


        // Resize the container of list matchings
        window.addEventListener("resize",
                                function () { controller.globalView.resizeListMatchings(); },
                                false);
    }


    /**
     * Set listeners on the view to load examples.
     */
    _setEventListenersLoadExample() {
        const controller = this;
        const imgs = document.getElementsByTagName('img');

        for (let img of imgs) {
            const id = img.id;

            if (id) {
                const matches = /^(.+)-example$/.exec(id);
                const name = matches[1];

                if (name) {
                    img.title = "Click to this picture to load it to the two interactive zones.";
                    img.addEventListener("click",
                                         function (event) { controller._loadUrl("public/examples/" + name + ".txt"); },
                                         false);
                }
            }
        }
    }


    /**
     * Hide the waiting message.
     */
    _waitHide() {
        document.getElementById("wait-container").style.display = "none";
    }


    /**
     * Show the waiting message.
     */
    _waitShow() {
        document.getElementById("wait-container").style.display = "flex";
    }


    /**
     * Show temporary the warning message.
     */
    _warningMessage(message) {
        assert(typeof message === "string", message);

        const container = document.getElementById("warning-container");

        container.innerHTML = message;
        container.style.display = "flex";

        window.setTimeout(function () {
            container.style.display = "none";
        }, 2000);
    }
}