Leaflet Draw: Implementing Custom Tools

As software developers, I think we can get caught up in the power and elegance of our own creations and fail to consider the importance of explaining their inner workings in a way that is understandable to those who were not intimately involved in their creation. Another way of saying this: teaching is hard. We don’t always know what we know. This has been on my mind this week as I’ve found myself struggling to learn a number of very useful but not-very-well-documented tools by a popular web cartography startup.

Over the past few months, I have been working on an interesting project: building a Leaflet-based wikimap of herding routes in eastern Senegal for use by academics, NGOs, and government officials examining land use conflicts between farmers and herders in the area. Because the users will not be computer experts, I am particularly concerned about not making skill-based assumptions and am trying to carefully think through the interface and how to make it as simple and novice-friendly as possible, while also providing a powerful suite of analysis tools.

A screenshot of my latest wikimap project, showing my custom tools interface.
A screenshot of my latest wikimap project, showing my custom tools interface.

Leaflet Draw tools

Some of these are measurement tools. The best out-of-the-box tools for drawing vectors and measuring lengths and areas on a Leaflet map are included in the Leaflet Draw library. This library has become the standard drawing plug-in for Leaflet, used for such apps as geojson.io, and for good reason: it’s lightweight, elegant, and functionally versatile. Unfortunately for me, all of the documentation in the README is geared toward using the toolbar that is integrated into the library (left). What to do if I need to design my own toolbar? While developers at Mapbox and CartoDB are super good at reverse-engineering and editing tools for their own needs, I’m still at a API-reading and Google-for-examples skill level. Plus I didn’t really want to modify the library itself to fit my needs; I just wanted to make use of its internal structure in a way that didn’t require the vertical toolbar.

There IS a way to do this, but it took me several hours to figure out, mainly because tapping into the library’s innards isn’t covered in the API.  I’ll paste the code I came up with below, then walk through it. First, for measuring distances:

function measure(){
  var stopclick = false; //to prevent more than one click listener

  var polyline = new L.Draw.Polyline(map, {
    shapeOptions: {
      color: '#FFF'
    }
  });
  polyline.enable();

  //user affordance
  $("button[name=measure] span").html(messages.beginmeasure);
	
  //listeners active during drawing
  function measuremove(){
    $("button[name=measure] span").html(messages.distance + polyline._getMeasurementString());
  };
  function measurestart(){
    if (stopclick == false){
      stopclick = true;
      $("button[name=measure] span").html(messages.distance);
      map.on("mousemove", measuremove);
    };
  };
  function measurestop(){
    //reset button
    $("button[name=measure] span").html(messages.measure);
    //remove listeners
    map.off("click", measurestart);
    map.off("mousemove", measuremove);
    map.off("draw:drawstop", measurestop);
  };

  map.on("click", measurestart);
  map.on("draw:drawstop", measurestop);
}

First, I should point out that the measure() function is called by the onclick attribute of the “Measure distance” <button>. Immediately, I define a boolean variable, stopclick, which will be used to ensure that the following code won’t set duplicate event listeners, which can get messy. Then, to start drawing the measure line, I use:

var polyline = new L.Draw.Polyline(map, {
  shapeOptions: {
    color: '#FFF'
  }
});
polyline.enable();

That’s it. Just a new object instance and a call to the .enable() method (although it isn’t shown in the API) starts up the drawing tool. But it’s not really obvious to the user what to do next, and I also want to display the distance inside the interface button to make it extra easy to see. So the first thing to do is tell the user how to use the draw tool:

//user affordance
$("button[name=measure] span").html(messages.beginmeasure);

Here I’m referencing a separate object, messages, that holds every word of human language that appears anywhere in the interface. In this case, messages.beginmeasure holds the string "Click map to begin". I do this because the map must be bilingual, and storing my interface strings in a separate object with a copy of each in French (the official language of Senegal) and one in English will facilitate switching between the languages.

Next, I have three event listener handlers:

//listeners active during drawing
function measuremove(){
  $("button[name=measure] span").html(messages.distance + polyline._getMeasurementString());
};
function measurestart(){
  if (stopclick == false){
    stopclick = true;
    $("button[name=measure] span").html(messages.distance);
    map.on("mousemove", measuremove);
  };
};
function measurestop(){
  //reset button
  $("button[name=measure] span").html(messages.measure);
  //remove listeners
  map.off("click", measurestart);
  map.off("mousemove", measuremove);
  map.off("draw:drawstop", measurestop);
};

The first handler, measuremove(), simply updates the button contents with the measurement string constantly as the user moves their cursor across the map. Notice I had to use an internal function, _getMeasurementString() to get at this info, which is unfortunate. The library really should include a simple getMeasurement() method available and published in the API. But it doesn’t. Oh well.

The next handler, measurestart(), makes sure that we’re not trying to pull measurement data before the user starts drawing, because that gets messy and starts throwing errors in the console. I have to use a click listener on the map to trigger this, but only want to trigger it once instead of each time the user clicks on the map. Hence stopclick. The handler only executes its code if stopclick hasn’t been altered from false to true yet, and within that code it sets stopclick to true. It’s going to change the contents of the button to say "Distance: " and apply a mousemove listener with the measuremove() handler discussed above.

Finally, we have the measurestop handler. This is going to reset the button contents to the original "Measure distance" string, then remove the three event listeners added within the measure() function so we don’t place any duplicate listeners.

Then finally:

map.on("click", measurestart);
map.on("draw:drawstop", measurestop);

The listeners above should be fairly self-explanatory: when to start measuring (when the user clicks on the map) and when to stop. Ah! With the second listener, we finally get to use something that’s actually specified in the API: the "draw:drawstop" map event. Well, okay, the initial Polyline options are listed in the API too. But not a lot else.

I got all done making this linear measure go, then decided: why stop there? Wouldn’t it be nice for my users to be able to measure areas too? Like, the area of a farmer’s field or the size of a village, perhaps? So I did one for area too:

function measureArea(){
  var stopclick = false; //to prevent more than one click listener

  var polygon = new L.Draw.Polygon(map, {
    showArea: true,
    allowIntersection: false,
    shapeOptions: {
      color: '#FFF'
    }
  });
  polygon.enable();

  //user affordance
  $("button[name=measureArea] span").html(messages.beginmeasure);
	
  //listeners active during drawing
  function measuremove(){
    $("button[name=measureArea] span").html(messages.area + polygon._getMeasurementString());
  };
  function measurestart(){
    if (stopclick == false){
      stopclick = true;
      $("button[name=measureArea] span").html(messages.area);
      map.on("mousemove", measuremove);
    };
  };
  function measurestop(){
    //reset button
    $("button[name=measureArea] span").html(messages.measureArea);
    //remove listeners
    map.off("click", measurestart);
    map.off("mousemove", measuremove);
    map.off("draw:drawstop", measurestop);
  };

  map.on("click", measurestart);
  map.on("draw:drawstop", measurestop);
};

This one’s very similar to the first one, except for the options and a few different messages. One important thing with the options that, again, the API doesn’t tell you is that if you want the Polygon tool to show the area measurement, you have to set allowIntersection to false. That took digging through the source code to figure out.

I hope my multiple hours of trial and error can help prevent the same headache for someone else. And, ideally, I hope these holes in the Leaflet Draw documentation get filled. It really is a powerful set of tools. Happy playing!

Measure Area
Leaflet Draw custom area measurement tool

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s