From 7c7eb6dc208688a9f288f52ec20f58eaa02f4548 Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Fri, 3 May 2019 14:41:51 -0600 Subject: [PATCH 1/8] Add initial RFC --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 dev-docs/RFCs/v1.0/generic-edit-mode.md diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md new file mode 100644 index 000000000..0dbc1fd8b --- /dev/null +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -0,0 +1,75 @@ +# Generic EditMode + +Create a generic `EditMode` class in `@nebula/core` that is independent of deck.gl, react-map-gl, and GeoJSON. This generic class can then be integrated into `EditableGeoJsonLayer` as well as the [upcoming DrawControl feature of react-map-gl](https://github.com/uber/react-map-gl/issues/734) + +We will also refactor all the existing `ModeHandler` implementations of nebula to extend this class so that they can be used seamlessly between nebula.gl and react-map-gl. + +## API + +```javascript +export type ModeState = { + data: TData, + modeConfig: any, + selectedIndexes: number[], + guides: ?TGuides, + onEdit: (data: TData, editContext: any) => void + onUpdateGuides: (guides: ?TGuides) => void, +}; + +export class EditMode { + state: ModeState; + + getState(): ModeState { + return this.state; + } + + updateState(state: ModeState) { + const changedEvents: (() => void)[] = []; + if (this.state && this.state.data !== state.data) { + changedEvents.push(this.onDataChanged); + } + if (this.state && this.state.modeConfig !== state.modeConfig) { + changedEvents.push(this.onModeConfigChanged); + } + if (this.state && this.state.selectedIndexes !== state.selectedIndexes) { + changedEvents.push(this.onSelectedIndexesChanged); + } + if (this.state && this.state.guides !== state.guides) { + changedEvents.push(this.onGuidesChanged); + } + this.state = state; + + changedEvents.forEach(fn => fn.bind(this)()); + } + + getModeConfig(): any { + return this.state.modeConfig; + } + + getSelectedIndexes(): number[] { + return this.state.selectedIndexes; + } + + getGuides(): ?TGuides { + return this.state && this.state.guides; + } + + onDataChanged() {} + onModeConfigChanged() {} + onSelectedIndexesChanged() {} + onGuidesChanged() {} + + handleClick(event: ClickEvent): ?EditAction {} + handlePointerMove(event: PointerMoveEvent): { editAction: ?EditAction, cancelMapPan: boolean } {} + handleStartDragging(event: StartDraggingEvent): ?EditAction {} + handleStopDragging(event: StopDraggingEvent): ?EditAction {} +} +``` + +A user of this class will need to call `updateState` anytime the data within `ModeState` change. This is similar to how React calls `render` and how deck.gl calls each layer's `updateState` function. + +An implementation of a mode is intended to override the `handle...` functions in order to handle user input. The mode can then call the callbacks provided in `ModeState` (e.g. `onEdit`). + +## Breaking changes to nebula + +TODO From 9f101c1aa0a9c4e2a001b6790ad45a0855d1c01b Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Fri, 3 May 2019 14:50:17 -0600 Subject: [PATCH 2/8] Add motivation --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index 0dbc1fd8b..e236fb686 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -1,9 +1,21 @@ # Generic EditMode +* **Author**: Clay Anderson + +## Summary + Create a generic `EditMode` class in `@nebula/core` that is independent of deck.gl, react-map-gl, and GeoJSON. This generic class can then be integrated into `EditableGeoJsonLayer` as well as the [upcoming DrawControl feature of react-map-gl](https://github.com/uber/react-map-gl/issues/734) We will also refactor all the existing `ModeHandler` implementations of nebula to extend this class so that they can be used seamlessly between nebula.gl and react-map-gl. +## Motivation + +There are two limitations with nebula's `ModeHandler` interface. + +1. It is dependent on deck.gl. This makes it unusable for `react-map-gl` which doesn't have a dependency on deck.gl. + +2. It is specific to GeoJSON. But there are desires to support editing other kinds of geometries (e.g. Hexagons using [H3](https://uber.github.io/h3/#/)). + ## API ```javascript @@ -70,6 +82,12 @@ A user of this class will need to call `updateState` anytime the data within `Mo An implementation of a mode is intended to override the `handle...` functions in order to handle user input. The mode can then call the callbacks provided in `ModeState` (e.g. `onEdit`). -## Breaking changes to nebula +## Integration to react-map-gl + +TODO + +## Integration with nebula's `ModeHandler` + +### Breaking changes TODO From 3da0f2b77873899ad10f776eac87dfa5c488e340 Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Fri, 3 May 2019 14:53:20 -0600 Subject: [PATCH 3/8] More updates --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index e236fb686..ad419f56c 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -66,15 +66,15 @@ export class EditMode { return this.state && this.state.guides; } - onDataChanged() {} - onModeConfigChanged() {} - onSelectedIndexesChanged() {} - onGuidesChanged() {} - - handleClick(event: ClickEvent): ?EditAction {} - handlePointerMove(event: PointerMoveEvent): { editAction: ?EditAction, cancelMapPan: boolean } {} - handleStartDragging(event: StartDraggingEvent): ?EditAction {} - handleStopDragging(event: StopDraggingEvent): ?EditAction {} + onDataChanged(): void {} + onModeConfigChanged(): void {} + onSelectedIndexesChanged(): void {} + onGuidesChanged(): void {} + + handleClick(event: ClickEvent): void {} + handlePointerMove(event: PointerMoveEvent): { cancelMapPan: boolean } {} + handleStartDragging(event: StartDraggingEvent): void {} + handleStopDragging(event: StopDraggingEvent): void {} } ``` From 87226c758db85e99cb5fe28b44a80df5109ffe90 Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Fri, 3 May 2019 14:55:02 -0600 Subject: [PATCH 4/8] typo --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index ad419f56c..65ad050e4 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -24,7 +24,7 @@ export type ModeState = { modeConfig: any, selectedIndexes: number[], guides: ?TGuides, - onEdit: (data: TData, editContext: any) => void + onEdit: (data: TData, editContext: any) => void, onUpdateGuides: (guides: ?TGuides) => void, }; From be0b4ab32b2b97ae84af8c3021c4b0e4cdef616c Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Mon, 6 May 2019 10:41:45 -0600 Subject: [PATCH 5/8] Update API --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 116 +++++++++++++++++++++--- 1 file changed, 102 insertions(+), 14 deletions(-) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index 65ad050e4..a160eaea0 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -19,13 +19,91 @@ There are two limitations with nebula's `ModeHandler` interface. ## API ```javascript +// Represents an edit action, i.e. a suggestion to update the data based on user interaction events +export type EditAction = { + updatedData: TData, + editType: string, + affectedIndexes: number[], + editContext: any +}; + +// Represents an object "picked" from the screen. This usually reflects an object under the cursor +export type Pick = { + object: any, + index: number, + isGuide: boolean +}; + +// Represents a click event +export type ClickEvent = { + picks: Pick[], + screenCoords: Position, + mapCoords: Position, + sourceEvent: any +}; + +// Represents a double-click event +export type DoubleClickEvent = { + mapCoords: Position, + sourceEvent: any +}; + +// Represents an event that occurs when the pointer goes down and the cursor starts moving +export type StartDraggingEvent = { + picks: Pick[], + screenCoords: Position, + mapCoords: Position, + pointerDownScreenCoords: Position, + pointerDownMapCoords: Position, + sourceEvent: any +}; + +// Represents an event that occurs after the pointer goes down, moves some, then the pointer goes back up +export type StopDraggingEvent = { + picks: Pick[], + screenCoords: Position, + mapCoords: Position, + pointerDownScreenCoords: Position, + pointerDownMapCoords: Position, + sourceEvent: any +}; + +// Represents an event that occurs every time the pointer moves +export type PointerMoveEvent = { + screenCoords: Position, + mapCoords: Position, + picks: Pick[], + isDragging: boolean, + pointerDownPicks: ?(Pick[]), + pointerDownScreenCoords: ?Position, + pointerDownMapCoords: ?Position, + sourceEvent: any +}; + export type ModeState = { + // The data being edited, this can be an array or an object data: TData, + + // Additional configuration for this mode modeConfig: any, + + // The indexes of the selected features selectedIndexes: number[], + + // Features that can be used as a guide for editing the data guides: ?TGuides, - onEdit: (data: TData, editContext: any) => void, + + // The cursor type, as a [CSS Cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) + cursor: string, + + // Callback used to notify applications of an edit action + onEdit: (editAction: EditAction) => void, + + // Callback used to update guides onUpdateGuides: (guides: ?TGuides) => void, + + // Callback used to update cursor + onUpdateCursor: (cursor: string) => void }; export class EditMode { @@ -54,27 +132,37 @@ export class EditMode { changedEvents.forEach(fn => fn.bind(this)()); } + // Overridable user interaction handlers + handleClick(event: ClickEvent): void {} + handlePointerMove(event: PointerMoveEvent): void {} + handleStartDragging(event: StartDraggingEvent): void {} + handleStopDragging(event: StopDraggingEvent): void {} + + // Convenience functions to handle state changes + onDataChanged(): void {} + onModeConfigChanged(): void {} + onSelectedIndexesChanged(): void {} + onGuidesChanged(): void {} + + // Convenience functions to access state getModeConfig(): any { return this.state.modeConfig; } - getSelectedIndexes(): number[] { return this.state.selectedIndexes; } - getGuides(): ?TGuides { return this.state && this.state.guides; } - - onDataChanged(): void {} - onModeConfigChanged(): void {} - onSelectedIndexesChanged(): void {} - onGuidesChanged(): void {} - - handleClick(event: ClickEvent): void {} - handlePointerMove(event: PointerMoveEvent): { cancelMapPan: boolean } {} - handleStartDragging(event: StartDraggingEvent): void {} - handleStopDragging(event: StopDraggingEvent): void {} + onEdit(editAction: EditAction): void { + this.state.onEdit(editAction); + } + onUpdateGuides(guides: ?TGuides): void { + this.state.onUpdateGuides(guides); + } + onUpdateCursor(cursor: string): void { + this.state.onUpdateCursor(cursor); + } } ``` @@ -82,7 +170,7 @@ A user of this class will need to call `updateState` anytime the data within `Mo An implementation of a mode is intended to override the `handle...` functions in order to handle user input. The mode can then call the callbacks provided in `ModeState` (e.g. `onEdit`). -## Integration to react-map-gl +## Integration to react-map-gl-draw TODO From 510e313fc0b5542a849554b5eed37367834a7e50 Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Mon, 6 May 2019 11:23:53 -0600 Subject: [PATCH 6/8] Add proposal for module layout --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 27 +++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index a160eaea0..9293d8523 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -4,9 +4,9 @@ ## Summary -Create a generic `EditMode` class in `@nebula/core` that is independent of deck.gl, react-map-gl, and GeoJSON. This generic class can then be integrated into `EditableGeoJsonLayer` as well as the [upcoming DrawControl feature of react-map-gl](https://github.com/uber/react-map-gl/issues/734) +Create a generic `EditMode` class in `@nebula/core` that is independent of deck.gl, react-map-gl, and GeoJSON. This generic class can then be integrated into `EditableGeoJsonLayer` as well as the [upcoming DrawControl feature for react-map-gl](https://github.com/uber/react-map-gl/issues/734) -We will also refactor all the existing `ModeHandler` implementations of nebula to extend this class so that they can be used seamlessly between nebula.gl and react-map-gl. +We will also refactor all the existing `ModeHandler` implementations of nebula to extend this class so that they can be used seamlessly between `nebula.gl` and `react-map-gl-draw`. ## Motivation @@ -174,8 +174,27 @@ An implementation of a mode is intended to override the `handle...` functions in TODO -## Integration with nebula's `ModeHandler` +## Integration with nebula + +### Module layout + +We will need a `@nebula.gl/core` module separate from `nebula.gl` module. The reason is because this new `@nebula/core` should have no deck.gl dependency. + +* `nebula.gl` + * depends on `@nebula.gl/core`, `@nebula.gl/layers`, and all the other `@nebula/...` modules. + * doesn't have anything in it, just basically imports from the others and re-exports them +* `@nebula.gl/core` + * no (large) dependencies, ideally no dependencies + * contains `EditMode` class + * contains other general purpose types and classes (e.g. event types like `ClickEvent`) +* `@nebula.gl/layers` + * depends on `@nebula.gl/core` and `deck.gl` + * contains `EditableGeoJsonLayer`, a deck.gl `CompositeLayer` +* `@nebula.gl/geojson-modes` + * depends on `@nebula.gl/core` and [turf.js](http://turfjs.org/) + * contains all the modes for editing GeoJSON + * this module can then be reused by `react-map-gl-draw` ### Breaking changes -TODO +There will be breaking changes to refactor nebula's `ModeHandler` interface to adhere to `EditMode`'s interface. Specifics will be listed in the changelog. From 31cb022b5b9fce1a1e033097b0813ab48c597a04 Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Wed, 8 May 2019 14:31:16 -0600 Subject: [PATCH 7/8] Just document interface instead of base class --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 138 ++++-------------------- 1 file changed, 19 insertions(+), 119 deletions(-) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index 9293d8523..6a29a3472 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -18,67 +18,9 @@ There are two limitations with nebula's `ModeHandler` interface. ## API -```javascript -// Represents an edit action, i.e. a suggestion to update the data based on user interaction events -export type EditAction = { - updatedData: TData, - editType: string, - affectedIndexes: number[], - editContext: any -}; - -// Represents an object "picked" from the screen. This usually reflects an object under the cursor -export type Pick = { - object: any, - index: number, - isGuide: boolean -}; - -// Represents a click event -export type ClickEvent = { - picks: Pick[], - screenCoords: Position, - mapCoords: Position, - sourceEvent: any -}; - -// Represents a double-click event -export type DoubleClickEvent = { - mapCoords: Position, - sourceEvent: any -}; - -// Represents an event that occurs when the pointer goes down and the cursor starts moving -export type StartDraggingEvent = { - picks: Pick[], - screenCoords: Position, - mapCoords: Position, - pointerDownScreenCoords: Position, - pointerDownMapCoords: Position, - sourceEvent: any -}; - -// Represents an event that occurs after the pointer goes down, moves some, then the pointer goes back up -export type StopDraggingEvent = { - picks: Pick[], - screenCoords: Position, - mapCoords: Position, - pointerDownScreenCoords: Position, - pointerDownMapCoords: Position, - sourceEvent: any -}; +The `EditMode` interface serves as the core abstraction to editing using nebula.gl. It uses a reactive style approach using callbacks to notify of changes to state and reacting to state changes via exposing an `updateState` function. -// Represents an event that occurs every time the pointer moves -export type PointerMoveEvent = { - screenCoords: Position, - mapCoords: Position, - picks: Pick[], - isDragging: boolean, - pointerDownPicks: ?(Pick[]), - pointerDownScreenCoords: ?Position, - pointerDownMapCoords: ?Position, - sourceEvent: any -}; +```javascript export type ModeState = { // The data being edited, this can be an array or an object @@ -106,67 +48,25 @@ export type ModeState = { onUpdateCursor: (cursor: string) => void }; -export class EditMode { - state: ModeState; - - getState(): ModeState { - return this.state; - } - - updateState(state: ModeState) { - const changedEvents: (() => void)[] = []; - if (this.state && this.state.data !== state.data) { - changedEvents.push(this.onDataChanged); - } - if (this.state && this.state.modeConfig !== state.modeConfig) { - changedEvents.push(this.onModeConfigChanged); - } - if (this.state && this.state.selectedIndexes !== state.selectedIndexes) { - changedEvents.push(this.onSelectedIndexesChanged); - } - if (this.state && this.state.guides !== state.guides) { - changedEvents.push(this.onGuidesChanged); - } - this.state = state; - - changedEvents.forEach(fn => fn.bind(this)()); - } - - // Overridable user interaction handlers - handleClick(event: ClickEvent): void {} - handlePointerMove(event: PointerMoveEvent): void {} - handleStartDragging(event: StartDraggingEvent): void {} - handleStopDragging(event: StopDraggingEvent): void {} - - // Convenience functions to handle state changes - onDataChanged(): void {} - onModeConfigChanged(): void {} - onSelectedIndexesChanged(): void {} - onGuidesChanged(): void {} - - // Convenience functions to access state - getModeConfig(): any { - return this.state.modeConfig; - } - getSelectedIndexes(): number[] { - return this.state.selectedIndexes; - } - getGuides(): ?TGuides { - return this.state && this.state.guides; - } - onEdit(editAction: EditAction): void { - this.state.onEdit(editAction); - } - onUpdateGuides(guides: ?TGuides): void { - this.state.onUpdateGuides(guides); - } - onUpdateCursor(cursor: string): void { - this.state.onUpdateCursor(cursor); - } +export interface EditMode { + // Called every time something in `state` changes + updateState(state: ModeState): void; + + // Called when the pointer went down and up without dragging regardless of whether something was picked + handleClick(event: ClickEvent): void; + + // Called when the pointer moved, regardless of whether the pointer is down, up, and whether something was picked + handlePointerMove(event: PointerMoveEvent): void; + + // Called when the pointer went down on something rendered by this layer and the pointer started to move + handleStartDragging(event: StartDraggingEvent): void; + + // Called when the pointer went down on something rendered by this layer, the pointer moved, and now the pointer is up + handleStopDragging(event: StopDraggingEvent): void; } ``` -A user of this class will need to call `updateState` anytime the data within `ModeState` change. This is similar to how React calls `render` and how deck.gl calls each layer's `updateState` function. +A user of this interface will need to call `updateState` anytime the data within `ModeState` change. This is similar to how React calls `render` and how deck.gl calls `Layer.updateState` function. An implementation of a mode is intended to override the `handle...` functions in order to handle user input. The mode can then call the callbacks provided in `ModeState` (e.g. `onEdit`). @@ -192,7 +92,7 @@ We will need a `@nebula.gl/core` module separate from `nebula.gl` module. The re * contains `EditableGeoJsonLayer`, a deck.gl `CompositeLayer` * `@nebula.gl/geojson-modes` * depends on `@nebula.gl/core` and [turf.js](http://turfjs.org/) - * contains all the modes for editing GeoJSON + * contains all the modes for editing GeoJSON (e.g. `DrawPolygonMode`) * this module can then be reused by `react-map-gl-draw` ### Breaking changes From 328f4b66836c863be42010dea4982fc6374729da Mon Sep 17 00:00:00 2001 From: Clay Anderson Date: Thu, 18 Jul 2019 21:25:24 -0600 Subject: [PATCH 8/8] Add example of guides --- dev-docs/RFCs/v1.0/generic-edit-mode.md | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/dev-docs/RFCs/v1.0/generic-edit-mode.md b/dev-docs/RFCs/v1.0/generic-edit-mode.md index 6a29a3472..096f5cd2b 100644 --- a/dev-docs/RFCs/v1.0/generic-edit-mode.md +++ b/dev-docs/RFCs/v1.0/generic-edit-mode.md @@ -70,6 +70,55 @@ A user of this interface will need to call `updateState` anytime the data within An implementation of a mode is intended to override the `handle...` functions in order to handle user input. The mode can then call the callbacks provided in `ModeState` (e.g. `onEdit`). +### Guides + +```js +// Example guides after drawing two points of a line string +// eslint-disable-next-line +const exampleGuides = { + type: 'FeatureCollection', + features: [ + // Line string that follows the mouse as it moves + { + type: 'Feature', + properties: { + guideType: 'tentative' + }, + geometry: { + type: 'LineString', + coordinates: [] + } + }, + // Point 0 (first one clicked) + { + type: 'Feature', + properties: { + guideType: 'existingEditHandle', + positionIndexes: [0], + featureIndex: 0 + }, + geometry: { + type: 'Point', + coordinates: [] + } + }, + // Point 1 (second one clicked) + { + type: 'Feature', + properties: { + guideType: 'existingEditHandle', + positionIndexes: [1], + featureIndex: 0 + }, + geometry: { + type: 'Point', + coordinates: [] + } + } + ] +}; +``` + ## Integration to react-map-gl-draw TODO