A Map, a Marker, a Cat: Mapping with Leaflet

2015-05-25 in js, leaflet, mapping

Maps are cool. Really, really cool. They're an intuitive way to view mass amounts of data in a quick manner, making it easy to tell where to find a thing and how to get to said thing. Maps have long existed in a variety of physical media, but with the advent of the internet came the interactive map. This article will teach you how to make your own map using JS, Leaflet, and the Cat API. This is a long post with both background and a tutorial; you may want to skip to the part where we build our Cat-powered map.

MapQuest, despite now having fallen to the wayside, popularized what is now called a Slippy Map: a map that moves around as you click and drag your cursor around. The modern Slippy Map lets you pan and zoom without having to use controls, and seemlessly transitions between locations without requiring a page refresh.

As a programmer, you can recognize that a massive amount of work is required in to both render maps and implement Slippy Map functionality in the browser. Luckily, as programmers, we have many options: prebuilt tile (what a map is made of) servers and free libraries to handle the bulk of the functionality. This frees us up to build new applications on top of base map functionality, saving great amounts of time.

Implementing Mapping Online

There's a wealth of options to choose from when building a new map-based application online, with the most well-known name being Google Maps. Google Maps offers a JS API for implementing map-based applications or displays, using the same technology that powers their flagship offering. Google packages both tile rendering and its JS library into one service.

While Google Maps is well-known, it has a few big disadvantages:

  • It's closed source, making fixing issues impossible and leaving you at the mercy of Google's developers
  • Google has awful support, referring users to Stack Overflow when issues arise
  • It's a proprietary solution that doesn't play well with non-Google techs and libraries

Luckily, there is a pair of technologies well-positioned to replace Google Maps: OpenStreetMap and Leaflet.

What is OpenStreetMap?

OpenStreetMap is the open map dataset that anyone can contribute to or consume. It's an online collection of topography, streets, addresses, and points of interest that can be queried or updated for free. OpenStreetMap also provides free tile rendering and an interactive browser-based tool to update data. OpenStreetMap provides all their data for free under the ODbL (for the data) and CC BY-SA licenses.

What is Leaflet?

Leaflet is a lightweight, open-source, and extensible JS library for building interactive maps. It's also very fast and has a clean, well-documented API, something that Google Maps is lacking in. Converting TurfCutter from Google Maps to Leaflet + Mapbox (a tile renderer using OpenStreetMap data) took very little development time and allowed me to bump the limit of points on the map from 5000 to 10,000, while still being faster than the original implementation. It also has a large community with many plugins available, making building an app that might have been mostly custom work using Google Map a matter of applying glue to the various plugins.

Your First Map

Our first project will be a simple one: make a map with markers representing cats, with popups showing cats, and the ability to add more markers (and thus more cats). You can see a demo of the completed project here - try clicking on the map and clicking on the markers to see what we'll be building. You can download the full source at the time of writing by viewing the repository. This tutorial assumes basic familirity with JavaScript and Bower.

The first step is to make a new folder and add these two files:

index.html
src/app.js

We'll be putting all our code in app.js for now. Next, run bower init . and follow the prompts to make a blank bower.json file. After that, run:

bower install --save leaflet
bower install --save "http://maps.stamen.com/js/tile.stamen.js?v1.3.0"

to install the libraries needed for this demo. Of note is tile.stamen - Stamen provides wonderful free map tiles with a generous use policy, making them a great resource for blog posts. Let's fill in the code for the index and app.js now:

app.js:
(function(){
  var map;

  function init() {
  // create a new map with no base layer
    map = new L.Map("map", {
      center: new L.LatLng(42.3964631, -71.1205171),
      zoom: 16
    });
    // use Stamen's 'terrain' base layer
    var layer = new L.StamenTileLayer("terrain");
    map.addLayer(layer);
  }

  document.addEventListener('DOMContentLoaded', init);
})();
index.html:
<html>
<head>
  <title>Leaflet Map</title>
</head>
<body>
  <div id="map"></div>
  <script src="/bower_components/leaflet/dist/leaflet.js"></script>
  <link rel="stylesheet" href="/bower_components/leaflet/dist/leaflet.css">
  <style type="text/css">
  #map {
    width: 1280px;
    height: 720px;
  }
  </style>
  <script src="/bower_components/tile.stamen/index.js"></script>
  <script src="src/app.js"></script>
</body>
</html>

The index file is very minimal - we load the required libraries and their stylesheets, load our script, and add a <div> to put the map in. We also add some minimal styling - the map has no size by default when loaded by Leaflet.

The entire app.js file is wrapped in a function to prevent leaking variables, with the code only being executed once the page is loaded. You can see that we don't use the common $(document).ready() structure here; while that's handy if jQuery is loaded, we wouldn't need it for anything else, and we don't particularly care about Internet Explorer < 8.

Let's go through the lines of the init() function.

map = new L.Map("map", {
  center: new L.LatLng(42.3964631, -71.1205171),
  zoom: 16
});

This creates a new Leaflet map object and centers it on what is arguably the best spot for a new map: the Davis Square busway in Somerville, MA.

Creating a new map object doesn't do much on its own. If you were to run it with just that code, you'd see only a grey rectangle where the map should be. This is because a new map in Leaflet doesn't load any tiles until you tell it what to load from. While there are many tile sets to choose from, we're using Stamen's:

var layer = new L.StamenTileLayer("terrain");
map.addLayer(layer);

The StamenTileLayer is added by the tile.stamen file as a Leaflet plugin. The leaflet plugin is very simple - it just tells Leaflet how to map standardized tile coordinates to Stamen tile URLs. Here, we create a new layer and add it to the map.

