Skip to content

Commit

Permalink
Merge 9afddca into 165cb98
Browse files Browse the repository at this point in the history
  • Loading branch information
Xintong Xia committed Jun 7, 2019
2 parents 165cb98 + 9afddca commit e055b55
Show file tree
Hide file tree
Showing 25 changed files with 1,732 additions and 1,037 deletions.
3 changes: 3 additions & 0 deletions .flowconfig
Expand Up @@ -2,6 +2,9 @@
<PROJECT_ROOT>/examples/.*

<PROJECT_ROOT>/node_modules/jsonlint-lines-primitives/.*
<PROJECT_ROOT>/node_modules/@mapbox/jsonlint-lines-primitives/.*
<PROJECT_ROOT>/node_modules/mapbox-gl/src/style-spec/node_modules/jsonlint-lines-primitives/.*

<PROJECT_ROOT>/website/node_modules/.*
<PROJECT_ROOT>/website/examples/.*

Expand Down
164 changes: 164 additions & 0 deletions docs/api-reference/react-map-gl-draw/react-map-gl-draw.md
@@ -0,0 +1,164 @@
# RFC: react-map-gl-draw

## Background

react-map-gl currently does not support drawing functions. However, we have got a couple of [users](https://github.com/uber/react-map-gl/issues/725) interested in this capability. Also it is one of P0 features on Kepler.gl 2019 [roadmap](https://github.com/uber/kepler.gl/wiki/Kepler.gl-2019-Roadmap#allow-drawing-on-map-to-create-paths-and-polygons--).

Although [Mapbox/mapbox-gl-draw](https://github.com/mapbox/mapbox-gl-draw) provides quite nice drawing and editing features, because of its manipulating internal states, it cannot work well with React / Redux framework and therefore cannot be integrated with `react-map-gl`.
[vis.gl](http://vis.gl/) offers another geo editing library [Nebula.gl](http://neb.gl), but it is an overkill while adding heavy dependencies such as deck.gl.

## Proposal

`react-map-gl` can provide a `EditorModes`, starts from simple functions like the following.

### Options

- `mode` (String, Optional) - `react-map-gl` is stateless, user has complete control of the `mode`.
- `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.DRAW_PATH` - Lets you draw a LineString feature.
- `EditorModes.DRAW_POLYGON` - Lets you draw a Polygon feature.
- `EditorModes.DRAW_POINT` - Lets you draw a Point feature.
- `EditorModes.DRAW_RECTANGLE` - Lets you draw a Rectangle feature.

- `getStyle` (Function, Optional) : Object

A function to style features, function parameters are

- `feature`: feature to style
- `featureState`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`
- `vertexId`: id of vertex to style
- `vertexState`: one of `SELECTED`, `HOVERED`, `INACTIVE`, `UNCOMMITTED`

Return is a map of [style objects](https://reactjs.org/docs/dom-elements.html#style) passed to DOM elements. The following keys are supported.
- `vertex` (Object, Optional)
- `shape` - `rect` or `circle`
- `clickRadius` (Number, optional) - Radius to detect features around a hovered or clicked point. fall back to radius. Default value is `0`
- any style which could be applied to SVG `circle|rect`.
- `line` (Object, Optional):
- `clickRadius`: (Number, Optional) - Radius to detect features around a hovered or clicked point line. Default is `0`
- any style which could be applied to SVG `path`

- `features` (Array, Optional) - A list of Point, LineString, or Polygon features.
- `selectedId` (String, Optional) - id of the selected feature. `EditorModes` assigns a unique id to each feature which is stored in `feature.properties.id`.
- `onSelect` (Function, Required) - callback when a feature is selected. Receives an object containing `{selectedId}`.
- `onUpdate` (Function, Required) - callback when anything is updated. Receives one argument `features` that is the updated list of GeoJSON features.
- `onAdd` (Function, Optional) - callback when a new feature is finished drawing. Receives one argument `featureId`.
- `onDelete` (Function, Optional) - callback when a feature is being deleted. Receives one argument `featureId`.


### Code Example
```js
import React, { Component } from "react";
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.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'}
];

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
};
this._mapRef = null;
}

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

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

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

