/**
* @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">✔</span>'
: '<span class="false">✗</span>');
}
/**
* Returns a HTML entity for true or false.
*
* @param {boolean} bool
*
* @returns {String}
*/
function htmlTrueFalse(bool) {
assert(typeof bool === "boolean", bool);
return (bool
? "✔"
: "✗");
}
/**
* 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);
}
}
}