Source: view/MatchingView.js

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


/**
 * Returns a <span> with class "true" and true symbol if bool,
 * else with class "false" and false symbol.
 *
 * @param {boolean} bool
 *
 * @returns {String}
 */
function classHtmlTrueFalse(bool) {
    assert(typeof bool === "boolean", bool);

    return (bool
            ? '<span class="true">&#10004;</span>'
            : '<span class="false">&#10007;</span>');
}


/**
 * Returns a HTML entity for true or false.
 *
 * @param {boolean} bool
 *
 * @returns {String}
 */
function htmlTrueFalse(bool) {
    assert(typeof bool === "boolean", bool);

    return (bool
            ? "&#10004;"
            : "&#10007;");
}



/**
 * View to draw Matching to HTML canvas.
 */
class MatchingView {
    /**
     * Construct a view with 2 superposed HTML canvas to draw a matching.
     *
     * If linkedMatchingView !== null
     * then update method may also update linked views.
     *
     * If onlyPermanentCanvas
     * then don't use of temporary canvas.
     *
     * @param {Matching} matching
     * @param {HTMLElement} matchingHtmlElement that will contains HTML canvas
     * @param {null|MatchingView} linkedMatchingView
     * @param {boolean} onlyPermanentCanvas
     */
    constructor(matching, matchingHtmlElement, linkedMatchingView=null, onlyPermanentCanvas=false) {
        assert(matching instanceof Matching, matching);
        assert(matchingHtmlElement instanceof HTMLElement, matchingHtmlElement);
        assert((linkedMatchingView===null) || (linkedMatchingView instanceof MatchingView),
               linkedMatchingView);
        assert(typeof onlyPermanentCanvas === "boolean", onlyPermanentCanvas);

        // Constant to distinct drawing mode
        this.DRAW_SEGMENTS_ONLY = 1;
        this.DRAW_SEGMENTS_CONSECUTIVE = 2;
        this.DRAW_SEGMENTS_ALL = 3;


        // Attributes
        this._matching = matching;

        this._canvasContainer = matchingHtmlElement.children[0];  // container permanent and temporary canvas
        this._infosContainer = (matchingHtmlElement.children[1]
                                ? matchingHtmlElement.children[1].children[1]
                                : null);  // container for matching information

        if (linkedMatchingView === null) {
            this._linkedMatchingViews = [this];  // ordonned sequence of MatchingView

            this._drawSegments = this.DRAW_SEGMENTS_ONLY;  // draw segments only from this matching or also from linked matchings
        }
        else {
            linkedMatchingView.linkedMatchingViews.push(this);
            this._linkedMatchingViews = linkedMatchingView.linkedMatchingViews;

            this._drawSegments = linkedMatchingView._drawSegments;
        }


        // Create permanent canvas
        const width = 420;
        const height = 300;
        const permanentCanvas = document.createElement("canvas");

        permanentCanvas.setAttribute("width", width);
        permanentCanvas.setAttribute("height", height);

        this._canvasContainer.appendChild(permanentCanvas);

        this._permanentCanvas = permanentCanvas;

        if (onlyPermanentCanvas) {
            this._temporaryCanvas = null;
        }
        else {
            // Create temporary canvas
            const temporaryCanvas = document.createElement("canvas");

            temporaryCanvas.setAttribute("width", width);
            temporaryCanvas.setAttribute("height", height);

            this._canvasContainer.appendChild(temporaryCanvas);

            this._temporaryCanvas = temporaryCanvas;
        }
    }



    /**
     * Returns the container of canvas.
     *
     * @returns {HTMLElement}
     */
    get canvasContainer() { return this._canvasContainer; }


    /**
     * Returns the view.
     *
     * @returns {MatchingView}
     */
    get linkedMatchingViews() { return this._linkedMatchingViews; }


    /**
     * Returns the matching.
     *
     * @returns {Matching}
     */
    get matching() { return this._matching; }



    /**
     * Set drawSegment property from HTML element
     * and update the view.
     *
     * @param {number} drawSegments DRAW_SEGMENTS_ONLY, DRAW_SEGMENTS_CONSECUTIVE or DRAW_SEGMENTS_ALL
     */
    setDrawSegments(drawSegments) {
        assert((drawSegments === this.DRAW_SEGMENTS_ONLY)
               || (drawSegments === this.DRAW_SEGMENTS_CONSECUTIVE)
               || (drawSegments === this.DRAW_SEGMENTS_ALL), drawSegments);

        this._drawSegments = drawSegments;

        this.update();
    }


