Atomic Smash homepage splash

Build an interactive map with Leaflet JS

Words by Anthony HartnellSeptember 20, 2017

In this tutorial, I am going to demonstrate how to create a simple full-screen map using a javascript library called Leaflet JS and populate it with clickable icons using data from Open Data Bristol. I’ll explain how to gather and format the data so that it becomes interactive. Finally, I’ll explain how to customise the styling of the map tiles and ways the map can be improved.

What is Leaflet JS?

Leaflet JS as the name implies is a Javascript library for creating mobile-friendly interactive maps. It is open-source and has a tiny footprint of just 38KB which is able to include most of the mapping features you could ever need.

From their website Leaflet is:

…designed with simplicityperformance and usability in mind. It works efficiently across all major desktop and mobile platforms, can be extended with lots of plugins, has a beautiful, easy to use and well-documented API and a simple, readable source code that is a joy to contribute to.

I think this library is perfect for anyone who wants to learn how to build custom maps as it is generally simpler to understand than Google Maps Libraries and each bit of functionality such as plotting a marker on the map is achievable with around 1-2 lines of code!

Project Setup

To start the project off you’ll need to create the project structure you’ll be working with. To do this, create a folder called Leaflet Map and add three more folders inside called ‘css’, ‘js’ and images. Inside ‘css/’ create a file called style.css and map.js inside of ‘js/’. Also, create a file called index.html. Your structure should resemble this:

index.html
    css/
        style.css
    js/
        main.js
        markers.js
    images/

Open index.html in your code editor and enter the following code:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="x-ua-compatible" content="ie=edge">
        <title>Interactive Map - Leaflet JS</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

        <!-- Include the Leaflet CSS base stlyes -->
        <link rel="stylesheet" href="https://unpkg.com/leaflet@1.1.0/dist/leaflet.css" integrity="sha512-wcw6ts8Anuw10Mzh9Ytw4pylW8+NAD4ch3lqm9lzAsTxg0GFeJgoAtxuCLREZSC5lUXdVyo/7yfsqFjQ4S+aKw==" crossorigin=""/>

        <!-- Include the Leaflet JS Library -->
        <script src="https://unpkg.com/leaflet@1.1.0/dist/leaflet.js" integrity="sha512-mNqn2Wg7tSToJhvHcqfzLMU6J4mkOImSPTxVZAdo+lcPlk+GhZmYgACEe0x35K7YzW1zJ7XyJV/TT1MrdXvMcA==" crossorigin=""></script>

        <!-- Include our styles -->
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="map"></div>
        <script src="js/markers.js"></script>
        <script src="js/main.js"></script>
    </body>
</html>

In the head of the HTML document, I’m including the necessary leaflet JS and CSS files outlined in their quick start guide. I then have a div with an id of ‘map’ which I’ll attach the leaflet initialization code to. The map container needs to have a defined width and height so that it appears on the page. Add the following code to css/style.css:

html, body {
    padding: 0;
    margin: 0;
    bottom: 0;
    height: 100vh;
    overflow: hidden;
}

#map {
	position: absolute;
	bottom: 0;
	height: 100vh;
	width: 100vw;
}

Now let’s initialize the map by adding some options and set the map to show the center of Bristol. Maps rely on Latitude and Longitude coordinates (known as Lat/Lng). To get these for Bristol, visit http://www.latlong.net/ and enter Bristol UK into the Place Name.

Screenshot of lat/lng finder at latlong.net

Add the following code to js/main.js:

var mymap = L.map('map', {
	center: [51.454513, -2.587910],
	zoom: 16
});

Open up index.html in the browser and you’ll the zoom controls in the top left but the map is actually not there like this picture:

Map Initialized

Why can’t I see the map?

The reason you’re not seeing a recognisable map is that the map doesn’t have a tileset yet. Tilesets are essentially pictures of the world broken up into squares of different sizes. The size of the squares corresponds to the zoom level, increasing or decreasing in detail. Leaflet JS can utilise any tileset and Mapbox, an open source mapping platform for custom designed maps is a resource I’ll be using. Mapbox offers complete customisation and control of maps as well as hosting extensive APIs and Libraries for 3D mapping, Game development and more.

Example of Mapbox Tileset Customisation

Example of Mapbox Tileset Customisation

Sign up for an API Key

Leaflet requires you to create an API key to use the Mapbox tileset. This allows them to track the number of requests made to their resources per month. The current maximum is 50,000 requests per month, after which they offer a pay as you use model.

  1. Sign up to an account here
  2. Click API Access Tokens
  3. Click ‘Create a new token’
  4. Copy the generated key and edit js/main.js
  5. Enter your API key into the value for accessToken: ‘your.mapbox.access.token’
