Source: control/cad.js

  1. import { RegularShape, Style, Fill, Stroke } from 'ol/style';
  2. import { Point, LineString, Polygon, MultiPoint } from 'ol/geom';
  3. import { fromExtent } from 'ol/geom/Polygon';
  4. import Feature from 'ol/Feature';
  5. import Vector from 'ol/layer/Vector';
  6. import VectorSource from 'ol/source/Vector';
  7. import { Pointer, Snap } from 'ol/interaction';
  8. import Control from './control';
  9. import cadSVG from '../../img/cad.svg';
  10. import SnapEvent, { SnapEventType } from '../event/snap-event';
  11. /**
  12. * Control with snapping functionality for geometry alignment.
  13. * @extends {ole.Control}
  14. * @alias ole.CadControl
  15. */
  16. class CadControl extends Control {
  17. /**
  18. * @param {Object} [options] Tool options.
  19. * @param {Function} [options.filter] Returns an array containing the features
  20. * to include for CAD (takes the source as a single argument).
  21. * @param {Number} [options.snapTolerance] Snap tolerance in pixel
  22. * for snap lines. Default is 10.
  23. * @param {Boolean} [options.showSnapLines] Whether to show
  24. * snap lines (default is true).
  25. * @param {Boolean} [options.showSnapPoints] Whether to show
  26. * snap points around the closest feature.
  27. * @param {Number} [options.snapPointDist] Distance of the
  28. * snap points (default is 30).
  29. * @param {Boolean} [options.useMapUnits] Whether to use map units
  30. * as measurement for point snapping. Default is false (pixel are used).
  31. * @param {ol.style.Style.StyleLike} [options.snapStyle] Style used for the snap layer.
  32. * @param {ol.style.Style.StyleLike} [options.linesStyle] Style used for the lines layer.
  33. */
  34. constructor(options) {
  35. super({
  36. title: 'CAD control',
  37. className: 'ole-control-cad',
  38. image: cadSVG,
  39. showSnapPoints: true,
  40. showSnapLines: false,
  41. snapPointDist: 10,
  42. ...options,
  43. });
  44. /**
  45. * Interaction for handling move events.
  46. * @type {ol.interaction.Pointer}
  47. * @private
  48. */
  49. this.pointerInteraction = new Pointer({
  50. handleMoveEvent: this.onMove.bind(this),
  51. });
  52. /**
  53. * Layer for drawing snapping geometries.
  54. * @type {ol.layer.Vector}
  55. * @private
  56. */
  57. this.snapLayer = new Vector({
  58. source: new VectorSource(),
  59. style: options.snapStyle || [
  60. new Style({
  61. image: new RegularShape({
  62. fill: new Fill({
  63. color: '#E8841F',
  64. }),
  65. stroke: new Stroke({
  66. width: 1,
  67. color: '#618496',
  68. }),
  69. points: 4,
  70. radius: 5,
  71. radius2: 0,
  72. angle: Math.PI / 4,
  73. }),
  74. stroke: new Stroke({
  75. width: 1,
  76. lineDash: [5, 10],
  77. color: '#618496',
  78. }),
  79. }),
  80. ],
  81. });
  82. /**
  83. * Layer for colored lines indicating
  84. * intesection point between snapping lines.
  85. * @type {ol.layer.Vector}
  86. * @private
  87. */
  88. this.linesLayer = new Vector({
  89. source: new VectorSource(),
  90. style: options.linesStyle || [
  91. new Style({
  92. stroke: new Stroke({
  93. width: 1,
  94. lineDash: [5, 10],
  95. color: '#FF530D',
  96. }),
  97. }),
  98. ],
  99. });
  100. /**
  101. * Snap tolerance in pixel.
  102. * @type {Number}
  103. * @private
  104. */
  105. this.snapTolerance =
  106. options.snapTolerance === undefined ? 10 : options.snapTolerance;
  107. /**
  108. * Filter the features to snap with.
  109. * @type {Function}
  110. * @private
  111. */
  112. this.filter = options.filter || null;
  113. /**
  114. * Interaction for snapping
  115. * @type {ol.interaction.Snap}
  116. * @private
  117. */
  118. this.snapInteraction = new Snap({
  119. pixelTolerance: this.snapTolerance,
  120. source: this.snapLayer.getSource(),
  121. });
  122. this.standalone = false;
  123. }
  124. /**
  125. * @inheritdoc
  126. */
  127. getDialogTemplate() {
  128. const distLabel = this.properties.useMapUnits ? 'map units' : 'px';
  129. return `
  130. <div>
  131. <input
  132. id="aux-cb"
  133. type="radio"
  134. name="radioBtn"
  135. ${this.properties.showSnapLines ? 'checked' : ''}
  136. >
  137. <label>Show snap lines</label>
  138. </div>
  139. <div>
  140. <input
  141. id="dist-cb"
  142. type="radio"
  143. name="radioBtn"
  144. ${this.properties.showSnapPoints ? 'checked' : ''}
  145. >
  146. <label>Show snap points. Distance (${distLabel}):</label>
  147. <input type="text" id="width-input"
  148. value="${this.properties.snapPointDist}">
  149. </div>
  150. `;
  151. }
  152. /**
  153. * @inheritdoc
  154. */
  155. setMap(map) {
  156. super.setMap(map);
  157. // Ensure that the snap interaction is at the last position
  158. // as it must be the first to handle the pointermove event.
  159. this.map.getInteractions().on(
  160. 'add',
  161. ((e) => {
  162. const pos = e.target.getArray().indexOf(this.snapInteraction);
  163. if (
  164. this.snapInteraction.getActive() &&
  165. pos > -1 &&
  166. pos !== e.target.getLength() - 1
  167. ) {
  168. this.deactivate(true);
  169. this.activate(true);
  170. }
  171. // eslint-disable-next-line no-extra-bind
  172. }).bind(this),
  173. );
  174. }
  175. /**
  176. * Handle move event.
  177. * @private
  178. * @param {ol.MapBrowserEvent} evt Move event.
  179. */
  180. onMove(evt) {
  181. const features = this.getClosestFeatures(evt.coordinate, 5);
  182. this.linesLayer.getSource().clear();
  183. this.snapLayer.getSource().clear();
  184. this.pointerInteraction.dispatchEvent(
  185. new SnapEvent(SnapEventType.SNAP, features.length ? features : null, evt),
  186. );
  187. if (this.properties.showSnapLines) {
  188. this.drawSnapLines(features, evt.coordinate);
  189. }
  190. if (this.properties.showSnapPoints && features.length) {
  191. this.drawSnapPoints(evt.coordinate, features[0]);
  192. }
  193. }
  194. /**
  195. * Returns a list of the {num} closest features
  196. * to a given coordinate.
  197. * @private
  198. * @param {ol.Coordinate} coordinate Coordinate.
  199. * @param {Number} numFeatures Number of features to search.
  200. * @returns {Array.<ol.Feature>} List of closest features.
  201. */
  202. getClosestFeatures(coordinate, numFeatures) {
  203. const num = numFeatures || 1;
  204. const ext = [-Infinity, -Infinity, Infinity, Infinity];
  205. const featureDict = {};
  206. const pushSnapFeatures = (f) => {
  207. const cCoord = f.getGeometry().getClosestPoint(coordinate);
  208. const dx = cCoord[0] - coordinate[0];
  209. const dy = cCoord[1] - coordinate[1];
  210. const dist = dx * dx + dy * dy;
  211. featureDict[dist] = f;
  212. };
  213. this.source.forEachFeatureInExtent(ext, (f) => {
  214. if (!this.filter || (this.filter && this.filter(f))) {
  215. pushSnapFeatures(f);
  216. }
  217. });
  218. const dists = Object.keys(featureDict);
  219. let features = [];
  220. const count = Math.min(dists.length, num);
  221. dists.sort((a, b) => a - b);
  222. for (let i = 0; i < count; i += 1) {
  223. features.push(featureDict[dists[i]]);
  224. }
  225. const drawFeature = this.editor.getDrawFeature();
  226. if (drawFeature) {
  227. // Include all but the last vertex (at mouse position) to prevent snapping on mouse cursor node
  228. const currentDrawFeature = drawFeature.clone();
  229. currentDrawFeature
  230. .getGeometry()
  231. .setCoordinates(
  232. drawFeature.getGeometry().getCoordinates().slice(0, -1),
  233. );
  234. features = [currentDrawFeature, ...features];
  235. }
  236. const editFeature = this.editor.getEditFeature();
  237. if (editFeature && this.editor.modifyStartCoordinate) {
  238. /* Include all nodes of the edit feature except the node being modified */
  239. // First exclude the edit feature from snap detection
  240. if (features.indexOf(editFeature) > -1) {
  241. features.splice(features.indexOf(editFeature), 1);
  242. }
  243. // Convert to MultiPoint and get the node coordinate closest to mouse cursor
  244. const snapGeom = new MultiPoint(
  245. editFeature.getGeometry().getCoordinates(),
  246. );
  247. const editNodeCoordinate = snapGeom.getClosestPoint(
  248. this.editor.modifyStartCoordinate,
  249. );
  250. // Exclude the node being modified
  251. snapGeom.setCoordinates(
  252. snapGeom.getCoordinates().filter((coord) => {
  253. return coord.toString() !== editNodeCoordinate.toString();
  254. }),
  255. );
  256. // Clone editFeature and apply adjusted snap geometry
  257. const snapEditFeature = editFeature.clone();
  258. snapEditFeature.getGeometry().setCoordinates(snapGeom.getCoordinates());
  259. features = [snapEditFeature, ...features];
  260. }
  261. return features;
  262. }
  263. /**
  264. * Draws snap lines by building the extent for
  265. * a pair of features.
  266. * @private
  267. * @param {Array.<ol.Feature>} features List of features.
  268. * @param {ol.Coordinate} coordinate Mouse pointer coordinate.
  269. */
  270. drawSnapLines(features, coordinate) {
  271. let auxCoords = [];
  272. for (let i = 0; i < features.length; i += 1) {
  273. const geom = features[i].getGeometry();
  274. const featureCoord = geom.getCoordinates();
  275. if (featureCoord.length) {
  276. if (geom instanceof Point) {
  277. auxCoords.push(featureCoord);
  278. } else {
  279. // filling snapLayer with features vertex
  280. if (geom instanceof LineString) {
  281. for (let j = 0; j < featureCoord.length; j += 1) {
  282. auxCoords.push(featureCoord[j]);
  283. }
  284. } else if (geom instanceof Polygon) {
  285. for (let j = 0; j < featureCoord[0].length; j += 1) {
  286. auxCoords.push(featureCoord[0][j]);
  287. }
  288. }
  289. // filling auxCoords
  290. const coords = fromExtent(geom.getExtent()).getCoordinates()[0];
  291. auxCoords = auxCoords.concat(coords);
  292. }
  293. }
  294. }
  295. const px = this.map.getPixelFromCoordinate(coordinate);
  296. let lineCoords = null;
  297. for (let i = 0; i < auxCoords.length; i += 1) {
  298. const tol = this.snapTolerance;
  299. const auxPx = this.map.getPixelFromCoordinate(auxCoords[i]);
  300. const drawVLine =
  301. px[0] > auxPx[0] - this.snapTolerance / 2 &&
  302. px[0] < auxPx[0] + this.snapTolerance / 2;
  303. const drawHLine =
  304. px[1] > auxPx[1] - this.snapTolerance / 2 &&
  305. px[1] < auxPx[1] + this.snapTolerance / 2;
  306. if (drawVLine) {
  307. let newY = px[1];
  308. newY += px[1] < auxPx[1] ? -tol * 2 : tol * 2;
  309. const newPt = this.map.getCoordinateFromPixel([auxPx[0], newY]);
  310. lineCoords = [[auxCoords[i][0], newPt[1]], auxCoords[i]];
  311. } else if (drawHLine) {
  312. let newX = px[0];
  313. newX += px[0] < auxPx[0] ? -tol * 2 : tol * 2;
  314. const newPt = this.map.getCoordinateFromPixel([newX, auxPx[1]]);
  315. lineCoords = [[newPt[0], auxCoords[i][1]], auxCoords[i]];
  316. }
  317. if (lineCoords) {
  318. const g = new LineString(lineCoords);
  319. this.snapLayer.getSource().addFeature(new Feature(g));
  320. }
  321. }
  322. let vertArray = null;
  323. let horiArray = null;
  324. const snapFeatures = this.snapLayer.getSource().getFeatures();
  325. if (snapFeatures.length) {
  326. snapFeatures.forEach((feature) => {
  327. const featureCoord = feature.getGeometry().getCoordinates();
  328. const x0 = featureCoord[0][0];
  329. const x1 = featureCoord[1][0];
  330. const y0 = featureCoord[0][1];
  331. const y1 = featureCoord[1][1];
  332. if (x0 === x1) {
  333. vertArray = x0;
  334. }
  335. if (y0 === y1) {
  336. horiArray = y0;
  337. }
  338. });
  339. const snapPt = [];
  340. if (vertArray && horiArray) {
  341. snapPt.push(vertArray);
  342. snapPt.push(horiArray);
  343. this.linesLayer.getSource().addFeatures(snapFeatures);
  344. this.snapLayer.getSource().clear();
  345. const snapGeom = new Point(snapPt);
  346. this.snapLayer.getSource().addFeature(new Feature(snapGeom));
  347. }
  348. }
  349. }
  350. /**
  351. * Adds snap points to the snapping layer.
  352. * @private
  353. * @param {ol.Coordinate} coordinateMouse cursor coordinate.
  354. * @param {ol.eaturee} feature Feature to draw the snap points for.
  355. */
  356. drawSnapPoints(coordinate, feature) {
  357. const featCoord = feature.getGeometry().getClosestPoint(coordinate);
  358. const px = this.map.getPixelFromCoordinate(featCoord);
  359. let snapCoords = [];
  360. if (this.properties.useMapUnits) {
  361. snapCoords = [
  362. [featCoord[0] - this.properties.snapPointDist, featCoord[1]],
  363. [featCoord[0] + this.properties.snapPointDist, featCoord[1]],
  364. [featCoord[0], featCoord[1] - this.properties.snapPointDist],
  365. [featCoord[0], featCoord[1] + this.properties.snapPointDist],
  366. ];
  367. } else {
  368. const snapPx = [
  369. [px[0] - this.properties.snapPointDist, px[1]],
  370. [px[0] + this.properties.snapPointDist, px[1]],
  371. [px[0], px[1] - this.properties.snapPointDist],
  372. [px[0], px[1] + this.properties.snapPointDist],
  373. ];
  374. for (let j = 0; j < snapPx.length; j += 1) {
  375. snapCoords.push(this.map.getCoordinateFromPixel(snapPx[j]));
  376. }
  377. }
  378. const snapGeom = new MultiPoint(snapCoords);
  379. this.snapLayer.getSource().addFeature(new Feature(snapGeom));
  380. }
  381. /**
  382. * @inheritdoc
  383. */
  384. activate(silent) {
  385. super.activate(silent);
  386. this.snapLayer.setMap(this.map);
  387. this.linesLayer.setMap(this.map);
  388. this.map.addInteraction(this.pointerInteraction);
  389. this.map.addInteraction(this.snapInteraction);
  390. document.getElementById('aux-cb').addEventListener('change', (evt) => {
  391. this.setProperties({
  392. showSnapLines: evt.target.checked,
  393. showSnapPoints: !evt.target.checked,
  394. });
  395. });
  396. document.getElementById('dist-cb').addEventListener('change', (evt) => {
  397. this.setProperties({
  398. showSnapPoints: evt.target.checked,
  399. showSnapLines: !evt.target.checked,
  400. });
  401. });
  402. document.getElementById('width-input').addEventListener('keyup', (evt) => {
  403. const snapPointDist = parseFloat(evt.target.value);
  404. if (!Number.isNaN(snapPointDist)) {
  405. this.setProperties({ snapPointDist });
  406. }
  407. });
  408. }
  409. /**
  410. * @inheritdoc
  411. */
  412. deactivate(silent) {
  413. super.deactivate(silent);
  414. this.snapLayer.setMap(null);
  415. this.linesLayer.setMap(null);
  416. this.map.removeInteraction(this.pointerInteraction);
  417. this.map.removeInteraction(this.snapInteraction);
  418. }
  419. }
  420. export default CadControl;