    /**
     * Update the temporary canvas.
     *
     * If options.currentPoint !== null
     * then draw also the moving current point (pointed by mouse).
     *
     * If options.nearestPoint !== null
     * then draw also its nearest point in big.
     *
     * If options.temporarySegment !== null
     * then draw also the segment being built.
     *
     *
     * If options.updatePermanent
     * then update also the permanent canvas.
     *
     *
     * If options.updateLinkedMatchingViews
     * then update also other linked matching views.
     *
     *
     * If this._drawSegments !== DRAW_SEGMENTS_CONSECUTIVE or DRAW_SEGMENTS_ALL
     * then draw also segments of consecutive or all linked matchings.
     *
     * @param {table} options
     */
    update(options={}) {
        assert(options instanceof Object, options);

        // Copy options
        options = Object.assign({}, options);

        // Set default options
        if (options.currentPoint === undefined) {
            options.currentPoint = null;
        }
        if (options.nearestPoint === undefined) {
            options.nearestPoint = null;
        }
        if (options.temporarySegment === undefined) {
            options.temporarySegment = null;
        }
        if (options.updateLinkedMatchingViews === undefined) {
            options.updateLinkedMatchingViews = true;
        }
        if (options.updatePermanent === undefined) {
            options.updatePermanent = true;
        }

        assert((options.currentPoint === null) || (options.currentPoint instanceof Point), options);
        assert((options.nearestPoint === null) || (options.nearestPoint instanceof Point), options);
        assert((options.temporarySegment === null) || (options.temporarySegment instanceof Segment), options);
        assert(typeof options.updateLinkedMatchingViews === "boolean", options);
        assert(typeof options.updatePermanent === "boolean", options);

        if (options.updatePermanent) {
            // Update the permanent canvas
            this._updatePermanentCanvas();

            const isCanonical = this.matching.isCanonical();

            if (this._infosContainer) {  // isolated view of left or right matching
                cssSetClass(this._canvasContainer, "canonical", isCanonical);

                assert((this.matching === this.matching.linkedMatchings[0])
                       || (this.matching === this.matching.linkedMatchings[this.matching.linkedMatchings.length - 1]));

                // Update infos
                const buttonSide = (this.matching === this.matching.linkedMatchings[0]
                                    ? "left"
                                    : "right");

                this._infosContainer.innerHTML
                    = ("<div><span>" + this.matching.segments.length + " segment"
                       + s(this.matching.segments.length)
                       + '<span class="margin-left-2m">Even? ' + htmlTrueFalse(this.matching.segments.length % 2 === 0) + "</span></span></div>"
                       + "<div><span>Perfect? " + classHtmlTrueFalse(this.matching.isPerfect()) + "</span>"
                       + '<span class="' + (isCanonical
                                            ? ""
                                            : "not-") + 'canonical">Canonical? ' + htmlTrueFalse(isCanonical) + "</span>"
                       + "<span>Vertical-horizontal? " + htmlTrueFalse(this.matching.isVerticalHorizontal()) + "</span></div>");
            }
            else {                       // view of list matchings
                cssSetClass(this._canvasContainer.parentNode.parentNode, "canonical", isCanonical);
            }
        }


        if (this._temporaryCanvas !== null) {
            // Update the temporary canvas
            this._updateTemporaryCanvas(options.currentPoint, options.nearestPoint,
                                        options.temporarySegment);


            if (options.updateLinkedMatchingViews) {
                // Update other linked matching views
                options.updateLinkedMatchingViews = false;

                for (let linkedMatchingView of this._linkedMatchingViews) {
                    if (linkedMatchingView !== this) {
                        linkedMatchingView.update(options);
                    }
                }
            }
        }
    }



    /**
     * Clear canvas.
     *
     * @param {HTMLElement} canvas
     */
    _canvasClear(canvas) {
        assert(canvas instanceof HTMLElement, canvas);

        canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
    }


    /**
     * Draw a point.
     *
     * @param {HTMLElement} canvas
     * @param {Point} point
     * @param {String} color
     * @param {number} radius > 0
     */
    _drawPoint(canvas, point, color="black", radius=5) {
        assert(canvas instanceof HTMLElement, canvas);
        assert(point instanceof Point, point);
        assert(typeof color === "string", color);

        assert(typeof radius === "number", radius);
        assert(radius > 0, radius);

        const xY = this._pointToCanvasXY(canvas, point);

        const canvasContext = canvas.getContext("2d");

        canvasContext.fillStyle = color;
        canvasContext.beginPath();
        canvasContext.arc(xY[0], xY[1], radius, 0, Math.PI*2);
        canvasContext.fill();
    }


