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

feat: polygon prevent overlapping in ModifyMode #900

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 67 additions & 46 deletions examples/advanced/src/example.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -525,16 +525,37 @@ export default class Example extends React.Component<

_renderModifyModeControls() {
return (
<ToolboxRow key="modify">
<ToolboxTitle>Allow removing points</ToolboxTitle>
<ToolboxControl>
<input
type="checkbox"
checked={this.state.pointsRemovable}
onChange={() => this.setState({ pointsRemovable: !this.state.pointsRemovable })}
/>
</ToolboxControl>
</ToolboxRow>
<>
<ToolboxRow key="modify">
<ToolboxTitle>Allow removing points</ToolboxTitle>
<ToolboxControl>
<input
type="checkbox"
checked={this.state.pointsRemovable}
onChange={() => this.setState({ pointsRemovable: !this.state.pointsRemovable })}
/>
</ToolboxControl>
</ToolboxRow>
<ToolboxRow key="modfiy-2">
<ToolboxTitle>Prevent overlapping lines</ToolboxTitle>
<ToolboxControl>
<input
type="checkbox"
checked={Boolean(
this.state.modeConfig && this.state.modeConfig.preventOverlappingLines
)}
onChange={(event) =>
this.setState({
modeConfig: {
...(this.state.modeConfig || {}),
preventOverlappingLines: Boolean(event.target.checked),
},
})
}
/>
</ToolboxControl>
</ToolboxRow>
</>
);
}

Expand Down
18 changes: 15 additions & 3 deletions modules/edit-modes/src/lib/draw-polygon-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -119,9 +119,21 @@ export class DrawPolygonMode extends GeoJsonEditMode {
coordinates: [[...clickSequence, clickSequence[0]]],
};

const editAction = this.getAddFeatureOrBooleanPolygonAction(polygonToAdd, props);

if (
Copy link
Author

Choose a reason for hiding this comment

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

This fixes If first editHandle was clicked and last line being created would overlapp

props.modeConfig?.preventOverlappingLines &&
editAction.updatedData &&
hasPolygonCrossingLines(
editAction.updatedData.features[editAction.editContext.featureIndexes[0]].geometry
.coordinates[0] as Position[]
Copy link
Author

Choose a reason for hiding this comment

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

Any ideas to remove this cast to Position[]?

)
) {
return;
}

this.resetClickSequence();

const editAction = this.getAddFeatureOrBooleanPolygonAction(polygonToAdd, props);
if (editAction) {
props.onEdit(editAction);
}
Expand Down
71 changes: 49 additions & 22 deletions modules/edit-modes/src/lib/modify-mode.ts
Original file line number Diff line number Diff line change
@@ -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<FeatureCollection>): GuideFeatureCollection {
const handles = [];

Expand Down Expand Up @@ -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<Polygon>,
Expand Down Expand Up @@ -245,8 +247,12 @@ export class ModifyMode extends GeoJsonEditMode {
}

handleStartDragging(event: StartDraggingEvent, props: ModeProps<FeatureCollection>) {
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;
Expand Down Expand Up @@ -274,11 +280,32 @@ export class ModifyMode extends GeoJsonEditMode {
handleStopDragging(event: StopDraggingEvent, props: ModeProps<FeatureCollection>) {
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) || [];

Expand Down
102 changes: 102 additions & 0 deletions modules/edit-modes/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}