var mymap = L.map('map', {
	center: [51.454513, -2.587910],
	zoom: 15
});

L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
    attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
    maxZoom: 18,
    id: 'mapbox.streets',
    accessToken: 'your.mapbox.access.token'
}).addTo(mymap);

In the code above you see the capital L followed by .tileLayer; the L being the Leaflet class name. Save the main.js code and refresh the page you should see the map centralized on Bristol. You can now use the map as you would, zoom and pan etc. I find that the map appears more detailed especially around the buildings and if you remove or increase the maxZoom value you can see greater detail.

Leaflet JS Map of Bristol

Map of Central Bristol using the Mapbox tileset

Fetch and Prepare the Data

Maps work best when they have some amount of data that you can interact with and find more geographical information. Google Maps is an example that uses very extensive information such as their Directions and ‘Places’ Library.

The functions in the Google Places JavaScript library enable your application to search for places (defined in this API as establishments, geographic locations, or prominent points of interest) contained within a defined area, such as the bounds of a map, or around a fixed point.

Mapbox provide a number of their own Libraries for things like places, directions and real time integrations. This example doesn’t use any other libraries but they can be added to Leaflet following the instructions here.

The type of data that Leaflet requires

Leaflet can plot markers, polylines, popups, shapes, polygons, and circles onto the map providing you supply a Lat/Lng and some basic options. Here is how to add a circle and a marker to the map:

var circle = L.circle([51.45055,-2.59455], {
    color: '#9416b7',
    fillColor: '#9416b7',
    fillOpacity: 0.5,
    radius: 100
}).addTo(mymap);

var marker = L.marker([51.4508553,-2.593530]).addTo(mymap);

That should produce this on the map below:

Leaflet JS Marker and Circle

Leaflet JS Marker and Circle

 

If you want to plot multiple markers onto the map there is a better way than individually writing a new L.marker for each one. You can group all the marker information together into one big file in a format called geoJSON which is a format for encoding a variety of geographic data structures to include coordinates (Lat/Lng) and properties (description, title).

Aside from the coordinates, you can create unlimited properties and use them to affect certain functionality on the marker. For example, you could create a property called iconType and set its value to redMarker. Then in the code, you can automatically set the icon image based on the value of iconType.

An example of geoJSON

An example of geoJSON

 

Where to find geoJSON

There is an excellent resource called Open Data Bristol where you can search for freely available datasets for a huge amount of uses. In this example, I’m going to find all the Recycling Banks for Bristol from Bristol City Council. To do this, navigate to https://opendata.bristol.gov.uk, click Data, search for a ‘recycling’ and click on the card that appears. The Table tab displays all the rows of data available. Click on the Export tab and select geoJSON (Whole Dataset).

Plotting the data on the map

Open up recycling-banks.geojson in a text editor, select everything and then copy and paste into the project js/markers.js. You will need to assign the entire geoJSON file to a variable so it can be accessed in the main js file. It should start like with var markers =

var markers = {"type":"FeatureCollection","features"....

Now I’m going to remove the purple circle and marker that I created earlier and plot all the markers that exist in the markers variable. I need to build a function that loops over each marker and takes the coordinates values inside of the geometry object and plots each one on the map. I can be done simply with the following code:

L.geoJSON(markers, {
    // Options here
}).addTo(mymap);

Now if you save and load up index.html in the browser you should see all the plotted recycling banks on the map!

Bristol Open Data recycling banks plotted on the map

Bristol Open Data recycling banks plotted on the map

In the code above you can see I’ve written // Options here, well the options that can be passed to this function are specified in the docs for geoJSON at http://leafletjs.com/reference-1.2.0.html#geojson

I want to be able to bind a popup to each marker which appears when you click on it. By default, the popup is blank so it needs to be populated with some HTML content. The most obvious thing to do is pass get the site_name from inside the properties object in markers.js.  The option onEachFeature provides the functionality we need:

onEachFeature – Function that will be called once for each created Feature, after it has been created and styled. Useful for attaching events and popups to features

A ‘Feature’ is described on http://geojson.org/ as Geometric objects with additional properties.

onEachFeature requires a function that passes two arguments via a callback:

Image showing the specification for the onEachFeature method

Image showing the specification for the onEachFeature method

Adding the following code gives me full access to the marker data:

function queryMarker(feature, layer) {
	console.log(feature);
}

L.geoJSON(markers, {
    onEachFeature: queryMarker
}).addTo(mymap);

createMarkerContent Function

Add a Popup to each Marker

Finally, I’ll add the code to render some of the content into each marker popup. Inside the images folder, I’ve added two emoji png images:

Marker Popup Icons

Marker Popup Icons

The full code now looks like:

var mymap = L.map('map', {
	center: [51.454513, -2.587910],
	zoom: 15
});

L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
    attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
    // maxZoom: 18,
    id: 'mapbox.streets',
    accessToken: 'your.mapbox.access.token'
}).addTo(mymap);

