Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: DrawControl #734

Closed
xintongxia opened this issue Feb 21, 2019 · 17 comments
Closed

RFC: DrawControl #734

xintongxia opened this issue Feb 21, 2019 · 17 comments
Assignees
Labels

Comments

@xintongxia
Copy link

xintongxia commented Feb 21, 2019

RFC: DrawControl

Background

react-map-gl currently does not support drawing functions. However, we have got a couple of users interested in this capability. Also it is one of P0 features on Kepler.gl 2019 roadmap.

Although 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 offers another geo editing library Nebula.gl, but it is an overkill while adding heavy dependencies such as deck.gl.

Proposal

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

Options

  • mode (String, Optional) - react-map-gl is stateless, user has complete control of the mode.

    • DrawControl.READ_ONLY - Not interactive. This is the default mode.
    • DrawControl.SELECT_FEATURE - Lets you select, delete, and drag features.
    • DrawControl.SELECT_VERTEX - Lets you select, delete, and drag vertices; and drag features.
    • DrawControl.DRAW_PATH - Lets you draw a LineString feature.
    • DrawControl.DRAW_POLYGON - Lets you draw a Polygon feature.
    • DrawControl.DRAW_POINT - Lets you draw a Point feature.
  • styles (Object, Optional) - A map of style objects passed to DOM elements. The following keys are supported.

    • point - SVG circle element.
    • path - SVG path element.
    • polygon - SVG path element.
    • vertex - SVG circle element
    • selectedVertex
    • selectedPoint
    • selectedPath
    • selectedPolygon
  • clickRadius (Number, Optional) - Radius to detect features around a clicked point. Default is 0.

  • features (Array, Optional) - A list of GeoJSON Point, LineString, or Polygon features.

  • selectedId (String, Optional) - id of the selected feature. DrawControl assigns a unique id to each feature which is stored in feature.properties.id.

  • onSelect (Function, Required) - callback when a feature is selected. Receives one argument 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

import React, { Component } from "react";
import ReactMapGL, {DrawControl} from "react-map-gl";

const MODES = [
  {name: 'Read Only', value: DrawControl.READ_ONLY},
  {name: 'Select FEATURE', value: DrawControl.SELECT_FEATURE},
  {name: 'Select Vertex', value: DrawControl.SELECT_VERTEX},
  {name: 'Draw Path', value: DrawControl.DRAW_PATH}, 
  {name: 'Draw Polygon', value: DrawControl.DRAW_POLYGON}, 
  {name: 'Draw Point', value: DrawControl.DRAW_POINT}
];

class Map extends Component {
  constructor(props) {
    super(props);
    this.state = {
      viewport: {
        width: 800,
        height: 600,
        longitude: -122.45,
        latitude: 37.78,
        zoom: 14
      },
      mode: DrawControl.READ_ONLY,
      features: [],
      selectedId: null
    }
  }
  
  _updateViewport = (viewport) => {
    this.setState({viewport});
  }
  
  _onSelect = (selectedId) => {
    this.setState({selectedId});
  }
  
  _onUpdate = (features) => {
    this.setState({
      features,
      selectedId: 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.value}>{mode.name}</option>)}
        </select>
      </div>
    );
  }
  
  _switchMode = (evt) => {
    const mode = evt.target.value;
    this.setState({mode});
  }

  render() {
    const {viewport, mode, selectedId, features} = this.state;
    return (
      <ReactMapGL {...viewport} onViewportChange={updateViewport}>
        <DrawControl
          mode={mode}
          features={features}
          styles={}
          selectedId={selectedId}
          onSelect={this._onSelect}
          onUpdate={this._onUpdate}
        />
        {this._renderControlPanel()} 
      </ReactMapGL>
    );
  }
}

Compare with mapbox-gl-draw

  • DrawControl is a stateless component. To manipulate the features, simply change the features prop. This is different from calling the class methods of MapboxDraw.
  • DrawControl 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 DrawControl: keyboard navigation, box select.
@Pessimistress
Copy link
Collaborator

@heshan0131 @rolyatmax

@macobo
Copy link
Contributor

macobo commented Feb 21, 2019

Nice! I think this could work, looking forward to playing with this.

Few questions since Pessimistress asked to provice feedback:

  • Will DRAW_* modes also allow dragging vertices around?
  • How does selecting vertices work? I don't see a special callback/argument that deals with what vertex is selected.
  • styles - are these purely overrides? Will react-map-gl provide default styles?
  • An interesting usability function is an undo button while drawing/editing. Could this be implemented by storing a history of states in onUpdate and using that to move back in history?

@rolyatmax
Copy link

rolyatmax commented Feb 21, 2019

This proposal is great! Thanks for putting this together - and especially for including a wonderful example!

