Leaflet: Make a web map!

So. You want to make a web map. Don't worry; it's easy! This is an introduction to web maps using Leaflet. It was written by Andy Woodruff and Ryan Mullins for Maptime Boston and adapted by RK Aranas for FOSS4G-PH 2014. Let's begin!

What is Leaflet?

Leaflet is an open-source JavaScript library for interactive web maps. It's lightweight, simple, and flexible, and is probably the most popular open-source mapping library at the moment. Leaflet is developed by Vladimir Agafonkin (currently of MapBox) and other contributors.

What Leaflet does: "Slippy" maps with tiled base layers, panning and zooming, and feature layers that you supply. It handles various basic tasks like converting data to map layers and mouse interactions, and it's easy to extend with plugins. It will also work well across most types of devices. See Anatomy of a Web Map for an introduction to the most common kinds of web maps, which is what Leaflet is good for.

What Leaflet does not do: Provide any data for you! Leaflet is a framework for showing and interacting with map data, but it's up to you to provide that data, including a basemap. Leaflet is also not GIS, although it can be combined with tools like CartoDB for GIS-like capabilities. If you need total freedom of form, interaction, transitions, and map projections, consider working with something like D3.

How this tutorial works: It's structured around examples that progressively build upon one another, starting from scratch and ending with slightly advanced techniques. It assumes a basic knowledge of HTML and JavaScript, or at the very least assumes the will to tinker with the code to better understand what it does—and how to use it for your own work. It won't explain every little object or array, but will contain plenty of links. Many code blocks show only a snippet of code, highlighting the changes over previous examples.

BEFORE YOU START!

  1. You'll want a proper text editor. We recommend Sublime Text. Or Notepad. I'm joking. Please don't use Notepad.
  2. If you want to follow along on your own computer, your maps will need be on a local web server. Easy ways of doing this include running Python's SimpleHTTPServer in your map directory, installing MAMP (for Mac), or installing WampServer (for Windows).

This is a simple Leaflet map. A few lines of code.

Let's make a map!

The simple map above requires only a few things:

  1. An html page
  2. The Leaflet CSS styles
  3. The Leaflet JavaScript library
  4. A <div> element to hold the map
  5. A height style specified for the map div.
  6. A short script to create the map in that <div>


<html>
<head>
  <title>A Leaflet map!</title>
  <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css"/>
  <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
  <style>
    #map{ height: 100% }
  </style>
</head>
<body>

  <div id="map"></div>

  <script>

  // initialize the map
  var map = L.map('map').setView([14.654343736166062,  121.0664176940918], 16);

  // load a tile layer
  L.tileLayer('http://{s}.tiles.mapbox.com/v3/rukku.map-ynzkstd9/{z}/{x}/{y}.png',
    {
      attribution: 'Tiles by <a href="http://mapbox.com">DevelopmentSeed</a>, Data by <a href="http://openstreetmap.org">OpenStreetMap Contributors</a>',
      maxZoom: 17,
      minZoom: 9
    }).addTo(map);

  </script>
</body>
</html>
	

Easy! Now let's map our own data!

Sometimes base tiles are all you need, but usually your web map will show some specific data besides general reference features. Generally these data will be displayed as vector features, in contrast to the raster base map. Vector features come in three varieties:

UP Diliman just recently concluded its student council elections so we're going to make an election #precinctmap for this example. Below is a map of the election precincts for UP Diliman

Precincts everywhere! The data come from the official list of precincts. We have geocoded the places in the list and saved the locations as GeoJSON ahead of time using geojson.io. GeoJSON is the de facto standard data type for web maps, and Leaflet has built-in methods to make it easy to map GeoJSON data. For more about GeoJSON, start with its Wikipedia article.


<html>
<head>
  <title>A Leaflet map!</title>
  <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css"/>
  <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
   OOO <script src="jquery-2.1.1.min.js"></script> CCC 
  <style>
    #map{ height: 100% }
  </style>
</head>
<body>

  <div id="map"></div>

  <script>

  // initialize the map
  var map = L.map('map').setView([14.654343736166062,  121.0664176940918], 16);

  // load a tile layer
  L.tileLayer('http://{s}.tiles.mapbox.com/v3/rukku.map-ynzkstd9/{z}/{x}/{y}.png',
    {
      attribution: 'Tiles by <a href="http://mapc.org">MAPC</a>, Data by <a href="http://mass.gov/mgis">MassGIS</a>',
      maxZoom: 17,
      minZoom: 9
    }).addTo(map);
 OOO 
  // load GeoJSON from an external file
  $.getJSON("precincts.geojson",function(data){
    // add GeoJSON layer to the map once the file is loaded
    L.geoJson(data).addTo(map);
  });
 CCC 
  </script>
</body>
</html>
	