/**
 * Process the stored value to return a custom image icon
 * @param  string value "Yes" or "No"
 * @return string       html image tag with alt text
 */
function getAvailabilityIcon( value ) {

	var icon = '';
	if( value === "Yes" ) {
		icon = '<img class="site__availability__icon" src="images/icon_yes.png" alt="Yes" />';
	} else if( value === "No" ) {
		icon = '<img class="site__availability__icon" src="images/icon_no.png" alt="No" />';
	}

	return icon;
}

/**
 * This function creates html content based on the properties inside each marker
 * feature.
 * 
 * @param  object feature a singular marker object passed via the geoJSON 
 * onEachFeature method
 * @return string html content for the bindPopup method
 */
function createPopupContent(feature) {

	// Initalise the container to hold the popup content
	var html = '<div class="popup__content">';

	// Get the site name
	if( feature.properties.hasOwnProperty('site_name')) {
		html += '<h2 class="site__name">' +feature.properties.site_name+ '</h2>';
	}

	// Get the full address
	if( feature.properties.hasOwnProperty('full_address')) {
		html += '<p class="site__address">' +feature.properties.full_address+ '</p>';
	}

	/*
     * Each recycling bank allows certain materials to be recycled, these are
     * indicated with a the property, such as 'Glass', and the value- either 'Yes'
     * or 'No'. Below I'll display some of these in a little list.
	 */
	html += '<div class="site__availability"><ul>';

	// Plastic Bottles
	if( feature.properties.hasOwnProperty('plastic_bottles')) {
		html += '<li><span>Plastic Bottles:</span>' + getAvailabilityIcon(feature.properties.plastic_bottles)+ '</li>';
	}

	// Batteries
	if( feature.properties.hasOwnProperty('batteries')) {
		html += '<li><span>Batteries:</span>' + getAvailabilityIcon(feature.properties.batteries) + '</li>';
	}

	// Paper
	if( feature.properties.hasOwnProperty('paper')) {
		html += '<li><span>Paper:</span>' + getAvailabilityIcon(feature.properties.paper) + '</li>';
	}

	// Cardboard
	if( feature.properties.hasOwnProperty('cardboard')) {
		html += '<li><span>Cardboard:</span>' + getAvailabilityIcon(feature.properties.cardboard) + '</li>';
	}

	// Shoes
	if( feature.properties.hasOwnProperty('shoes')) {
		html += '<li><span>Shoes:</span>' + getAvailabilityIcon(feature.properties.shoes) + '</li>';
	}

	html += '</ul></div>'; // End .site__availability

	html += '</div>'; // End .popup__content
	return html;
}

function queryMarker(feature, layer) {

	/*
     * Bind a new popup to each Feature and call a custom
     * function to create the popup content
	 */
	layer.bindPopup(createPopupContent(feature));
}

L.geoJSON(markers, {
    onEachFeature: queryMarker
}).addTo(mymap);

Here is the result now when clicking on one of the markers:

Marker Icon for Asda Bedminster Recycling Availability

Marker Icon for Asda Bedminster Recycling Availability

Styling the popup content

As you can see the styling of the popup content is messy and it’s not very clear what the icons relate to. I’ll fix that with some css:

/* Style the marker popup html */
.site__name {
    margin: 0 0 .8em 0;
}

.site__address {
    font-size: 1.3em;
}

.site__availability {
    max-width: 16em;
}

.site__availability ul {
    padding: 0;
    margin: 0 0 1.5em;
    list-style: none;
}

.site__availability ul li {
    margin-bottom: .8em;
}

/* Property name */
.site__availability ul li span {
    font-size: 1.4em;
    font-weight: bold;
    line-height: 1.5;
}

/* Property value (icon) */
.site__availability__icon {
    width: 2em;
    float: right;
}

The Final Result

Voila! The popup content looks much more readable now:

Map with CSS styling

Map with CSS styling

There are so many ways to this map can be improved, just think of any location-based app you may have. For example, this map could be extended to provide a filtering system or location based directions to the nearest recycling bank.

I hope this tutorial has been useful and please leave any comments or suggestions you may have below.

Profile picture of Anthony Hartnell

Anthony HartnellDeveloper

Anthony works in the development team and enjoys creating plugins and integrating maps, libraries and other APIs into Wordpress.

Go back to top

Keep up to date with Atomic news