Country border highlighting with Leaflet.js

Not too long ago, I finished a project on Big Data for the University. One part of this vast project was implementing a dynamics map responsive to mouse hover and mouse click. When the mouse would hover over a country, the whole country borders would be highlighted in the map, and if the user clicked inside those borders, we would display tons of exciting data using D3.js.

For map displaying and operations on Google Style maps, look no further than Leaflet.js.

All examples I found on the Internet would either not provide proper callbacks or provide these features for the entire world map. Providing callbacks for a world map is extremely difficult in terms of performance optimization for the reasons explained below. For now, let’s briefly explain how it works.

Mapping Systems

Mapping representation systems, in general, are based on tiling. In a server somewhere in the Universe, many square tiles represent different parts of the globe in different zoom levels. These tiles have been created by companies dedicated to topography and map creation, usually with tools such as the ones provided by ArcGIS. A library like Leaflet (or Google Maps, or Apple) works by performing requests to this server, with X, Y, Z (zoom level) as arguments. The server responds with square images representing parts of the map requested, and it is up to the framework to place these tiles in place in a viewport. Many servers provide tiles for these purposes, called “Tile Map Services. “

Most map javascript web applications are tied into a specific service (like Google’s), but the advantage of Leaflet.js is that the tile URL is provided as an argument upon creating a layer to get tiles from tiles many services.

Creating a map with Leaflet

Leaflet works with layers. Inside a viewport, the leaflet applies one layer on top of each other. A layer for a Leaflet can be anything, from a map to an SVG element or a pin image. Usually, the first layer that is created is the layer that holds the map tiles. All other layers will come on top of it.

<style type="text/css">
#map {
	width: 80%;
	height: 500px;
    margin-left: auto;
    margin-right: auto;
}
</style>
<div id="map"></div>

mapBounds = L.latLngBounds(southWest, northEast);
var map = new L.Map(mapDivName,
	{
		center: [20, <code>0],
		zoom:</code> 3,
		attributionControl: false
	}).addLayer(
		new L.TileLayer("http://{s}.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png")
	).setMaxBounds(mapBounds);

The mapDivName is the name of the HTML element that we are going to create our map in. We make a map with the coordinates “20,0” (latitude, longitude) as the center, with the specified zoom level. Optionally, we can set the map bounds to something that we prefer to avoid infinitely scrolling left and right and seeing many versions of the same map (Leaflet has this little “problem,” although many would consider it a “feature”).

The first layer that we create is a tile layer, with tiles taken from Opencycle map. The {z},{x},{y} attributes are used to get tiles at specific coordinates and zoom level. Try copying the URL, paste it into your browser’s address field, and giving different numbers as arguments to those fields and see the results.

Up until now, you should see a map that works a bit like Google Maps.

Adding the country highlighting layer

We want to add now a country highlighter. When the mouse moves over a country, it should highlight the borders of the country (and the interior). The image below shows France highlighted.

For this, we must add a new type of layer on top of the tile layer, a GeoJSON layer, that will provide Leaflet with bounding polygons so that we can attach our callbacks to them.

For attaching such a layer, we should find a GeoJSOn file containing country borders. In many cases, such a GeoJSON file is not available. Fortunately, there a way to produce a GeoJSON file by taking a shapefile produced by an ESRI-format-compatible tool. Personally, I found a shapefile that contains all country borders here:

http://thematicmapping.org/downloads/world_borders.php

The next step is to convert its contents to GeoJSON format. The shapefiles are usually accompanied by other files which the same prefix but different suffixes. We need those files, as they are descriptors about the polygons contained in a shapefile. To convert all of these files into a single GeoJSON file, you can either use ogr2ogr in your local machine or an OGR2OGR web service such as this one. ogr2ogr is part of the enormous GDAL package containing utilities for working with shape polygons and maps.

You can choose whatever you want; however, be aware: The resulting GeoJSON file should be small enough to cope with the limited performance capabilities of Leaflet, Javascript, and Firefox. If you use the same file I linked in this post, it will cripple the performance of Leaflet in Firefox, as well as any browser on any mobile platform. For this reason, I personally prefer to use my local ogr2ogr utility and produce a lower-resolution file that better suits my needs. After we have installed GDAL (whose installation is out of the scope of this article), we can open a terminal and execute the following command:

ogr2ogr -f GeoJSON -lco COORDINATE_PRECISION=2 "path_to_output.json" "path_to_input.shp"

The COORDINATE_PRECISION argument is used to cut the precision of each polygon vertex in each country, resulting in a smaller JSON file and much-improved performance for browsers with limited capabilities.

