From 56955a35d96c6d326cea9e18d8c15a9aa437c3e7 Mon Sep 17 00:00:00 2001 From: James Milner Date: Mon, 23 Dec 2024 12:36:45 +0000 Subject: [PATCH] feat: visualise snapping point and allow it to be styled --- development/src/index.ts | 4 + src/common.ts | 3 +- src/modes/freehand/freehand.mode.ts | 4 +- src/modes/linestring/linestring.mode.spec.ts | 32 +++- src/modes/linestring/linestring.mode.ts | 84 ++++++++- .../behaviors/closing-points.behavior.ts | 6 +- src/modes/polygon/polygon.mode.spec.ts | 32 +++- src/modes/polygon/polygon.mode.ts | 166 +++++++++++++----- 8 files changed, 274 insertions(+), 57 deletions(-) diff --git a/development/src/index.ts b/development/src/index.ts index aae6b55e..22a1aebc 100644 --- a/development/src/index.ts +++ b/development/src/index.ts @@ -178,6 +178,7 @@ const getModes = () => { // }, }), new TerraDrawPolygonMode({ + pointerDistance: 20, snapping: { toLine: true, toCoordinate: true, @@ -188,6 +189,9 @@ const getModes = () => { } return { valid: true }; }, + styles: { + snappingPointColor: "#3fc8e0", + }, }), new TerraDrawRectangleMode(), new TerraDrawCircleMode(), diff --git a/src/common.ts b/src/common.ts index 8c216f50..e72cfaa9 100644 --- a/src/common.ts +++ b/src/common.ts @@ -171,6 +171,7 @@ export const SELECT_PROPERTIES = { SELECTION_POINT: "selectionPoint", } as const; -export const POLYGON_PROPERTIES = { +export const COMMON_PROPERTIES = { CLOSING_POINT: "closingPoint", + SNAPPING_POINT: "snappingPoint", }; diff --git a/src/modes/freehand/freehand.mode.ts b/src/modes/freehand/freehand.mode.ts index 15a78cdc..83366bd3 100644 --- a/src/modes/freehand/freehand.mode.ts +++ b/src/modes/freehand/freehand.mode.ts @@ -6,7 +6,7 @@ import { NumericStyling, Cursor, UpdateTypes, - POLYGON_PROPERTIES, + COMMON_PROPERTIES, } from "../../common"; import { Polygon } from "geojson"; @@ -300,7 +300,7 @@ export class TerraDrawFreehandMode extends TerraDrawBaseDrawMode { }); describe("styleFeature", () => { - it("returns the correct styles for point", () => { + it("returns the correct styles for closing point", () => { const lineStringMode = new TerraDrawLineStringMode({ styles: { lineStringColor: "#ffffff", @@ -719,7 +719,33 @@ describe("TerraDrawLineStringMode", () => { lineStringMode.styleFeature({ type: "Feature", geometry: { type: "Point", coordinates: [] }, - properties: { mode: "linestring" }, + properties: { mode: "linestring", closingPoint: true }, + }), + ).toMatchObject({ + pointColor: "#111111", + pointWidth: 3, + pointOutlineColor: "#222222", + pointOutlineWidth: 2, + }); + }); + + it("returns the correct styles for snapping point", () => { + const lineStringMode = new TerraDrawLineStringMode({ + styles: { + lineStringColor: "#ffffff", + lineStringWidth: 4, + snappingPointColor: "#111111", + snappingPointWidth: 3, + snappingPointOutlineColor: "#222222", + snappingPointOutlineWidth: 2, + }, + }); + + expect( + lineStringMode.styleFeature({ + type: "Feature", + geometry: { type: "Point", coordinates: [] }, + properties: { mode: "linestring", snappingPoint: true }, }), ).toMatchObject({ pointColor: "#111111", @@ -745,7 +771,7 @@ describe("TerraDrawLineStringMode", () => { lineStringMode.styleFeature({ type: "Feature", geometry: { type: "Point", coordinates: [] }, - properties: { mode: "linestring" }, + properties: { mode: "linestring", closingPoint: true }, }), ).toMatchObject({ pointColor: "#111111", diff --git a/src/modes/linestring/linestring.mode.ts b/src/modes/linestring/linestring.mode.ts index 29d71e89..5c3bca6c 100644 --- a/src/modes/linestring/linestring.mode.ts +++ b/src/modes/linestring/linestring.mode.ts @@ -7,6 +7,7 @@ import { Cursor, UpdateTypes, CartesianPoint, + COMMON_PROPERTIES, } from "../../common"; import { LineString, Point, Position } from "geojson"; import { @@ -43,6 +44,10 @@ type LineStringStyling = { closingPointWidth: NumericStyling; closingPointOutlineColor: HexColorStyling; closingPointOutlineWidth: NumericStyling; + snappingPointColor: HexColorStyling; + snappingPointWidth: NumericStyling; + snappingPointOutlineColor: HexColorStyling; + snappingPointOutlineWidth: NumericStyling; }; interface Cursors { @@ -78,6 +83,7 @@ export class TerraDrawLineStringMode extends TerraDrawBaseDrawMode { }); }); - it("returns the correct styles for point", () => { + it("returns the correct styles for closing point", () => { const polygonMode = new TerraDrawPolygonMode({ styles: { fillColor: "#ffffff", @@ -881,7 +881,35 @@ describe("styleFeature", () => { polygonMode.styleFeature({ type: "Feature", geometry: { type: "Point", coordinates: [] }, - properties: { mode: "polygon" }, + properties: { mode: "polygon", closingPoint: true }, + }), + ).toMatchObject({ + pointWidth: 2, + pointColor: "#dddddd", + pointOutlineColor: "#222222", + pointOutlineWidth: 1, + }); + }); + + it("returns the correct styles for snapping point", () => { + const polygonMode = new TerraDrawPolygonMode({ + styles: { + fillColor: "#ffffff", + outlineColor: "#111111", + outlineWidth: 2, + fillOpacity: 0.5, + snappingPointWidth: 2, + snappingPointColor: "#dddddd", + snappingPointOutlineWidth: 1, + snappingPointOutlineColor: "#222222", + }, + }); + + expect( + polygonMode.styleFeature({ + type: "Feature", + geometry: { type: "Point", coordinates: [] }, + properties: { mode: "polygon", snappingPoint: true }, }), ).toMatchObject({ pointWidth: 2, diff --git a/src/modes/polygon/polygon.mode.ts b/src/modes/polygon/polygon.mode.ts index b008753f..40d108b9 100644 --- a/src/modes/polygon/polygon.mode.ts +++ b/src/modes/polygon/polygon.mode.ts @@ -6,6 +6,7 @@ import { NumericStyling, Cursor, UpdateTypes, + COMMON_PROPERTIES, } from "../../common"; import { Polygon, Position } from "geojson"; import { @@ -43,6 +44,10 @@ type PolygonStyling = { closingPointColor: HexColorStyling; closingPointOutlineWidth: NumericStyling; closingPointOutlineColor: HexColorStyling; + snappingPointWidth: NumericStyling; + snappingPointColor: HexColorStyling; + snappingPointOutlineWidth: NumericStyling; + snappingPointOutlineColor: HexColorStyling; }; interface Cursors { @@ -74,6 +79,8 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode } | undefined; + private snappedPointId: FeatureId | undefined; + // Behaviors private lineSnapping!: LineSnappingBehavior; private coordinateSnapping!: CoordinateSnappingBehavior; @@ -139,8 +146,13 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode const finishedId = this.currentId; + if (this.snappedPointId) { + this.store.delete([this.snappedPointId]); + } + this.currentCoordinate = 0; this.currentId = undefined; + this.snappedPointId = undefined; this.closingPoints.delete(); // Go back to started state @@ -190,17 +202,47 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode return; } - const closestCoord = this.snapCoordinate(event); + const snappedCoordinate = this.snapCoordinate(event); + + if (snappedCoordinate) { + if (this.snappedPointId) { + this.store.updateGeometry([ + { + id: this.snappedPointId, + geometry: { + type: "Point", + coordinates: snappedCoordinate, + }, + }, + ]); + } else { + const [snappedPointId] = this.store.create([ + { + geometry: { + type: "Point", + coordinates: snappedCoordinate, + }, + properties: { + mode: this.mode, + [COMMON_PROPERTIES.SNAPPING_POINT]: true, + }, + }, + ]); + + this.snappedPointId = snappedPointId; + } + + event.lng = snappedCoordinate[0]; + event.lat = snappedCoordinate[1]; + } else if (this.snappedPointId) { + this.store.delete([this.snappedPointId]); + this.snappedPointId = undefined; + } const currentPolygonCoordinates = this.store.getGeometryCopy( this.currentId, ).coordinates[0]; - if (closestCoord) { - event.lng = closestCoord[0]; - event.lat = closestCoord[1]; - } - let updatedCoordinates; if (this.currentCoordinate === 1) { @@ -227,6 +269,11 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode this.closingPoints.isClosingPoint(event); if (isPreviousClosing || isClosing) { + if (this.snappedPointId) { + this.store.delete([this.snappedPointId]); + this.snappedPointId = undefined; + } + this.setCursor(this.cursors.close); updatedCoordinates = [ @@ -286,35 +333,42 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode } private snapCoordinate(event: TerraDrawMouseEvent): undefined | Position { - let snappedCoordinated: Position | undefined = undefined; + let snappedCoordinate: Position | undefined = undefined; if (this.snappingEnabled?.toLine) { + let snapped: Position | undefined; if (this.currentId) { - snappedCoordinated = this.lineSnapping.getSnappableCoordinate( + snapped = this.lineSnapping.getSnappableCoordinate( event, this.currentId, ); } else { - snappedCoordinated = - this.lineSnapping.getSnappableCoordinateFirstClick(event); + snapped = this.lineSnapping.getSnappableCoordinateFirstClick(event); + } + + if (snapped) { + snappedCoordinate = snapped; } } if (this.snappingEnabled?.toCoordinate) { + let snapped: Position | undefined = undefined; if (this.currentId) { - snappedCoordinated = - this.coordinateSnapping.getSnappableCoordinate( - event, - this.currentId, - ) || snappedCoordinated; + snapped = this.coordinateSnapping.getSnappableCoordinate( + event, + this.currentId, + ); } else { - snappedCoordinated = - this.coordinateSnapping.getSnappableCoordinateFirstClick(event) || - snappedCoordinated; + snapped = + this.coordinateSnapping.getSnappableCoordinateFirstClick(event); + } + + if (snapped) { + snappedCoordinate = snapped; } } - return snappedCoordinated; + return snappedCoordinate; } /** @internal */ @@ -328,12 +382,18 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode } this.mouseMove = false; + // Reset the snapping point + if (this.snappedPointId) { + this.store.delete([this.snappedPointId]); + this.snappedPointId = undefined; + } + if (this.currentCoordinate === 0) { - const closestCoord = this.snapCoordinate(event); + const snappedCoordinate = this.snapCoordinate(event); - if (closestCoord) { - event.lng = closestCoord[0]; - event.lat = closestCoord[1]; + if (snappedCoordinate) { + event.lng = snappedCoordinate[0]; + event.lat = snappedCoordinate[1]; } const [newId] = this.store.create([ @@ -358,11 +418,11 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode // Ensure the state is updated to reflect drawing has started this.setDrawing(); } else if (this.currentCoordinate === 1 && this.currentId) { - const closestCoord = this.snapCoordinate(event); + const snappedCoordinate = this.snapCoordinate(event); - if (closestCoord) { - event.lng = closestCoord[0]; - event.lat = closestCoord[1]; + if (snappedCoordinate) { + event.lng = snappedCoordinate[0]; + event.lat = snappedCoordinate[1]; } const currentPolygonGeometry = this.store.getGeometryCopy( @@ -395,11 +455,11 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode this.currentCoordinate++; } else if (this.currentCoordinate === 2 && this.currentId) { - const closestCoord = this.snapCoordinate(event); + const snappedCoordinate = this.snapCoordinate(event); - if (closestCoord) { - event.lng = closestCoord[0]; - event.lat = closestCoord[1]; + if (snappedCoordinate) { + event.lng = snappedCoordinate[0]; + event.lat = snappedCoordinate[1]; } const currentPolygonCoordinates = this.store.getGeometryCopy( @@ -437,8 +497,6 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode this.currentCoordinate++; } else if (this.currentId) { - const closestCoord = this.snapCoordinate(event); - const currentPolygonCoordinates = this.store.getGeometryCopy( this.currentId, ).coordinates[0]; @@ -449,9 +507,11 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode if (isPreviousClosing || isClosing) { this.close(); } else { - if (closestCoord) { - event.lng = closestCoord[0]; - event.lat = closestCoord[1]; + const snappedCoordinate = this.snapCoordinate(event); + + if (snappedCoordinate) { + event.lng = snappedCoordinate[0]; + event.lat = snappedCoordinate[1]; } const previousCoordinate = @@ -522,8 +582,10 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode /** @internal */ cleanUp() { const cleanUpId = this.currentId; + const snappedPointId = this.snappedPointId; this.currentId = undefined; + this.snappedPointId = undefined; this.currentCoordinate = 0; if (this.state === "drawing") { this.setStarted(); @@ -533,6 +595,9 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode if (cleanUpId !== undefined) { this.store.delete([cleanUpId]); } + if (snappedPointId !== undefined) { + this.store.delete([snappedPointId]); + } if (this.closingPoints.ids.length) { this.closingPoints.delete(); } @@ -572,26 +637,47 @@ export class TerraDrawPolygonMode extends TerraDrawBaseDrawMode styles.zIndex = 10; return styles; } else if (feature.geometry.type === "Point") { + const closingPoint = + feature.properties[COMMON_PROPERTIES.CLOSING_POINT]; + const snappingPoint = + feature.properties[COMMON_PROPERTIES.SNAPPING_POINT]; + styles.pointWidth = this.getNumericStylingValue( - this.styles.closingPointWidth, + closingPoint + ? this.styles.closingPointWidth + : snappingPoint + ? this.styles.snappingPointWidth + : styles.pointWidth, styles.pointWidth, feature, ); styles.pointColor = this.getHexColorStylingValue( - this.styles.closingPointColor, + closingPoint + ? this.styles.closingPointColor + : snappingPoint + ? this.styles.snappingPointColor + : styles.pointColor, styles.pointColor, feature, ); styles.pointOutlineColor = this.getHexColorStylingValue( - this.styles.closingPointOutlineColor, + closingPoint + ? this.styles.closingPointOutlineColor + : snappingPoint + ? this.styles.snappingPointOutlineColor + : styles.pointOutlineColor, styles.pointOutlineColor, feature, ); styles.pointOutlineWidth = this.getNumericStylingValue( - this.styles.closingPointOutlineWidth, + closingPoint + ? this.styles.closingPointOutlineWidth + : snappingPoint + ? this.styles.snappingPointOutlineWidth + : 2, 2, feature, );