Okay, so what's new here? We've added a couple of files but only a few more lines of code for the #precinctmap.

  1. We have two additional files: jQuery, a super common and super useful JavaScript library, and our precinct GeoJSON file. If you're following along, download both and place them in the same directory as your HTML file.
  2. Near the top, we've loaded the jQuery script into the document. jQuery makes it easy to manipulate a web page by finding elements on the page, setting their styles and properties, handling interaction events, and more. Learn more at jquery.com. Right now we're going to use one of its helper methods to load our external GeoJSON file.
  3. After adding the base layer (we don't need the extra layer from the previous example), we use jQuery's getJSON() method to load the rodent file. We pass this method two things: 1) the path to the rodent file, which in this case is just the file name because it's in the same directory, and 2) a function that will run once the file has been loaded and parsed. The data argument in that function represents the JSON data that jQuery reads from our external file.
  4. Inside that function, we use L.geoJson() to create a vector layer from GeoJSON, passing it the same data, and again using addTo() to put the layer on the map.

Phew. There were some new pieces to understand here, but the code remains very simple. jQuery is one of several ways to load GeoJSON data. For other options, see this post and this one from Lyzi Diamond. We're only going to focus on GeoJSON in this tutorial, but check out Leaflet Omnivore by Tom MacWright for a plugin that makes it easy to load various data types. Need to convert your data to GeoJSON? Try some of these options: QGIS, OGRE, Shape Escape, mapshaper, geojson.io.

Add some style

Our precincts showed up as default blue markers in the map above. But, although the markers aren't ugly, defaults are rarely a good idea. Besides, this is a #precinctmap, so let's see some precincts!


$.getJSON("precincts.geojson",function(data){
   OOO var precinctIcon = L.icon({
    iconUrl: 'precinct.png',
    iconSize: [30,70]
  }); CCC 
  L.geoJson(data OOO ,{
    pointToLayer: function(feature,latlng){
	  return L.marker(latlng,{icon: precinctIcon});
    }
  } CCC ).addTo(map);
});

Leaflet is flexible and smart. As we saw in the previous example, it will draw maps just fine by default, but here we've specified some options to override a default. There are two main additions:

  1. We have used L.icon() to define the icon we're going to use for the rodent points. We have given it an object with a couple of options. Many options are available, but we just need two for now.
    • iconUrl is the path to the image file, in this case precinct.png (designed by Mapbox) which sits in the same directory as the HTML page.
    • iconSize is a two-number array of the pixel width and height of the icon. Leaflet needs to know the size in order to position the icon properly. This property could be used to scale the images, but here we are just using the actual pixel dimension of the PNG.
  2. In addition to the GeoJSON data, L.geoJson has been passed an options object. We have given it just one option, a pointToLayer function. When pointToLayer is defined, Leaflet uses it to determine how to convert a point feature into a map layer. pointToLayer always accepts two arguments: the GeoJSON feature, and a LatLng object representing its location. (We don't need to figure these out; Leaflet will automatically pass them to the function.) pointToLayer needs to return some kind of Leaflet layer. We'll cover several layer types later, but right now we're using a Marker, which is also what the default blue symbols are. Our function returns L.marker() to create a new Marker, which is passed:
    • The latlng that was sent to pointToLayer behind the scenes. This is the location of the point feature.
    • An options object with icon defined as the precinctIcon object we created in the previous step.

Interaction

The true power of web maps is in interaction, and not just panning and zooming. Let's make the #precinctmap a bit more useful by adding popups showing the code for each precinct. Click on the precinct icons below to see that information.


pointToLayer: function(feature,latlng){
   OOO var marker = L.marker(latlng,{icon: precinctIcon});
  marker.bindPopup(feature.properties.name);
  return marker; CCC 
}

Piece of cake. Before returning the Marker in pointToLayer, we just need to use the bindPopup() method to enable the popup on click. bindPopup() only needs to be given the content that is to appear in the popup. In this case, we pass it an HTML string: the Location and OPEN_DT properties from the GeoJSON feature, and a line break im between. Leaflet handles the interaction and everything else. Like most Leaflet objects, though, we could customize the popup if we wanted to.

Polygons

Great. We've got a functional #precinctmap now. Let's open the door to more advanced mapping techniques by moving beyond point data. Here's a map with UP Diliman building polygons underneath our precincts.

These polygons were loaded from another GeoJSON file with minimal effort. By default, Leaflet renders polygon and line data as SVG (Scalable Vector Graphics) paths, making interaction and styling easy. We'll get to that in a moment (that blue doesn't look great!), but first the simple code to load this layer.


 OOO $.getJSON("buildings.geojson",function(buildingData){
  L.geoJson( buildingData ).addTo(map);
}); CCC 

$.getJSON("precincts.geojson",function(data){
  var precinctIcon = L.icon({
    iconUrl: 'precinct.png',
    iconSize: [30,70]
  });
  L.geoJson(data,{
    pointToLayer: function(feature,latlng){
      var marker = L.marker(latlng,{icon: precinctIcon});
      marker.bindPopup(feature.properties.name);
      return marker;
    }
  }).addTo(map);
});