A few initial thoughts/questions:

  1. The example has a geometries prop, while the proposal names a features prop. I'm guessing this is just a typo in the example?

  2. I wonder if users might also need custom styling for features the mouse is hovering over?

  3. To make sure I'm clear on the difference between a point and a vertex, I'm assuming a point refers to the GeoJSON feature, while a vertex is a coordinate in the geometry of a path or polygon GeoJSON feature? Does a point feature contain a single vertex?

  4. Will this component support drawing multi-polygons or polygons with holes?

  5. Users might want to apply custom styling to features that are currently being drawn, and in some common cases, these stylings might be relatively complex, taking into account the mouse position. For example, one common polygon-drawing treatment I've noticed is where, after selecting 2 vertices, the application shows a sort of "polygon preview" with the mouse position functioning as a temporary third vertex. I imagine this might be difficult to implement with the proposed API, especially if this preview is meant to be styled differently from other polygons on the map.

  6. Related to number 5, I imagine another common need would be to give different styles to features of the same type. For example, I might want to draw several geofences on a map and have each geofence be a different color.

  7. I'm curious to hear about how the delete functionality would work. Would this component contain a bit of UI (e.g. a "delete" button) for the user to remove a feature?

Thanks again for putting this together! Hope I didn't give too much feedback there. Looking forward to hearing your thoughts!

@Pessimistress
Copy link
Collaborator

Pessimistress commented Feb 22, 2019

styles - are these purely overrides?

My understanding of the plan is to provide both JS styling and classname-based styling. The JavaScript styles object will be merged with the default styles.

The example has a geometries prop

Corrected.

I'm assuming a point refers to the GeoJSON feature, while a vertex is a coordinate in the geometry of a path or polygon GeoJSON feature?

Yes.

@goldpbear
Copy link

goldpbear commented Feb 22, 2019

This is fantastic! Thanks for putting this together.

In terms of functionality I think it looks pretty comprehensive, though I'm also wondering about the ability to drag vertices once they're created.

I also wanted to double check my understanding of how geometry drawn on the proposed overlay would relate to other geometry being rendered by the map. In our use case, we want users to draw on the map and then persist their drawn geometry such that it can be rendered by the map once the user leaves draw mode. So, if I'm understanding correctly, a full draw workflow under the current proposal might look like this:

  1. User enters draw mode and creates and styles geometry to their liking
  2. User "saves" their styled geometry, at which point the GeoJSON features stored in the features state would need to be updated with whatever styling the user settled on
  3. The drawn GeoJSON with styling information would then get added as a source to the map's mapStyle prop
  4. Assuming the map also contains layers capable of rendering the new geometry, the user's drawn geometry would now exist alongside all other map geometry
  5. All state associated with the draw overlay would be discarded

Which I think would be a reasonable workflow-- I just wanted to make sure I understand where the responsibilities of the draw overlay end and the underlying map begin.

Thanks again for your work on this, and I'm looking forward to playing around with the overlay!

@xintongxia
Copy link
Author

  • Will DRAW_* modes also allow dragging vertices around?

I think it is not possible to support drawing and dragging at the same time, for example while drawing a polygon, when the user interacts with a vertex that is already drawn, DrawControl cannot decide the user's intention is to close the polygon or to drag the vertex.

  • How does selecting vertices work? I don't see a special callback/argument that deals with what vertex is selected.

I am thinking of changing DrawControl.SELECT_VERTEX to DrawControl.EDIT_VERTEX, as most of time when user interacts with an existing vertex, the user intends to drag the vertex.

  • An interesting usability function is an undo button while drawing/editing. Could this be implemented by storing a history of states in onUpdate and using that to move back in history?

We are not going to include this for the initial release. But I agree with you, this is an interesting feature, we probably will leverage it in future.

@Pessimistress
Copy link
Collaborator

@goldpbear Your workflow looks about right to me. The only thing I would like to bring attention to is that the proposed DrawControl renders an SVGOverlay, so there are these limitations:

  • While editing, the features will always be drawn on top of the map
  • You do need to maintain two sets of styles, one for SVG and one for the Mapbox layer once you insert the drawn features as a data source.

@xintongxia
Copy link
Author

  1. Will this component support drawing multi-polygons or polygons with holes?

We do not include multi-polygons or polygons with holes for the initial release. But I will investigate how to implement these features.

  1. I wonder if users might also need custom styling for features the mouse is hovering over?
  2. Users might want to apply custom styling to features that are currently being drawn, and in some common cases, these stylings might be relatively complex, taking into account the mouse position. For example, one common polygon-drawing treatment I've noticed is where, after selecting 2 vertices, the application shows a sort of "polygon preview" with the mouse position functioning as a temporary third vertex. I imagine this might be difficult to implement with the proposed API, especially if this preview is meant to be styled differently from other polygons on the map.
  3. Related to number 5, I imagine another common need would be to give different styles to features of the same type. For example, I might want to draw several geofences on a map and have each geofence be a different color.

I am thinking to change the styles API, allow user to pass in either a map of style objects as mentioned in this proposal, or a function which receives the following parameters.

styles({vertex, feature, selected, ...})

  1. I'm curious to hear about how the delete functionality would work. Would this component contain a bit of UI (e.g. a "delete" button) for the user to remove a feature?

Thanks for pointing out. I am going to remove onDelete callback from this proposal. Since there is already onSelect callback, the users could easily implement their own deleting logic.

@heshan0131
Copy link

