Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(CAD): snap enabled on edit feature vertices, CAD support for rotated maps #234

Merged
merged 16 commits into from
Aug 10, 2022
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"
danji90 marked this conversation as resolved.
Show resolved Hide resolved
"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) {
danji90 marked this conversation as resolved.
Show resolved Hide resolved
if (geom instanceof Point) {
auxCoords.push(featureCoord);
} else {
// Add feature vertices
if (geom instanceof LineString) {
danji90 marked this conversation as resolved.
Show resolved Hide resolved
for (let j = 0; j < featureCoord.length; j += 1) {
auxCoords.push(featureCoord[j]);
danji90 marked this conversation as resolved.
Show resolved Hide resolved
}
} 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