Skip to content

Commit

Permalink
feat: use minimum distance approach instead of nth event for freehand
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesLMilner committed Nov 20, 2022
1 parent 5065f83 commit 240952d
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 50 deletions.
4 changes: 4 additions & 0 deletions development/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const getModes = () => {
},
},
},
freehand: {
feature: { draggable: true, coordinates: {} }
},
linestring: {
feature: {
draggable: true,
Expand All @@ -85,6 +88,7 @@ const getModes = () => {
}),
point: new TerraDrawPointMode(),
linestring: new TerraDrawLineStringMode({
snapping: true,
allowSelfIntersections: false,
}),
polygon: new TerraDrawPolygonMode({
Expand Down
116 changes: 89 additions & 27 deletions src/modes/freehand/freehand.mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Polygon } from "geojson";
import { TerraDrawBaseDrawMode } from "../base.mode";
import { getDefaultStyling } from "../../util/styling";
import { GeoJSONStoreFeatures } from "../../store/store";
import { pixelDistance } from "../../geometry/measure/pixel-distance";

type TerraDrawFreehandModeKeyEvents = {
cancel: KeyboardEvent["key"];
Expand All @@ -20,32 +21,38 @@ type FreehandPolygonStyling = {
outlineColor: HexColor,
outlineWidth: number,
fillOpacity: number,
closingPointColor: HexColor,
closingPointWidth: number,
closingPointOutlineColor: HexColor,
closingPointOutlineWidth: number
}

export class TerraDrawFreehandMode extends TerraDrawBaseDrawMode<FreehandPolygonStyling> {
mode = "freehand";

private startingClick = false;
private currentId: string | undefined;
private skip = 0;
private everyNthMouseEvent: number;
private closingPointId: string | undefined;
private minDistance: number;
private keyEvents: TerraDrawFreehandModeKeyEvents;

constructor(options?: {
styles?: Partial<FreehandPolygonStyling>;
everyNthMouseEvent?: number;
minDistance?: number;
keyEvents?: TerraDrawFreehandModeKeyEvents;
}) {
super(options);

this.everyNthMouseEvent = (options && options.everyNthMouseEvent) || 10;
this.minDistance = (options && options.minDistance) || 20;
this.keyEvents =
options && options.keyEvents ? options.keyEvents : { cancel: "Escape", finish: 'Enter' };
}

private close() {
this.closingPointId && this.store.delete([this.closingPointId]);
this.startingClick = false;
this.currentId = undefined;
this.closingPointId = undefined;
}

start() {
Expand All @@ -63,37 +70,61 @@ export class TerraDrawFreehandMode extends TerraDrawBaseDrawMode<FreehandPolygon
return;
}

if (this.skip > this.everyNthMouseEvent) {
this.skip = 0;
const currentLineGeometry = this.store.getGeometryCopy<Polygon>(
this.currentId
);
const currentLineGeometry = this.store.getGeometryCopy<Polygon>(
this.currentId
);

const [previousLng, previousLat] =
currentLineGeometry.coordinates[0][
currentLineGeometry.coordinates[0].length - 2
];
const { x, y } = this.project(previousLng, previousLat);
const distance = pixelDistance(
{ x, y },
{ x: event.containerX, y: event.containerY }
);

const [closingLng, closingLat] = currentLineGeometry.coordinates[0][0];
const { x: closingX, y: closingY } = this.project(closingLng, closingLat);
const closingDistance = pixelDistance(
{ x: closingX, y: closingY },
{ x: event.containerX, y: event.containerY }
);

if (closingDistance < this.pointerDistance) {
this.setCursor('pointer');
} else {
this.setCursor('crosshair');
}

currentLineGeometry.coordinates[0].pop();
// The cusor must have moved a minimum distance
// before we add another coordinate
if (distance < this.minDistance) {
return;
}

this.store.updateGeometry([
{
id: this.currentId,
geometry: {
type: "Polygon",
coordinates: [
[
...currentLineGeometry.coordinates[0],
[event.lng, event.lat],
currentLineGeometry.coordinates[0][0],
],
currentLineGeometry.coordinates[0].pop();

this.store.updateGeometry([
{
id: this.currentId,
geometry: {
type: "Polygon",
coordinates: [
[
...currentLineGeometry.coordinates[0],
[event.lng, event.lat],
currentLineGeometry.coordinates[0][0],
],
},
],
},
]);
}

this.skip++;
},
]);
}

onClick(event: TerraDrawMouseEvent) {
if (this.startingClick === false) {
const [createdId] = this.store.create([
const [createdId, closingPointId] = this.store.create([
{
geometry: {
type: "Polygon",
Expand All @@ -108,9 +139,17 @@ export class TerraDrawFreehandMode extends TerraDrawBaseDrawMode<FreehandPolygon
},
properties: { mode: this.mode },
},
{
geometry: {
type: "Point",
coordinates: [event.lng, event.lat],
},
properties: { mode: this.mode },
},
]);

this.currentId = createdId;
this.closingPointId = closingPointId;
this.startingClick = true;
return;
}
Expand All @@ -134,7 +173,11 @@ export class TerraDrawFreehandMode extends TerraDrawBaseDrawMode<FreehandPolygon
if (this.currentId) {
this.store.delete([this.currentId]);
}
if (this.closingPointId) {
this.store.delete([this.closingPointId]);
}
} catch (error) { }
this.closingPointId = undefined;
this.currentId = undefined;
this.startingClick = false;
}
Expand Down Expand Up @@ -163,6 +206,25 @@ export class TerraDrawFreehandMode extends TerraDrawBaseDrawMode<FreehandPolygon
styles.polygonFillOpacity = this.styles.fillOpacity;
}

return styles;
} else if (
feature.type === 'Feature' &&
feature.geometry.type === 'Point' &&
feature.properties.mode === this.mode
) {

if (this.styles.closingPointColor) {
styles.pointColor = this.styles.closingPointColor;
}
if (this.styles.closingPointWidth) {
styles.pointWidth = this.styles.closingPointWidth;
}

styles.pointOutlineColor = this.styles.closingPointOutlineColor !== undefined ?
this.styles.closingPointOutlineColor : '#ffffff';
styles.pointOutlineWidth = this.styles.closingPointOutlineWidth !== undefined ?
this.styles.closingPointOutlineWidth : 2;

return styles;
}

Expand Down
56 changes: 33 additions & 23 deletions src/modes/freehand/freehand.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Project } from "../../common";
import { GeoJSONStore } from "../../store/store";
import { getMockModeConfig } from "../../test/mock-config";
import { getDefaultStyling } from "../../util/styling";
import { TerraDrawFreehandMode } from "./freehand.mode";

describe("TerraDrawFreehandMode", () => {
Expand All @@ -15,7 +15,7 @@ describe("TerraDrawFreehandMode", () => {
it("constructs with options", () => {
const freehandMode = new TerraDrawFreehandMode({
styles: { outlineColor: "#ffffff" },
everyNthMouseEvent: 5,
minDistance: 5,
keyEvents: {
cancel: "Backspace",
finish: 'Enter'
Expand Down Expand Up @@ -127,7 +127,7 @@ describe("TerraDrawFreehandMode", () => {
freehandMode.register(mockConfig);
});

it("adds a polygon to store if registered", () => {
it("adds a polygon and closing point to store if registered", () => {
freehandMode.onClick({
lng: 0,
lat: 0,
Expand All @@ -138,7 +138,7 @@ describe("TerraDrawFreehandMode", () => {
});

expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith([expect.any(String)], "create");
expect(onChange).toBeCalledWith([expect.any(String), expect.any(String)], "create");
});

it("finishes drawing polygon on second click", () => {
Expand All @@ -152,7 +152,7 @@ describe("TerraDrawFreehandMode", () => {
});

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

freehandMode.onClick({
lng: 0,
Expand All @@ -163,11 +163,12 @@ describe("TerraDrawFreehandMode", () => {
heldKeys: [],
});

// No more closing coordinate so we drop to 1 feature
features = store.copyAll();
expect(features.length).toBe(1);

expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith([expect.any(String)], "create");
expect(onChange).toBeCalledTimes(2);
expect(onChange).toBeCalledWith([expect.any(String), expect.any(String)], "create");
});
});
});
Expand All @@ -176,17 +177,19 @@ describe("TerraDrawFreehandMode", () => {
let freehandMode: TerraDrawFreehandMode;
let store: GeoJSONStore;
let onChange: jest.Mock;
let project: Project;

beforeEach(() => {
freehandMode = new TerraDrawFreehandMode();

const mockConfig = getMockModeConfig(freehandMode.mode);
store = mockConfig.store;
onChange = mockConfig.onChange;
project = mockConfig.project;
freehandMode.register(mockConfig);
});

it("updates the freehand polygon on 10th mouse event", () => {
it("updates the freehand polygon when the mouse cursor has moved a minimum amount", () => {
freehandMode.onClick({
lng: 0,
lat: 0,
Expand All @@ -199,29 +202,34 @@ describe("TerraDrawFreehandMode", () => {
expect(onChange).toBeCalledTimes(1);
expect(onChange).toHaveBeenNthCalledWith(
1,
[expect.any(String)],
[expect.any(String), expect.any(String)],
"create"
);

const feature = store.copyAll()[0];

for (let i = 0; i < 12; i++) {
for (let i = 0; i < 5; i++) {
(project as jest.Mock).mockReturnValueOnce({
x: (i * 20) - 20,
y: (i * 20) - 20
});

(project as jest.Mock).mockReturnValueOnce({
x: 1000,
y: 1000
});

freehandMode.onMouseMove({
lng: i,
lat: i,
containerX: i,
containerY: i,
containerX: i * 20,
containerY: i * 20,
button: "left",
heldKeys: [],
});
}

expect(onChange).toBeCalledTimes(2);
expect(onChange).toHaveBeenNthCalledWith(
2,
[expect.any(String)],
"update"
);
expect(onChange).toBeCalledTimes(6);

const updatedFeature = store.copyAll()[0];

Expand Down Expand Up @@ -275,7 +283,7 @@ describe("TerraDrawFreehandMode", () => {

freehandMode.cleanUp();

expect(onChange).toBeCalledTimes(2);
expect(onChange).toBeCalledTimes(3);
expect(onChange).toHaveBeenNthCalledWith(
2,
[expect.any(String)],
Expand Down Expand Up @@ -318,7 +326,7 @@ describe("TerraDrawFreehandMode", () => {
});

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

freehandMode.onKeyUp({ key: "Escape" });

Expand All @@ -340,7 +348,7 @@ describe("TerraDrawFreehandMode", () => {
});

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

freehandMode.onKeyUp({
key: 'Enter'
Expand All @@ -349,8 +357,10 @@ describe("TerraDrawFreehandMode", () => {
features = store.copyAll();
expect(features.length).toBe(1);

expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith([expect.any(String)], "create");
expect(onChange).toBeCalledTimes(2);
expect(onChange).toHaveBeenNthCalledWith(1, [expect.any(String), expect.any(String)], "create");
expect(onChange).toHaveBeenNthCalledWith(2, [expect.any(String)], "delete");

});
});

Expand Down

0 comments on commit 240952d

Please sign in to comment.