@nebula.gl/layers
Version:
A suite of 3D-enabled data editing layers, suitable for deck.gl
199 lines (177 loc) • 7.07 kB
text/typescript
import destination from '@turf/destination';
import bearing from '@turf/bearing';
import lineIntersect from '@turf/line-intersect';
import turfDistance from '@turf/distance';
import { point, lineString } from '@turf/helpers';
import { Polygon, Position } from '@nebula.gl/edit-modes';
import { generatePointsParallelToLinePoints } from '../utils';
import { ClickEvent, PointerMoveEvent } from '../event-types';
import {
EditAction,
EditHandle,
ModeHandler,
getPickedEditHandle,
getEditHandlesForGeometry,
} from './mode-handler';
// TODO edit-modes: delete handlers once EditMode fully implemented
export class Draw90DegreePolygonHandler extends ModeHandler {
getEditHandles(picks?: Array<Record<string, any>>, groundCoords?: Position): EditHandle[] {
let handles = super.getEditHandles(picks, groundCoords);
const tentativeFeature = this.getTentativeFeature();
if (tentativeFeature) {
handles = handles.concat(getEditHandlesForGeometry(tentativeFeature.geometry, -1));
// Slice off the handles that are are next to the pointer
if (tentativeFeature && tentativeFeature.geometry.type === 'LineString') {
// Remove the last existing handle
handles = handles.slice(0, -1);
} else if (tentativeFeature && tentativeFeature.geometry.type === 'Polygon') {
// Remove the last existing handle
handles = handles.slice(0, -1);
}
}
return handles;
}
handlePointerMove({
groundCoords,
}: PointerMoveEvent): { editAction: EditAction | null | undefined; cancelMapPan: boolean } {
const clickSequence = this.getClickSequence();
const result = { editAction: null, cancelMapPan: false };
if (clickSequence.length === 0) {
// nothing to do yet
return result;
}
const tentativeFeature = this.getTentativeFeature();
if (tentativeFeature && tentativeFeature.geometry.type === 'Polygon') {
clickSequence[clickSequence.length - 1] =
tentativeFeature.geometry.coordinates[0][clickSequence.length - 1];
} else if (tentativeFeature && tentativeFeature.geometry.type === 'LineString') {
clickSequence[clickSequence.length - 1] =
tentativeFeature.geometry.coordinates[clickSequence.length - 1];
}
let p3;
if (clickSequence.length === 1) {
p3 = groundCoords;
} else {
const p1 = clickSequence[clickSequence.length - 2];
const p2 = clickSequence[clickSequence.length - 1];
[p3] = generatePointsParallelToLinePoints(p1, p2, groundCoords);
}
if (clickSequence.length < 3) {
// Draw a LineString connecting all the clicked points with the hovered point
this._setTentativeFeature({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [...clickSequence, p3],
},
});
} else {
// Draw a Polygon connecting all the clicked points with the hovered point
this._setTentativeFeature({
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[...clickSequence, p3, clickSequence[0]]],
},
});
}
return result;
}
handleClick(event: ClickEvent): EditAction | null | undefined {
super.handleClick(event);
const { picks } = event;
const tentativeFeature = this.getTentativeFeature();
let editAction: EditAction | null | undefined = null;
const clickedEditHandle = getPickedEditHandle(picks);
if (tentativeFeature && tentativeFeature.geometry.type === 'Polygon') {
const polygon: Polygon = tentativeFeature.geometry;
if (
clickedEditHandle &&
clickedEditHandle.featureIndex === -1 &&
(clickedEditHandle.positionIndexes[1] === 0 ||
clickedEditHandle.positionIndexes[1] === polygon.coordinates[0].length - 3)
) {
// They clicked the first or last point (or double-clicked), so complete the polygon
const polygonToAdd: Polygon = {
type: 'Polygon',
coordinates: this.finalizedCoordinates([...polygon.coordinates[0]]),
};
this.resetClickSequence();
this._setTentativeFeature(null);
editAction = this.getAddFeatureOrBooleanPolygonAction(polygonToAdd);
}
}
// Trigger pointer move right away in order for it to update edit handles (to support double-click)
const fakePointerMoveEvent = {
screenCoords: [-1, -1],
groundCoords: event.groundCoords,
picks: [],
isDragging: false,
pointerDownPicks: null,
pointerDownScreenCoords: null,
pointerDownGroundCoords: null,
sourceEvent: null,
};
// @ts-ignore
this.handlePointerMove(fakePointerMoveEvent);
return editAction;
}
finalizedCoordinates(coords: Position[]) {
// Remove the hovered position
let coordinates = [[...coords.slice(0, -2), coords[0]]];
let pt = this.getIntermediatePoint([...coords]);
if (!pt) {
// if intermediate point with 90 degree not available
// try remove the last clicked point and get the intermediate point.
const tc = [...coords];
tc.splice(-3, 1);
pt = this.getIntermediatePoint([...tc]);
if (pt) {
coordinates = [[...coords.slice(0, -3), pt, coords[0]]];
}
} else {
coordinates = [[...coords.slice(0, -2), pt, coords[0]]];
}
return coordinates;
}
getIntermediatePoint(coordinates: Position[]) {
let pt;
if (coordinates.length > 4) {
const [p1, p2] = [...coordinates];
const angle1 = bearing(p1, p2);
const p3 = coordinates[coordinates.length - 3];
const p4 = coordinates[coordinates.length - 4];
const angle2 = bearing(p3, p4);
const angles = { first: [], second: [] };
// calculate 3 right angle points for first and last points in lineString
[1, 2, 3].forEach((factor) => {
const newAngle1 = angle1 + factor * 90;
// convert angles to 0 to -180 for anti-clock and 0 to 180 for clock wise
angles.first.push(newAngle1 > 180 ? newAngle1 - 360 : newAngle1);
const newAngle2 = angle2 + factor * 90;
angles.second.push(newAngle2 > 180 ? newAngle2 - 360 : newAngle2);
});
const distance = turfDistance(point(p1), point(p3));
// Draw imaginary right angle lines for both first and last points in lineString
// If there is intersection point for any 2 lines, will be the 90 degree point.
[0, 1, 2].forEach((indexFirst) => {
const line1 = lineString([
p1,
destination(p1, distance, angles.first[indexFirst]).geometry.coordinates,
]);
[0, 1, 2].forEach((indexSecond) => {
const line2 = lineString([
p3,
destination(p3, distance, angles.second[indexSecond]).geometry.coordinates,
]);
const fc = lineIntersect(line1, line2);
if (fc && fc.features.length) {
// found the intersect point
pt = fc.features[0].geometry.coordinates;
}
});
});
}
return pt;
}
}