A custom map made with TileMill

Maps can be a fantastic addition to your website or application. The coolest of which are slippy maps: ones you can scroll or zoom interactively, the most famous of which being Google Maps.

For the amateur or cost-conscious engineer, however, many of the common slippy maps (Google Maps, MapBox, etc.) can be expensive. What you're actually paying for is access to sophisticated features, rich data, and hosting/bandwidth that each of these service providers have painstakingly built.

But not all projects need these options: if you're building a small, low-traffic website that requires only a simple map, these enterprise-minded products may add value for which you cannot justify the cost.

But frugal and open-source saavy software engineers rejoice! There is an open source, virtually-free* solution for those who require a map with basic features.

(* There are storage and bandwidth costs based on your traffic and cloud-provider's pricing, albeit, usually minimal.)

What's involved

In this article, we'll cover the steps to setting up your own map.

In the end, we'll have:

  • A fully customized map with any visual features you want on it
  • A means of loading and scrolling this slippy map (like Google Maps)
  • A means of serving the map to our visitors at great speed and minimal cost.

To do this we'll need:

  • A Linux-based OS (this tutorial will use Ubuntu)
  • TileMill: An application for styling and generating map tiles.
  • LeafletJS: A JavaScript library for slippy maps.
  • CDN or other web-host which can store and serve images over HTTP to the web. (This tutorial will cover Amazon S3 and CloudFront.)

And the basic steps involve:

  1. Generating map tiles with TileMill
  2. Converting the tiles to images
  3. Setting up Leaflet to use your tiles
  4. Deploying your tiles to a CDN (optional)

So if you're ready to roll-up your sleeves, and dig in, let's go!

How slippy maps work

A typical, static map is just an image; nothing fancy there. Slippy maps aren't too dissimilar, but have a special requirement that the user be able to pan and zoom to various levels of detail. If we only had one image in our map, it would need to be massive to accommodate detail when zoomed in, or otherwise remain blurry at high-power settings.

The slippy map solution is to break a map into a set of tiles, images of smaller sections of a map, which can be pieced back together on a grid to form the whole map. There are a different set of tiles for each zoom-level. When zoomed out, you require fewer tiles to complete the map, but with each higher-power zoom, you require exponentially more tiles to complete the map.

For the user's purpose, we only need the tiles that fit on the screen for that particular zoom-level. Now we have two options of serving them these tiles:

  1. We pre-generate all of the tiles and store them somewhere, which requires a lot of storage space.
  2. We generate map tiles on-the-fly as the user requests them. If the user requests a tile we've already generated, we serve back a stored copy.

Option #2 has a lot of drawbacks: although it saves on unnecessary storage space, it requires a server dedicated to processing tiles on-demand, can be slow under load, and very expensive. It's typically reserved for users with advanced needs.

For the purpose of this article, we'll cover Option #1, since storage is cheap and plentiful, and it allows us to make the entire map static. This means no web-server, and option to scale massively, quickly, and cheaply by leveraging the power of cloud-based CDNs.

Generating slippy maps

First, we'll need to generate all of the tiles we need for our map.

  1. Download and install TileMill from MapBox, which we can use to design the style of our map.
  2. Launch the Studio and click New Project, give it a name, and click Add.

New project

Now we land on the Editor screen for MapBox. On the right-hand side, the editor panel for changing the styles of the map, and on the left are the map tiles with the applied style.

These tiles are static images, and have all of their visuals baked into them: this includes roads, names, geographic features, colors, and other aesthetics. These will chiefly compose the style of your map, so we should style them with all the visuals you require before generating the tiles.

Styling the map

To style them, we use a language called CartoCSS, which itself is not too different than CSS. The principle of selecting elements and applying styles is same, but includes the new concept of layers: data that describes geographic elements (like lakes, borders, etc.) We can select data within a layer and apply styles to further customize the look and feel of our map.

Let's try adding a style. The US doesn't have any state borders drawn on the map, so let's add them.

  1. In the bottom left corner, click the Layers icon, and click Add Layer. Add layer
  2. In the ID and Class fields, name them subnational_boundaries
  3. In the Datasource field, we'll need to provide it shape data that describes the boundaries themselves. For our example, MapBox provides a shape file: enter http://mapbox-geodata.s3.amazonaws.com/natural-earth-1.4.0/cultural/10m-admin-1-states-provinces-lines.zip
  4. Click Save to add the layer.
  5. Add the following to your style.mss file then save with Ctrl+S:
#subnational_boundaries[ADM0_A3='USA'] {
  line-color:#02A;
  line-dasharray:4,2;
  line-opacity:0.2;
  line-width:0.4;
  [zoom=5] { line-width:0.5; }
  [zoom=6] { line-width:0.6; }
  [zoom=7] { line-width:0.8; }
  [zoom=8] { line-width:1.0; }
  [zoom>8] { line-width:1.2; }
}

