Adding GeoJSON to Leaflet with Link Relations

Editor’s note: This post was updated in June 2014 to reflect changes in Leaflet and update examples to new versions. Shortly after publishing this post, I discovered a different way to add GeoJSON to a Leaflet map that I like a lot better. You can read a blog post about that here.


Wednesday night, I was fortunate to attend the August meeting of PDX OSGEO, Portland’s meetup for open source geo shenanigans. I learned some new stuff, got to see a 3D-printed map tile (!!!!!), and got a chance to show off some of my recent work. The bulk of my contribution to the meeting was showing off a new technique for adding GeoJSON to a Leaflet map. This is an overview of what I talked about, with more detail and background.

Leaflet is one of the best mapping libraries out there; it’s certainly my favorite! But one thing that’s somewhat convoluted is adding and using GeoJSON layers inside your Leaflet map. The Leaflet API does have a constructor for adding a GeoJSON layer, which is great! But when you have a GeoJSON file with hundreds or thousands of features, it makes most sense to house that data in an external file. The challenge then comes when trying to use or reference that file when constructing your map.

Let’s look at an example. (This is the same example I used at the meeting.) A few months ago, my friend Liza tweeted at me:

And so, with all of my infinite free time*, I set off to construct a GeoJSON file of places to buy cupcakes in Portland. (Disclaimer: It’s not comprehensive and it’s not at all done, so expect it to change significantly and frequently.) But when I set off to create my map, I ran into the problem of referencing this file. How was I to assign the data to a variable such that I could use it in the L.geoJson constructor and add it as a layer to my map?

Typically when I have an issue like this, I turn to the best tech support at my disposal: Twitter.

The folks at Leaflet agreed that this would be the best way to go:

One of my Twitter friends, Sean Gillies, had another suggestion:

I decided to play with this a little more, and it totally worked! And it worked well! So let’s talk about it.

GeoJSON: The Challenges

JSON and GeoJSON files are just JavaScript files. (JSON stands for JavaScript Object Notation; you can read more about JSON as a file type in a previous post, here.) As such, you can reference a JSON or GeoJSON file as a script, the same way you would for, say, including the Leaflet or jQuery libraries in your code.

<script src="./cupcakes.json"></script>

Adding a link to a script allows you access to its content; it’s as if you copy-pasted the code in the script into your HTML file. Everything that is in that file is now effectively inside your HTML file and can be referenced and used freely. Adding the GeoJSON at the top does the same thing, but the GeoJSON data has no name. It’s not attached to a variable. It’s just features that can’t be referenced, containing geometry and properties that can’t be referenced. For the purposes of our map, it is essentially useless.

The obvious solution is to add a variable definition to our cupcakes.json file. That would mean that instead of this:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          -122.65335738658904,
          45.512083676585156
        ]
      },
      "properties": {
          "name": "Hungry Heart Cupcakes",
          "address": "1212 SE Hawthorne Boulevard",
          "website": "http://www.hungryheartcupcakes.com",
          "gluten free": "no",
          "open1": "Monday - Sunday, 11am - 9pm"
      }
    },

...

we would have something like this:

var cupcakes = {
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          -122.65335738658904,
          45.512083676585156
        ]
      },
      "properties": {
          "name": "Hungry Heart Cupcakes",
          "address": "1212 SE Hawthorne Boulevard",
          "website": "http://www.hungryheartcupcakes.com",
          "gluten free": "no",
          "open1": "Monday - Sunday, 11am - 9pm"
      }
    },

...

This would totally work, but it would mean that our cupcakes.json file would no longer be a true JSON/GeoJSON file. If you tried to show the file in GitHub, for example, the points would not be interpreted and a map would not be rendered. Additionally, you don’t always have direct control over the GeoJSON file itself, especially if it’s a shared dataset or if it’s being returned from some other process. Adding a variable definition could also potentially mess up the work of anyone else trying to use your dataset, if it is indeed a dataset you constructed.

So what can you do? You can used a typed link/link relation and a little bit of jQuery. It’s really quite magical.

GeoJSON and Leaflet: The Solution

Let’s look at the code, and then talk through it. You can view a live version of this here.

<html>
<head>
  <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
  <style type="text/css">
  body {  padding: 0; margin: 0;  }
  html, body, #cupcake-map {  height: 100%;  }
  </style>
  <script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
  <script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>
  <link rel="points" type="application/json" href=".cupcakes/cupcakes.json">
</head>
<body>
  <div id="cupcake-map"></div>
  <script>
  var cupcakeTiles = L.tileLayer('http://a.tiles.mapbox.com/v3/lyzidiamond.map-ietb6srb/{z}/{x}/{y}.png', {
    maxZooom: 18  
  });

  $.getJSON($('link[rel="points"]').attr("href"), function(data) {
    var geojson = L.geoJson(data, {
      onEachFeature: function (feature, layer) {
        layer.bindPopup(feature.properties.name);
      }
    });
    var map = L.map('cupcake-map').fitBounds(geojson.getBounds());
    cupcakeTiles.addTo(map);
    geojson.addTo(map);
  });
  </script>
</body>
</html>

The Head

