visgl/react-map-gl

DrawControl

DeMol-EE opened this issue ยท 26 comments

Is there a way to add a mapbox-gl-draw control to <ReactMapGL>? I was able to get it to appear on the screen by using ref to get a reference to the react component, which allowed me to access the underlying mapbox with getMap(). On that reference was able to successfully call addControl and though it appears correctly, it does not seem to be functioning. Is there anyone who has done this before and can help me out? Even if it's not actually by linking mapbox-gl-draw, any way to allow the user to interactively draw polygons/linestrings in a way that emits events that can be subscribed to would solve my problem.
Thanks in advance!

rimig commented

Hey @Warkst , I have this working by doing exactly what you do and I also have .overlays { pointer-events:none } to pass the clicks down to mapbox canvas (I don't care about overlay interactions in my use-case) so the draw control can do it's thing.

Though interestingly enough it appears the new Mapbox v0.44.0 causes some weird issues with drag/pan, so use <0.44.0. Investigating that now, I think the real solution here will be a pure React draw component overlay that does the drawing.

Hey @rimig , thanks for sharing. Indeed, turning of pointer-events allows the controls to work properly! However, the shapes that are being drawn are rendered behind some other layers (added by deck.gl), so I'm afraid that the only real solution would indeed be a pure React component.

rimig commented

Yea, you could probably get it to work but you'd have to give up other functionality here and in deck.gl.

I agree about the need for a draw component, I'm currently working on such a thing.

@Warkst @rimig Hi guys, sorry to bother you. Do u have any solutions yet?

@akiori Yes and no... let me explain.

In my original question, I tried to keep the problem description minimal and simple, but my actual problem involved the particular combination of deck.gl as well. If your problem only uses react-map-gl, I think you should be able to craft a working solution. Consider adding a custom control to the map that toggles the app state to "user drawing". When pressing this button, you could add the draw control to the map and simultaneously disable the pointer events, allowing you to draw. In that case, you'll also want a way to toggle back to regular mode, hiding the draw controls and re-enabling the pointer events so the user can pan and zoom.

In my case, I re-evaluated the situation and came to the conclusion that I really only needed a subset of the DrawControl functionality. There was no particular need for multiple simultaneous shapes or lines to be drawn, nor to move or edit them, so I ended up crafting a custom solution by creating my own MapControls. You might find this and this useful if you're considering this. The gist of this solution lies in overriding handleEvent and having the entire app switch between modes, like described above.

If you want some example code, let me know.

rimig commented

@akiori - I do something pretty similar to what @Warkst is describing. I have a simple react component which is a <canvas> overlay on top of the map. I have different "modes" already builtin to my app so I just added something like DrawPolygon, etc. modes as well to track the drawing state (level up from the actual draw component).

For drawing the features passed in as props: It takes the geojson featureCollection as a prop, uses this.context.viewport.project(lat,lng) to convert points to x,y positions on the viewport, then draws them with d3-geo.

For user drawing: It handles the onClick and onMove mouse events on the <canvas>, uses the this.context.viewport.unproject(x,y) function to get the lat/lng, builds a geojson feature, then uses the same draw function as above.

This repo has some good examples for drawing on an overlay like this:
https://github.com/uber/react-map-gl/tree/master/examples/additional-overlays

@Warkst thanks for your reply. My situation is almost the same with yours. Can I have your example code? I'm really a code newbie at present :)

Hi @Warkst , sorry to bother you again. Can you show me some example code?

@akiori Sorry to keep you waiting for so long. I've attached some code from my project regarding the custom map controls. I did not create a minimal runnable example, but I've added the two most important files you need to figure things out.

The file "TILESMapControls.js" contains the custom map controls. This is pretty straight forward. It overrides "handleEvent". The behavior depends on the value of a property, "mode". The custom controls class also has a method "setMode" to update the behavior of the map controls. The default behavior ("else" case) is to use the default map controls ("super.handleEvent") - which means panning when dragging, zooming when scrolling, etc.

Then there is "Map2.js", which contains a React.Component class. It manages the ReactMapGL instance. In its state, "type" tracks the current mode the app is in. The default mode in my example is "CLEAR". In the constructor, create an instance of the custom map controls. In the "render" method, update these custom map controls by setting its mode to the current app mode and return JFX for ReactMapGL. Additionally, add a layer showing the line if the user is drawing one. Important here, if you want to use GeoJsonLayer, is that you use a WebMercatorViewport (the default), or otherwise the viewport you are using for ReactMapGL, to "unproject" the x,y coordinates on the screen to lng, lat coordinates on the world.
The most important thing here is that you don't forget to add the line "mapControls = {this.mapcontrols}", which tells the ReactMapGL component to use your custom map controls instead of the default.

I also added a button which sets the mode to DRAWINGCROSSSECTION. After clicking this, the callbacks onPanStart, onPanMove and onPanEnd will start being called by the custom map controls. In my example, onPanStart adds the start coordinates and end coordinates to the state and onPanMove updates the end coordinates in the state. In onPanEnd, you can do something with the coordinates (call the backend, do a computation... whatever), but don't forget to reset the app mode to CLEAR to make sure the behavior is restored to default. If the user wants to draw a new line, he must first click the button again.

The attachments are of type "txt" because github does not accept attachments of type "js", but they are actually really js ;)