_switchMode = evt => {
this.setState({
selectedMode: evt.target.id,
selectedFeatureId: null
});
};

_renderControlPanel = () => {
return (
<div style={{position: absolute, top: 0, right: 0, maxWidth: '320px'}}>
<select onChange={this._switchMode}>
<option value="">--Please choose a mode--</option>
{MODES.map(mode => <option value={mode.id}>{mode.text}</option>)}
</select>
</div>
);
}

_getStyle = ({feature, featureState, vertexId, vertexState}) => {
return {
vertex: {
clickRadius: 12,
shape: `rect`,
fill: vertexState === `SELECTED` ? '#000' : '#aaa'
},
line: {
clickRadius: 12,
shape: `rect`,
fill: featureState === `SELECTED` ? '#080' : 'none',
fillOpacity: 0.8
}
}
}

render() {
const { viewport, selectedMode, selectedFeatureId, features } = this.state;
return (
<MapGL
{...viewport}
ref={_ => (this._mapRef = _)}
width="100%"
height="100%"
mapStyle="mapbox://styles/uberdata/cive48w2e001a2imn5mcu2vrs"
onViewportChange={this._updateViewport}
>
<Editor
viewport={viewport}
eventManager={this._mapRef && this._mapRef._eventManager}
width="100%"
height="100%"
mode={selectedMode}
features={features}
selectedFeatureId={selectedFeatureId}
onSelect={this._onSelect}
onUpdate={this._onUpdate}
getStyle={this._getStyle}
/>
{this._renderToolbar()}
</MapGL>
);
}
}
```

## Compare with `mapbox-gl-draw`
- `EditorModes` is a stateless component. To manipulate the features, simply change the `features` prop. This is different from calling the class methods of `MapboxDraw`.
- `EditorModes` does not contain UI for mode selection, giving user application the flexibility to control their user experience.
- Features of `MapboxDraw` that are not planned for the initial release of `EditorModes`: keyboard navigation, box select.
126 changes: 42 additions & 84 deletions examples/react-map-gl-draw/app.js
Expand Up @@ -3,24 +3,11 @@ import React, { Component } from 'react';
import { render } from 'react-dom';
import MapGL from 'react-map-gl';
import { Editor, EditorModes } from 'react-map-gl-draw';
import {
ToolboxRow,
ToolboxRowWrapping,
ToolboxLabel,
ToolboxDivider,
ToolboxButton,
styles as ToolboxStyles
} from './toolbox';

const MODES = [
{ name: 'Read Only', value: EditorModes.READ_ONLY },
{ name: 'Select Feature', value: EditorModes.SELECT_FEATURE },
{ name: 'Edit Vertex', value: EditorModes.EDIT_VERTEX },
{ name: 'Draw Point', value: EditorModes.DRAW_POINT },
{ name: 'Draw Path', value: EditorModes.DRAW_PATH },
{ name: 'Draw Polygon', value: EditorModes.DRAW_POLYGON },
{ name: 'Draw Rectangle', value: EditorModes.DRAW_RECTANGLE }
];

import Toolbar from './toolbar';

// eslint-disable-next-line no-process-env, no-undef
const MAP_STYLE = process.env.MapStyle || 'mapbox://styles/mapbox/light-v9';

export default class App extends Component {
constructor(props) {
Expand All @@ -33,9 +20,9 @@ export default class App extends Component {
latitude: 37.78,
zoom: 14
},
mode: EditorModes.READ_ONLY,
selectedMode: EditorModes.READ_ONLY,
features: [],
selectedId: null
selectedFeatureId: null
};
this._mapRef = null;
}
Expand All @@ -51,28 +38,29 @@ export default class App extends Component {
_onKeydown = evt => {
if (evt.keyCode === 27) {
// esc key
this.setState({ selectedId: null });
this.setState({ selectedFeatureId: null });
}
};

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

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

_onDelete = () => {
const { selectedId } = this.state;
if (selectedId === null || selectedId === undefined) {
const { selectedFeatureId } = this.state;
if (selectedFeatureId === null || selectedFeatureId === undefined) {
return;
}
const selectedIndex = this.state.features.findIndex(f => f.id === selectedId);

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

Expand All @@ -82,83 +70,53 @@ export default class App extends Component {
});
};

_renderToolbox = () => {
const drawModes = MODES.filter(mode => mode.value.startsWith('DRAW'));
const otherModes = MODES.filter(mode => !mode.value.startsWith('DRAW'));
_switchMode = evt => {
let selectedMode = evt.target.id;
if (selectedMode === this.state.selectedMode) {
selectedMode = null;
}

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

_renderToolbar = () => {
return (
<div style={ToolboxStyles.toolbox}>
<ToolboxRowWrapping>
<ToolboxLabel style={{ paddingLeft: '2px' }}>Modes</ToolboxLabel>
<ToolboxRow>
{otherModes.map(mode => (
<ToolboxButton
id={mode.value}
key={mode.value}
style={{
backgroundColor: this.state.mode === mode.value ? '#a0cde8' : ''
}}
onClick={this._switchMode}
>
{mode.name}
</ToolboxButton>
))}
</ToolboxRow>
<ToolboxRow>
{drawModes.map(mode => (
<ToolboxButton
id={mode.value}
key={mode.value}
style={{
backgroundColor: this.state.mode === mode.value ? '#a0cde8' : ''
}}
onClick={this._switchMode}
>
{mode.name}
</ToolboxButton>
))}
</ToolboxRow>
</ToolboxRowWrapping>
<ToolboxDivider />
<ToolboxRowWrapping>
<ToolboxRow>
<ToolboxButton onClick={this._onDelete}>Delete</ToolboxButton>
</ToolboxRow>
</ToolboxRowWrapping>
</div>
<Toolbar
selectedMode={this.state.selectedMode}
onSwitchMode={this._switchMode}
onDelete={this._onDelete}
/>
);
};

_switchMode = evt => {
const mode = evt.target.id;
this.setState({
mode,
selectedId: null
});
_getEditHandleShape = ({ feature }) => {
return feature.properties.renderType === 'Point' ? 'circle' : 'rect';
};

render() {
const { viewport, mode, selectedId, features } = this.state;
const { viewport, selectedMode, selectedFeatureId, features } = this.state;
return (
<MapGL
{...viewport}
ref={_ => (this._mapRef = _)}
width="100%"
height="100%"
mapStyle="mapbox://styles/mapbox/light-v9"
mapStyle={MAP_STYLE}
onViewportChange={this._updateViewport}
>
<Editor
viewport={viewport}
eventManager={this._mapRef && this._mapRef._eventManager}
width="100%"
height="100%"
mode={mode}
clickRadius={12}
mode={selectedMode}
features={features}
selectedId={selectedId}
selectedFeatureId={selectedFeatureId}
onSelect={this._onSelect}
onUpdate={this._onUpdate}
getEditHandleShape={this._getEditHandleShape}
/>
{this._renderToolbox()}
{this._renderToolbar()}
</MapGL>
);
}
Expand Down
11 changes: 8 additions & 3 deletions examples/react-map-gl-draw/package.json
@@ -1,13 +1,16 @@
{
"scripts": {
"start": "webpack-dev-server --progress --hot --open",
"start-local": "webpack-dev-server --env.local --progress --hot --open"
"start-local": "webpack-dev-server --env.local --progress --hot --open",
"build-clean": "rm -rf ./dist && mkdir dist",
"build-static": "cp -r ./src/static dist/",
"build-script": "webpack -p --env.prod",
"build": "node scripts/validate-token.js && npm run build-clean && npm run build-static && npm run build-script"
},
"dependencies": {
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-map-gl": "^4.0.0",
"nebula.gl": "^0.12.0"
"react-map-gl": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
Expand All @@ -16,6 +19,8 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-loader": "^8.0.0",
"styled-components": "^4.2.0",
"url-loader": "^1.1.2",
"webpack": "^4.20.0",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.0"
Expand Down

0 comments on commit e055b55

Please sign in to comment.