Leaflet, as discussed previously, is a JavaScript mapping library with a lot of great functionality. (For more info on getting started with and using Leaflet, check out their Quick Start Tutorial.) In order to use it, we need to include both a Leaflet CSS file (for map styling) and the Leaflet JavaScript file, which is what the stylesheet on line 3 and the script on line 8 are all about. Lines 4 through 7 are CSS rules to give our map a height and ensure that it is actually full-screen on the page.

<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
...
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>

The other library we need to make this work is called jQuery. jQuery is a JavaScript library that can make developing way easier, as it takes some of the complicated parts of the language and synthesizes them into easy-to-use pieces. Line 9 includes jQuery in our map, which allows us to use its functionality.

  <script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>

The interesting part comes at Line 10. This is where we are including our JSON** file using a link relation.

  <link rel="points" type="application/json" href=".cupcakes/cupcakes.json">

As defined on the WHATWG blog post to which I just linked, “Regular links simply point to another page. Link relations are a way to explain why you’re pointing to another page. They finish the sentence, ‘I’m pointing to this other page because…’”

Typcailly, link relations are used with a common set of keywords that have specific value to the browser. For example, our CSS stylesheet on line 3 has the link relation rel="stylesheet". This is the most common link relation, and every browser knows to download the data from the link before loading the page, as the relation tells it that the linked data contains important style information.

In our case, we are sort of hacking around that. We don’t necessarily want the browser to fetch the data on load or at any particular time. Indeed, we don’t want the browser to do anything with our GeoJSON file until we tell it to. Thus, we supply the link with a rel="points", which isn’t going to have any unintended consequences. (For more information on link relation keywords and what they do, check out this comprehensive list.)

The Body

Now let’s get into the body of our HTML. The first think you see on line 13 is an empty div with id “cupcake-map.” This is how Leaflet works: you create an empty div, and then write some JavaScript to fill it in. For us, this JavaScript starts on the very next line, as it’s a full-screen web map.

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

Our JavaScript is sandwiched between script tags, which lets the browser know that anything inside of them is JavaScript and should be treated as such. (This was mirrored in lines 4 and 7 in the head, with style tags.) The first thing we do in our script (line 15) is define a variable called cupcakeTiles, and use the Leaflet tileLayer constructor to define a map tile layer (more info on that at the link). This map uses custom tiles I made using MapBox, so this constructor links to those tiles. It also adds a property for maxZoom, ensuring that the map will not be zoomed in past zoom level 18. (For more information on web maps and zoom levels and how they work, check out this blog post.)

Line 19 is where the code gets interesting, as we employ jQuery’s getJSON method. This method takes three parameters: A URL of the location of the data, a plain object or string that gets sent with the request for the data, and a function to execute if the request for data is successful. The second parameter is optional, and you’ll see we don’t actually use it in our example.

 $.getJSON($('link[rel="points"]').attr("href"), function(data) {

We pass $('link[rel="points"]').attr("href") as our first parameter, which actually does translate to a URL. It finds a link by its rel value ($('link[rel="points"]')), and then uses a getter method attr to pull the value of the href parameter in the link. This returns "./cupcakes.json", which is the location of our JSON file. (I should also note that our link has an attribute type that is defined as 'application/json'. This helps the browser understand that the data in the link is JSON and should be treated as such.)

The second parameter is actually a function to be executed upon successful retrieval of data, called a callback function. (If that blows your mind, take some time to read about asynchronous programming. It’s really fascinating.) We actually have housed all the rest of the map creation and layer adding functionality of our map inside of this callback function, which ensures that the map won’t draw unless the GeoJSON has successfully been retrieved.

Our callback function takes one parameter, data, which represents the data that will be returned from the getJSON method. The first thing the function does is create a variable geojson and use the Leaflet geoJson constructor to define a GeoJSON layer. The constructor takes two parameters: one for the actual GeoJSON that will make the layer (data, in our case), and any options you wish to specify. One of these options is onEachFeature, which we use to attach popups to our GeoJSON As the value for onEachFeature, we define a function that takes our data and adds a popup to each feature that shows whatever value is in that feature’s name property.

$.getJSON($('link[rel="points"]').attr("href"), function(data) {
    var geojson = L.geoJson(data, {
      onEachFeature: function (feature, layer) {
        layer.bindPopup(feature.properties.name);
      }
    });
...

After we have defined our GeoJSON layer, we have to actually create our map. This is done on line 23, where we create a variable map and use the Leaflet map constructor to put our map in the "cupcake-map" div. We then also add a method fitBounds() and pass it the extent of our GeoJSON data, which sets the bounds of our map to match the bounds of our data.

In the last two lines, we add our cupcakeTiles layer to the map and we add our geojson layer to the map.

$.getJSON($('link[rel="points"]').attr("href"), function(data) {
  var geojson = L.geoJson(data, {
    onEachFeature: function (feature, layer) {
      layer.bindPopup(feature.properties.name);
    }
  });
  var map = L.map('cupcake-map').fitBounds(geojson.getBounds());
  cupcakeTiles.addTo(map);
  geojson.addTo(map);
});

We did it!

So what did we just do? We made a map with custom tiles that grabs GeoJSON from an external file without changing that file. Oh, and we added some popups to the GeoJSON features and set the bounds of our map to match the bounds of the data. Not too bad for 29 lines of code!

*This is a joke.
**JSON and GeoJSON are just JavaScript objects. A GeoJSON file can be saved as type .geojson, .json, or .js. All of them work.