Hope this helps.

Map2.txt
TILESMapControls.txt

@Warkst Hi, your method works, but there is a small bug. I don't use panstart panmove panend to draw lines, i use click, mousemove, mimic the polygon-selection here http://geojson.io. However, I found that the click event is so slow (if putting a console.log in your TILESMapControl of click, you will see how slow it is) that will cause a problem like this (if cursor moves quickly):

bug

I click on one point, and move my mouse, there should be a dynamic moving line connecting the point and my cursor. however, if my mouse moves too soon after the click, the click hasn't response, so there is a dynamic line connecting my mouse and the previous point

Current Solution

I replace click with mousedown and it seems ok now :). but still it's weird for me why click is so slow

@akiori Good to hear you were able to make my code work. I too have the same behaviour you described. I will try changing click to mousedown and hope this also speeds it up in my project. Nice find!

@Warkst by the way, as you can see from http://geojson.io, when you draw a polygon, the vertexes of a polygon are highlighted using circle. currently i plan to implement them as geojson points. what's your opinion?

@akiori I've changed the event to mousedown as you suggested and indeed, it's much faster. Really nice! I think adding the points is a good idea. There are several ways to do this. You could use two separate layers, but you could also combine them into one large GeoJSON "FeatureCollection" containing a MultiPoint feature (the points) and a LineString feature (the edges of the polygon). Obviously you can also use Polygon instead of LineString if you want to color the area inside the polygon.

@Warkst Hi! Do you happen to have any news on that?
Thanks!

Yea, you could probably get it to work but you'd have to give up other functionality here and in deck.gl.

I agree about the need for a draw component, I'm currently working on such a thing.

Just stumbled on this - also found https://github.com/amaurymartiny/react-mapbox-gl-draw if anyone else is still looking for a solution.

csi0n commented

Just stumbled on this - also found https://github.com/amaurymartiny/react-mapbox-gl-draw if anyone else is still looking for a solution.

This package uses react-mapbox-gl instead of react-map-gl

@csi0n - Woops my bad, read that project readme too fast. If that's the case I'm going to need to build this for a project I'm working on next week. Will post the package info when done.

@contra Let me know if you were able to get around and do this. We should add a link to it from the README.

FWIW - I realize that this is a much more elaborate solution with significantly bigger bundle sizes etc, but just want to make a small plug for nebula.gl. nebula provides a set of geospatial editing layers for deck.gl, and deck and nebula are a "companion frameworks" of react-map-gl, all designed to work seamlessly together.

@ibgreen Awesome, I hadn't seen that - will definitely use this instead of making something from scratch.

Uber's package is great and quick/easy to implement but just as a heads up...

If anyone else gets stuck here with adding the to react-map-gl

