Source: controller/MatchingController.js

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

/**
 * Controller for a matching and its view.
 */
class MatchingController {
    /**
     * Construct a controller for this matching and this view.
     *
     * @param {Matching} matching
     * @param {MatchingView} view
     */
    constructor(matching, view) {
        assert(matching instanceof Matching, matching);
        assert(view instanceof MatchingView, view);

        // Constant to distinct action mode (point or segment)
        this.ACTION_POINT   = 1;
        this.ACTION_SEGMENT = 2;


        // Attributes
        this._matching = matching;
        this._view = view;

        this._segmentFirstPoint = null;  // Used as first point when build segment

        this._action = null;  // add/remove point or segment
        this._verticalHorizontal = null;  // draw any segment or only vertical or horizontal

        this._globalView = null;  // global view

        this._setEventListeners();


        // Set values from HTML elements
        this.changeDrawSegments();
    }



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


    /**
     * Returns the view.
     */
    get view() { return this._view; }



    /**
     * Set action property from HTML element
     * and reset the first point used to build segment.
     */
    changeAction() {
        this._action = (document.getElementById("radio-action-point").checked
                        ? this.ACTION_POINT
                        : this.ACTION_SEGMENT);

        this.clearSegmentFirstPoint();
    }


    /**
     * Set drawSegment property from HTML element
     * and update the view.
     */
    changeDrawSegments() {
        const drawSegments = (document.getElementById("radio-draw-segments-only").checked
                              ? this.view.DRAW_SEGMENTS_ONLY
                              : (document.getElementById("radio-draw-segments-consecutive").checked
                                 ? this.view.DRAW_SEGMENTS_CONSECUTIVE
                                 : this.view.DRAW_SEGMENTS_ALL));

        if (this._globalView !== null) {
            for (let view of this._globalView._leftView._linkedMatchingViews) {
                view.setDrawSegments(drawSegments);
            }
        }
    }


    /**
     * Set vertical-horizontal property from HTML element.
     */
    changeVerticalHorizontal() {
        this._verticalHorizontal = document.getElementById("checkbox-vertical-horizontal").checked;
    }


    /**
     * Reset the controller,
     * clear and update the view,
     * and update the global view.
     */
    clear() {
        this.clearSegmentFirstPoint();

        this.matching.clear();
        this.view.update();

        this._globalView.update(null, false);
    }


    /**
     * Reset the first point used to build segment.
     */
    clearSegmentFirstPoint() {
        this._segmentFirstPoint = null;
    }



    /**
     * If point is already in the matching
     * then remove it,
     * else add it.
     *
     * @param {Point} point
     */
    addOrRemovePoint(point) {
        assert(point instanceof Point, point);

        this._matching.clearIntermediaryLinkedMatchings();

        if (this.matching.pointIsInMatching(point)) {  // remove existing point
            this.matching.pointRemove(point);
        }
        else {                                         // add new point
            this.matching.pointAdd(point);
        }
    }


    /**
     * If segment is already in the matching
     * then remove it,
     * else if the segment don't intersects other segment in this matching then add it.
     *
     * @param {Segment} segment
     */
    addOrRemoveSegment(segment) {
        assert(segment instanceof Segment, segment);

        if (this.matching.segmentIsInMatching(segment)) {
            // Remove the existing segment
            this._matching.clearIntermediaryLinkedMatchings();

            this.matching.segmentRemove(segment);
        }
        else if (!this.matching.segmentIsIntersect(segment)) {
            // If no intersection then add the segment and non existing points
            this._matching.clearIntermediaryLinkedMatchings();

            if (!this.matching.pointIsInMatching(segment.a)) {
                this.matching.pointAdd(segment.a);
            }

            if (!this.matching.pointIsInMatching(segment.b)) {
                this.matching.pointAdd(segment.b);
            }

            this.matching.segmentAdd(segment);
        }
    }


    /**
     * Set the global view.
     *
     * @param {GlobalView} globalView
     */
    setGlobalView(globalView) {
        assert(globalView instanceof GlobalView, globalView);

        this._globalView = globalView;
        this.changeDrawSegments();
    }