Leaflet groups features on the map in Layers. There are several types of layers: tile layers, markers, popups, image overlays, and vector layers. Layers can be grouped together into layer groups, which are themselves a type of layer. Layers are attached to the map at certain points, and dragging the map drags all layers attached to it. This is how roughly how tiles work - conceptually, as individual tiles added to a layer group that is then attached to the map canvas.

At this point we have a blank map with the Stamen tiles loaded in. While a great first step, it doesn't satisfy the urge for interactivity that we should be getting from an online map. Let's solve that problem by adding cats.

Adding Markers

It's often helpful to draw attention to a point on a map. In the physical world, this might involve pushing a pin into the map at the right location. With Leaflet, we add Markers. A marker is an image that has a specific location on the map. It's often the case that clicking on them will do something special: clicking a marker that's on top of a restaurant might give its reviews and address, for instance.

We're going to add a marker to represent a cat. When we click the marker, we want to know which cat it is and to see a picture of it. To accomplish this, we'll be using the Cat API to get random cat pictures. The entire code for the new feature is below:

app.js:
(function(){
  var map;
  var catUrl = 'http://thecatapi.com/api/images/get?format=src&type=png&size=small';
  var catImg = '<img src="' + catUrl + '">';

  function init() {
    // create a new map with no base layer
    map = new L.Map("map", {
      center: new L.LatLng(42.3964631, -71.1205171),
      zoom: 16
    });

    // use Stamen's 'terrain' base layer
    var layer = new L.StamenTileLayer("terrain");
    map.addLayer(layer);

    // add a marker at the center of the map
    addMarker([42.3964631, -71.1205171]);
  }

  /**
   * add a marker with a popup at the specified coordinates
   */
  function addMarker(coords) {
    var marker = L.marker(coords);
    marker.bindPopup('<h3>I\'m Fluffy</h3>' + catImg);
    marker.addTo(map);
  }

  document.addEventListener('DOMContentLoaded', init);
})();

Let's take a look into the lines of addMarker():

  • 24: creates a new marker from the coordinates passed in. In Leaflet, you can pass coordinates as an array, a plain object with 'lat' and 'lng', and as a Leaflet LatLng object.
  • 25: binds a Popup to the marker. When the marker is clicked, it will show the contents of whatever is passed in here - in this case, the name of the cat and a random cat picture.
  • 26: adds the marker to the map as a new layer, making it appear immediately

Running the app now will give a single marker in the center of the map, which, when clicked, will show a cat.

We could keep adding more cats manually, but that would be an issue for several reasons:

  • It's hard to come up with geocodes at random
  • Typing each cat's location manually takes time
  • Manual entry defeats the point of the internet: crowd-sourcing labor to build your business for you

With that in mind, let's add a new feature to Cat Map: clicking the map adds a new cat.

Adding Cats: A Leaflet Events Case Study

Side note: this section requires this Leaflet plugin to be added to the page. Simply download it and reference it after the main leaflet.js file. This plugin adds a distinct single-click event to Leaflet, since otherwise double-clicking triggers both a double-click event and two click events.

The final step of our project is to make it so that clicking the map adds new cat markers, each with a distinct name. We're going to accomplish this by taking advantage of a powerful core Leaflet feature: Events. Almost every visible object in Leaflet has a handful of events that it can emit; markers and maps have 'click' (for when the object is clicked), for instance, while tile layers have 'load' (for when the layer is done loading). Events work similarly to jQuery's events: they're bound with .on(), call the supplied callback with event data, and support unlimited numbers of event handlers. Without any further delay, here's the final code:

app.js:
(function(){
  var map;
  var nextMarkerNumber = 1;
  var catUrl = 'http://thecatapi.com/api/images/get?format=src&type=png&size=small';
  var catImg = '<img src="' + catUrl + '">';

  function init() {
    // create a new map with no base layer
    map = new L.Map("map", {
      center: new L.LatLng(42.3964631, -71.1205171),
      zoom: 16
    });

    // use Stamen's 'terrain' base layer
    var layer = new L.StamenTileLayer("terrain");
    map.addLayer(layer);

    // add a marker at the center of the map
    addMarker([42.3964631, -71.1205171]);

    // add a marker when the map is clicked
    map.on('singleclick', onClick);
  }

  /**
   * add a marker with a popup at the specified coordinates
   */
  function addMarker(coords) {
    var marker = L.marker(coords);
    marker.bindPopup('<h3>I\'m #' + nextMarkerNumber++ + '</h3>' + catImg);
    marker.addTo(map);
  }

  function onClick(e) {
    addMarker(e.latlng);
  }

  document.addEventListener('DOMContentLoaded', init);
})();

The new code consists of the function onClick() and lines 3 and 21. In line 21, we bind the singleclick event, emitted by the map, to onClick().

onClick() takes in one parameter: the event data created by Leaflet by the mouse click event. Amongh the properties of e are the coordinates of the click, in LatLng format. We use this to then add a new marker at that position.

Line 3 is a counter, used by the updated addMarker() function to give each cat a unique name. Unfortunately, names are finite, so the cats must make do with numbers (cat #19 is my favorite).

Epilogue

We now have a fully functioning map of cats with the ability to add more cats by clicking; see the demo if you want to see it in action, or the full source if you want to see the code as it evolved. While simple, this is the basis for many applications, such as OpenStreetMap's own editor, where map features can be changed right in the browser.

What happens if we have too many cats to effectively manage? How do we use pre-built Leaflet plugins to our advantage? Find out in the next part of this mapping series: Cat Herding (coming soon).