Now we are ready to load the GeoJSON file as a layer. Personally, I used D3 to load the JSON asynchronously.

d3.json(Globals.resourceWithPath("ne-countries-50m.json"), function (json){
	function style(feature) {
		return {
			fillColor: "#E3E3E3",
			weight: 1,
			opacity: 0.4,
			color: 'white',
			fillOpacity: 0.3
		};
	}
	C.geojson = L.geoJson(json, {
		onEachFeature: onEachFeature,
		style : style
	}).addTo(map);

	function onEachFeature(feature, layer){
		layer.on({
			click : onCountryClick,
			mouseover : onCountryHighLight,
			mouseout : onCountryMouseOut
		});
	}

Note that the “C” prefix is a custom prefix defined by me. I keep the C.geojson layer created because I will need to access it from different parts of the code later.

We create the GeoJSON layer, and we pass as arguments a function to call when certain events happen in each polygon contained in the GeoJSON. We also pass as an argument the default style adopted by each polygon in the GeoJSON. This will apply this style to all polygons that are displayed on top of the map.

In the onEachFeature class, we define the attributes and the events supported by each of the polygons in this layer. I personally passed as arguments three functions, whose definitions are given below:

/**
 * Callback for mouse out of the country border. Will take care of the ui aspects, and will call
 * other callbacks after done.
 * @param e the event
 */
function onCountryMouseOut(e){
	C.geojson.resetStyle(e.target);
//	$("#countryHighlighted").text("No selection");

	var countryName = e.target.feature.properties.name;
	var countryCode = e.target.feature.properties.iso_a2;
//callback when mouse exits a country polygon goes here, for additional actions
}

/**
 * Callback for when a country is clicked. Will take care of the ui aspects, and it will call
 * other callbacks when done
 * @param e
 */
function onCountryClick(e){
//callback for clicking inside a polygon
}

/**
 * Callback for when a country is highlighted. Will take care of the ui aspects, and it will call
 * other callbacks after done.
 * @param e
 */
function onCountryHighLight(e){
	var layer = e.target;

	layer.setStyle({
		weight: 2,
		color: '#666',
		dashArray: '',
		fillOpacity: 0.7
	});

	if (!L.Browser.ie && !L.Browser.opera) {
		layer.bringToFront();
	}

	var countryName = e.target.feature.properties.name;
	var countryCode = e.target.feature.properties.iso_a2;
//callback when mouse enters a country polygon goes here, for additional actions
}

As you can see, we set a style and unset it depending on whether the mouse hovers over a country polygon. The code is pretty simple, and in case you need to provide additional callbacks, you can add them to the bottom of these three functions.

The first thing I should explain is the properties “iso_a2” and “name.” Those properties come from the GeoJSON file itself. I am attaching one country’s description from the file in JSON format:

{
  "type" : "Feature",
  "properties" : {
    "iso_a3" : "ABW",
    "name" : "Aruba",
    "iso_a2" : "AW",
    "iso_n3" : "533"
  },
  "geometry" : {
    "type" : "Polygon",
    "coordinates" : [
      [
        [
          -69.90000000000001,
          12.45
        ],
        [
          -69.90000000000001,
          12.42
        ],
        [
          -69.94,
          12.44
        ],
        [
          -70,
          12.5
        ],
        [
          -70.06999999999999,
          12.55
        ],
        [
          -70.05,
          12.6
        ],
        [
          -70.04000000000001,
          12.61
        ],
        [
          -69.97,
          12.57
        ],
        [
          -69.91,
          12.48
        ],
        [
          -69.90000000000001,
          12.45
        ]
      ]
    ]
  }
}

The geometry object is taken from the original shapefile itself. However, the other properties are handled by combining the original .shp file with the accompanying .dbf file. And that’s the reason why you should not get rid of these files yet. The names of the attributes can be acquired by examining these files with GDAL or by looking at the resulting GeoJSON file.

That’s it!

I’m not good at writing tutorials, but I wanted to share this with others. It’s not that the code itself is something strange or difficult. Still, other sources I have found on the internet either followed processes that apply only to a specific area on a map (thus not needing shape file normalization) or did not work well with Firefox (Firefox is slow with SVG layers, at least until today’s version).

Mapping in the digital world is a vast and challenging concept, and libraries such as Leaflet.js simplify using maps and some ideas surrounding them. I hope I helped some newbies avoid the holes that I fell into when I used Leaflet.

Comments are welcome.