    /**
     * Add/remove a point or a segment,
     * update the view,
     * and update the global view.
     *
     * @param {MouseEvent} event
     */
    _click(event) {
        assert(event instanceof MouseEvent, event);

        var currentPoint;
        var nearestPoint;

        [currentPoint, nearestPoint] = this._nearestPointIfExist(this._eventMouseToPoint(event));


        if (this._action === this.ACTION_POINT) {  // point
            this.addOrRemovePoint(currentPoint);
        }
        else {                                      // segment
            assert(this._action === this.ACTION_SEGMENT);

            if (this._segmentFirstPoint === null) {  // set a first point
                this._segmentFirstPoint = currentPoint;
            }
            else {                                   // second point of the segment
                if (this._verticalHorizontal) {
                    currentPoint = this._verticalHorizontalSecondPoint(currentPoint);
                }
                this.addOrRemoveSegment(new Segment(this._segmentFirstPoint, currentPoint));
                this.clearSegmentFirstPoint();
            }
        }


        // Update views
        this.view.update({"currentPoint": currentPoint});

        this._globalView.update(currentPoint, false);
    }


    /**
     * Returns a point corresponding to the position in the mouse event.
     *
     * The vertical coordinate is reversed to have (*, 0) in the bottom.
     *
     * @param {MouseEvent} event
     *
     * @returns {Point}
     */
    _eventMouseToPoint(event) {
        assert(event instanceof MouseEvent, event);

        return new Point(Math.max(0, event.layerX),
                         Math.max(0, event.target.height - 1 - event.layerY));
    }


    /**
     * Move the current point
     * and if a first point was fixed the building segment,
     * and update the view of the matching.
     *
     * @param {MouseEvent} event
     */
    _move(event) {
        assert(event instanceof MouseEvent, event);

        var currentPoint;
        var nearestPoint;

        [currentPoint, nearestPoint] = this._nearestPointIfExist(this._eventMouseToPoint(event));


        // Building segment or not
        var temporarySegment = null;

        if (this._segmentFirstPoint !== null) {
            // Building a segment with first point already fixed
            if (this._verticalHorizontal) {
                // Set vertical or horizontal segment
                currentPoint = this._verticalHorizontalSecondPoint(currentPoint);
            }

            if (!this._segmentFirstPoint.isEquals(currentPoint)) {
                // It's a real segment (with 2 distinct points)
                temporarySegment = new Segment(this._segmentFirstPoint, currentPoint);
            }
        }


        // Update the coordinates of the current point
        this._globalView.updateInfosCurrentPoint(currentPoint);

        // Update the temporary part of the view
        this.view.update({"updatePermanent": false,
                          "currentPoint": currentPoint,
                          "nearestPoint": nearestPoint,
                          "temporarySegment": temporarySegment});
    }


    /**
     * If there exist an point closed enough to point
     * then return [this closed point, this close point],
     * else return [point, null].
     *
     * @param {Point} point
     *
     * @returns {Array} [Point, null or Point]
     */
    _nearestPointIfExist(point) {
        const nearestPoint = this.matching.nearestPoint(point);

        if (nearestPoint !== null) {
            // there exist a point closed enough so it become the current point
            point = nearestPoint;
        }

        return [point, nearestPoint];
    }


    /**
     * Reset the first point used to build segment
     * and update the view.
     */
    _out() {
        this.clearSegmentFirstPoint();

        this._globalView.updateInfosCurrentPoint();

        this.view.update({"updatePermanent": false});
    }


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

        // Add/remove point or segment
        this.view.canvasContainer.addEventListener("click",
                                                   function (event) { controller._click(event); },
                                                   false);

        // Move pointer or temporary segment
        this.view.canvasContainer.addEventListener("mousemove",
                                                   function (event) { controller._move(event); },
                                                   false);

        // Out canvas
        this.view.canvasContainer.addEventListener("mouseout",
                                                   function () { controller._out(); },
                                                   false);
    }


    /**
     * If point is in a vertical or horizontal position compared to _segmentFirstPoint
     * then return point,
     * else return a new point in the nearest good vertical or horizontal position.
     *
     * @param {Point} point
     *
     * @returns {Point}
     */
    _verticalHorizontalSecondPoint(point) {
        assert(point instanceof Point, point);

        assert(this._segmentFirstPoint instanceof Point, this._segmentFirstPoint);

        const horizontalPoint = new Point(point.x, this._segmentFirstPoint.y);
        const verticalPoint = new Point(this._segmentFirstPoint.x, point.y);

        const newPoint = (point.distanceSqr(horizontalPoint) <= point.distanceSqr(verticalPoint)
                          ? horizontalPoint
                          : verticalPoint);

        return (point.isEquals(newPoint)
                ? point
                : newPoint);
    }
}