Skip to content

Commit

Permalink
feat(terra-draw): allow updating mode options dynamically via updateM…
Browse files Browse the repository at this point in the history
…odeOptions method
  • Loading branch information
JamesLMilner committed Feb 23, 2025
1 parent 3e29292 commit 4f9290a
Show file tree
Hide file tree
Showing 31 changed files with 1,074 additions and 280 deletions.
43 changes: 42 additions & 1 deletion guides/5.STYLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const draw = new TerraDraw({
> [!IMPORTANT]
> Colors in Terra Draw are modelled as 6 digit Hex colors (e.g., #00FFFF). This is to ensure consistency across different mapping libraries.
Each Mode has a different set of styles that can be passed to the constructor.
Each Mode has a different set of styles that can be passed to the constructor:

### Points

Expand Down Expand Up @@ -157,6 +157,47 @@ The `TerraDrawSensorMode` is styled using the following properties:
| `fillOpacity` | Number (0-1) | `0.9` | The fill opacity of the sensor |


## Dynamically Changing Styling

You can update styles after a mode has been initialised using the `setModeOptions` method, which takes a `styles` object. This can be done like so:

```typescript
const draw = new TerraDraw({
adapter: new TerraDrawMapboxGLAdapter({ map, lib }),
modes: [
new TerraDrawPolygonMode({
// Pass styles to the constructor
styles: {
fillColor: "#00FFFF",
fillOpacity: 0.7,
outlineColor: "#00FF00",
outlineWidth: 2,
},
}),
],
});

// Later on...

draw.setModeOptions<typeof TerraDrawPolygonMode>('polygon', {
// We can pass in a partial styles object and it will just update the fields passed
styles: {
fillColor: "#b3250a",
fillOpacity: 0.85
}
})
```

This will tigger a `styling` change event, which can be listened to like so:

```typescript
draw.on('change', (ids, type) => {
if (type === 'styling') {
console.log('styling has changed');
}
})
```

## Selection Mode

To style selected data, pass the `styles` property to the `TerraDrawSelectMode` constructor. For example, to style the selected polygon:
Expand Down
1 change: 1 addition & 0 deletions packages/development/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ const getModes = () => {
toLine: false,
toCoordinate: false,
},
editable: true,
validation: (feature, { updateType }) => {
if (updateType === "finish" || updateType === "commit") {
return ValidateNotSelfIntersecting(feature);
Expand Down
18 changes: 18 additions & 0 deletions packages/development/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "./../../tsconfig.base.json",
"include": [
"src",
"../terra-draw/src",
"../terra-draw-google-maps-adapter/src",
"../terra-draw-mapbox-gl-adapter/src",
"../terra-draw-maplibre-gl-adapter/src",
"../terra-draw-leaflet-adapter/src",
"../terra-draw-arcgis-adapter/src",
"../terra-draw-openlayers-adapter/src"
],
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "../"
}
}
2 changes: 1 addition & 1 deletion packages/e2e/tests/leaflet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,7 @@ test.describe("select mode", () => {
await page.mouse.click(mapDiv.width - 10, mapDiv.height / 2);

// Dragged the square up and to the left
await expectGroupPosition({ page, x: 490, y: 408 });
await expectGroupPosition({ page, x: 490, y: 407 });
});

test("selected circle has it's shape maintained from center origin when coordinates are dragged with resizable flag", async ({
Expand Down
13 changes: 11 additions & 2 deletions packages/e2e/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ export const expectPathDimensions = async ({
expect(boundingBox?.height).toBe(height);
};

const expectCloseTo = (actual: number, expected: number, tolerance = 1) => {
expect(actual).toBeGreaterThanOrEqual(expected - tolerance);
expect(actual).toBeLessThanOrEqual(expected + tolerance);
};

export const expectGroupPosition = async ({
page,
x,
Expand All @@ -147,8 +152,12 @@ export const expectGroupPosition = async ({

const boundingBox = await page.locator(selector).boundingBox();

expect(boundingBox?.x).toBe(x);
expect(boundingBox?.y).toBe(y);
if (!boundingBox) {
throw new Error(`Selector ${selector} bounding box not found`);
}

expectCloseTo(boundingBox.x, x);
expectCloseTo(boundingBox.y, y);
};

export const drawRectangularPolygon = async ({
Expand Down
1 change: 1 addition & 0 deletions packages/terra-draw-mapbox-gl-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"type": "module",
"source": "src/terra-draw-mapbox-gl-adapter.ts",
"exports": {
"source": "./src/terra-draw-mapbox-gl-adapter.ts",
"types": "./dist/terra-draw-mapbox-gl-adapter.d.ts",
"require": "./dist/terra-draw-mapbox-gl-adapter.cjs",
"default": "./dist/terra-draw-mapbox-gl-adapter.modern.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MockCursorEvent } from "../../test/mock-cursor-event";
import { TerraDrawAngledRectangleMode } from "./angled-rectangle.mode";
import { Polygon } from "geojson";
import { followsRightHandRule } from "../../geometry/boolean/right-hand-rule";
import { MockKeyboardEvent } from "../../test/mock-keyboard-event";

describe("TerraDrawAngledRectangleMode", () => {
describe("constructor", () => {
Expand Down Expand Up @@ -112,6 +113,65 @@ describe("TerraDrawAngledRectangleMode", () => {
});
});

describe("updateOptions", () => {
it("can change cursors", () => {
const angledRectangleMode = new TerraDrawAngledRectangleMode();
angledRectangleMode.updateOptions({
cursors: {
start: "pointer",
close: "pointer",
},
});
const mockConfig = MockModeConfig(angledRectangleMode.mode);
angledRectangleMode.register(mockConfig);
angledRectangleMode.start();
expect(mockConfig.setCursor).toHaveBeenCalledWith("pointer");
});

it("can change key events", () => {
const angledRectangleMode = new TerraDrawAngledRectangleMode();
angledRectangleMode.updateOptions({
keyEvents: {
cancel: "C",
finish: "F",
},
});
const mockConfig = MockModeConfig(angledRectangleMode.mode);
angledRectangleMode.register(mockConfig);
angledRectangleMode.start();

angledRectangleMode.onClick(MockCursorEvent({ lng: 0, lat: 0 }));

let features = mockConfig.store.copyAll();
expect(features.length).toBe(1);

angledRectangleMode.onKeyUp(MockKeyboardEvent({ key: "C" }));

features = mockConfig.store.copyAll();
expect(features.length).toBe(0);
});

it("can update styles", () => {
const angledRectangleMode = new TerraDrawAngledRectangleMode();

const mockConfig = MockModeConfig(angledRectangleMode.mode);

angledRectangleMode.register(mockConfig);
angledRectangleMode.start();

angledRectangleMode.updateOptions({
styles: {
fillColor: "#ffffff",
},
});
expect(angledRectangleMode.styles).toStrictEqual({
fillColor: "#ffffff",
});

expect(mockConfig.onChange).toHaveBeenCalledTimes(1);
});
});

describe("onMouseMove", () => {
let angledRectangleMode: TerraDrawAngledRectangleMode;
let store: GeoJSONStore;
Expand Down Expand Up @@ -489,6 +549,8 @@ describe("TerraDrawAngledRectangleMode", () => {
},
});

rectangleMode.register(MockModeConfig(rectangleMode.mode));

expect(
rectangleMode.styleFeature({
type: "Feature",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type TerraDrawPolygonModeKeyEvents = {
finish?: KeyboardEvent["key"] | null;
};

const defaultKeyEvents = { cancel: "Escape", finish: "Enter" };

type PolygonStyling = {
fillColor: HexColorStyling;
outlineColor: HexColorStyling;
Expand All @@ -50,6 +52,11 @@ interface Cursors {
close?: Cursor;
}

const defaultCursors = {
start: "crosshair",
close: "pointer",
} as Required<Cursors>;

interface TerraDrawPolygonModeOptions<T extends CustomStyling>
extends BaseModeOptions<T> {
pointerDistance?: number;
Expand All @@ -58,40 +65,34 @@ interface TerraDrawPolygonModeOptions<T extends CustomStyling>
}

export class TerraDrawAngledRectangleMode extends TerraDrawBaseDrawMode<PolygonStyling> {
mode = "angled-rectangle";
mode = "angled-rectangle" as const;

private currentCoordinate = 0;
private currentId: FeatureId | undefined;
private keyEvents: TerraDrawPolygonModeKeyEvents;
private keyEvents: TerraDrawPolygonModeKeyEvents = defaultKeyEvents;

// Behaviors
private cursors: Required<Cursors>;
private cursors: Required<Cursors> = defaultCursors;
private mouseMove = false;

constructor(options?: TerraDrawPolygonModeOptions<PolygonStyling>) {
super(options);
super(options, true);
this.updateOptions(options);
}

const defaultCursors = {
start: "crosshair",
close: "pointer",
} as Required<Cursors>;
override updateOptions(
options?: TerraDrawPolygonModeOptions<PolygonStyling>,
) {
super.updateOptions(options);

if (options && options.cursors) {
this.cursors = { ...defaultCursors, ...options.cursors };
} else {
this.cursors = defaultCursors;
if (options?.cursors) {
this.cursors = { ...this.cursors, ...options.cursors };
}

// We want to have some defaults, but also allow key bindings
// to be explicitly turned off
if (options?.keyEvents === null) {
this.keyEvents = { cancel: null, finish: null };
} else {
const defaultKeyEvents = { cancel: "Escape", finish: "Enter" };
this.keyEvents =
options && options.keyEvents
? { ...defaultKeyEvents, ...options.keyEvents }
: defaultKeyEvents;
} else if (options?.keyEvents) {
this.keyEvents = { ...this.keyEvents, ...options.keyEvents };
}
}

Expand Down
64 changes: 44 additions & 20 deletions packages/terra-draw/src/modes/base.mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,57 +35,81 @@ export enum ModeTypes {
Render = "render",
}

export type BaseModeOptions<T extends CustomStyling> = {
styles?: Partial<T>;
export type BaseModeOptions<Styling extends CustomStyling> = {
styles?: Partial<Styling>;
pointerDistance?: number;
validation?: Validation;
projection?: Projection;
};

export abstract class TerraDrawBaseDrawMode<T extends CustomStyling> {
protected _state: TerraDrawModeState;
export abstract class TerraDrawBaseDrawMode<Styling extends CustomStyling> {
// State
protected _state: TerraDrawModeState = "unregistered";
get state() {
return this._state;
}
set state(_) {
throw new Error("Please use the modes lifecycle methods");
}

protected _styles: Partial<T>;

get styles(): Partial<T> {
// Styles
protected _styles: Partial<Styling> = {};
get styles(): Partial<Styling> {
return this._styles;
}
set styles(styling: Partial<T>) {
set styles(styling: Partial<Styling>) {
if (typeof styling !== "object") {
throw new Error("Styling must be an object");
}
this.onStyleChange([], "styling");

// Note: This may not be initialised yet as styles can be set/changed pre-registration
if (this.onStyleChange) {
this.onStyleChange([], "styling");
}
this._styles = styling;
}

protected behaviors: TerraDrawModeBehavior[] = [];
protected validate: Validation | undefined;
protected pointerDistance: number;
protected pointerDistance: number = 40;
protected coordinatePrecision!: number;
protected onStyleChange!: StoreChangeHandler;
protected store!: GeoJSONStore;
protected projection: Projection = "web-mercator";

protected setDoubleClickToZoom!: TerraDrawModeRegisterConfig["setDoubleClickToZoom"];
protected unproject!: TerraDrawModeRegisterConfig["unproject"];
protected project!: TerraDrawModeRegisterConfig["project"];
protected setCursor!: TerraDrawModeRegisterConfig["setCursor"];
protected registerBehaviors(behaviorConfig: BehaviorConfig): void {}
protected projection!: Projection;

constructor(options?: BaseModeOptions<T>) {
this._state = "unregistered";
this._styles =
options && options.styles ? { ...options.styles } : ({} as Partial<T>);
this.pointerDistance = (options && options.pointerDistance) || 40;
constructor(
options?: BaseModeOptions<Styling>,
willCallUpdateOptionsInParentClass = false,
) {
// Note: We want to updateOptions on the base class by default, but we don't want it to be
// called twice if the extending class is going to call it as well
if (!willCallUpdateOptionsInParentClass) {
this.updateOptions(options);
}
}

this.validate = options && options.validation;
updateOptions(options?: BaseModeOptions<Styling>) {
if (options?.styles) {
// Note: we are updating this.styles and not this._styles - this is because
// once registered we want to trigger the onStyleChange
this.styles = { ...this._styles, ...options.styles };
}

this.projection = (options && options.projection) || "web-mercator";
if (options?.pointerDistance) {
this.pointerDistance = options.pointerDistance;
}
if (options?.validation) {
this.validate = options && options.validation;
}
if (options?.projection) {
this.projection = options.projection;
}
}

type = ModeTypes.Drawing;
Expand Down Expand Up @@ -269,8 +293,8 @@ export abstract class TerraDrawBaseDrawMode<T extends CustomStyling> {
}

export abstract class TerraDrawBaseSelectMode<
T extends CustomStyling,
> extends TerraDrawBaseDrawMode<T> {
Styling extends CustomStyling,
> extends TerraDrawBaseDrawMode<Styling> {
public type = ModeTypes.Select;

public abstract selectFeature(featureId: FeatureId): void;
Expand Down
Loading

0 comments on commit 4f9290a

Please sign in to comment.