Source: control/cad.js

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