Polygon and line GeoJSON data is added in the same basic way as points. (In fact, all three could be loaded from a single GeoJSON file.) We just repeat the step from a few examples back, using neighborhoods.geojson this time.

Savvy coders at this point may notice that the two asynchronous $.getJSON requests are not guaranteed to finish in the order that the layers need to be stacked. That is, we don't want precincts drawing first, underneath the neighborhoods. Don't worry, and remember that Leaflet is clever. If you dig into your web inspector, you will find that the map consists of several panes. SVG paths, such as our buildings, will always be drawn in the overlay pane, which is beneath the marker pane into which things like our precinct markers were drawn. Thus it doesn't matter which of our two layers draws first; they will both be drawn in the right place. (If we had multiple polygon or point layers, however, we would need to be more careful.)

Thematic styles

Okay. Let's do something about that default blue. Let's make these polygons useful by turning them into a choropleth layer. The buildings GeoJSON file contains information about the building like its name and id.


$.getJSON("buildings.geojson",function(buildingData){
  L.geoJson( buildingData OOO , {
    style: function(feature){
      var fillColor,
          buildingtype = feature.properties.type;
      if ( buildingtype == "school") fillColor = "#006837";
      else fillColor = "black";  // no data
      return { color: "#999", weight: 1, fillColor: fillColor, fillOpacity: .6 };
    },
    onEachFeature: function( feature, layer ){
      layer.bindPopup( "<strong>" + feature.properties.type + "</strong>")
    }
  } CCC ).addTo(map);
});

At this point we begin to see the power of combining built-in Leaflet features with our own code and logic. Leaflet provides convenient methods of styling those polygons, but it's up to us to figure out what styles to use. The code above creates a simple data classification and assigns green to buildings that are classified as school buildings and black otherwise.

  1. Following the pattern of several previous steps, we now pass an options object to the neighborhoods GeoJSON layer. The two options we'll provide are the two functions described below.
  2. First, a style function. When this is defined, Leaflet uses it to determine what style to apply to each polygon based on the GeoJSON feature data. The function takes one argument, which is that feature. It needs to return an object with any path styles that we want to override defaults. The function here uses a series of if ... else statements to find where the neighborhood's density property falls within a pre-defined classification, and assigns a fill color accordingly. It then returns an object with that fill color and several other styles defined. When Leaflet adds features in this layer to the map, it will run each of them through this style function and apply the results.
  3. onEachFeature is a more general-purpose function that Leaflet will invoke for each feature as it is added to the map. It takes two arguments: the GeoJSON feature, and the actual map layer (the polygon, in this case). Here we use it to bind a popup to each polygon, much like what we did for the precincts layer. However, for the precincts we could do this in the pointToLayer function, which we were already using to make custom markers. pointToLayer only applies to point features and thus is not available to this polygon layer. onEachFeature is used instead for popups (among other things).
  4. Combining style with onEachFeature accomplishes something similar to what pointToLayer did for the precincts. style provides some instructions for how to turn the GeoJSON feature into a map layer, and onEachFeature provides some instructions for what to do with that layer.

Plug in and thin out

The beauty of Leaflet being open source—and of the particular way it's written—is that it's functionality and features can be extended and customized to your heart's content. There's a whole slew of plugins people have written to extend the Leaflet core, most of them very easy to drop right into your project. We'll use a popular one, Leaflet.markercluster. A map cluttered with precinct symbols isn't great, and this plugin will help make it more readable and usable. Poke around on the map below.


<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css"/>
 OOO <link rel="stylesheet" href="MarkerCluster.css"/> CCC 
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
 OOO <script src="leaflet.markercluster.js"></script> CCC 
<script src="jquery-2.1.1.min.js"></script>

...

<script>

...

 OOO var precincts =  CCC L.geoJson(data,{
  pointToLayer: function(feature,latlng){
    var marker = L.marker(latlng,{icon: precinctIcon});
    marker.bindPopup(feature.properties.name);
    return marker;
  }
});
 OOO var clusters = L.markerClusterGroup();
clusters.addLayer(precincts);
map.addLayer(clusters); CCC 

...

</script>

Look at all that added functionality from just a few lines of code! The plugin takes care of figuring out clusters, displaying them, and breaking them apart as you zoom in. (This is just the simplest implementation; there are many options you can explore.)

  1. The first thing we have to do is add the CSS and JavaScript sources for the Marker Cluster library, available from the dist folder of the plugin repository.
  2. In the code, we assign that GeoJSON layer to a precincts variable instead of immediately adding it to the map. Its options remain the same, but we need to save it to a variable in order to use it later.
  3. Next we create a marker cluster layer with L.markerClusterGroup(), a function that is part of the plugin, and assign it to a clusters variable.
  4. The cluster layer is a group to which we can add individual markers which will then be clustered. Conveniently, we can also just add the entire precincts layer (which is a collection of markers) at once.
  5. Finally, we add the cluster layer to the map, and magically our precincts are organized into smaller precinct clusters.

Go forth