    /**
     * Draw a segment in color
     * with its two endpoints in black (with default radius).
     *
     * @param {HTMLElement} canvas
     * @param {Segment} segment
     * @param {String} color
     * @param {number} lineWidth > 0
     */
    _drawSegment(canvas, segment, color="black", lineWidth=2) {
        assert(canvas instanceof HTMLElement, canvas);
        assert(segment instanceof Segment, segment);
        assert(typeof color === "string", color);

        assert(typeof lineWidth === "number", lineWidth);
        assert(lineWidth > 0, lineWidth);

        const xYA = this._pointToCanvasXY(canvas, segment.a);
        const xYB = this._pointToCanvasXY(canvas, segment.b);

        const canvasContext = canvas.getContext("2d");

        canvasContext.strokeStyle = color;
        canvasContext.lineWidth = lineWidth;
        canvasContext.beginPath();
        canvasContext.moveTo(xYA[0], xYA[1]);
        canvasContext.lineTo(xYB[0], xYB[1]);
        canvasContext.stroke();

        this._drawPoint(canvas, segment.a);
        this._drawPoint(canvas, segment.b);
    }


    /**
     * Returns (x, y) coordinates of point in the canvas.
     *
     * The vertical coordinate is reversed to have (*, 0) in the bottom.
     *
     * @param {HTMLElement} canvas
     * @param {Point} point
     *
     * @returns {Array} [number, number]
     */
    _pointToCanvasXY(canvas, point) {
        assert(canvas instanceof HTMLElement, canvas);
        assert(point instanceof Point, point);

        return [point.x, canvas.height - 1 - point.y];
    }


    /**
     * Draw in the canvas all points and segments of this matching.
     *
     * If this._drawSegments === DRAW_SEGMENTS_CONSECUTIVE
     * then draw also segments (in thin silver) of consecutive linked matchings.
     *
     * If this._drawSegments === DRAW_SEGMENTS_ALL
     * then draw also segments (in thin silver) of all linked matchings.
     *
     * @param {number} drawSegments DRAW_SEGMENTS_ONLY, DRAW_SEGMENTS_CONSECUTIVE or DRAW_SEGMENTS_ALL
     */
    _updatePermanentCanvas() {
        this._canvasClear(this._permanentCanvas);

        if (this._drawSegments !== this.DRAW_SEGMENTS_ONLY) {
            // Draw segments of other linked matchings
            var segments = new Set();

            if (this._drawSegments === this.DRAW_SEGMENTS_CONSECUTIVE) {
                // Only consecutive linked matchings
                const i = this.matching.linkedMatchingsIndex();

                if (i > 0) {
                    segments = new Set(this.matching.linkedMatchings[i - 1].segments);
                }
                if (i < this.matching._linkedMatchings.length - 1) {
                    for (let segment of this.matching.linkedMatchings[i + 1].segments) {
                        segments.add(segment);
                    }
                }
            }
            else {
                // All other linked matchings
                for (let matching of this.matching.linkedMatchings) {
                    if (matching !== this.matching) {
                        for (let segment of matching.segments) {
                            segments.add(segment);
                        }
                    }
                }
            }

            for (let segment of segments) {
                this._drawSegment(this._permanentCanvas, segment, "#d0d0d0", 1);
            }
        }


        // Draw all segments
        const commonSegments = this.matching.commonSegmentsWithConsecutiveMatchings();
        const intersectSegments = this.matching.properIntersectSegmentsWithConsecutiveMatchings();

        for (let segment of this.matching.segments) {
            const color = (intersectSegments.has(segment)
                           ? "red"
                           : (commonSegments.has(segment)
                              ? "orange"
                              : "black"))

            this._drawSegment(this._permanentCanvas, segment, color);
        }


        // Draw isolated points in red
        for (let point of this.matching.isolatedPoints()) {
            this._drawPoint(this._permanentCanvas, point, "red");
        }
    }


    /**
     * Draw in the canvas
     * the moving current point (pointed by mouse) (if not null),
     * its nearest point in big (if not null)
     * and the segment being built (if not null).
     *
     * @param {null|Point} currentPoint
     * @param {null|Point} nearestPoint
     * @param {null|Segment} temporarySegment
     */
    _updateTemporaryCanvas(currentPoint=null, nearestPoint=null, temporarySegment=null) {
        assert((currentPoint === null) || (currentPoint instanceof Point), currentPoint);
        assert((nearestPoint === null) || (nearestPoint instanceof Point), nearestPoint);
        assert((temporarySegment === null)
               || (temporarySegment instanceof Segment), temporarySegment);

        this._canvasClear(this._temporaryCanvas);

        if (temporarySegment) {
            this._drawSegment(this._temporaryCanvas, temporarySegment, "silver");
        }

        if (currentPoint) {
            this._drawPoint(this._temporaryCanvas, currentPoint, "silver", 3);
        }

        if (nearestPoint) {
            this._drawPoint(this._temporaryCanvas, nearestPoint,
                            (this.matching.isolatedPoints().has(nearestPoint)
                             ? "red"
                             : "black"), 10);
        }
    }
}