Source: control/cad.js

  1. import { Style, Stroke } from 'ol/style';
  2. import { Point, LineString, Polygon, MultiPoint } from 'ol/geom';
  3. import Feature from 'ol/Feature';
  4. import Vector from 'ol/layer/Vector';
  5. import VectorSource from 'ol/source/Vector';
  6. import { Pointer, Snap } from 'ol/interaction';
  7. import { OverlayOp } from 'jsts/org/locationtech/jts/operation/overlay';
  8. import Control from './control';
  9. import cadSVG from '../../img/cad.svg';
  10. import { SnapEvent, SnapEventType } from '../event';
  11. import {
  12. parser,
  13. getProjectedPoint,
  14. getEquationOfLine,
  15. getShiftedMultiPoint,
  16. getIntersectedLinesAndPoint,
  17. isSameLines,
  18. defaultSnapStyles,
  19. VH_LINE_KEY,
  20. SNAP_POINT_KEY,
  21. SNAP_FEATURE_TYPE_PROPERTY,
  22. SEGMENT_LINE_KEY,
  23. ORTHO_LINE_KEY,
  24. CUSTOM_LINE_KEY,
  25. } from '../helper';
  26. /**
  27. * Control with snapping functionality for geometry alignment.
  28. * @extends {ole.Control}
  29. * @alias ole.CadControl
  30. */
  31. class CadControl extends Control {
  32. /**
  33. * @param {Object} [options] Tool options.
  34. * @param {Function} [options.drawCustomSnapLines] Allow to draw more snapping lines using selected corrdinaites.
  35. * @param {Function} [options.filter] Returns an array containing the features
  36. * to include for CAD (takes the source as a single argument).
  37. * @param {Number} [options.nbClosestFeatures] Number of features to use for snapping (closest first). Default is 5.
  38. * @param {Number} [options.snapTolerance] Snap tolerance in pixel
  39. * for snap lines. Default is 10.
  40. * @param {Boolean} [options.showSnapLines] Whether to show
  41. * snap lines (default is true).
  42. * @param {Boolean} [options.showSnapPoints] Whether to show
  43. * snap points around the closest feature.
  44. * @param {Boolean} [options.showOrthoLines] Whether to show
  45. * snap lines that arae perpendicular to segment (default is true).
  46. * @param {Boolean} [options.showSegmentLines] Whether to show
  47. * snap lines that extends a segment (default is true).
  48. * @param {Boolean} [options.snapLinesOrder] Define order of display of snap lines,
  49. * must be an array containing the following values 'ortho', 'segment', 'vh'. Default is ['ortho', 'segment', 'vh', 'custom'].
  50. * @param {Number} [options.snapPointDist] Distance of the
  51. * snap points (default is 30).
  52. * @param {Boolean} [options.useMapUnits] Whether to use map units
  53. * as measurement for point snapping. Default is false (pixel are used).
  54. * @param {ol.style.Style.StyleLike} [options.snapStyle] Style used for the snap layer.
  55. * @param {ol.style.Style.StyleLike} [options.linesStyle] Style used for the lines layer.
  56. * @param {ol.style.Style.StyleLike} [options.orthoLinesStyle] Style used for the lines layer.
  57. * @param {ol.style.Style.StyleLike} [options.segmentLinesStyle] Style used for the lines layer.
  58. * @param {ol.style.Style.StyleLike} [options.Style] Style used for the lines layer.
  59. *
  60. */
  61. constructor(options) {
  62. super({
  63. title: 'CAD control',
  64. className: 'ole-control-cad',
  65. image: cadSVG,
  66. showSnapPoints: true,
  67. showSnapLines: false,
  68. showOrthoLines: true,
  69. showSegmentLines: true,
  70. showVerticalAndHorizontalLines: true,
  71. snapPointDist: 10,
  72. snapLinesOrder: ['ortho', 'segment', 'vh'],
  73. ...options,
  74. });
  75. /**
  76. * Interaction for handling move events.
  77. * @type {ol.interaction.Pointer}
  78. * @private
  79. */
  80. this.pointerInteraction = new Pointer({
  81. handleMoveEvent: this.onMove.bind(this),
  82. });
  83. /**
  84. * Layer for drawing snapping geometries.
  85. * @type {ol.layer.Vector}
  86. * @private
  87. */
  88. this.snapLayer = new Vector({
  89. source: new VectorSource(),
  90. style: options.snapStyle || [
  91. defaultSnapStyles[VH_LINE_KEY],
  92. defaultSnapStyles[SNAP_POINT_KEY],
  93. ],
  94. });
  95. /**
  96. * Layer for colored lines indicating
  97. * intersection point between snapping lines.
  98. * @type {ol.layer.Vector}
  99. * @private
  100. */
  101. this.linesLayer = new Vector({
  102. source: new VectorSource(),
  103. style: options.linesStyle || [
  104. new Style({
  105. stroke: new Stroke({
  106. width: 1,
  107. lineDash: [5, 10],
  108. color: '#FF530D',
  109. }),
  110. }),
  111. ],
  112. });
  113. /**
  114. * Function to draw more snapping lines.
  115. * @type {Function}
  116. * @private
  117. */
  118. this.drawCustomSnapLines = options.drawCustomSnapLines;
  119. /**
  120. * Number of features to use for snapping (closest first). Default is 5.
  121. * @type {Number}
  122. * @private
  123. */
  124. this.nbClosestFeatures =
  125. options.nbClosestFeatures === undefined ? 5 : options.nbClosestFeatures;
  126. /**
  127. * Snap tolerance in pixel.
  128. * @type {Number}
  129. * @private
  130. */
  131. this.snapTolerance =
  132. options.snapTolerance === undefined ? 10 : options.snapTolerance;
  133. /**
  134. * Filter the features to snap with.
  135. * @type {Function}
  136. * @private
  137. */
  138. this.filter = options.filter || null;
  139. /**
  140. * Interaction for snapping
  141. * @type {ol.interaction.Snap}
  142. * @private
  143. */
  144. this.snapInteraction = new Snap({
  145. pixelTolerance: this.snapTolerance,
  146. source: this.snapLayer.getSource(),
  147. });
  148. this.standalone = false;
  149. }
  150. /**
  151. * @inheritdoc
  152. */
  153. getDialogTemplate() {
  154. const distLabel = this.properties.useMapUnits ? 'map units' : 'px';
  155. return `
  156. <div>
  157. <input
  158. id="aux-cb"
  159. type="radio"
  160. name="radioBtn"
  161. ${this.properties.showSnapLines ? 'checked' : ''}
  162. >
  163. <label>Show snap lines</label>
  164. </div>
  165. <div>
  166. <input
  167. id="dist-cb"
  168. type="radio"
  169. name="radioBtn"
  170. ${this.properties.showSnapPoints ? 'checked' : ''}
  171. >
  172. <label>Show snap points. Distance (${distLabel}):</label>
  173. <input type="text" id="width-input"
  174. value="${this.properties.snapPointDist}">
  175. </div>
  176. `;
  177. }
  178. /**
  179. * @inheritdoc
  180. */
  181. setMap(map) {
  182. super.setMap(map);
  183. // Ensure that the snap interaction is at the last position
  184. // as it must be the first to handle the pointermove event.
  185. this.map.getInteractions().on(
  186. 'add',
  187. ((e) => {
  188. const pos = e.target.getArray().indexOf(this.snapInteraction);
  189. if (
  190. this.snapInteraction.getActive() &&
  191. pos > -1 &&
  192. pos !== e.target.getLength() - 1
  193. ) {
  194. this.deactivate(true);
  195. this.activate(true);
  196. }
  197. // eslint-disable-next-line no-extra-bind
  198. }).bind(this),
  199. );
  200. }
  201. /**
  202. * Handle move event.
  203. * @private
  204. * @param {ol.MapBrowserEvent} evt Move event.
  205. */
  206. onMove(evt) {
  207. const features = this.getClosestFeatures(
  208. evt.coordinate,
  209. this.nbClosestFeatures,
  210. );
  211. this.linesLayer.getSource().clear();
  212. this.snapLayer.getSource().clear();
  213. this.pointerInteraction.dispatchEvent(
  214. new SnapEvent(SnapEventType.SNAP, features.length ? features : null, evt),
  215. );
  216. if (this.properties.showSnapLines) {
  217. this.drawSnapLines(evt.coordinate, features);
  218. }
  219. if (this.properties.showSnapPoints && features.length) {
  220. this.drawSnapPoints(evt.coordinate, features[0]);
  221. }
  222. }
  223. /**
  224. * Returns a list of the {num} closest features
  225. * to a given coordinate.
  226. * @private
  227. * @param {ol.Coordinate} coordinate Coordinate.
  228. * @param {Number} numFeatures Number of features to search.
  229. * @returns {Array.<ol.Feature>} List of closest features.
  230. */
  231. getClosestFeatures(coordinate, numFeatures) {
  232. const num = numFeatures || 1;
  233. const ext = [-Infinity, -Infinity, Infinity, Infinity];
  234. const featureDict = {};
  235. const pushSnapFeatures = (f) => {
  236. const cCoord = f.getGeometry().getClosestPoint(coordinate);
  237. const dx = cCoord[0] - coordinate[0];
  238. const dy = cCoord[1] - coordinate[1];
  239. const dist = dx * dx + dy * dy;
  240. featureDict[dist] = f;
  241. };
  242. this.source.forEachFeatureInExtent(ext, (f) => {
  243. if (!this.filter || (this.filter && this.filter(f))) {
  244. pushSnapFeatures(f);
  245. }
  246. });
  247. const dists = Object.keys(featureDict);
  248. let features = [];
  249. const count = Math.min(dists.length, num);
  250. dists.sort((a, b) => a - b);
  251. for (let i = 0; i < count; i += 1) {
  252. features.push(featureDict[dists[i]]);
  253. }
  254. // Remove edit and draw feature for snapping list.
  255. const editFeature = this.editor.getEditFeature();
  256. const drawFeature = this.editor.getDrawFeature();
  257. [editFeature, drawFeature].forEach((feature) => {
  258. const index = features.indexOf(feature);
  259. if (index > -1) {
  260. features.splice(index, 1);
  261. }
  262. });
  263. // When using showSnapPoints, return all features except edit/draw features
  264. if (this.properties.showSnapPoints) {
  265. return features;
  266. }
  267. // When using showSnapLines, return all features but edit/draw features are
  268. // cloned to remove the node at the mouse position.
  269. [editFeature, drawFeature]
  270. .filter((f) => f)
  271. .forEach((feature) => {
  272. const geom = feature.getGeometry();
  273. const snapGeom = getShiftedMultiPoint(geom, coordinate);
  274. const isPolygon = geom instanceof Polygon;
  275. const snapFeature = feature.clone();
  276. snapFeature
  277. .getGeometry()
  278. .setCoordinates(
  279. isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
  280. );
  281. features = [snapFeature, ...features];
  282. });
  283. return features;
  284. }
  285. /**
  286. * Returns an extent array, considers the map rotation.
  287. * @private
  288. * @param {ol.Geometry} geometry An OL geometry.
  289. * @returns {Array.<number>} extent array.
  290. */
  291. getRotatedExtent(geometry, coordinate) {
  292. const coordinates =
  293. geometry instanceof Polygon
  294. ? geometry.getCoordinates()[0]
  295. : geometry.getCoordinates();
  296. if (!coordinates.length) {
  297. // Polygons initially return a geometry with an empty coordinate array, so we need to catch it
  298. return [coordinate];
  299. }
  300. // Get the extreme X and Y using pixel values so the rotation is considered
  301. const xMin = coordinates.reduce((finalMin, coord) => {
  302. const pixelCurrent = this.map.getPixelFromCoordinate(coord);
  303. const pixelFinal = this.map.getPixelFromCoordinate(
  304. finalMin || coordinates[0],
  305. );
  306. return pixelCurrent[0] <= pixelFinal[0] ? coord : finalMin;
  307. });
  308. const xMax = coordinates.reduce((finalMax, coord) => {
  309. const pixelCurrent = this.map.getPixelFromCoordinate(coord);
  310. const pixelFinal = this.map.getPixelFromCoordinate(
  311. finalMax || coordinates[0],
  312. );
  313. return pixelCurrent[0] >= pixelFinal[0] ? coord : finalMax;
  314. });
  315. const yMin = coordinates.reduce((finalMin, coord) => {
  316. const pixelCurrent = this.map.getPixelFromCoordinate(coord);
  317. const pixelFinal = this.map.getPixelFromCoordinate(
  318. finalMin || coordinates[0],
  319. );
  320. return pixelCurrent[1] <= pixelFinal[1] ? coord : finalMin;
  321. });
  322. const yMax = coordinates.reduce((finalMax, coord) => {
  323. const pixelCurrent = this.map.getPixelFromCoordinate(coord);
  324. const pixelFinal = this.map.getPixelFromCoordinate(
  325. finalMax || coordinates[0],
  326. );
  327. return pixelCurrent[1] >= pixelFinal[1] ? coord : finalMax;
  328. });
  329. // Create four infinite lines through the extremes X and Y and rotate them
  330. const minVertLine = new LineString([
  331. [xMin[0], -20037508.342789],
  332. [xMin[0], 20037508.342789],
  333. ]);
  334. minVertLine.rotate(this.map.getView().getRotation(), xMin);
  335. const maxVertLine = new LineString([
  336. [xMax[0], -20037508.342789],
  337. [xMax[0], 20037508.342789],
  338. ]);
  339. maxVertLine.rotate(this.map.getView().getRotation(), xMax);
  340. const minHoriLine = new LineString([
  341. [-20037508.342789, yMin[1]],
  342. [20037508.342789, yMin[1]],
  343. ]);
  344. minHoriLine.rotate(this.map.getView().getRotation(), yMin);
  345. const maxHoriLine = new LineString([
  346. [-20037508.342789, yMax[1]],
  347. [20037508.342789, yMax[1]],
  348. ]);
  349. maxHoriLine.rotate(this.map.getView().getRotation(), yMax);
  350. // Use intersection points of the four lines to get the extent
  351. const intersectTopLeft = OverlayOp.intersection(
  352. parser.read(minVertLine),
  353. parser.read(minHoriLine),
  354. );
  355. const intersectBottomLeft = OverlayOp.intersection(
  356. parser.read(minVertLine),
  357. parser.read(maxHoriLine),
  358. );
  359. const intersectTopRight = OverlayOp.intersection(
  360. parser.read(maxVertLine),
  361. parser.read(minHoriLine),
  362. );
  363. const intersectBottomRight = OverlayOp.intersection(
  364. parser.read(maxVertLine),
  365. parser.read(maxHoriLine),
  366. );
  367. return [
  368. [intersectTopLeft.getCoordinate().x, intersectTopLeft.getCoordinate().y],
  369. [
  370. intersectBottomLeft.getCoordinate().x,
  371. intersectBottomLeft.getCoordinate().y,
  372. ],
  373. [
  374. intersectTopRight.getCoordinate().x,
  375. intersectTopRight.getCoordinate().y,
  376. ],
  377. [
  378. intersectBottomRight.getCoordinate().x,
  379. intersectBottomRight.getCoordinate().y,
  380. ],
  381. ];
  382. }
  383. // Calculate lines that are vertical or horizontal to a coordinate.
  384. getVerticalAndHorizontalLines(coordinate, snapCoords) {
  385. // Draw snaplines when cursor vertically or horizontally aligns with a snap feature.
  386. // We draw only on vertical and one horizontal line to avoid crowded lines when polygons or lines have a lot of coordinates.
  387. const halfTol = this.snapTolerance / 2;
  388. const doubleTol = this.snapTolerance * 2;
  389. const mousePx = this.map.getPixelFromCoordinate(coordinate);
  390. const [mouseX, mouseY] = mousePx;
  391. let vLine;
  392. let hLine;
  393. let closerDistanceWithVLine = Infinity;
  394. let closerDistanceWithHLine = Infinity;
  395. for (let i = 0; i < snapCoords.length; i += 1) {
  396. const snapCoord = snapCoords[i];
  397. const snapPx = this.map.getPixelFromCoordinate(snapCoords[i]);
  398. const [snapX, snapY] = snapPx;
  399. const drawVLine = mouseX > snapX - halfTol && mouseX < snapX + halfTol;
  400. const drawHLine = mouseY > snapY - halfTol && mouseY < snapY + halfTol;
  401. const distanceWithVLine = Math.abs(mouseX - snapX);
  402. const distanceWithHLine = Math.abs(mouseY - snapY);
  403. if (
  404. (drawVLine && distanceWithVLine > closerDistanceWithVLine) ||
  405. (drawHLine && distanceWithHLine > closerDistanceWithHLine)
  406. ) {
  407. // eslint-disable-next-line no-continue
  408. continue;
  409. }
  410. let newPt;
  411. if (drawVLine) {
  412. closerDistanceWithVLine = distanceWithVLine;
  413. const newY = mouseY + (mouseY < snapY ? -doubleTol : doubleTol);
  414. newPt = this.map.getCoordinateFromPixel([snapX, newY]);
  415. } else if (drawHLine) {
  416. closerDistanceWithHLine = distanceWithHLine;
  417. const newX = mouseX + (mouseX < snapX ? -doubleTol : doubleTol);
  418. newPt = this.map.getCoordinateFromPixel([newX, snapY]);
  419. }
  420. if (newPt) {
  421. const lineCoords = [newPt, snapCoord];
  422. const geom = new LineString(lineCoords);
  423. const feature = new Feature(geom);
  424. feature.set(SNAP_FEATURE_TYPE_PROPERTY, VH_LINE_KEY);
  425. if (drawVLine) {
  426. vLine = feature;
  427. }
  428. if (drawHLine) {
  429. hLine = feature;
  430. }
  431. }
  432. }
  433. const lines = [];
  434. if (hLine) {
  435. lines.push(hLine);
  436. }
  437. if (vLine && vLine !== hLine) {
  438. lines.push(vLine);
  439. }
  440. return lines;
  441. }
  442. /**
  443. * For each segment, we calculate lines that extends it.
  444. */
  445. getSegmentLines(coordinate, snapCoords, snapCoordsBefore) {
  446. const mousePx = this.map.getPixelFromCoordinate(coordinate);
  447. const doubleTol = this.snapTolerance * 2;
  448. const [mouseX, mouseY] = mousePx;
  449. const lines = [];
  450. for (let i = 0; i < snapCoords.length; i += 1) {
  451. if (!snapCoordsBefore[i]) {
  452. // eslint-disable-next-line no-continue
  453. continue;
  454. }
  455. const snapCoordBefore = snapCoordsBefore[i];
  456. const snapCoord = snapCoords[i];
  457. const snapPxBefore = this.map.getPixelFromCoordinate(snapCoordBefore);
  458. const snapPx = this.map.getPixelFromCoordinate(snapCoord);
  459. const [snapX] = snapPx;
  460. // Calculate projected point
  461. const projMousePx = getProjectedPoint(mousePx, snapPxBefore, snapPx);
  462. const [projMouseX, projMouseY] = projMousePx;
  463. const distance = Math.sqrt(
  464. (projMouseX - mouseX) ** 2 + (projMouseY - mouseY) ** 2,
  465. );
  466. let newPt;
  467. if (distance <= this.snapTolerance) {
  468. // lineFunc is undefined when it's a vertical line
  469. const lineFunc = getEquationOfLine(snapPxBefore, snapPx);
  470. const newX = projMouseX + (projMouseX < snapX ? -doubleTol : doubleTol);
  471. if (lineFunc) {
  472. newPt = this.map.getCoordinateFromPixel([
  473. newX,
  474. lineFunc ? lineFunc(newX) : projMouseY,
  475. ]);
  476. }
  477. }
  478. if (newPt) {
  479. const lineCoords = [snapCoordBefore, snapCoord, newPt];
  480. const geom = new LineString(lineCoords);
  481. const feature = new Feature(geom);
  482. feature.set(SNAP_FEATURE_TYPE_PROPERTY, SEGMENT_LINE_KEY);
  483. lines.push(feature);
  484. }
  485. }
  486. return lines;
  487. }
  488. /**
  489. * For each segment, we calculate lines that are perpendicular.
  490. */
  491. getOrthoLines(coordinate, snapCoords, snapCoordsBefore) {
  492. const mousePx = this.map.getPixelFromCoordinate(coordinate);
  493. const doubleTol = this.snapTolerance * 2;
  494. const [mouseX, mouseY] = mousePx;
  495. const lines = [];
  496. for (let i = 0; i < snapCoords.length; i += 1) {
  497. if (!snapCoordsBefore[i]) {
  498. // eslint-disable-next-line no-continue
  499. continue;
  500. }
  501. const snapCoordBefore = snapCoordsBefore[i];
  502. const snapCoord = snapCoords[i];
  503. const snapPxBefore = this.map.getPixelFromCoordinate(snapCoordBefore);
  504. const snapPx = this.map.getPixelFromCoordinate(snapCoord);
  505. const orthoLine1 = new LineString([snapPxBefore, snapPx]);
  506. orthoLine1.rotate((90 * Math.PI) / 180, snapPxBefore);
  507. const orthoLine2 = new LineString([snapPx, snapPxBefore]);
  508. orthoLine2.rotate((90 * Math.PI) / 180, snapPx);
  509. [orthoLine1, orthoLine2].forEach((line) => {
  510. const [anchorPx, last] = line.getCoordinates();
  511. const projMousePx = getProjectedPoint(mousePx, anchorPx, last);
  512. const [projMouseX, projMouseY] = projMousePx;
  513. const distance = Math.sqrt(
  514. (projMouseX - mouseX) ** 2 + (projMouseY - mouseY) ** 2,
  515. );
  516. let newPt;
  517. if (distance <= this.snapTolerance) {
  518. // lineFunc is undefined when it's a vertical line
  519. const lineFunc = getEquationOfLine(anchorPx, projMousePx);
  520. const newX =
  521. projMouseX + (projMouseX < anchorPx[0] ? -doubleTol : doubleTol);
  522. if (lineFunc) {
  523. newPt = this.map.getCoordinateFromPixel([
  524. newX,
  525. lineFunc ? lineFunc(newX) : projMouseY,
  526. ]);
  527. }
  528. }
  529. if (newPt) {
  530. const coords = [this.map.getCoordinateFromPixel(anchorPx), newPt];
  531. const geom = new LineString(coords);
  532. const feature = new Feature(geom);
  533. feature.set(SNAP_FEATURE_TYPE_PROPERTY, ORTHO_LINE_KEY);
  534. lines.push(feature);
  535. }
  536. });
  537. }
  538. return lines;
  539. }
  540. /**
  541. * Draws snap lines by building the extent for
  542. * a pair of features.
  543. * @private
  544. * @param {ol.Coordinate} coordinate Mouse pointer coordinate.
  545. * @param {Array.<ol.Feature>} features List of features.
  546. */
  547. drawSnapLines(coordinate, features) {
  548. // First get all snap points: neighbouring feature vertices and extent corners
  549. const snapCoordsBefore = []; // store the direct before point in the coordinate array
  550. const snapCoords = [];
  551. const snapCoordsAfter = []; // store the direct next point in the coordinate array
  552. for (let i = 0; i < features.length; i += 1) {
  553. const geom = features[i].getGeometry();
  554. const featureCoord = geom.getCoordinates();
  555. // Polygons initially return a geometry with an empty coordinate array, so we need to catch it
  556. if (featureCoord.length) {
  557. if (geom instanceof Point) {
  558. snapCoordsBefore.push();
  559. snapCoords.push(featureCoord);
  560. snapCoordsAfter.push();
  561. } else {
  562. // Add feature vertices
  563. // eslint-disable-next-line no-lonely-if
  564. if (geom instanceof LineString) {
  565. for (let j = 0; j < featureCoord.length; j += 1) {
  566. snapCoordsBefore.push(featureCoord[j - 1]);
  567. snapCoords.push(featureCoord[j]);
  568. snapCoordsAfter.push(featureCoord[j + 1]);
  569. }
  570. } else if (geom instanceof Polygon) {
  571. for (let j = 0; j < featureCoord[0].length; j += 1) {
  572. snapCoordsBefore.push(featureCoord[0][j - 1]);
  573. snapCoords.push(featureCoord[0][j]);
  574. snapCoordsAfter.push(featureCoord[0][j + 1]);
  575. }
  576. }
  577. // Add extent vertices
  578. // const coords = this.getRotatedExtent(geom, coordinate);
  579. // for (let j = 0; j < coords.length; j += 1) {
  580. // snapCoordsBefore.push();
  581. // snapCoords.push(coords[j]);
  582. // snapCoordsNext.push();
  583. // }
  584. }
  585. }
  586. }
  587. const {
  588. showVerticalAndHorizontalLines,
  589. showOrthoLines,
  590. showSegmentLines,
  591. snapLinesOrder,
  592. } = this.properties;
  593. const lines = [];
  594. const helpLinesOrdered = [];
  595. const helpLines = {
  596. [ORTHO_LINE_KEY]: [],
  597. [SEGMENT_LINE_KEY]: [],
  598. [VH_LINE_KEY]: [],
  599. [CUSTOM_LINE_KEY]: [],
  600. };
  601. if (showOrthoLines) {
  602. helpLines[ORTHO_LINE_KEY] =
  603. this.getOrthoLines(coordinate, snapCoords, snapCoordsBefore) || [];
  604. }
  605. if (showSegmentLines) {
  606. helpLines[SEGMENT_LINE_KEY] =
  607. this.getSegmentLines(coordinate, snapCoords, snapCoordsBefore) || [];
  608. }
  609. if (showVerticalAndHorizontalLines) {
  610. helpLines[VH_LINE_KEY] =
  611. this.getVerticalAndHorizontalLines(coordinate, snapCoords) || [];
  612. }
  613. // Add custom lines
  614. if (this.drawCustomSnapLines) {
  615. helpLines[CUSTOM_LINE_KEY] =
  616. this.drawCustomSnapLines(
  617. coordinate,
  618. snapCoords,
  619. snapCoordsBefore,
  620. snapCoordsAfter,
  621. ) || [];
  622. }
  623. // Add help lines in a defined order.
  624. snapLinesOrder.forEach((lineType) => {
  625. helpLinesOrdered.push(...(helpLines[lineType] || []));
  626. });
  627. // Remove duplicated lines, comparing their equation using pixels.
  628. helpLinesOrdered.forEach((lineA) => {
  629. if (
  630. !lines.length ||
  631. !lines.find((lineB) => isSameLines(lineA, lineB, this.map))
  632. ) {
  633. lines.push(lineA);
  634. }
  635. });
  636. // We snap on intersections of lines (distance < this.snapTolerance) or on all the help lines.
  637. const intersectFeatures = getIntersectedLinesAndPoint(
  638. coordinate,
  639. lines,
  640. this.map,
  641. this.snapTolerance,
  642. );
  643. if (intersectFeatures?.length) {
  644. intersectFeatures.forEach((feature) => {
  645. if (feature.getGeometry().getType() === 'Point') {
  646. this.snapLayer.getSource().addFeature(feature);
  647. } else {
  648. this.linesLayer.getSource().addFeature(feature);
  649. }
  650. });
  651. } else {
  652. this.snapLayer.getSource().addFeatures(lines);
  653. }
  654. }
  655. /**
  656. * Adds snap points to the snapping layer.
  657. * @private
  658. * @param {ol.Coordinate} coordinate cursor coordinate.
  659. * @param {ol.eaturee} feature Feature to draw the snap points for.
  660. */
  661. drawSnapPoints(coordinate, feature) {
  662. const featCoord = feature.getGeometry().getClosestPoint(coordinate);
  663. const px = this.map.getPixelFromCoordinate(featCoord);
  664. let snapCoords = [];
  665. if (this.properties.useMapUnits) {
  666. snapCoords = [
  667. [featCoord[0] - this.properties.snapPointDist, featCoord[1]],
  668. [featCoord[0] + this.properties.snapPointDist, featCoord[1]],
  669. [featCoord[0], featCoord[1] - this.properties.snapPointDist],
  670. [featCoord[0], featCoord[1] + this.properties.snapPointDist],
  671. ];
  672. } else {
  673. const snapPx = [
  674. [px[0] - this.properties.snapPointDist, px[1]],
  675. [px[0] + this.properties.snapPointDist, px[1]],
  676. [px[0], px[1] - this.properties.snapPointDist],
  677. [px[0], px[1] + this.properties.snapPointDist],
  678. ];
  679. for (let j = 0; j < snapPx.length; j += 1) {
  680. snapCoords.push(this.map.getCoordinateFromPixel(snapPx[j]));
  681. }
  682. }
  683. const snapGeom = new MultiPoint(snapCoords);
  684. this.snapLayer.getSource().addFeature(new Feature(snapGeom));
  685. }
  686. /**
  687. * @inheritdoc
  688. */
  689. activate(silent) {
  690. super.activate(silent);
  691. this.snapLayer.setMap(this.map);
  692. this.linesLayer.setMap(this.map);
  693. this.map.addInteraction(this.pointerInteraction);
  694. this.map.addInteraction(this.snapInteraction);
  695. document.getElementById('aux-cb')?.addEventListener('change', (evt) => {
  696. this.setProperties({
  697. showSnapLines: evt.target.checked,
  698. showSnapPoints: !evt.target.checked,
  699. });
  700. });
  701. document.getElementById('dist-cb')?.addEventListener('change', (evt) => {
  702. this.setProperties({
  703. showSnapPoints: evt.target.checked,
  704. showSnapLines: !evt.target.checked,
  705. });
  706. });
  707. document.getElementById('width-input')?.addEventListener('keyup', (evt) => {
  708. const snapPointDist = parseFloat(evt.target.value);
  709. if (!Number.isNaN(snapPointDist)) {
  710. this.setProperties({ snapPointDist });
  711. }
  712. });
  713. }
  714. /**
  715. * @inheritdoc
  716. */
  717. deactivate(silent) {
  718. super.deactivate(silent);
  719. this.snapLayer.setMap(null);
  720. this.linesLayer.setMap(null);
  721. this.map.removeInteraction(this.pointerInteraction);
  722. this.map.removeInteraction(this.snapInteraction);
  723. }
  724. }
  725. export default CadControl;