import { RegularShape, Style, Fill, Stroke, Circle } 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}
* @alias ole.CadControl
*/
class CadControl extends Control {
/**
* @param {Object} [options] Tool options.
* @param {Function} [options.filter] Returns an array containing the features
* to include for CAD (takes the source as a single argument).
* @param {Number} [options.snapTolerance] Snap tolerance in pixel
* for snap lines. Default is 10.
* @param {Boolean} [options.showSnapLines] Whether to show
* snap lines (default is true).
* @param {Boolean} [options.showSnapPoints] Whether to show
* snap points around the closest feature.
* @param {Number} [options.snapPointDist] Distance of the
* snap points (default is 30).
* @param {Boolean} [options.useMapUnits] Whether to use map units
* as measurement for point snapping. Default is false (pixel are used).
* @param {ol.style.Style.StyleLike} [options.snapStyle] Style used for the snap layer.
* @param {ol.style.Style.StyleLike} [options.linesStyle] Style used for the lines layer.
*/
constructor(options) {
super({
title: 'CAD control',
className: 'ole-control-cad',
image: cadSVG,
showSnapPoints: true,
showSnapLines: false,
snapPointDist: 10,
...options,
});
/**
* Interaction for handling move events.
* @type {ol.interaction.Pointer}
* @private
*/
this.pointerInteraction = new Pointer({
handleMoveEvent: this.onMove.bind(this),
});
/**
* Layer for drawing snapping geometries.
* @type {ol.layer.Vector}
* @private
*/
this.snapLayer = new Vector({
source: new VectorSource(),
style: options.snapStyle || [
new Style({
image: new RegularShape({
fill: new Fill({
color: '#E8841F',
}),
stroke: new Stroke({
width: 1,
color: '#618496',
}),
points: 4,
radius: 5,
radius2: 0,
angle: Math.PI / 4,
}),
stroke: new Stroke({
width: 1,
lineDash: [5, 10],
color: '#618496',
}),
}),
],
});
/**
* Layer for colored lines indicating
* intesection point between snapping lines.
* @type {ol.layer.Vector}
* @private
*/
this.linesLayer = new Vector({
source: new VectorSource(),
style: options.linesStyle || [
new Style({
image: new Circle({
fill: new Fill({
color: '#E8841F',
}),
stroke: new Stroke({
width: 1,
color: '#618496',
}),
radius: 5,
}),
stroke: new Stroke({
width: 1,
lineDash: [5, 10],
color: '#FF530D',
}),
}),
],
});
/**
* Snap tolerance in pixel.
* @type {Number}
* @private
*/
this.snapTolerance =
options.snapTolerance === undefined ? 10 : options.snapTolerance;
/**
* Filter the features to snap with.
* @type {Function}
* @private
*/
this.filter = options.filter || null;
/**
* Interaction for snapping
* @type {ol.interaction.Snap}
* @private
*/
this.snapInteraction = new Snap({
pixelTolerance: this.snapTolerance,
source: this.snapLayer.getSource(),
});
this.standalone = false;
}
/**
* @inheritdoc
*/
getDialogTemplate() {
const distLabel = this.properties.useMapUnits ? 'map units' : 'px';
return `
<div>
<input
id="aux-cb"
type="radio"
name="radioBtn"
${this.properties.showSnapLines ? 'checked' : ''}
>
<label>Show snap lines</label>
</div>
<div>
<input
id="dist-cb"
type="radio"
name="radioBtn"
${this.properties.showSnapPoints ? 'checked' : ''}
>
<label>Show snap points. Distance (${distLabel}):</label>
<input type="text" id="width-input"
value="${this.properties.snapPointDist}">
</div>
`;
}
/**
* @inheritdoc
*/
setMap(map) {
super.setMap(map);
// Ensure that the snap interaction is at the last position
// as it must be the first to handle the pointermove event.
this.map.getInteractions().on(
'add',
((e) => {
const pos = e.target.getArray().indexOf(this.snapInteraction);
if (
this.snapInteraction.getActive() &&
pos > -1 &&
pos !== e.target.getLength() - 1
) {
this.deactivate(true);
this.activate(true);
}
// eslint-disable-next-line no-extra-bind
}).bind(this),
);
}
/**
* Handle move event.
* @private
* @param {ol.MapBrowserEvent} evt Move event.
*/
onMove(evt) {
const features = this.getClosestFeatures(evt.coordinate, 5);
this.linesLayer.getSource().clear();
this.snapLayer.getSource().clear();
this.pointerInteraction.dispatchEvent(
new SnapEvent(SnapEventType.SNAP, features.length ? features : null, evt),
);
if (this.properties.showSnapLines) {
this.drawSnapLines(features, evt.coordinate);
}
if (this.properties.showSnapPoints && features.length) {
this.drawSnapPoints(evt.coordinate, features[0]);
}
}
/**
* Returns a list of the {num} closest features
* to a given coordinate.
* @private
* @param {ol.Coordinate} coordinate Coordinate.
* @param {Number} numFeatures Number of features to search.
* @returns {Array.<ol.Feature>} List of closest features.
*/
getClosestFeatures(coordinate, numFeatures) {
const num = numFeatures || 1;
const ext = [-Infinity, -Infinity, Infinity, Infinity];
const featureDict = {};
const pushSnapFeatures = (f) => {
const cCoord = f.getGeometry().getClosestPoint(coordinate);
const dx = cCoord[0] - coordinate[0];
const dy = cCoord[1] - coordinate[1];
const dist = dx * dx + dy * dy;
featureDict[dist] = f;
};
this.source.forEachFeatureInExtent(ext, (f) => {
if (!this.filter || (this.filter && this.filter(f))) {
pushSnapFeatures(f);
}
});
const dists = Object.keys(featureDict);
let features = [];
const count = Math.min(dists.length, num);
dists.sort((a, b) => a - b);
for (let i = 0; i < count; i += 1) {
features.push(featureDict[dists[i]]);
}
const drawFeature = this.editor.getDrawFeature();
if (drawFeature) {
// Include all but the last vertex (at mouse position) to prevent snapping on mouse cursor node
const currentDrawFeature = drawFeature.clone();
currentDrawFeature
.getGeometry()
.setCoordinates(
drawFeature.getGeometry().getCoordinates().slice(0, -1),
);
features = [currentDrawFeature, ...features];
}
const editFeature = this.editor.getEditFeature();
if (editFeature && this.editor.modifyStartCoordinate) {
/* Include all nodes of the edit feature except the node being modified */
// First exclude the edit feature from snap detection
if (features.indexOf(editFeature) > -1) {
features.splice(features.indexOf(editFeature), 1);
}
// Convert to MultiPoint and get the node coordinate closest to mouse cursor
const isPolygon = editFeature.getGeometry() instanceof Polygon;
const snapGeom = new MultiPoint(
isPolygon
? editFeature.getGeometry().getCoordinates()[0]
: editFeature.getGeometry().getCoordinates(),
);
const editNodeCoordinate = snapGeom.getClosestPoint(
this.editor.modifyStartCoordinate,
);
// Exclude the node being modified
snapGeom.setCoordinates(
snapGeom.getCoordinates().filter((coord) => {
return coord.toString() !== editNodeCoordinate.toString();
}),
);
// Clone editFeature and apply adjusted snap geometry
const snapEditFeature = editFeature.clone();
snapEditFeature
.getGeometry()
.setCoordinates(
isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
);
features = [snapEditFeature, ...features];
}
return features;
}
getRotatedExtent(geometry) {
const coordinates =
geometry instanceof Polygon
? geometry.getCoordinates()[0]
: geometry.getCoordinates();
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;
});
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);
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),
);
// this.linesLayer
// .getSource()
// .addFeatures([
// new Feature(
// new Point([
// intersectTopLeft.getCoordinate().x,
// intersectTopLeft.getCoordinate().y,
// ]),
// ),
// new Feature(
// new Point([
// intersectBottomLeft.getCoordinate().x,
// intersectBottomLeft.getCoordinate().y,
// ]),
// ),
// new Feature(
// new Point([
// intersectTopRight.getCoordinate().x,
// intersectTopRight.getCoordinate().y,
// ]),
// ),
// new Feature(
// new Point([
// intersectBottomRight.getCoordinate().x,
// intersectBottomRight.getCoordinate().y,
// ]),
// ),
// new Feature(minVertLine),
// new Feature(maxVertLine),
// new Feature(minHoriLine),
// new Feature(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.
* @private
* @param {Array.<ol.Feature>} features List of features.
* @param {ol.Coordinate} coordinate Mouse pointer coordinate.
*/
drawSnapLines(features, coordinate) {
let auxCoords = [];
for (let i = 0; i < features.length; i += 1) {
const geom = features[i].getGeometry();
const featureCoord = geom.getCoordinates();
if (featureCoord.length) {
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]);
}
}
// filling auxCoords
const coords = fromExtent(geom.getExtent()).getCoordinates()[0];
// const coords = this.getRotatedExtent(geom);
console.log(this.getRotatedExtent(geom));
console.log(coords);
this.getRotatedExtent(geom).forEach((coord) =>
this.linesLayer
.getSource()
.addFeature(new Feature(new Point(coord))),
);
auxCoords = auxCoords.concat(coords);
}
}
}
const px = this.map.getPixelFromCoordinate(coordinate);
let lineCoords = null;
for (let i = 0; i < auxCoords.length; i += 1) {
const tol = this.snapTolerance;
const auxPx = this.map.getPixelFromCoordinate(auxCoords[i]);
const drawVLine =
px[0] > auxPx[0] - this.snapTolerance / 2 &&
px[0] < auxPx[0] + this.snapTolerance / 2;
const drawHLine =
px[1] > auxPx[1] - this.snapTolerance / 2 &&
px[1] < auxPx[1] + this.snapTolerance / 2;
if (drawVLine) {
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]];
} 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]];
}
if (lineCoords) {
const geom = new LineString(lineCoords);
const mapRotation = this.map.getView().getRotation();
if (mapRotation !== 0) {
geom.rotate(mapRotation, lineCoords[1]);
}
this.snapLayer.getSource().addFeature(new Feature(geom));
// this.linesLayer
// .getSource()
// .addFeature(new Feature(new Point(lineCoords[0])));
// this.linesLayer
// .getSource()
// .addFeature(new Feature(new Point(lineCoords[1])));
}
}
let vertArray = null;
let horiArray = null;
const snapFeatures = this.snapLayer.getSource().getFeatures();
if (snapFeatures.length) {
snapFeatures.forEach((feature) => {
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;
}
if (y0 === y1) {
horiArray = y0;
}
});
const snapPt = [];
if (vertArray && horiArray) {
snapPt.push(vertArray);
snapPt.push(horiArray);
this.linesLayer.getSource().addFeatures(snapFeatures);
this.snapLayer.getSource().clear();
const snapGeom = new Point(snapPt);
this.snapLayer.getSource().addFeature(new Feature(snapGeom));
}
}
}
/**
* Adds snap points to the snapping layer.
* @private
* @param {ol.Coordinate} coordinateMouse cursor coordinate.
* @param {ol.eaturee} feature Feature to draw the snap points for.
*/
drawSnapPoints(coordinate, feature) {
const featCoord = feature.getGeometry().getClosestPoint(coordinate);
const px = this.map.getPixelFromCoordinate(featCoord);
let snapCoords = [];
if (this.properties.useMapUnits) {
snapCoords = [
[featCoord[0] - this.properties.snapPointDist, featCoord[1]],
[featCoord[0] + this.properties.snapPointDist, featCoord[1]],
[featCoord[0], featCoord[1] - this.properties.snapPointDist],
[featCoord[0], featCoord[1] + this.properties.snapPointDist],
];
} else {
const snapPx = [
[px[0] - this.properties.snapPointDist, px[1]],
[px[0] + this.properties.snapPointDist, px[1]],
[px[0], px[1] - this.properties.snapPointDist],
[px[0], px[1] + this.properties.snapPointDist],
];
for (let j = 0; j < snapPx.length; j += 1) {
snapCoords.push(this.map.getCoordinateFromPixel(snapPx[j]));
}
}
const snapGeom = new MultiPoint(snapCoords);
this.snapLayer.getSource().addFeature(new Feature(snapGeom));
}
/**
* @inheritdoc
*/
activate(silent) {
super.activate(silent);
this.snapLayer.setMap(this.map);
this.linesLayer.setMap(this.map);
this.map.addInteraction(this.pointerInteraction);
this.map.addInteraction(this.snapInteraction);
document.getElementById('aux-cb').addEventListener('change', (evt) => {
this.setProperties({
showSnapLines: evt.target.checked,
showSnapPoints: !evt.target.checked,
});
});
document.getElementById('dist-cb').addEventListener('change', (evt) => {
this.setProperties({
showSnapPoints: evt.target.checked,
showSnapLines: !evt.target.checked,
});
});
document.getElementById('width-input').addEventListener('keyup', (evt) => {
const snapPointDist = parseFloat(evt.target.value);
if (!Number.isNaN(snapPointDist)) {
this.setProperties({ snapPointDist });
}
});
}
/**
* @inheritdoc
*/
deactivate(silent) {
super.deactivate(silent);
this.snapLayer.setMap(null);
this.linesLayer.setMap(null);
this.map.removeInteraction(this.pointerInteraction);
this.map.removeInteraction(this.snapInteraction);
}
}
export default CadControl;