Skip to content

Commit

Permalink
Improve react-map-gl-draw API (uber#270)
Browse files Browse the repository at this point in the history
  • Loading branch information
Xintong Xia authored Sep 16, 2019
1 parent a6289a4 commit b6a4b80
Show file tree
Hide file tree
Showing 15 changed files with 823 additions and 443 deletions.
173 changes: 81 additions & 92 deletions docs/api-reference/react-map-gl-draw/react-map-gl-draw.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# React Map GL Draw

`react-map-gl-draw` is a react based drawing library tailored for [`react-map-gl`](https://github.com/uber/react-map-gl).

## Options
- `mode` (String, Optional)
- `EditorModes.READ_ONLY` - Not interactive. This is the default mode.
- `EditorModes.SELECT_FEATURE` - Lets you select, delete, and drag features.
- `EditorModes.EDIT_VERTEX` - Lets you select, delete, and drag vertices; and drag features.
- `EditorModes.SELECT` - Lets you select, delete, and drag features.
- `EditorModes.EDITTING` - Lets you select, delete, and drag vertices; and drag features.
- `EditorModes.DRAW_PATH` - Lets you draw a GeoJson `LineString` feature.
- `EditorModes.DRAW_POLYGON` - Lets you draw a GeoJson `Polygon` feature.
- `EditorModes.DRAW_POINT` - Lets you draw a GeoJson `Point` feature.
- `EditorModes.DRAW_RECTANGLE` - Lets you draw a `Rectangle` (represented as GeoJson `Polygon` feature).

- `selectedFeatureId` (String, Optional) - id of the selected feature. `EditorModes` assigns a unique id to each feature which is stored in `feature.properties.id`.
- `clickRadius` (Number, optional) - Radius to detect features around a hovered or clicked point. Default value is `0`

- `onSelect` (Function, Required) - callback when a feature is selected. Receives an object containing `selectedFeatureId`.
- `onUpdate` (Function, Required) - callback when anything is updated. Receives one argument `features` that is the updated list of GeoJSON features.
- `features` (Feature[], Optional) - List of features in GeoJson format. If `features` are provided from users, then `react-map-gl-draw` respect the users' input, and therefore ignore any internal `features`. But if `features` are not provided, then `react-map-gl-draw` manages `features` internally, and users can access and manipulate the features by calling `getFeatures`, `addFeatures`, and `deleteFeatures`.
- `selectedFeatureIndex` (String, Optional) - Index of the selected feature.
- `clickRadius` (Number, Optional) - Radius to detect features around a hovered or clicked point. Default value is `0`

Feature object structure:
`react-map-gl-draw` is stateful component.
- `onSelect` (Function, Optional) - callback when clicking a position under `SELECT` and `EDITTING` mode. Receives an object containing the following parameters
- `selectedFeature`: selected feature. `null` if clicked an empty space.
- `selectedFeatureIndex`: selected feature index.`null` if clicked an empty space.
- `editHandleIndex`: selected editHandle index. `null` if clicked an empty space.
- `screenCoords`: screen coordinates of the clicked position.
- `mapCoords`: map coordinates of the clicked position.

- `onUpdate` (Function, Optional) - callback when anything is updated. Receives an object containing the following parameters
- `features` (Feature[]) - the updated list of GeoJSON features.
- `editType` (String) - `addFeature`, `addPosition`, `finishMovePosition`
- `editContext` (Array) - list of edit objects, depend on `editType`, each object may contain `featureIndexes`, `editHandleIndexes`, `screenCoords`, `mapCoords`.

**Feature object structure:**
```js
{
id, // an unique identified generated inside react-map-gl-draw library
Expand All @@ -32,24 +43,31 @@ Feature object structure:
}
```

### Styling related Options
- `getFeatureStyle` (Function, Optional) : Object - A function to style a feature, function parameters are
- `feature`: feature to style .
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`.
### Styling related options
- `featureStyle` (Object|Function, Optional) : Object - Either a [style objects](https://reactjs.org/docs/dom-elements.html#style) or a function to style a feature, function parameters are
- `feature`: feature to style.
- `index`: index of the feature.
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`, `CLOSING`.

Returns is a map of [style objects](https://reactjs.org/docs/dom-elements.html#style) passed to SVG `path` elements.

- `getEditHandleStyle` (Function, Optional) : Object - A function to style an `editHandle, function parameters are
- `featureShape` (String|Function, Optional): if is a string, should be one of `rect` or `circle`. If is a function, will receive the following parameters
- `feature`: feature to style.
- `index`: index of the feature.
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`, `CLOSING`.

- `editHandleStyle` (Object|Function, Optional) : Object - Either a [style objects](https://reactjs.org/docs/dom-elements.html#style) or a function to style an `editHandle, function parameters are
- `feature`: feature to style.
- `index`: index of the editHandle vertex in the feature.
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`.
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`, `CLOSING`.
- `shape`: shape resolved from `editHandleShape`.

Returns is a map of [style objects](https://reactjs.org/docs/dom-elements.html#style) passed to SVG `circle` or `rect` elements.

- `getEditHandleShape` (String|Function, Optional): if is a string, should be one of `rect` or `circle`. If is a function, will receive the following parameters
- `editHandleShape` (String|Function, Optional): if is a string, should be one of `rect` or `circle`. If is a function, will receive the following parameters
- `feature`: feature to style.
- `index`: index of the editHandle vertex in the feature.
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`.
- `state`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`, `CLOSING`.

## Explanations
- `Feature`: any drawn shape, one of point, line, polygon or rectangle.
Expand All @@ -60,78 +78,70 @@ Returns is a map of [style objects](https://reactjs.org/docs/dom-elements.html#s
- `SELECTED`: being clicked or dragged.
- `HOVERED`: hovered over by the mouse pointer.
- `UNCOMMITTED`: in the middle of drawing, not yet added to the feature being edited.
- `CLOSING`: closing a polygon.

### Styling based on `state`:

![img](https://raw.githubusercontent.com/uber-common/deck.gl-data/master/nebula.gl/edit-handle.png)
![img](https://raw.githubusercontent.com/uber-common/deck.gl-data/master/nebula.gl/react-map-gl-draw.png)

As shown in the above image, for the feature currently being edited,
- `getFeatureStyle({feature, state: SELECTED})` will be applied to the committed parts of the feature. (Green strokes)
- `getEditHandleStyle({state: SELECTED})` will be applied to the committed editHandle vertices. (Vertices with black stroke)
- `getFeatureStyle({feature, state: UNCOMMITTED})` will be applied to the uncommitted parts of the feature. (Gray stroke)
- `getEditHandleStyle({state: UNCOMMITTED})` will be applied to the uncommitted editHandle vertex. (Gray vertex)
- `featureStyle({feature, state: SELECTED})` will be applied to the committed parts of the feature. (Green strokes)
- `editHandleStyle({state: SELECTED})` will be applied to the committed editHandle vertices. (Vertices with black stroke)
- `featureStyle({feature, state: UNCOMMITTED})` will be applied to the uncommitted parts of the feature. (Gray stroke)
- `editHandleStyle({state: UNCOMMITTED})` will be applied to the uncommitted editHandle vertex. (Gray vertex)

## Methods

##### `getFeatures`

- Return a list of finished GeoJson features.

##### `addFeatures` (Feature | Feature[])

- Add a single or multiple GeoJson features to editor.

##### `deleteDeatures` (Feature | Feature[])

- Delete a single or multiple GeoJson features to editor.

## Code Example
```js
import React, { Component } from 'react';
import MapGL, {_MapContext as MapContext} from 'react-map-gl';
import MapGLDraw, { EditorModes } from 'react-map-gl-draw';
import MapGL from 'react-map-gl';
import { Editor, EditorModes } from 'react-map-gl-draw';

const MODES = [
{ id: EditorModes.EDIT_VERTEX, text: 'Select and Edit Feature'},
{ id: EditorModes.EDITING, text: 'Select and Edit Feature'},
{ id: EditorModes.DRAW_POINT, text: 'Draw Point'},
{ id: EditorModes.DRAW_PATH, text: 'Draw Polyline'},
{ id: EditorModes.DRAW_POLYGON, text: 'Draw Polygon'},
{ id: EditorModes.DRAW_RECTANGLE, text: 'Draw Rectangle'}
];

const DEFAULT_VIEWPORT = {
width: 800,
height: 600,
longitude: -122.45,
latitude: 37.78,
zoom: 14
};

class App extends Component {
constructor(props) {
super(props);
this.state = {
viewport: {
width: 800,
height: 600,
longitude: -122.45,
latitude: 37.78,
zoom: 14
},
selectedMode: EditorModes.READ_ONLY,
features: [],
selectedFeatureId: null
};
}

componentDidMount() {
// add features
const initialFeatures = [{...}];
this._mapRef.add(initialFeatures);
}

_updateViewport = (viewport) => {
this.setState({viewport});
}

_onSelect = ({ selectedFeatureId }) => {
this.setState({ selectedFeatureId });
};

_onUpdate = features => {
this.setState({
features
});
state = {
// map
viewport: DEFAULT_VIEWPORT,
// editor
selectedMode: EditorModes.READ_ONLY
};

_switchMode = evt => {
const selectedMode = evt.target.id === this.state.selectedMode ? EditorModes.READ_ONLY : evt.target.id;
const selectedMode = evt.target.id;
this.setState({
selectedMode,
selectedFeatureId: null
selectedMode: selectedMode === this.state.selectedMode ? null : selectedMode
});
};
_renderControlPanel = () => {

_renderToolbar = () => {
return (
<div style={{position: absolute, top: 0, right: 0, maxWidth: '320px'}}>
<select onChange={this._switchMode}>
Expand All @@ -140,44 +150,23 @@ class App extends Component {
</select>
</div>
);
}

_getEditHandleStyle = ({feature, featureState, vertexIndex, vertexState}) => {
return {
fill: vertexState === `SELECTED` ? '#000' : '#aaa',
stroke: vertexState === `SELECTED` ? '#000' : 'none'
}
}

_getFeatureStyle = ({feature, featureState}) => {
return {
stroke: featureState === `SELECTED` ? '#000' : 'none',
fill: featureState === `SELECTED` ? '#080' : 'none',
fillOpacity: 0.8
}
}
};

render() {
const { viewport, selectedMode, selectedFeatureId, features } = this.state;
const { viewport, selectedMode } = this.state;
return (
<MapGL
{...viewport}
width="100%"
height="100%"
mapStyle="mapbox://styles/uberdata/cive48w2e001a2imn5mcu2vrs"
onViewportChange={this._updateViewport}
mapStyle={'mapbox://styles/mapbox/light-v9'}
onViewportChange={this.setState({ viewport })}
>
<MapGLDraw
ref={_ => this._drawRef = _}
<Editor
clickRadius={12}
mode={selectedMode}
features={features}
selectedFeatureId={selectedFeatureId}
onSelect={this._onSelect}
onUpdate={this._onUpdate}
getEditHandleStyle={this._getEditHandleStyle}
getFeatureStyle={this._getFeatureStyle}
/>
{this._renderControlPanel()}
{this._renderToolbar()}
</MapGL>
);
}
Expand Down
86 changes: 26 additions & 60 deletions examples/react-map-gl-draw/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* global window */
import React, { Component } from 'react';
import { render } from 'react-dom';
import MapGL from 'react-map-gl';
Expand All @@ -9,65 +8,36 @@ import Toolbar from './toolbar';
// eslint-disable-next-line no-process-env, no-undef
const MAP_STYLE = process.env.MapStyle || 'mapbox://styles/mapbox/light-v9';

const DEFAULT_VIEWPORT = {
width: 800,
height: 600,
longitude: -122.45,
latitude: 37.78,
zoom: 14
};

export default class App extends Component {
constructor(props) {
super(props);
this.state = {
viewport: {
width: 800,
height: 600,
longitude: -122.45,
latitude: 37.78,
zoom: 14
},
// map
viewport: DEFAULT_VIEWPORT,
// editor
selectedMode: EditorModes.READ_ONLY,
features: [],
selectedFeatureId: null
selectedFeatureIndex: null
};
this._mapRef = null;
this._editorRef = null;
}

componentDidMount() {
window.addEventListener('keydown', this._onKeydown);
}

componentWillUnmount() {
window.removeEventListener('keydown', this._onKeydown);
}

_onKeydown = evt => {
if (evt.keyCode === 27) {
// esc key
this.setState({ selectedFeatureId: null });
}
};

_updateViewport = viewport => {
this.setState({ viewport });
};

_onSelect = ({ selectedFeatureId }) => {
this.setState({ selectedFeatureId });
};

_onDelete = () => {
const { selectedFeatureId } = this.state;
if (selectedFeatureId === null || selectedFeatureId === undefined) {
const { selectedFeatureIndex } = this.state;
if (selectedFeatureIndex === null || selectedFeatureIndex === undefined) {
return;
}

const selectedIndex = this.state.features.findIndex(f => f.properties.id === selectedFeatureId);
if (selectedIndex >= 0) {
const newFeatures = [...this.state.features];
newFeatures.splice(selectedIndex, 1);
this.setState({ features: newFeatures, selectedFeatureId: null });
}
};

_onUpdate = features => {
this.setState({
features
});
this._editorRef.deleteFeatures(selectedFeatureIndex);
};

_switchMode = evt => {
Expand All @@ -76,10 +46,11 @@ export default class App extends Component {
selectedMode = null;
}

this.setState({
selectedMode,
selectedFeatureId: null
});
this.setState({ selectedMode });
};

_updateViewport = viewport => {
this.setState({ viewport });
};

_renderToolbar = () => {
Expand All @@ -92,12 +63,8 @@ export default class App extends Component {
);
};

_getEditHandleShape = ({ feature }) => {
return feature.properties.renderType === 'Point' ? 'circle' : 'rect';
};

render() {
const { viewport, selectedMode, selectedFeatureId, features } = this.state;
const { viewport, selectedMode } = this.state;
return (
<MapGL
{...viewport}
Expand All @@ -108,13 +75,12 @@ export default class App extends Component {
onViewportChange={this._updateViewport}
>
<Editor
ref={_ => (this._editorRef = _)}
clickRadius={12}
onSelect={selected => {
this.setState({ selectedFeatureIndex: selected && selected.selectedFeatureIndex });
}}
mode={selectedMode}
features={features}
selectedFeatureId={selectedFeatureId}
onSelect={this._onSelect}
onUpdate={this._onUpdate}
getEditHandleShape={this._getEditHandleShape}
/>
{this._renderToolbar()}
</MapGL>
Expand Down
2 changes: 1 addition & 1 deletion examples/react-map-gl-draw/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from 'styled-components';
import { EditorModes } from 'react-map-gl-draw';

const MODES = [
{ id: EditorModes.EDITING, text: 'Edit Feature', icon: 'icon-select.svg' },
{ id: EditorModes.SELECT, text: 'Edit Feature', icon: 'icon-select.svg' },
{ id: EditorModes.DRAW_POINT, text: 'Draw Point', icon: 'icon-point.svg' },
{ id: EditorModes.DRAW_PATH, text: 'Draw Polyline', icon: 'icon-path.svg' },
{ id: EditorModes.DRAW_POLYGON, text: 'Draw Polygon', icon: 'icon-polygon.svg' },
Expand Down
Loading

0 comments on commit b6a4b80

Please sign in to comment.