LGTM, couple questions.

  1. mode: does it make sense to support draw rectangle? It is common to drag and draw a rectangle then select feature based on whether they intersect with it

  2. Agree that users might want to apply custom styling to features that are currently being drawn. But there seems to be no way to know which feature is currently being edited besides listening on theonUpdate call back.

  3. A path and a polygon is technically the same thing, people start by drawing path, once a path is closed it becomes a polygon. I wonder having 2 separate draw modes is necessary.

@xintongxia
Copy link
Author

  1. mode: does it make sense to support draw rectangle? It is common to drag and draw a rectangle then select feature based on whether they intersect with it

Agreed, will also support drawing rectangle in this DrawControl.

  1. Agree that users might want to apply custom styling to features that are currently being drawn. But there seems to be no way to know which feature is currently being edited besides listening on theonUpdate call back.

I am thinking, styles prop could be a callback function, which will receive the selected status of a feature, then user can dynamically decide styles.
In the DrawControl, only one feature could be selected and editable at one time.

  1. A path and a polygon is technically the same thing, people start by drawing path, once a path is closed it becomes a polygon. I wonder having 2 separate draw modes is necessary.

Yes, I think you are right. We don't really need differentiate DRAW_PATH and DRAW_POLYGON mode.

@mmaclach
Copy link

mmaclach commented Mar 1, 2019

It might still be useful to differentiate between polygon and path in order to close the polygon automatically when the user is done editing. It seems like it might be difficult for the user to add a vertex at the exact point to close the path, but maybe this is not an issue.

@ibgreen
Copy link
Contributor

ibgreen commented Mar 1, 2019

Adding some thoughts on the relation to nebula.gl and the fact that we are creating two features with overlapping functionality:

@georgios-uber @supersonicclay Could make sense for the nebula team to keep an eye on this RFC. It would for instance be nice if the API/functionality remains a reasonably clean subset of nebula.gl, facilitating a seamless upgrade path to nebula.gl for users who start with this feature once more advanced features become needed.

@xintongxia I think it would be neat if the docs of this react-map-gl feature linked to nebula.gl and gave some recommendations when to use which version:
https://github.com/uber/nebula.gl/blob/master/docs/overview.md

nebula.gl has already done the reciprocal doc update, I paste the current overview text in nebula.gl.


Overview

nebula.gl provides editable and interactive map overlay layers, built using the power of deck.gl.

Design Goals

nebula.gl aspires to be an ultra-performant, fully 3D-enabled GeoJSON editing system primarily focused on geospatial editing use cases.

  • Maximal rendering and editing performance, without need for complex application logic (such as splitting data into subgroups etc).
  • Target performance: Editing at 60fps (e.g. dragging sub objects) in GeoJSON payloads with 100K features (points, lines or polygons).
  • Handles GeoJSON corner cases, e.g. automatically changing object types from Polygon to MultiPolygon when addition polygons are added.
  • Fully 3D enabled (Can e.g. use WebGL z-buffer so that lines being rendered are properly occluded by other geometry).
  • Seamless integration with deck.gl and all geospatial deck.gl layers, allowing for GeoJSON editing to be interleaved with rich 3D visualizations.
  • Handle all aspects of event handling, including touch screen support.

Why nebula.gl?

You should strongly consider nebula.gl:

  • You want a full-featured, ultra-high-performance editing solution for GeoJson.
  • You are already using e.g. deck.gl or react-map-gl.

You may want to look at alternatives if:

  • If you have very simple editing requirements (just a simple polygon etc)

If nebula.gl is more than what you need (e.g. in terms of bundle size), and you may want to look at other solutions, e.g. the simple polygon editor overlay being developed in react-map-gl.

That said, if you are already using deck.gl the additional overhead of nebula.gl is small, and the seamless integration with deck.gl should be valuable.

@aptlin
Copy link

aptlin commented Mar 20, 2019

It might still be useful to differentiate between polygon and path in order to close the polygon automatically when the user is done editing. It seems like it might be difficult for the user to add a vertex at the exact point to close the path, but maybe this is not an issue.

This was the first impression I got from playing with the mapbox-gl-draw demo. Having the option to skip redundant clicking would be nice.

@ctaylor4874
Copy link
Collaborator

Is there any chance we can add a free-hand drawing mode? The workflow could be

  1. Disable map pan when in free-hand mode.
  2. Mouse down + drag to create a line, recording the location of the mouse at intervals such as 0.05 seconds.
  3. Append the line string with the value recorded.
  4. Release mouse to finish free-hand draw.

@ibgreen
Copy link
Contributor

ibgreen commented May 15, 2019

This module is now a submodule of nebula.gl. The nebula team have an extensible draw mode system and are implementing many draw modes that users can choose from.

The idea is that those editing modes will be made compatible not only with nebula's main module (the editable deck.gl layers) but also with this little React component.

For quickest response, recommend asking this and other related questions in the nebula.gl repo.

@supersonicclay
Copy link

We have a feature request for free-hand drawing in nebula.gl. It's here: uber/nebula.gl#172

@georgios-uber
Copy link

@xintongxia can you add this rfc to nebula repo and .md file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests