diff --git a/examples/advanced/src/example.tsx b/examples/advanced/src/example.tsx index 36ad46db7..7b4fcda1b 100644 --- a/examples/advanced/src/example.tsx +++ b/examples/advanced/src/example.tsx @@ -1,50 +1,50 @@ /* eslint-env browser */ -import * as React from 'react'; +import { MapController, MapView } from '@deck.gl/core/typed'; import DeckGL from '@deck.gl/react/typed'; -import { MapView, MapController } from '@deck.gl/core/typed'; -import StaticMap from 'react-map-gl'; import GL from '@luma.gl/constants'; import circle from '@turf/circle'; +import * as React from 'react'; +import StaticMap from 'react-map-gl'; import { - EditableGeoJsonLayer, - SelectionLayer, - ModifyMode, - ResizeCircleMode, - TranslateMode, - TransformMode, - ScaleMode, - RotateMode, - DuplicateMode, - ExtendLineStringMode, - SplitPolygonMode, - ExtrudeMode, - ElevationMode, - DrawPointMode, - DrawLineStringMode, - DrawPolygonMode, - DrawRectangleMode, - DrawSquareMode, - DrawRectangleFromCenterMode, - DrawSquareFromCenterMode, + Color, + CompositeMode, + Draw90DegreePolygonMode, DrawCircleByDiameterMode, DrawCircleFromCenterMode, DrawEllipseByBoundingBoxMode, DrawEllipseUsingThreePointsMode, - DrawRectangleUsingThreePointsMode, - Draw90DegreePolygonMode, + DrawLineStringMode, + DrawPointMode, DrawPolygonByDraggingMode, - MeasureDistanceMode, - MeasureAreaMode, - MeasureAngleMode, - ViewMode, - CompositeMode, - SnappableMode, + DrawPolygonMode, + DrawRectangleFromCenterMode, + DrawRectangleMode, + DrawRectangleUsingThreePointsMode, + DrawSquareFromCenterMode, + DrawSquareMode, + DuplicateMode, + EditableGeoJsonLayer, ElevatedEditHandleLayer, + ElevationMode, + ExtendLineStringMode, + ExtrudeMode, + GeoJsonEditMode, + MeasureAngleMode, + MeasureAreaMode, + MeasureDistanceMode, + ModifyMode, PathMarkerLayer, + ResizeCircleMode, + RotateMode, SELECTION_TYPE, - GeoJsonEditMode, - Color, + ScaleMode, + SelectionLayer, + SnappableMode, + SplitPolygonMode, + TransformMode, + TranslateMode, + ViewMode, } from 'nebula.gl'; import sampleGeoJson from '../../data/sample-geojson.json'; @@ -53,11 +53,11 @@ import iconSheet from '../../data/edit-handles.png'; import { Toolbox, - ToolboxControl, - ToolboxTitle, - ToolboxRow, ToolboxButton, ToolboxCheckbox, + ToolboxControl, + ToolboxRow, + ToolboxTitle, } from './toolbox'; type RGBAColor = Color; @@ -525,16 +525,37 @@ export default class Example extends React.Component< _renderModifyModeControls() { return ( - - Allow removing points - - this.setState({ pointsRemovable: !this.state.pointsRemovable })} - /> - - + <> + + Allow removing points + + this.setState({ pointsRemovable: !this.state.pointsRemovable })} + /> + + + + Prevent overlapping lines + + + this.setState({ + modeConfig: { + ...(this.state.modeConfig || {}), + preventOverlappingLines: Boolean(event.target.checked), + }, + }) + } + /> + + + ); } diff --git a/modules/edit-modes/src/lib/draw-polygon-mode.ts b/modules/edit-modes/src/lib/draw-polygon-mode.ts index 270421c0b..5430131aa 100644 --- a/modules/edit-modes/src/lib/draw-polygon-mode.ts +++ b/modules/edit-modes/src/lib/draw-polygon-mode.ts @@ -8,8 +8,8 @@ import { TentativeFeature, GuideFeature, } from '../types'; -import { Polygon, FeatureCollection } from '../geojson-types'; -import { getPickedEditHandle } from '../utils'; +import { Polygon, FeatureCollection, Position } from '../geojson-types'; +import { getPickedEditHandle, hasPolygonCrossingLines } from '../utils'; import { GeoJsonEditMode } from './geojson-edit-mode'; export class DrawPolygonMode extends GeoJsonEditMode { @@ -119,9 +119,21 @@ export class DrawPolygonMode extends GeoJsonEditMode { coordinates: [[...clickSequence, clickSequence[0]]], }; + const editAction = this.getAddFeatureOrBooleanPolygonAction(polygonToAdd, props); + + if ( + props.modeConfig?.preventOverlappingLines && + editAction.updatedData && + hasPolygonCrossingLines( + editAction.updatedData.features[editAction.editContext.featureIndexes[0]].geometry + .coordinates[0] as Position[] + ) + ) { + return; + } + this.resetClickSequence(); - const editAction = this.getAddFeatureOrBooleanPolygonAction(polygonToAdd, props); if (editAction) { props.onEdit(editAction); } diff --git a/modules/edit-modes/src/lib/modify-mode.ts b/modules/edit-modes/src/lib/modify-mode.ts index 6a7dcf2c0..8bc542528 100644 --- a/modules/edit-modes/src/lib/modify-mode.ts +++ b/modules/edit-modes/src/lib/modify-mode.ts @@ -1,32 +1,34 @@ import { point, lineString as toLineString } from '@turf/helpers'; +import { FeatureCollection, FeatureOf, LineString, Point, Polygon } from '../geojson-types'; import { - recursivelyTraverseNestedArrays, - nearestPointOnProjectedLine, - nearestPointOnLine, - getEditHandlesForGeometry, - getPickedEditHandles, - getPickedEditHandle, - getPickedExistingEditHandle, - getPickedIntermediateEditHandle, - updateRectanglePosition, - NearestPointType, -} from '../utils'; -import { LineString, Point, Polygon, FeatureCollection, FeatureOf } from '../geojson-types'; -import { - ModeProps, ClickEvent, + DraggingEvent, + EditHandleFeature, + GuideFeatureCollection, + ModeProps, PointerMoveEvent, StartDraggingEvent, StopDraggingEvent, - DraggingEvent, Viewport, - GuideFeatureCollection, - EditHandleFeature, } from '../types'; +import { + NearestPointType, + getEditHandlesForGeometry, + getPickedEditHandle, + getPickedEditHandles, + getPickedExistingEditHandle, + getPickedIntermediateEditHandle, + hasPolygonCrossingLines, + nearestPointOnLine, + nearestPointOnProjectedLine, + recursivelyTraverseNestedArrays, + updateRectanglePosition, +} from '../utils'; import { GeoJsonEditMode } from './geojson-edit-mode'; import { ImmutableFeatureCollection } from './immutable-feature-collection'; export class ModifyMode extends GeoJsonEditMode { + private _featureCollectionBeforeDrag: FeatureCollection; getGuides(props: ModeProps): GuideFeatureCollection { const handles = []; @@ -207,7 +209,7 @@ export class ModifyMode extends GeoJsonEditMode { const editHandleProperties = editHandle.properties; const editedFeature = props.data.features[editHandleProperties.featureIndex]; - let updatedData; + let updatedData: FeatureCollection; if (props.modeConfig?.lockRectangles && editedFeature.properties.shape === 'Rectangle') { const coordinates = updateRectanglePosition( editedFeature as FeatureOf, @@ -245,8 +247,12 @@ export class ModifyMode extends GeoJsonEditMode { } handleStartDragging(event: StartDraggingEvent, props: ModeProps) { - const selectedFeatureIndexes = props.selectedIndexes; + // Stores previous state to revert to if drag ends with overlappingLines + if (props?.modeConfig?.preventOverlappingLines) { + this._featureCollectionBeforeDrag = new ImmutableFeatureCollection(props.data).getObject(); + } + const selectedFeatureIndexes = props.selectedIndexes; const editHandle = getPickedIntermediateEditHandle(event.picks); if (selectedFeatureIndexes.length && editHandle) { const editHandleProperties = editHandle.properties; @@ -274,11 +280,32 @@ export class ModifyMode extends GeoJsonEditMode { handleStopDragging(event: StopDraggingEvent, props: ModeProps) { const selectedFeatureIndexes = props.selectedIndexes; const editHandle = getPickedEditHandle(event.picks); - if (selectedFeatureIndexes.length && editHandle) { - this._dragEditHandle('finishMovePosition', props, editHandle, event); + + if (!editHandle || !selectedFeatureIndexes.length) { + return; } - } + if ( + this._featureCollectionBeforeDrag && + hasPolygonCrossingLines( + (props.data.features[editHandle.properties.featureIndex].geometry as Polygon).coordinates[0] + ) + ) { + props.onEdit({ + updatedData: this._featureCollectionBeforeDrag, + editType: 'finishMovePosition', + editContext: { + featureIndexes: [editHandle.properties.featureIndex], + positionIndexes: editHandle.properties.positionIndexes, + position: event.mapCoords, + }, + }); + + return; + } + + this._dragEditHandle('finishMovePosition', props, editHandle, event); + } getCursor(event: PointerMoveEvent): string | null | undefined { const picks = (event && event.picks) || []; diff --git a/modules/edit-modes/src/utils.ts b/modules/edit-modes/src/utils.ts index eac5e0069..15cd4daa3 100644 --- a/modules/edit-modes/src/utils.ts +++ b/modules/edit-modes/src/utils.ts @@ -511,3 +511,105 @@ export function mapCoords( }) .filter(Boolean); } + +/** + * Checks if two line segments intersect. + * + * @param {number} Ax - x-coordinate of the start point of the first line segment. + * @param {number} Ay - y-coordinate of the start point of the first line segment. + * @param {number} Bx - x-coordinate of the end point of the first line segment. + * @param {number} By - y-coordinate of the end point of the first line segment. + * @param {number} Cx - x-coordinate of the start point of the second line segment. + * @param {number} Cy - y-coordinate of the start point of the second line segment. + * @param {number} Dx - x-coordinate of the end point of the second line segment. + * @param {number} Dy - y-coordinate of the end point of the second line segment. + * @return {boolean} true if the line segments intersect, false otherwise. + */ +// eslint-disable-next-line max-params +function lineSegmentIntersection( + Ax: number, + Ay: number, + Bx: number, + By: number, + Cx: number, + Cy: number, + Dx: number, + Dy: number +): boolean { + let newX: number; + + if ((Ax === Bx && Ay === By) || (Cx === Dx && Cy === Dy)) { + return false; + } + + if ( + (Ax === Cx && Ay === Cy) || + (Bx === Cx && By === Cy) || + (Ax === Dx && Ay === Dy) || + (Bx === Dx && By === Dy) + ) { + return false; + } + + Bx -= Ax; + By -= Ay; + Cx -= Ax; + Cy -= Ay; + Dx -= Ax; + Dy -= Ay; + + const distAB = Math.sqrt(Bx * Bx + By * By); + + const theCos = Bx / distAB; + const theSin = By / distAB; + newX = Cx * theCos + Cy * theSin; + Cy = Cy * theCos - Cx * theSin; + Cx = newX; + newX = Dx * theCos + Dy * theSin; + Dy = Dy * theCos - Dx * theSin; + Dx = newX; + + if ((Cy < 0.0 && Dy < 0.0) || (Cy >= 0.0 && Dy >= 0.0)) { + return false; + } + + const ABpos = Dx + ((Cx - Dx) * Dy) / (Dy - Cy); + + if (ABpos < 0.0 || ABpos > distAB) { + return false; + } + + return true; +} + +/** + * Checks if a polygon defined by the coordinates has any crossing lines. + * + * @param {Position[]} coordinates - Array of coordinates defining the polygon. + * @return {boolean} True if the polygon has crossing lines, false otherwise. + */ +export function hasPolygonCrossingLines(coordinates: Position[]): boolean { + for (let i = 0; i < coordinates.length - 2; i++) { + const point1 = coordinates[i]; + const point2 = coordinates[i + 1]; + for (let j = i + 1; j < coordinates.length - 1; j++) { + const point3 = coordinates[j]; + const point4 = coordinates[j + 1]; + if ( + lineSegmentIntersection( + point1[0], + point1[1], + point2[0], + point2[1], + point3[0], + point3[1], + point4[0], + point4[1] + ) + ) { + return true; + } + } + } + return false; +}