i can confirm a quick switch to react-mapbox-gl instead (they are two different packages), got me where i needed to be:

how to draw polygons with react-mapbox-gl rather than react-map-gl

Put the following packages in your package.json

 "@mapbox/mapbox-gl-draw": "^1.1.1",
    "mapbox-gl": "^0.53.1",
    "react-mapbox-gl": "3.9.1",
    "react-mapbox-gl-draw": "^1.0.6",

yarn install

  1. import the packages and declare your Map component:

import ReactMapboxGl from 'react-mapbox-gl';
import DrawControl from 'react-mapbox-gl-draw';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';

...
...

const Map = ReactMapboxGl({
  accessToken: "pk.eyJ1dgioiwngonoidgn3094673746843" <-- get a new api token for mapbox here: https://account.mapbox.com/access-tokens/
});

  1. create the map component in your container:
    <div>
        <Map
            style="mapbox://styles/mapbox/streets-v9" // eslint-disable-line
            containerStyle={{
            height: '100vh',
            width: '100vw'
            }}
        >
    </div>
  1. Within the pop the feature in:
        <Map
          style="mapbox://styles/mapbox/streets-v9" // eslint-disable-line
          containerStyle={{
            height: '100vh',
            width: '100vw'
          }}
        >

        <DrawControl
            position="top-left"
            onDrawCreate={this.onDrawCreate}
            onDrawUpdate={this.onDrawUpdate}
          />
        </Map>
  1. add the handlers to your class/function for the returned geojson info:
    ...
  onDrawCreate = ({ features }) => {
    console.log(features);
  };

  onDrawUpdate = ({ features }) => {
    console.log({ features });
  };

  render() {
    return (...
  1. voila...

image

Thanks @Aid19801. I also added a small note on the README of react-mapbox-gl-draw to make it clearer.

Hi! I am pretty new to react-map-gl myself. I was trying to render lines on the map. Tried A LOT of things but I was not able draw lines on the map. I then tried the solution provided by @Warkst and kind of went into the right direction after that.
I am now using CanvasOverlay on react-map-gl. I am defining simple canvas functions(ctx.lineTo) on redraw property of CanvasOverlay. I am manipulating the state of the map on mousedown function that is being called on MapGL. So the mousedown function on MapGL and redraw property on CanvasOverlay is working for me. I hope this helps.
My polygon looks something like this.
polygon

@meghna0593 can you add example of this redraw function?

I have released a react-map-gl-draw library, here is an example using it with react-map-gl.

Example code draw-polygon

Also here is the sandbox example

Hi @spiotrowska ,

Sorry for the late reply.

Since the project I am working on is in its first iteration, I have put a static implementation of the no.of sides for the polygon. That is, only 4 sided polygon can be created. The first mousedown event starts the drawing. Second mousedown creates one line, third creates two lines, fourth one joins the first point with the last point. It then colors the polygon.

In the redraw function of CanvasOverlay, I am drawing these line segments based on different mouse events. Also, I am projecting the coordinates on the map using project function. I have an if-else condition handling the mousedown states.

You can use the function like this->
In render function -
<CanvasOverlay redraw={this._redrawCanvasOverlay}/>

In _redrawCanvasOverlay function, something like this-> (Haven't included my actual code, this is the gist of it)

 _redrawCanvasOverlay=(opt)=>{
          const p1 = opt.project([drawLineStartY, drawLineStartX]); 
          opt.ctx.lineWidth = 10;  // thick lines
          opt.ctx.beginPath();
          opt.ctx.lineJoin = "miter";  // default
          opt.ctx.strokeStyle = "blue";
          opt.ctx.moveTo(p1[0], p1[1]);  //first point
          opt.ctx.lineTo(p2[0],p2[1]);  //line to second point
          //...and so on
          opt.ctx.stroke();
          opt.ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
          opt.ctx.fill();  //once you create a closed polygon
}

I haven't used the new release react-map-gl-draw. I'll probably give it a try soon.

Hope this helps.