From 9a85b89c23e3422331ab584d8b564bc08a2d8156 Mon Sep 17 00:00:00 2001 From: James Milner Date: Fri, 24 Jan 2025 19:38:52 +0000 Subject: [PATCH 1/2] feat(terra-draw): add editable option for point mode to allow moving points whilst drawing (#443) --- packages/e2e/src/index.ts | 4 +- packages/e2e/tests/leaflet.spec.ts | 22 ++ packages/e2e/tests/setup.ts | 1 + packages/terra-draw/src/common.ts | 1 + .../src/modes/point/point.mode.spec.ts | 284 ++++++++++++++++-- .../terra-draw/src/modes/point/point.mode.ts | 191 +++++++++++- 6 files changed, 472 insertions(+), 31 deletions(-) diff --git a/packages/e2e/src/index.ts b/packages/e2e/src/index.ts index eeb36acf..92fa2483 100644 --- a/packages/e2e/src/index.ts +++ b/packages/e2e/src/index.ts @@ -131,7 +131,9 @@ const example = { }, }, }), - new TerraDrawPointMode(), + new TerraDrawPointMode({ + editable: this.config?.includes("pointEditable"), + }), new TerraDrawLineStringMode({ snapping: { toCoordinate: this.config?.includes("snappingCoordinate"), diff --git a/packages/e2e/tests/leaflet.spec.ts b/packages/e2e/tests/leaflet.spec.ts index 77b3cd28..4973f997 100644 --- a/packages/e2e/tests/leaflet.spec.ts +++ b/packages/e2e/tests/leaflet.spec.ts @@ -71,6 +71,28 @@ test.describe("point mode", () => { await expectPaths({ page, count: 3 }); }); + + test("mode can set with editable set to true and points can be moved", async ({ + page, + }) => { + const mapDiv = await setupMap({ + page, + configQueryParam: ["pointEditable"], + }); + await changeMode({ page, mode }); + + await page.mouse.click(mapDiv.width / 2, mapDiv.height / 2); + await expectGroupPosition({ page, x: 633, y: 353 }); + + await page.mouse.move(mapDiv.width / 2, mapDiv.height / 2); + await page.mouse.down(); + await page.mouse.move(mapDiv.width / 3, mapDiv.height / 3); + await page.mouse.up(); + + await expectPaths({ page, count: 1 }); + + await expectGroupPosition({ page, x: 419, y: 233 }); + }); }); test.describe("linestring mode", () => { diff --git a/packages/e2e/tests/setup.ts b/packages/e2e/tests/setup.ts index 18268d67..509678e2 100644 --- a/packages/e2e/tests/setup.ts +++ b/packages/e2e/tests/setup.ts @@ -3,6 +3,7 @@ import { Page, expect } from "@playwright/test"; export const pageUrl = "http://localhost:3000/"; export type TestConfigOptions = + | "pointEditable" | "validationSuccess" | "validationFailure" | "insertCoordinates" diff --git a/packages/terra-draw/src/common.ts b/packages/terra-draw/src/common.ts index e72cfaa9..cff35073 100644 --- a/packages/terra-draw/src/common.ts +++ b/packages/terra-draw/src/common.ts @@ -172,6 +172,7 @@ export const SELECT_PROPERTIES = { } as const; export const COMMON_PROPERTIES = { + EDITED: "edited", CLOSING_POINT: "closingPoint", SNAPPING_POINT: "snappingPoint", }; diff --git a/packages/terra-draw/src/modes/point/point.mode.spec.ts b/packages/terra-draw/src/modes/point/point.mode.spec.ts index 6ec7f936..0c77875e 100644 --- a/packages/terra-draw/src/modes/point/point.mode.spec.ts +++ b/packages/terra-draw/src/modes/point/point.mode.spec.ts @@ -14,6 +14,11 @@ describe("TerraDrawPointMode", () => { it("constructs with options", () => { const pointMode = new TerraDrawPointMode({ + cursors: { + create: "crosshair", + dragStart: "grabbing", + dragEnd: "crosshair", + }, styles: { pointOutlineColor: "#ffffff" }, }); expect(pointMode.styles).toStrictEqual({ @@ -180,7 +185,7 @@ describe("TerraDrawPointMode", () => { }); describe("cleanUp", () => { - it("does nothing", () => { + it("does not throw", () => { const pointMode = new TerraDrawPointMode(); expect(() => { @@ -189,36 +194,241 @@ describe("TerraDrawPointMode", () => { }); }); - describe("onDrag", () => { - it("does nothing", () => { - const pointMode = new TerraDrawPointMode(); + describe("onDragStart", () => { + it("does not set cursor on drag starting if editable false", () => { + const pointMode = new TerraDrawPointMode({ + editable: false, + }); - expect(() => { - pointMode.onDrag(); - }).not.toThrow(); + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + const setMapDraggability = jest.fn(); + pointMode.onDragStart( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + expect(mockConfig.setCursor).toHaveBeenCalledTimes(0); + expect(setMapDraggability).toHaveBeenCalledTimes(0); + }); + + it("sets the cursor on drag starting when editable true", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + }); + + const mockConfig = MockModeConfig("point"); + + // Trigger the codepath which ignores none point geometries + mockConfig.store.create([ + { + geometry: { + type: "Polygon", + coordinates: [ + [ + [0, 0], + [1, 0], + [1, 1], + [0, 1], + [0, 0], + ], + ], + }, + properties: { mode: "polygon" }, + }, + ]); + + mockConfig.store.create([ + { + geometry: { + type: "Point", + coordinates: [0.1, 0.1], + }, + properties: { mode: "point" }, + }, + ]); + + mockConfig.store.create([ + { + geometry: { + type: "Point", + coordinates: [0.2, 0.2], + }, + properties: { mode: "point" }, + }, + ]); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + const setMapDraggability = jest.fn(); + pointMode.onDragStart( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + expect(mockConfig.setCursor).toHaveBeenCalledTimes(1); + expect(setMapDraggability).toHaveBeenCalledWith(false); }); }); - describe("onDragStart", () => { - it("does nothing", () => { - const pointMode = new TerraDrawPointMode(); + describe("onDrag", () => { + it("does nothing if nothing currently edited when editable true", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + }); + const mockConfig = MockModeConfig("point"); - expect(() => { - pointMode.onDragStart(); - }).not.toThrow(); + pointMode.register(mockConfig); + + const setMapDraggability = jest.fn(); + pointMode.onDrag(MockCursorEvent({ lng: 0, lat: 0 }), setMapDraggability); + + expect(mockConfig.onChange).toHaveBeenCalledTimes(0); + }); + + it("updates the point geometry on drag when editable true", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + }); + + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + const setMapDraggability = jest.fn(); + pointMode.onDragStart( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + pointMode.onDrag(MockCursorEvent({ lng: 0, lat: 1 }), setMapDraggability); + + expect(mockConfig.onChange).toHaveBeenCalledTimes(3); + expect(mockConfig.onChange).toHaveBeenNthCalledWith( + 2, + [expect.any(String)], + "update", + ); + + // On finished called from onClick and is then only called after onDragEnd + expect(mockConfig.onFinish).toHaveBeenCalledTimes(1); + }); + + it("handles the falsy validation when editable true", () => { + let validations = 0; + const pointMode = new TerraDrawPointMode({ + editable: true, + validation: () => { + validations++; + return { + valid: validations === 1, + }; + }, + }); + + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + const setMapDraggability = jest.fn(); + pointMode.onDragStart( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + pointMode.onDrag(MockCursorEvent({ lng: 0, lat: 1 }), setMapDraggability); + + expect(mockConfig.onChange).toHaveBeenCalledTimes(1); + expect(mockConfig.onFinish).toHaveBeenCalledTimes(1); + }); + + it("handles the truthy validation when editable true", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + validation: () => ({ valid: true }), + }); + + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + const setMapDraggability = jest.fn(); + pointMode.onDragStart( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + pointMode.onDrag(MockCursorEvent({ lng: 0, lat: 1 }), setMapDraggability); + + expect(mockConfig.onChange).toHaveBeenCalledTimes(3); + // On finished called from onClick and is then only called after onDragEnd + expect(mockConfig.onFinish).toHaveBeenCalledTimes(1); }); }); describe("onDragEnd", () => { - it("does nothing", () => { - const pointMode = new TerraDrawPointMode(); + it("doesn't set the cursor on drag ending if nothing currently edited", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + }); - expect(() => { - pointMode.onDragEnd(); - }).not.toThrow(); + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + const setMapDraggability = jest.fn(); + pointMode.onDragEnd( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + expect(mockConfig.setCursor).toHaveBeenCalledTimes(0); + expect(setMapDraggability).toHaveBeenCalledTimes(0); }); - }); + it("sets the cursor on drag ending", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + }); + + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + const setMapDraggability = jest.fn(); + pointMode.onDragStart( + MockCursorEvent({ lng: 0, lat: 0 }), + setMapDraggability, + ); + + pointMode.onDrag(MockCursorEvent({ lng: 1, lat: 0 }), setMapDraggability); + + pointMode.onDragEnd( + MockCursorEvent({ lng: 1, lat: 0 }), + setMapDraggability, + ); + + expect(mockConfig.setCursor).toHaveBeenCalledTimes(2); + expect(mockConfig.setCursor).toHaveBeenNthCalledWith(1, "grabbing"); + expect(mockConfig.setCursor).toHaveBeenNthCalledWith(2, "crosshair"); + expect(setMapDraggability).toHaveBeenNthCalledWith(1, false); + expect(setMapDraggability).toHaveBeenNthCalledWith(2, true); + }); + }); describe("styling", () => { it("gets", () => { const pointMode = new TerraDrawPointMode(); @@ -299,6 +509,42 @@ describe("TerraDrawPointMode", () => { pointOutlineWidth: 2, }); }); + + it("returns the correct styles for edited point", () => { + const pointMode = new TerraDrawPointMode({ + editable: true, + styles: { + editedPointColor: "#222222", + editedPointWidth: 3, + editedPointOutlineColor: "#555555", + editedPointOutlineWidth: 3, + }, + }); + + const mockConfig = MockModeConfig("point"); + + pointMode.register(mockConfig); + + pointMode.onClick(MockCursorEvent({ lng: 0, lat: 0 })); + + pointMode.onDragStart(MockCursorEvent({ lng: 0, lat: 0 }), jest.fn()); + + const id = mockConfig.onChange.mock.calls[0][0][0]; + + expect( + pointMode.styleFeature({ + type: "Feature", + id, + geometry: { type: "Point", coordinates: [] }, + properties: { mode: "point", edited: true }, + }), + ).toMatchObject({ + pointColor: "#222222", + pointWidth: 3, + pointOutlineColor: "#555555", + pointOutlineWidth: 3, + }); + }); }); describe("validateFeature", () => { diff --git a/packages/terra-draw/src/modes/point/point.mode.ts b/packages/terra-draw/src/modes/point/point.mode.ts index c7174b63..7f41a09a 100644 --- a/packages/terra-draw/src/modes/point/point.mode.ts +++ b/packages/terra-draw/src/modes/point/point.mode.ts @@ -5,8 +5,14 @@ import { HexColorStyling, Cursor, UpdateTypes, + COMMON_PROPERTIES, } from "../../common"; -import { GeoJSONStoreFeatures, StoreValidation } from "../../store/store"; +import { + BBoxPolygon, + FeatureId, + GeoJSONStoreFeatures, + StoreValidation, +} from "../../store/store"; import { getDefaultStyling } from "../../util/styling"; import { BaseModeOptions, @@ -14,33 +20,51 @@ import { TerraDrawBaseDrawMode, } from "../base.mode"; import { ValidatePointFeature } from "../../validations/point.validation"; -import { Point } from "geojson"; +import { Point, Position } from "geojson"; +import { BehaviorConfig } from "../base.behavior"; +import { ClickBoundingBoxBehavior } from "../click-bounding-box.behavior"; +import { PixelDistanceBehavior } from "../pixel-distance.behavior"; type PointModeStyling = { pointWidth: NumericStyling; pointColor: HexColorStyling; pointOutlineColor: HexColorStyling; pointOutlineWidth: NumericStyling; + editedPointColor: HexColorStyling; + editedPointWidth: NumericStyling; + editedPointOutlineColor: HexColorStyling; + editedPointOutlineWidth: NumericStyling; }; interface Cursors { create?: Cursor; + dragStart?: Cursor; + dragEnd?: Cursor; } interface TerraDrawPointModeOptions extends BaseModeOptions { cursors?: Cursors; + editable?: boolean; } export class TerraDrawPointMode extends TerraDrawBaseDrawMode { mode = "point"; private cursors: Required; + private editable: boolean; + private editedFeatureId: FeatureId | undefined; + + // Behaviors + private pixelDistance!: PixelDistanceBehavior; + private clickBoundingBox!: ClickBoundingBoxBehavior; constructor(options?: TerraDrawPointModeOptions) { super(options); const defaultCursors = { create: "crosshair", + dragStart: "grabbing", + dragEnd: "crosshair", } as Required; if (options && options.cursors) { @@ -48,6 +72,12 @@ export class TerraDrawPointMode extends TerraDrawBaseDrawMode } else { this.cursors = defaultCursors; } + + if (options && options.editable) { + this.editable = options.editable; + } else { + this.editable = false; + } } /** @internal */ @@ -112,16 +142,147 @@ export class TerraDrawPointMode extends TerraDrawBaseDrawMode onKeyUp() {} /** @internal */ - cleanUp() {} + cleanUp() { + this.editedFeatureId = undefined; + } - /** @internal */ - onDragStart() {} + onDragStart( + event: TerraDrawMouseEvent, + setMapDraggability: (enabled: boolean) => void, + ) { + if (this.editable) { + const bbox = this.clickBoundingBox.create(event) as BBoxPolygon; + const features = this.store.search(bbox); + + let distance = Infinity; + let clickedFeature: GeoJSONStoreFeatures | undefined = undefined; + + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + const isPoint = + feature.geometry.type === "Point" && + feature.properties.mode === this.mode; + + if (!isPoint) { + continue; + } + + const position = feature.geometry.coordinates as Position; + const distanceToFeature = this.pixelDistance.measure(event, position); + + if ( + distanceToFeature > distance || + distanceToFeature > this.pointerDistance + ) { + continue; + } + + distance = distanceToFeature; + clickedFeature = feature; + } + + if (clickedFeature) { + this.editedFeatureId = clickedFeature.id; + } + } + + // We only need to stop the map dragging if + // we actually have something selected + if (!this.editedFeatureId) { + return; + } + + // Drag Feature + this.setCursor(this.cursors.dragStart); + + setMapDraggability(false); + } /** @internal */ - onDrag() {} + onDrag( + event: TerraDrawMouseEvent, + setMapDraggability: (enabled: boolean) => void, + ) { + if (this.editedFeatureId === undefined) { + return; + } + + const newGeometry = { + type: "Point", + coordinates: [event.lng, event.lat], + }; + + if (this.validate) { + const validationResult = this.validate( + { + type: "Feature", + geometry: newGeometry, + properties: this.store.getPropertiesCopy(this.editedFeatureId), + } as GeoJSONStoreFeatures, + { + project: this.project, + unproject: this.unproject, + coordinatePrecision: this.coordinatePrecision, + updateType: UpdateTypes.Finish, + }, + ); + + if (!validationResult.valid) { + return; + } + } + + // For cursor points we can simply move it + // to the dragged position + this.store.updateGeometry([ + { + id: this.editedFeatureId, + geometry: { + type: "Point", + coordinates: [event.lng, event.lat], + }, + }, + ]); + + this.store.updateProperty([ + { + id: this.editedFeatureId, + property: COMMON_PROPERTIES.EDITED, + value: true, + }, + ]); + + setMapDraggability(true); + } /** @internal */ - onDragEnd() {} + onDragEnd( + _: TerraDrawMouseEvent, + setMapDraggability: (enabled: boolean) => void, + ) { + if (this.editedFeatureId === undefined) { + return; + } + + this.onFinish(this.editedFeatureId, { mode: this.mode, action: "edit" }); + + this.setCursor(this.cursors.dragEnd); + + this.store.updateProperty([ + { + id: this.editedFeatureId, + property: COMMON_PROPERTIES.EDITED, + value: false, + }, + ]); + this.editedFeatureId = undefined; + setMapDraggability(true); + } + + registerBehaviors(config: BehaviorConfig) { + this.pixelDistance = new PixelDistanceBehavior(config); + this.clickBoundingBox = new ClickBoundingBoxBehavior(config); + } /** @internal */ styleFeature(feature: GeoJSONStoreFeatures): TerraDrawAdapterStyling { @@ -132,26 +293,34 @@ export class TerraDrawPointMode extends TerraDrawBaseDrawMode feature.geometry.type === "Point" && feature.properties.mode === this.mode ) { + const isEdited = Boolean( + feature.id && this.editedFeatureId === feature.id, + ); + styles.pointWidth = this.getNumericStylingValue( - this.styles.pointWidth, + isEdited ? this.styles.editedPointWidth : this.styles.pointWidth, styles.pointWidth, feature, ); styles.pointColor = this.getHexColorStylingValue( - this.styles.pointColor, + isEdited ? this.styles.editedPointColor : this.styles.pointColor, styles.pointColor, feature, ); styles.pointOutlineColor = this.getHexColorStylingValue( - this.styles.pointOutlineColor, + isEdited + ? this.styles.editedPointOutlineColor + : this.styles.pointOutlineColor, styles.pointOutlineColor, feature, ); styles.pointOutlineWidth = this.getNumericStylingValue( - this.styles.pointOutlineWidth, + isEdited + ? this.styles.editedPointOutlineWidth + : this.styles.pointOutlineWidth, 2, feature, ); From fc39d446a41935c8d5780bf8d9ffcc86b19b939f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serge=20Crois=C3=A9?= Date: Fri, 24 Jan 2025 20:47:10 +0100 Subject: [PATCH 2/2] docs(terra-draw): fix spelling and grammar issues in 4.MODES.md (#446) - its name... - performing the... - provisional - supported - Rectangle - Deselect Co-authored-by: James Milner --- guides/4.MODES.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/guides/4.MODES.md b/guides/4.MODES.md index 9056cc93..8d0be1b6 100644 --- a/guides/4.MODES.md +++ b/guides/4.MODES.md @@ -6,7 +6,7 @@ Modes are another important concept in Terra Draw, they encapsulate specific log - [Selection Mode](#selection-mode) - [Render Mode](#render-mode) -Once added to Terra Draw upon instantiation, they and can be enabled by calling the `setMode` method on the Terra Draw instance and providing the Mode Name. +Once added to Terra Draw upon instantiation, they can be enabled by calling the `setMode` method on the Terra Draw instance and providing the Mode Name. ## Mode Names @@ -66,7 +66,7 @@ For example, if you draw a polygon using the `TerraDrawPolygonMode` the `mode` p ### Switching Modes -You can swap to any given mode using it's name once we have instantiated and started our Terra Draw instance. For example, let's say we want to start in 'polygon' mode then later on switch to point mode, we could do that like so: +You can swap to any given mode using its name once we have instantiated and started our Terra Draw instance. For example, let's say we want to start in 'polygon' mode then later on switch to point mode, we could do that like so: ```javascript draw.setMode('polygon') @@ -129,11 +129,11 @@ This would stop the user from being able to create a polygon that is self inters You can combine these validations if you so wish. Any `validation` function will be run as part of `validateFeature` which is called by `addFeatures` on the public Terra Draw API/. -You'll notice there are two arguments to the `validation` function, the first being the feature, the second being the context object. This has useful properties which can help with peforming the validation. One of the most useful properties is the `updateType` property which tells you what type of update the feature is receiving, where the options are `finish`, `commit` or `provisional`. +You'll notice there are two arguments to the `validation` function, the first being the feature, the second being the context object. This has useful properties which can help with performing the validation. One of the most useful properties is the `updateType` property which tells you what type of update the feature is receiving, where the options are `finish`, `commit` or `provisional`. - `finish` - when the drawing of the feature is being finished - `commit` - when a coordinate has been added or removed from the feature -- `provisonal` - when the geometry has been update, but the coordinate has not been fully committed to the geometry +- `provisional` - when the geometry has been update, but the coordinate has not been fully committed to the geometry Using these can help you write more customised behaviours, for example you may only want to run the validation when the update is a `finish` or `commit` type, ensuring that validation is not prematurely preventing user interactions to update the feature. @@ -171,7 +171,7 @@ We can also provide a `toCustom` function which allows snapping to some arbitrar #### Projections in Drawing Modes -As we move forward Terra Draw will work on supporting Web Mercator maps out the box with the ability to support Globes (i.e. 3D spherical representations of the earth with no projection) as a secondary option. This is made slightly more complicated by the fact we know sometimes users want to draw geodesic geometries on a web mercator map, for example a geodesic circle or a great circle line. In future we will better align by assuming developers want web mercator first behaviours, with secondary support for globes via the `projection` property for built in modes. +As we move forward Terra Draw will work on supporting Web Mercator maps out the box with the ability to support Globes (i.e., 3D spherical representations of the earth with no projection) as a secondary option. This is made slightly more complicated by the fact we know sometimes users want to draw geodesic geometries on a web mercator map, for example a geodesic circle or a great circle line. In future we will better align by assuming developers want web mercator first behaviours, with secondary support for globes via the `projection` property for built in modes. * Circle mode currently supports both web mercator and geodesic circles, using the `projection` property, which can be `globe` or `web-mercator` (default is `web-mercator`) * Select mode currently supports both web mercator and geodesic editing (scaling, rotating), although resizeable property currently only supports `web-mercator` as `projection` (default is `web-mercator`) @@ -192,7 +192,7 @@ Note: If you want to draw great circle lines on a web mercator map, this is poss All modes work with keyboard and mouse interface. -There are varying degrees of support for touch devices. Currently on touch devices Select, Point, Line and Polygon Modes are fully supported. Circle, Rectangle and Angled Rectangle Modes work with the caveat with the UX is not ideal. Freehand is not currently suported. Currently on [terradraw.io](terradraw.io]) Circle, Rectange and Freehand modes are disabled on smaller devices. +There are varying degrees of support for touch devices. Currently on touch devices Select, Point, Line and Polygon Modes are fully supported. Circle, Rectangle and Angled Rectangle Modes work with the caveat with the UX is not ideal. Freehand is not currently supported. Currently on [terradraw.io](terradraw.io]) Circle, Rectangle and Freehand modes are disabled on smaller devices. If you want to experiment you can use the local development environment, as described at [Development](https://github.com/JamesLMilner/terra-draw/blob/main/development/README.md) @@ -423,7 +423,7 @@ It is possible to select and deselect a feature via the draw instance, which use // Select a given feature draw.selectFeature("f8e5a38d-ecfa-4294-8461-d9cff0e0d7f8"); - // Deslect the given feature + // Deselect the given feature draw.deselectFeature("f8e5a38d-ecfa-4294-8461-d9cff0e0d7f8"); ``` @@ -505,4 +505,4 @@ See the [Development](./6.DEVELOPMENT.md) guide for more information on creating 5. [ ] [Styling](./5.STYLING.md) 6. [ ] [Events](./6.EVENTS.md) 7. [ ] [Development](./7.DEVELOPMENT.md) -8. [ ] [Examples](./8.EXAMPLES.md) \ No newline at end of file +8. [ ] [Examples](./8.EXAMPLES.md)