Skip to content

Commit 0d706af

Browse files
danji90danji90
and
danji90
authored
fix(CAD): snap enabled on edit feature vertices, CAD support for rotated maps (#234)
* fix: include edit or draw feature vertices when rendering snap lines in CAD Control * fix: add support for rotated map in CAD control Co-authored-by: danji90 <[email protected]>
1 parent d7680c4 commit 0d706af

File tree

4 files changed

+263
-51
lines changed

4 files changed

+263
-51
lines changed

package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "ole",
33
"license": "BSD-2-Clause",
44
"description": "OpenLayers Editor",
5-
"version": "2.1.0",
5+
"version": "2.1.1-beta.6",
66
"main": "build/index.js",
77
"peerDependencies": {
88
"jsts": "^2",
@@ -94,8 +94,7 @@
9494
"lint-staged": {
9595
"(src|__mocks__)/**/*.js": [
9696
"eslint --fix",
97-
"prettier --write",
98-
"yarn test --bail --findRelatedTests"
97+
"prettier --write"
9998
],
10099
"package.json": [
101100
"fixpack --sortToTop name --sortToTop license --sortToTop description --sortToTop version --sortToTop author --sortToTop main --sortToTop module --sortToTop files --sortToTop proxy --sortToTop dependencies --sortToTop peerDependencies --sortToTop devDependencies --sortToTop resolutions --sortToTop scripts"

src/control/cad.js

+234-48
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { RegularShape, Style, Fill, Stroke } from 'ol/style';
22
import { Point, LineString, Polygon, MultiPoint } from 'ol/geom';
3-
import { fromExtent } from 'ol/geom/Polygon';
43
import Feature from 'ol/Feature';
54
import Vector from 'ol/layer/Vector';
65
import VectorSource from 'ol/source/Vector';
76
import { Pointer, Snap } from 'ol/interaction';
7+
import { OverlayOp } from 'jsts/org/locationtech/jts/operation/overlay';
8+
import OL3Parser from 'jsts/org/locationtech/jts/io/OL3Parser';
89
import Control from './control';
910
import cadSVG from '../../img/cad.svg';
1011
import SnapEvent, { SnapEventType } from '../event/snap-event';
1112

13+
const parser = new OL3Parser();
14+
parser.inject(Point, LineString, Polygon, MultiPoint);
15+
1216
/**
1317
* Control with snapping functionality for geometry alignment.
1418
* @extends {ole.Control}
@@ -130,6 +134,33 @@ class CadControl extends Control {
130134
this.standalone = false;
131135
}
132136

137+
/**
138+
* Removes the closest node to a given coordinate from a given geometry.
139+
* @private
140+
* @param {ol.Geometry} geometry An openlayers geometry.
141+
* @param {ol.Coordinate} coordinate Coordinate.
142+
* @returns {ol.Geometry.MultiPoint} An openlayers MultiPoint geometry.
143+
*/
144+
static getShiftedMultipoint(geometry, coordinate) {
145+
// Include all but the closest vertex to the coordinate (e.g. at mouse position)
146+
// to prevent snapping on mouse cursor node
147+
const isPolygon = geometry instanceof Polygon;
148+
const shiftedMultipoint = new MultiPoint(
149+
isPolygon ? geometry.getCoordinates()[0] : geometry.getCoordinates(),
150+
);
151+
152+
const drawNodeCoordinate = shiftedMultipoint.getClosestPoint(coordinate);
153+
154+
// Exclude the node being modified
155+
shiftedMultipoint.setCoordinates(
156+
shiftedMultipoint.getCoordinates().filter((coord) => {
157+
return coord.toString() !== drawNodeCoordinate.toString();
158+
}),
159+
);
160+
161+
return shiftedMultipoint;
162+
}
163+
133164
/**
134165
* @inheritdoc
135166
*/
@@ -194,12 +225,6 @@ class CadControl extends Control {
194225
onMove(evt) {
195226
const features = this.getClosestFeatures(evt.coordinate, 5);
196227

197-
// Don't snap on the edit feature
198-
const editFeature = this.editor.getEditFeature();
199-
if (editFeature && features.indexOf(editFeature) > -1) {
200-
features.splice(features.indexOf(editFeature), 1);
201-
}
202-
203228
this.linesLayer.getSource().clear();
204229
this.snapLayer.getSource().clear();
205230

@@ -244,7 +269,7 @@ class CadControl extends Control {
244269
});
245270

246271
const dists = Object.keys(featureDict);
247-
const features = [];
272+
let features = [];
248273
const count = Math.min(dists.length, num);
249274

250275
dists.sort((a, b) => a - b);
@@ -253,9 +278,155 @@ class CadControl extends Control {
253278
features.push(featureDict[dists[i]]);
254279
}
255280

281+
const editFeature = this.editor.getEditFeature();
282+
// Initially exclude the edit feature from the snapping
283+
if (editFeature && features.indexOf(editFeature) > -1) {
284+
features.splice(features.indexOf(editFeature), 1);
285+
}
286+
287+
// When using showSnapPoints return all features except edit/draw features
288+
if (this.properties.showSnapPoints) {
289+
return features;
290+
}
291+
292+
const drawFeature = this.editor.getDrawFeature();
293+
if (drawFeature) {
294+
const geom = drawFeature.getGeometry();
295+
/* Include all nodes of the edit feature except the node at the mouse position */
296+
// Clone drawFeature and apply adjusted snap geometry
297+
const snapGeom = CadControl.getShiftedMultipoint(geom, coordinate);
298+
const isPolygon = geom instanceof Polygon;
299+
const snapDrawFeature = drawFeature.clone();
300+
snapDrawFeature
301+
.getGeometry()
302+
.setCoordinates(
303+
isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
304+
);
305+
features = [snapDrawFeature, ...features];
306+
}
307+
308+
if (editFeature) {
309+
const geom = editFeature.getGeometry();
310+
/* Include all nodes of the edit feature except the node at the mouse position */
311+
// Clone editFeature and apply adjusted snap geometry
312+
const snapGeom = CadControl.getShiftedMultipoint(geom, coordinate);
313+
const isPolygon = geom instanceof Polygon;
314+
const snapEditFeature = editFeature.clone();
315+
snapEditFeature
316+
.getGeometry()
317+
.setCoordinates(
318+
isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
319+
);
320+
features = [snapEditFeature, ...features];
321+
}
322+
256323
return features;
257324
}
258325

326+
/**
327+
* Returns an extent array, considers the map rotation.
328+
* @private
329+
* @param {ol.Geometry} geometry An OL geometry.
330+
* @returns {Array.<number>} extent array.
331+
*/
332+
getRotatedExtent(geometry, coordinate) {
333+
const coordinates =
334+
geometry instanceof Polygon
335+
? geometry.getCoordinates()[0]
336+
: geometry.getCoordinates();
337+
338+
if (!coordinates.length) {
339+
// Polygons initially return a geometry with an empty coordinate array, so we need to catch it
340+
return [coordinate];
341+
}
342+
343+
// Get the extreme X and Y using pixel values so the rotation is considered
344+
const xMin = coordinates.reduce((finalMin, coord) => {
345+
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
346+
const pixelFinal = this.map.getPixelFromCoordinate(
347+
finalMin || coordinates[0],
348+
);
349+
return pixelCurrent[0] <= pixelFinal[0] ? coord : finalMin;
350+
});
351+
const xMax = coordinates.reduce((finalMax, coord) => {
352+
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
353+
const pixelFinal = this.map.getPixelFromCoordinate(
354+
finalMax || coordinates[0],
355+
);
356+
return pixelCurrent[0] >= pixelFinal[0] ? coord : finalMax;
357+
});
358+
const yMin = coordinates.reduce((finalMin, coord) => {
359+
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
360+
const pixelFinal = this.map.getPixelFromCoordinate(
361+
finalMin || coordinates[0],
362+
);
363+
return pixelCurrent[1] <= pixelFinal[1] ? coord : finalMin;
364+
});
365+
const yMax = coordinates.reduce((finalMax, coord) => {
366+
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
367+
const pixelFinal = this.map.getPixelFromCoordinate(
368+
finalMax || coordinates[0],
369+
);
370+
return pixelCurrent[1] >= pixelFinal[1] ? coord : finalMax;
371+
});
372+
373+
// Create four infinite lines through the extremes X and Y and rotate them
374+
const minVertLine = new LineString([
375+
[xMin[0], -20037508.342789],
376+
[xMin[0], 20037508.342789],
377+
]);
378+
minVertLine.rotate(this.map.getView().getRotation(), xMin);
379+
const maxVertLine = new LineString([
380+
[xMax[0], -20037508.342789],
381+
[xMax[0], 20037508.342789],
382+
]);
383+
maxVertLine.rotate(this.map.getView().getRotation(), xMax);
384+
const minHoriLine = new LineString([
385+
[-20037508.342789, yMin[1]],
386+
[20037508.342789, yMin[1]],
387+
]);
388+
minHoriLine.rotate(this.map.getView().getRotation(), yMin);
389+
const maxHoriLine = new LineString([
390+
[-20037508.342789, yMax[1]],
391+
[20037508.342789, yMax[1]],
392+
]);
393+
maxHoriLine.rotate(this.map.getView().getRotation(), yMax);
394+
395+
// Use intersection points of the four lines to get the extent
396+
const intersectTopLeft = OverlayOp.intersection(
397+
parser.read(minVertLine),
398+
parser.read(minHoriLine),
399+
);
400+
const intersectBottomLeft = OverlayOp.intersection(
401+
parser.read(minVertLine),
402+
parser.read(maxHoriLine),
403+
);
404+
const intersectTopRight = OverlayOp.intersection(
405+
parser.read(maxVertLine),
406+
parser.read(minHoriLine),
407+
);
408+
const intersectBottomRight = OverlayOp.intersection(
409+
parser.read(maxVertLine),
410+
parser.read(maxHoriLine),
411+
);
412+
413+
return [
414+
[intersectTopLeft.getCoordinate().x, intersectTopLeft.getCoordinate().y],
415+
[
416+
intersectBottomLeft.getCoordinate().x,
417+
intersectBottomLeft.getCoordinate().y,
418+
],
419+
[
420+
intersectTopRight.getCoordinate().x,
421+
intersectTopRight.getCoordinate().y,
422+
],
423+
[
424+
intersectBottomRight.getCoordinate().x,
425+
intersectBottomRight.getCoordinate().y,
426+
],
427+
];
428+
}
429+
259430
/**
260431
* Draws snap lines by building the extent for
261432
* a pair of features.
@@ -264,35 +435,37 @@ class CadControl extends Control {
264435
* @param {ol.Coordinate} coordinate Mouse pointer coordinate.
265436
*/
266437
drawSnapLines(features, coordinate) {
438+
// First get all snap points: neighbouring feature vertices and extent corners
267439
let auxCoords = [];
268-
269440
for (let i = 0; i < features.length; i += 1) {
270441
const geom = features[i].getGeometry();
271442
const featureCoord = geom.getCoordinates();
272-
273-
if (geom instanceof Point) {
274-
auxCoords.push(featureCoord);
275-
} else {
276-
// filling snapLayer with features vertex
277-
if (geom instanceof LineString) {
278-
for (let j = 0; j < featureCoord.length; j += 1) {
279-
auxCoords.push(featureCoord[j]);
280-
}
281-
} else if (geom instanceof Polygon) {
282-
for (let j = 0; j < featureCoord[0].length; j += 1) {
283-
auxCoords.push(featureCoord[0][j]);
443+
// Polygons initially return a geometry with an empty coordinate array, so we need to catch it
444+
if (featureCoord.length) {
445+
if (geom instanceof Point) {
446+
auxCoords.push(featureCoord);
447+
} else {
448+
// Add feature vertices
449+
if (geom instanceof LineString) {
450+
for (let j = 0; j < featureCoord.length; j += 1) {
451+
auxCoords.push(featureCoord[j]);
452+
}
453+
} else if (geom instanceof Polygon) {
454+
for (let j = 0; j < featureCoord[0].length; j += 1) {
455+
auxCoords.push(featureCoord[0][j]);
456+
}
284457
}
285-
}
286458

287-
// filling auxCoords
288-
const coords = fromExtent(geom.getExtent()).getCoordinates()[0];
289-
auxCoords = auxCoords.concat(coords);
459+
// Add extent vertices
460+
const coords = this.getRotatedExtent(geom, coordinate);
461+
auxCoords = auxCoords.concat(coords);
462+
}
290463
}
291464
}
292465

293-
const px = this.map.getPixelFromCoordinate(coordinate);
466+
// Draw snaplines when cursor vertically or horizontally aligns with a snap feature
294467
let lineCoords = null;
295-
468+
const px = this.map.getPixelFromCoordinate(coordinate);
296469
for (let i = 0; i < auxCoords.length; i += 1) {
297470
const tol = this.snapTolerance;
298471
const auxPx = this.map.getPixelFromCoordinate(auxCoords[i]);
@@ -307,49 +480,62 @@ class CadControl extends Control {
307480
let newY = px[1];
308481
newY += px[1] < auxPx[1] ? -tol * 2 : tol * 2;
309482
const newPt = this.map.getCoordinateFromPixel([auxPx[0], newY]);
310-
lineCoords = [[auxCoords[i][0], newPt[1]], auxCoords[i]];
483+
lineCoords = [newPt, auxCoords[i]];
311484
} else if (drawHLine) {
312485
let newX = px[0];
313486
newX += px[0] < auxPx[0] ? -tol * 2 : tol * 2;
314487
const newPt = this.map.getCoordinateFromPixel([newX, auxPx[1]]);
315-
lineCoords = [[newPt[0], auxCoords[i][1]], auxCoords[i]];
488+
lineCoords = [newPt, auxCoords[i]];
316489
}
317490

318491
if (lineCoords) {
319-
const g = new LineString(lineCoords);
320-
this.snapLayer.getSource().addFeature(new Feature(g));
492+
const geom = new LineString(lineCoords);
493+
this.snapLayer.getSource().addFeature(new Feature(geom));
321494
}
322495
}
323496

324-
let vertArray = null;
325-
let horiArray = null;
497+
// Snap to snap line intersection points
498+
let vertLine = null;
499+
let horiLine = null;
326500
const snapFeatures = this.snapLayer.getSource().getFeatures();
327-
328501
if (snapFeatures.length) {
329502
snapFeatures.forEach((feature) => {
503+
// The calculated pixels are used to get the vertical and horizontal lines
504+
// because using the coordinate doesn't work with a rotated map
330505
const featureCoord = feature.getGeometry().getCoordinates();
331-
const x0 = featureCoord[0][0];
332-
const x1 = featureCoord[1][0];
333-
const y0 = featureCoord[0][1];
334-
const y1 = featureCoord[1][1];
335-
336-
if (x0 === x1) {
337-
vertArray = x0;
506+
const pixelA = this.map.getPixelFromCoordinate(featureCoord[0]);
507+
const pixelB = this.map.getPixelFromCoordinate(featureCoord[1]);
508+
const x0 = pixelA[0];
509+
const x1 = pixelB[0];
510+
const y0 = pixelA[1];
511+
const y1 = pixelB[1];
512+
513+
// The pixels are rounded to avoid micro differences in the decimals
514+
if (x0.toFixed(4) === x1.toFixed(4)) {
515+
vertLine = feature;
338516
}
339-
if (y0 === y1) {
340-
horiArray = y0;
517+
if (y0.toFixed(4) === y1.toFixed(4)) {
518+
horiLine = feature;
341519
}
342520
});
343521

344-
const snapPt = [];
522+
// We check if horizontal and vertical snap lines intersect and calculate the intersection coordinate
523+
const snapLinesIntersectCoords =
524+
vertLine &&
525+
horiLine &&
526+
OverlayOp.intersection(
527+
parser.read(vertLine.getGeometry()),
528+
parser.read(horiLine.getGeometry()),
529+
)?.getCoordinates()[0];
345530

346-
if (vertArray && horiArray) {
347-
snapPt.push(vertArray);
348-
snapPt.push(horiArray);
531+
if (snapLinesIntersectCoords) {
349532
this.linesLayer.getSource().addFeatures(snapFeatures);
350533

351534
this.snapLayer.getSource().clear();
352-
const snapGeom = new Point(snapPt);
535+
const snapGeom = new Point([
536+
snapLinesIntersectCoords.x,
537+
snapLinesIntersectCoords.y,
538+
]);
353539
this.snapLayer.getSource().addFeature(new Feature(snapGeom));
354540
}
355541
}

0 commit comments

Comments
 (0)