Map with states

You should now see that TileMill drew the state border for the US for you: pretty neat!

There are a lot of more advanced things you can do with CartoCSS to style your maps, especially if you have shape data to accompany it. Interested readers should check out the CartoCSS documentation or an example project for more inspiration, but for now, we press on.

Generating the map tiles

Now that we've styled our map how we like it, its time to export it into tiles.

Export screen

  1. Click Export then MBTiles option from the menu in the top-right.
  2. Here you can adjust your map's settings. The most important of which is Zoom. The larger the range of zoom, the more tiles and greater the map size.

    At maximum range, 0-22, it generates a whopping 23,456,218,699,093 tiles! It's recommended you stay above a zoom level of 9, unless you are generating tiles for a limited area of the entire map (like a specific city or region.)

  3. When ready, click Export, which will begin generating the tiles.
  4. When finished, Save the .mbtiles files to the place of your choosing.

Our tiles are now exported, but they are in MapBox format: unsuitable for the web. We'll need to convert them to images.

Converting the map tiles

The MBTiles file you've generated has all our imagery packed into it, but now we need to convert them to PNG. To do so, we can use a handy library called mbutil which is capable of converting MBTiles to PNG.

  1. Download and install mbutil:
    1. git clone git://github.com/mapbox/mbutil.git
    2. sudo python setup.py install
    3. mb-util -h to verify it worked.
  2. Convert the tiles using the following command:
    mb-util --image_format=png DavidsMap.mbtiles DavidsMap
  3. It will generate a folder, in my case DavidsMap, which will contain all of your map tiles in PNG format.

Map tiles

Each numbered directory represents a zoom level of tiles. In my example, I generated 9 zoom levels, that in total are about 34MB in size. Not bad!

Setting up Leaflet with your map

We have our tiles, and now we're ready to add our slippy map to our website.

For testing purposes, we'll want the website to serve the tiles we just generated from the local file system instead of a remote CDN (that comes later.) If you are using a web server for your application, copy the tiles directory into the public directory such that they can be accessed by a http:// URL. For Rails applications, this is typically the public directory.

Now we're going to utilize LeafletJS, an awesome open-source library that will load and render our map tiles on our web page as the user pans and zooms.

Here's how to set it up:

<html>  
  <head>
    <title>David's Map</title>
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css">
    <script src="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>
    <style>
      #map {
        height: 100%;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      var map = L.map('map').setView([51.505, -0.09], 5);
      L.tileLayer('http://localhost:3000/DavidsMap/{z}/{x}/{y}.png', {
        maxZoom: 8
      }).addTo(map);
    </script>
  </body>
</html>  
  1. Add Leaflet to your document or application template by adding these to the <head> element:
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" /> <script src="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>
  2. Add a div to hold the map to your page, such as <div id="map"></div>
  3. Give the map a height and width using a style, e.g. #map { height: 100%; width: 100% }
  4. Then in a JavaScript file or <script> tag, add the following:
var map = L.map('map').setView([51.505, -0.09], 5);  
L.tileLayer('http://localhost:3000/DavidsMap/{z}/{x}/{y}.png', {  
  maxZoom: 8
}).addTo(map);

NOTE: The URL should be the path to your tiles as served by your web server. In my case, I was running WEBrick on port 3000, and the tiles directory was in public.

Leaflet map

And there you have it. Your very own, custom interactive slippy map. There are many more settings and features you can play with to make the map more useful, as outlined in the documentation.

But now it's time to get it out there on the web.

Deploying to a CDN

This next section is optional: it outlines a strategy for deploying your slippy map to AWS, so that your tiles may be served in a very fast, very cheap way. If you're an AWS customer, and this sounds like gravy to you, then read on.

The basic strategy

We need:

  1. A place to store our map tiles (Amazon S3)
  2. A way of serving files from this file store via HTTP (Amazon Cloudfront)
  3. Optional: A friendly DNS name for our map tile CDN (Amazon Route53 or your DNS provider)

To accomplish this, here's how:

  1. Create a new bucket in S3.
  2. Add a folder with a version number on it, e.g. v1. This is important because if you ever update your map tiles, you could end up with old, stale versions cached. The easiest way to avoid this is adding a version to the path.
  3. Inside your version folder, upload the contents of your map.
    Map bucket
  4. Then create a new CloudFront distribution for your S3 bucket.
  5. Optional: create a CNAME record in your DNS zone to point to the CloudFront distribution.
  6. Test that it works by visiting an image using the CloudFront DNS name. e.g. http://cdn.davidsmap.com/v1/0/0/0.png
  7. Update the tile layer URL in your Leaflet map to use the new CloudFront host.

And that's it: you now have your own custom slippy map that can scale easily and cheaply with your traffic.

References