Source: view/MatchingView.js

  1. /**
  2. * @file Class MatchingView
  3. * @version March 26, 2017
  4. *
  5. * @author Olivier Pirson --- http://www.opimedia.be/
  6. * @license GPLv3 --- Copyright (C) 2017 Olivier Pirson
  7. */
  8. /**
  9. * Returns a <span> with class "true" and true symbol if bool,
  10. * else with class "false" and false symbol.
  11. *
  12. * @param {boolean} bool
  13. *
  14. * @returns {String}
  15. */
  16. function classHtmlTrueFalse(bool) {
  17. assert(typeof bool === "boolean", bool);
  18. return (bool
  19. ? '<span class="true">&#10004;</span>'
  20. : '<span class="false">&#10007;</span>');
  21. }
  22. /**
  23. * Returns a HTML entity for true or false.
  24. *
  25. * @param {boolean} bool
  26. *
  27. * @returns {String}
  28. */
  29. function htmlTrueFalse(bool) {
  30. assert(typeof bool === "boolean", bool);
  31. return (bool
  32. ? "&#10004;"
  33. : "&#10007;");
  34. }
  35. /**
  36. * View to draw Matching to HTML canvas.
  37. */
  38. class MatchingView {
  39. /**
  40. * Construct a view with 2 superposed HTML canvas to draw a matching.
  41. *
  42. * If linkedMatchingView !== null
  43. * then update method may also update linked views.
  44. *
  45. * If onlyPermanentCanvas
  46. * then don't use of temporary canvas.
  47. *
  48. * @param {Matching} matching
  49. * @param {HTMLElement} matchingHtmlElement that will contains HTML canvas
  50. * @param {null|MatchingView} linkedMatchingView
  51. * @param {boolean} onlyPermanentCanvas
  52. */
  53. constructor(matching, matchingHtmlElement, linkedMatchingView=null, onlyPermanentCanvas=false) {
  54. assert(matching instanceof Matching, matching);
  55. assert(matchingHtmlElement instanceof HTMLElement, matchingHtmlElement);
  56. assert((linkedMatchingView===null) || (linkedMatchingView instanceof MatchingView),
  57. linkedMatchingView);
  58. assert(typeof onlyPermanentCanvas === "boolean", onlyPermanentCanvas);
  59. // Constant to distinct drawing mode
  60. this.DRAW_SEGMENTS_ONLY = 1;
  61. this.DRAW_SEGMENTS_CONSECUTIVE = 2;
  62. this.DRAW_SEGMENTS_ALL = 3;
  63. // Attributes
  64. this._matching = matching;
  65. this._canvasContainer = matchingHtmlElement.children[0]; // container permanent and temporary canvas
  66. this._infosContainer = (matchingHtmlElement.children[1]
  67. ? matchingHtmlElement.children[1].children[1]
  68. : null); // container for matching information
  69. if (linkedMatchingView === null) {
  70. this._linkedMatchingViews = [this]; // ordonned sequence of MatchingView
  71. this._drawSegments = this.DRAW_SEGMENTS_ONLY; // draw segments only from this matching or also from linked matchings
  72. }
  73. else {
  74. linkedMatchingView.linkedMatchingViews.push(this);
  75. this._linkedMatchingViews = linkedMatchingView.linkedMatchingViews;
  76. this._drawSegments = linkedMatchingView._drawSegments;
  77. }
  78. // Create permanent canvas
  79. const width = 420;
  80. const height = 300;
  81. const permanentCanvas = document.createElement("canvas");
  82. permanentCanvas.setAttribute("width", width);
  83. permanentCanvas.setAttribute("height", height);
  84. this._canvasContainer.appendChild(permanentCanvas);
  85. this._permanentCanvas = permanentCanvas;
  86. if (onlyPermanentCanvas) {
  87. this._temporaryCanvas = null;
  88. }
  89. else {
  90. // Create temporary canvas
  91. const temporaryCanvas = document.createElement("canvas");
  92. temporaryCanvas.setAttribute("width", width);
  93. temporaryCanvas.setAttribute("height", height);
  94. this._canvasContainer.appendChild(temporaryCanvas);
  95. this._temporaryCanvas = temporaryCanvas;
  96. }
  97. }
  98. /**
  99. * Returns the container of canvas.
  100. *
  101. * @returns {HTMLElement}
  102. */
  103. get canvasContainer() { return this._canvasContainer; }
  104. /**
  105. * Returns the view.
  106. *
  107. * @returns {MatchingView}
  108. */
  109. get linkedMatchingViews() { return this._linkedMatchingViews; }
  110. /**
  111. * Returns the matching.
  112. *
  113. * @returns {Matching}
  114. */
  115. get matching() { return this._matching; }
  116. /**
  117. * Set drawSegment property from HTML element
  118. * and update the view.
  119. *
  120. * @param {number} drawSegments DRAW_SEGMENTS_ONLY, DRAW_SEGMENTS_CONSECUTIVE or DRAW_SEGMENTS_ALL
  121. */
  122. setDrawSegments(drawSegments) {
  123. assert((drawSegments === this.DRAW_SEGMENTS_ONLY)
  124. || (drawSegments === this.DRAW_SEGMENTS_CONSECUTIVE)
  125. || (drawSegments === this.DRAW_SEGMENTS_ALL), drawSegments);
  126. this._drawSegments = drawSegments;
  127. this.update();
  128. }
  129. /**
  130. * Update the temporary canvas.
  131. *
  132. * If options.currentPoint !== null
  133. * then draw also the moving current point (pointed by mouse).
  134. *
  135. * If options.nearestPoint !== null
  136. * then draw also its nearest point in big.
  137. *
  138. * If options.temporarySegment !== null
  139. * then draw also the segment being built.
  140. *
  141. *
  142. * If options.updatePermanent
  143. * then update also the permanent canvas.
  144. *
  145. *
  146. * If options.updateLinkedMatchingViews
  147. * then update also other linked matching views.
  148. *
  149. *
  150. * If this._drawSegments !== DRAW_SEGMENTS_CONSECUTIVE or DRAW_SEGMENTS_ALL
  151. * then draw also segments of consecutive or all linked matchings.
  152. *
  153. * @param {table} options
  154. */
  155. update(options={}) {
  156. assert(options instanceof Object, options);
  157. // Copy options
  158. options = Object.assign({}, options);
  159. // Set default options
  160. if (options.currentPoint === undefined) {
  161. options.currentPoint = null;
  162. }
  163. if (options.nearestPoint === undefined) {
  164. options.nearestPoint = null;
  165. }
  166. if (options.temporarySegment === undefined) {
  167. options.temporarySegment = null;
  168. }
  169. if (options.updateLinkedMatchingViews === undefined) {
  170. options.updateLinkedMatchingViews = true;
  171. }
  172. if (options.updatePermanent === undefined) {
  173. options.updatePermanent = true;
  174. }
  175. assert((options.currentPoint === null) || (options.currentPoint instanceof Point), options);
  176. assert((options.nearestPoint === null) || (options.nearestPoint instanceof Point), options);
  177. assert((options.temporarySegment === null) || (options.temporarySegment instanceof Segment), options);
  178. assert(typeof options.updateLinkedMatchingViews === "boolean", options);
  179. assert(typeof options.updatePermanent === "boolean", options);
  180. if (options.updatePermanent) {
  181. // Update the permanent canvas
  182. this._updatePermanentCanvas();
  183. const isCanonical = this.matching.isCanonical();
  184. if (this._infosContainer) { // isolated view of left or right matching
  185. cssSetClass(this._canvasContainer, "canonical", isCanonical);
  186. assert((this.matching === this.matching.linkedMatchings[0])
  187. || (this.matching === this.matching.linkedMatchings[this.matching.linkedMatchings.length - 1]));
  188. // Update infos
  189. const buttonSide = (this.matching === this.matching.linkedMatchings[0]
  190. ? "left"
  191. : "right");
  192. this._infosContainer.innerHTML
  193. = ("<div><span>" + this.matching.segments.length + " segment"
  194. + s(this.matching.segments.length)
  195. + '<span class="margin-left-2m">Even? ' + htmlTrueFalse(this.matching.segments.length % 2 === 0) + "</span></span></div>"
  196. + "<div><span>Perfect? " + classHtmlTrueFalse(this.matching.isPerfect()) + "</span>"
  197. + '<span class="' + (isCanonical
  198. ? ""
  199. : "not-") + 'canonical">Canonical? ' + htmlTrueFalse(isCanonical) + "</span>"
  200. + "<span>Vertical-horizontal? " + htmlTrueFalse(this.matching.isVerticalHorizontal()) + "</span></div>");
  201. }
  202. else { // view of list matchings
  203. cssSetClass(this._canvasContainer.parentNode.parentNode, "canonical", isCanonical);
  204. }
  205. }
  206. if (this._temporaryCanvas !== null) {
  207. // Update the temporary canvas
  208. this._updateTemporaryCanvas(options.currentPoint, options.nearestPoint,
  209. options.temporarySegment);
  210. if (options.updateLinkedMatchingViews) {
  211. // Update other linked matching views
  212. options.updateLinkedMatchingViews = false;
  213. for (let linkedMatchingView of this._linkedMatchingViews) {
  214. if (linkedMatchingView !== this) {
  215. linkedMatchingView.update(options);
  216. }
  217. }
  218. }
  219. }
  220. }
  221. /**
  222. * Clear canvas.
  223. *
  224. * @param {HTMLElement} canvas
  225. */
  226. _canvasClear(canvas) {
  227. assert(canvas instanceof HTMLElement, canvas);
  228. canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
  229. }
  230. /**
  231. * Draw a point.
  232. *
  233. * @param {HTMLElement} canvas
  234. * @param {Point} point
  235. * @param {String} color
  236. * @param {number} radius > 0
  237. */
  238. _drawPoint(canvas, point, color="black", radius=5) {
  239. assert(canvas instanceof HTMLElement, canvas);
  240. assert(point instanceof Point, point);
  241. assert(typeof color === "string", color);
  242. assert(typeof radius === "number", radius);
  243. assert(radius > 0, radius);
  244. const xY = this._pointToCanvasXY(canvas, point);
  245. const canvasContext = canvas.getContext("2d");
  246. canvasContext.fillStyle = color;
  247. canvasContext.beginPath();
  248. canvasContext.arc(xY[0], xY[1], radius, 0, Math.PI*2);
  249. canvasContext.fill();
  250. }
  251. /**
  252. * Draw a segment in color
  253. * with its two endpoints in black (with default radius).
  254. *
  255. * @param {HTMLElement} canvas
  256. * @param {Segment} segment
  257. * @param {String} color
  258. * @param {number} lineWidth > 0
  259. */
  260. _drawSegment(canvas, segment, color="black", lineWidth=2) {
  261. assert(canvas instanceof HTMLElement, canvas);
  262. assert(segment instanceof Segment, segment);
  263. assert(typeof color === "string", color);
  264. assert(typeof lineWidth === "number", lineWidth);
  265. assert(lineWidth > 0, lineWidth);
  266. const xYA = this._pointToCanvasXY(canvas, segment.a);
  267. const xYB = this._pointToCanvasXY(canvas, segment.b);
  268. const canvasContext = canvas.getContext("2d");
  269. canvasContext.strokeStyle = color;
  270. canvasContext.lineWidth = lineWidth;
  271. canvasContext.beginPath();
  272. canvasContext.moveTo(xYA[0], xYA[1]);
  273. canvasContext.lineTo(xYB[0], xYB[1]);
  274. canvasContext.stroke();
  275. this._drawPoint(canvas, segment.a);
  276. this._drawPoint(canvas, segment.b);
  277. }
  278. /**
  279. * Returns (x, y) coordinates of point in the canvas.
  280. *
  281. * The vertical coordinate is reversed to have (*, 0) in the bottom.
  282. *
  283. * @param {HTMLElement} canvas
  284. * @param {Point} point
  285. *
  286. * @returns {Array} [number, number]
  287. */
  288. _pointToCanvasXY(canvas, point) {
  289. assert(canvas instanceof HTMLElement, canvas);
  290. assert(point instanceof Point, point);
  291. return [point.x, canvas.height - 1 - point.y];
  292. }
  293. /**
  294. * Draw in the canvas all points and segments of this matching.
  295. *
  296. * If this._drawSegments === DRAW_SEGMENTS_CONSECUTIVE
  297. * then draw also segments (in thin silver) of consecutive linked matchings.
  298. *
  299. * If this._drawSegments === DRAW_SEGMENTS_ALL
  300. * then draw also segments (in thin silver) of all linked matchings.
  301. *
  302. * @param {number} drawSegments DRAW_SEGMENTS_ONLY, DRAW_SEGMENTS_CONSECUTIVE or DRAW_SEGMENTS_ALL
  303. */
  304. _updatePermanentCanvas() {
  305. this._canvasClear(this._permanentCanvas);
  306. if (this._drawSegments !== this.DRAW_SEGMENTS_ONLY) {
  307. // Draw segments of other linked matchings
  308. var segments = new Set();
  309. if (this._drawSegments === this.DRAW_SEGMENTS_CONSECUTIVE) {
  310. // Only consecutive linked matchings
  311. const i = this.matching.linkedMatchingsIndex();
  312. if (i > 0) {
  313. segments = new Set(this.matching.linkedMatchings[i - 1].segments);
  314. }
  315. if (i < this.matching._linkedMatchings.length - 1) {
  316. for (let segment of this.matching.linkedMatchings[i + 1].segments) {
  317. segments.add(segment);
  318. }
  319. }
  320. }
  321. else {
  322. // All other linked matchings
  323. for (let matching of this.matching.linkedMatchings) {
  324. if (matching !== this.matching) {
  325. for (let segment of matching.segments) {
  326. segments.add(segment);
  327. }
  328. }
  329. }
  330. }
  331. for (let segment of segments) {
  332. this._drawSegment(this._permanentCanvas, segment, "#d0d0d0", 1);
  333. }
  334. }
  335. // Draw all segments
  336. const commonSegments = this.matching.commonSegmentsWithConsecutiveMatchings();
  337. const intersectSegments = this.matching.properIntersectSegmentsWithConsecutiveMatchings();
  338. for (let segment of this.matching.segments) {
  339. const color = (intersectSegments.has(segment)
  340. ? "red"
  341. : (commonSegments.has(segment)
  342. ? "orange"
  343. : "black"))
  344. this._drawSegment(this._permanentCanvas, segment, color);
  345. }
  346. // Draw isolated points in red
  347. for (let point of this.matching.isolatedPoints()) {
  348. this._drawPoint(this._permanentCanvas, point, "red");
  349. }
  350. }
  351. /**
  352. * Draw in the canvas
  353. * the moving current point (pointed by mouse) (if not null),
  354. * its nearest point in big (if not null)
  355. * and the segment being built (if not null).
  356. *
  357. * @param {null|Point} currentPoint
  358. * @param {null|Point} nearestPoint
  359. * @param {null|Segment} temporarySegment
  360. */
  361. _updateTemporaryCanvas(currentPoint=null, nearestPoint=null, temporarySegment=null) {
  362. assert((currentPoint === null) || (currentPoint instanceof Point), currentPoint);
  363. assert((nearestPoint === null) || (nearestPoint instanceof Point), nearestPoint);
  364. assert((temporarySegment === null)
  365. || (temporarySegment instanceof Segment), temporarySegment);
  366. this._canvasClear(this._temporaryCanvas);
  367. if (temporarySegment) {
  368. this._drawSegment(this._temporaryCanvas, temporarySegment, "silver");
  369. }
  370. if (currentPoint) {
  371. this._drawPoint(this._temporaryCanvas, currentPoint, "silver", 3);
  372. }
  373. if (nearestPoint) {
  374. this._drawPoint(this._temporaryCanvas, nearestPoint,
  375. (this.matching.isolatedPoints().has(nearestPoint)
  376. ? "red"
  377. : "black"), 10);
  378. }
  379. }
  380. }