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