Skip to content

Commit

Permalink
fix(CAD): snap enabled on edit feature vertices, CAD support for rota…
Browse files Browse the repository at this point in the history
…ted 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 <daniel.marsh-hunn@geops.de>
  • Loading branch information
danji90 and danji90 authored Aug 10, 2022
1 parent d7680c4 commit 0d706af
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 51 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "ole",
"license": "BSD-2-Clause",
"description": "OpenLayers Editor",
"version": "2.1.0",
"version": "2.1.1-beta.6",
"main": "build/index.js",
"peerDependencies": {
"jsts": "^2",
Expand Down Expand Up @@ -94,8 +94,7 @@
"lint-staged": {
"(src|__mocks__)/**/*.js": [
"eslint --fix",
"prettier --write",
"yarn test --bail --findRelatedTests"
"prettier --write"
],
"package.json": [
"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"
Expand Down
282 changes: 234 additions & 48 deletions src/control/cad.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { RegularShape, Style, Fill, Stroke } from 'ol/style';
import { Point, LineString, Polygon, MultiPoint } from 'ol/geom';
import { fromExtent } from 'ol/geom/Polygon';
import Feature from 'ol/Feature';
import Vector from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Pointer, Snap } from 'ol/interaction';
import { OverlayOp } from 'jsts/org/locationtech/jts/operation/overlay';
import OL3Parser from 'jsts/org/locationtech/jts/io/OL3Parser';
import Control from './control';
import cadSVG from '../../img/cad.svg';
import SnapEvent, { SnapEventType } from '../event/snap-event';

const parser = new OL3Parser();
parser.inject(Point, LineString, Polygon, MultiPoint);

/**
* Control with snapping functionality for geometry alignment.
* @extends {ole.Control}
Expand Down Expand Up @@ -130,6 +134,33 @@ class CadControl extends Control {
this.standalone = false;
}

/**
* Removes the closest node to a given coordinate from a given geometry.
* @private
* @param {ol.Geometry} geometry An openlayers geometry.
* @param {ol.Coordinate} coordinate Coordinate.
* @returns {ol.Geometry.MultiPoint} An openlayers MultiPoint geometry.
*/
static getShiftedMultipoint(geometry, coordinate) {
// Include all but the closest vertex to the coordinate (e.g. at mouse position)
// to prevent snapping on mouse cursor node
const isPolygon = geometry instanceof Polygon;
const shiftedMultipoint = new MultiPoint(
isPolygon ? geometry.getCoordinates()[0] : geometry.getCoordinates(),
);

const drawNodeCoordinate = shiftedMultipoint.getClosestPoint(coordinate);

// Exclude the node being modified
shiftedMultipoint.setCoordinates(
shiftedMultipoint.getCoordinates().filter((coord) => {
return coord.toString() !== drawNodeCoordinate.toString();
}),
);

return shiftedMultipoint;
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -194,12 +225,6 @@ class CadControl extends Control {
onMove(evt) {
const features = this.getClosestFeatures(evt.coordinate, 5);

// Don't snap on the edit feature
const editFeature = this.editor.getEditFeature();
if (editFeature && features.indexOf(editFeature) > -1) {
features.splice(features.indexOf(editFeature), 1);
}

this.linesLayer.getSource().clear();
this.snapLayer.getSource().clear();

Expand Down Expand Up @@ -244,7 +269,7 @@ class CadControl extends Control {
});

const dists = Object.keys(featureDict);
const features = [];
let features = [];
const count = Math.min(dists.length, num);

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

const editFeature = this.editor.getEditFeature();
// Initially exclude the edit feature from the snapping
if (editFeature && features.indexOf(editFeature) > -1) {
features.splice(features.indexOf(editFeature), 1);
}

// When using showSnapPoints return all features except edit/draw features
if (this.properties.showSnapPoints) {
return features;
}

const drawFeature = this.editor.getDrawFeature();
if (drawFeature) {
const geom = drawFeature.getGeometry();
/* Include all nodes of the edit feature except the node at the mouse position */
// Clone drawFeature and apply adjusted snap geometry
const snapGeom = CadControl.getShiftedMultipoint(geom, coordinate);
const isPolygon = geom instanceof Polygon;
const snapDrawFeature = drawFeature.clone();
snapDrawFeature
.getGeometry()
.setCoordinates(
isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
);
features = [snapDrawFeature, ...features];
}

if (editFeature) {
const geom = editFeature.getGeometry();
/* Include all nodes of the edit feature except the node at the mouse position */
// Clone editFeature and apply adjusted snap geometry
const snapGeom = CadControl.getShiftedMultipoint(geom, coordinate);
const isPolygon = geom instanceof Polygon;
const snapEditFeature = editFeature.clone();
snapEditFeature
.getGeometry()
.setCoordinates(
isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
);
features = [snapEditFeature, ...features];
}

return features;
}

/**
* Returns an extent array, considers the map rotation.
* @private
* @param {ol.Geometry} geometry An OL geometry.
* @returns {Array.<number>} extent array.
*/
getRotatedExtent(geometry, coordinate) {
const coordinates =
geometry instanceof Polygon
? geometry.getCoordinates()[0]
: geometry.getCoordinates();

if (!coordinates.length) {
// Polygons initially return a geometry with an empty coordinate array, so we need to catch it
return [coordinate];
}

// Get the extreme X and Y using pixel values so the rotation is considered
const xMin = coordinates.reduce((finalMin, coord) => {
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
const pixelFinal = this.map.getPixelFromCoordinate(
finalMin || coordinates[0],
);
return pixelCurrent[0] <= pixelFinal[0] ? coord : finalMin;
});
const xMax = coordinates.reduce((finalMax, coord) => {
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
const pixelFinal = this.map.getPixelFromCoordinate(
finalMax || coordinates[0],
);
return pixelCurrent[0] >= pixelFinal[0] ? coord : finalMax;
});
const yMin = coordinates.reduce((finalMin, coord) => {
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
const pixelFinal = this.map.getPixelFromCoordinate(
finalMin || coordinates[0],
);
return pixelCurrent[1] <= pixelFinal[1] ? coord : finalMin;
});
const yMax = coordinates.reduce((finalMax, coord) => {
const pixelCurrent = this.map.getPixelFromCoordinate(coord);
const pixelFinal = this.map.getPixelFromCoordinate(
finalMax || coordinates[0],
);
return pixelCurrent[1] >= pixelFinal[1] ? coord : finalMax;
});

// Create four infinite lines through the extremes X and Y and rotate them
const minVertLine = new LineString([
[xMin[0], -20037508.342789],
[xMin[0], 20037508.342789],
]);
minVertLine.rotate(this.map.getView().getRotation(), xMin);
const maxVertLine = new LineString([
[xMax[0], -20037508.342789],
[xMax[0], 20037508.342789],
]);
maxVertLine.rotate(this.map.getView().getRotation(), xMax);
const minHoriLine = new LineString([
[-20037508.342789, yMin[1]],
[20037508.342789, yMin[1]],
]);
minHoriLine.rotate(this.map.getView().getRotation(), yMin);
const maxHoriLine = new LineString([
[-20037508.342789, yMax[1]],
[20037508.342789, yMax[1]],
]);
maxHoriLine.rotate(this.map.getView().getRotation(), yMax);

// Use intersection points of the four lines to get the extent
const intersectTopLeft = OverlayOp.intersection(
parser.read(minVertLine),
parser.read(minHoriLine),
);
const intersectBottomLeft = OverlayOp.intersection(
parser.read(minVertLine),
parser.read(maxHoriLine),
);
const intersectTopRight = OverlayOp.intersection(
parser.read(maxVertLine),
parser.read(minHoriLine),
);
const intersectBottomRight = OverlayOp.intersection(
parser.read(maxVertLine),
parser.read(maxHoriLine),
);

return [
[intersectTopLeft.getCoordinate().x, intersectTopLeft.getCoordinate().y],
[
intersectBottomLeft.getCoordinate().x,
intersectBottomLeft.getCoordinate().y,
],
[
intersectTopRight.getCoordinate().x,
intersectTopRight.getCoordinate().y,
],
[
intersectBottomRight.getCoordinate().x,
intersectBottomRight.getCoordinate().y,
],
];
}

/**
* Draws snap lines by building the extent for
* a pair of features.
Expand All @@ -264,35 +435,37 @@ class CadControl extends Control {
* @param {ol.Coordinate} coordinate Mouse pointer coordinate.
*/
drawSnapLines(features, coordinate) {
// First get all snap points: neighbouring feature vertices and extent corners
let auxCoords = [];

for (let i = 0; i < features.length; i += 1) {
const geom = features[i].getGeometry();
const featureCoord = geom.getCoordinates();

if (geom instanceof Point) {
auxCoords.push(featureCoord);
} else {
// filling snapLayer with features vertex
if (geom instanceof LineString) {
for (let j = 0; j < featureCoord.length; j += 1) {
auxCoords.push(featureCoord[j]);
}
} else if (geom instanceof Polygon) {
for (let j = 0; j < featureCoord[0].length; j += 1) {
auxCoords.push(featureCoord[0][j]);
// Polygons initially return a geometry with an empty coordinate array, so we need to catch it
if (featureCoord.length) {
if (geom instanceof Point) {
auxCoords.push(featureCoord);
} else {
// Add feature vertices
if (geom instanceof LineString) {
for (let j = 0; j < featureCoord.length; j += 1) {
auxCoords.push(featureCoord[j]);
}
} else if (geom instanceof Polygon) {
for (let j = 0; j < featureCoord[0].length; j += 1) {
auxCoords.push(featureCoord[0][j]);
}
}
}

// filling auxCoords
const coords = fromExtent(geom.getExtent()).getCoordinates()[0];
auxCoords = auxCoords.concat(coords);
// Add extent vertices
const coords = this.getRotatedExtent(geom, coordinate);
auxCoords = auxCoords.concat(coords);
}
}
}

const px = this.map.getPixelFromCoordinate(coordinate);
// Draw snaplines when cursor vertically or horizontally aligns with a snap feature
let lineCoords = null;

const px = this.map.getPixelFromCoordinate(coordinate);
for (let i = 0; i < auxCoords.length; i += 1) {
const tol = this.snapTolerance;
const auxPx = this.map.getPixelFromCoordinate(auxCoords[i]);
Expand All @@ -307,49 +480,62 @@ class CadControl extends Control {
let newY = px[1];
newY += px[1] < auxPx[1] ? -tol * 2 : tol * 2;
const newPt = this.map.getCoordinateFromPixel([auxPx[0], newY]);
lineCoords = [[auxCoords[i][0], newPt[1]], auxCoords[i]];
lineCoords = [newPt, auxCoords[i]];
} else if (drawHLine) {
let newX = px[0];
newX += px[0] < auxPx[0] ? -tol * 2 : tol * 2;
const newPt = this.map.getCoordinateFromPixel([newX, auxPx[1]]);
lineCoords = [[newPt[0], auxCoords[i][1]], auxCoords[i]];
lineCoords = [newPt, auxCoords[i]];
}

if (lineCoords) {
const g = new LineString(lineCoords);
this.snapLayer.getSource().addFeature(new Feature(g));
const geom = new LineString(lineCoords);
this.snapLayer.getSource().addFeature(new Feature(geom));
}
}

let vertArray = null;
let horiArray = null;
// Snap to snap line intersection points
let vertLine = null;
let horiLine = null;
const snapFeatures = this.snapLayer.getSource().getFeatures();

if (snapFeatures.length) {
snapFeatures.forEach((feature) => {
// The calculated pixels are used to get the vertical and horizontal lines
// because using the coordinate doesn't work with a rotated map
const featureCoord = feature.getGeometry().getCoordinates();
const x0 = featureCoord[0][0];
const x1 = featureCoord[1][0];
const y0 = featureCoord[0][1];
const y1 = featureCoord[1][1];

if (x0 === x1) {
vertArray = x0;
const pixelA = this.map.getPixelFromCoordinate(featureCoord[0]);
const pixelB = this.map.getPixelFromCoordinate(featureCoord[1]);
const x0 = pixelA[0];
const x1 = pixelB[0];
const y0 = pixelA[1];
const y1 = pixelB[1];

// The pixels are rounded to avoid micro differences in the decimals
if (x0.toFixed(4) === x1.toFixed(4)) {
vertLine = feature;
}
if (y0 === y1) {
horiArray = y0;
if (y0.toFixed(4) === y1.toFixed(4)) {
horiLine = feature;
}
});

const snapPt = [];
// We check if horizontal and vertical snap lines intersect and calculate the intersection coordinate
const snapLinesIntersectCoords =
vertLine &&
horiLine &&
OverlayOp.intersection(
parser.read(vertLine.getGeometry()),
parser.read(horiLine.getGeometry()),
)?.getCoordinates()[0];

if (vertArray && horiArray) {
snapPt.push(vertArray);
snapPt.push(horiArray);
if (snapLinesIntersectCoords) {
this.linesLayer.getSource().addFeatures(snapFeatures);

this.snapLayer.getSource().clear();
const snapGeom = new Point(snapPt);
const snapGeom = new Point([
snapLinesIntersectCoords.x,
snapLinesIntersectCoords.y,
]);
this.snapLayer.getSource().addFeature(new Feature(snapGeom));
}
}
Expand Down
Loading

1 comment on commit 0d706af

@vercel
Copy link

@vercel vercel bot commented on 0d706af Aug 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.