/**
* @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);
}
}