Raster Maps

Viswesh Subramanian
JavaScript Store
Published in
6 min readNov 8, 2017

On December 16, 2010, Google announced Google Maps 5.0 with a blog post titled ‘The next generation of mobile maps’. They weren’t kidding! Google had rebuilt Maps with Vector graphics.

Today, Google Maps continues to be powered by the same tech they said is a game changer in 2010. In their own words –

Previously, Google Maps downloaded the map as sets of individual 256×256 pixel “image tiles.” Each pre-rendered image tile was downloaded with its own section of map imagery, roads, labels and other features baked right in. Google Maps would download each tile as you needed it and then stitch sets together to form the map you see. It takes more than 360 billion tiles to cover the whole world at 20 zoom levels!

Now, we use vector graphics to dynamically draw the map. Maps will download “vector tiles” that describe the underlying geometry of the map. You can think of them as the blueprints needed to draw a map, instead of static map images. Because you only need to download the blueprints, the amount of data needed to draw maps from vector tiles is drastically less than when downloading pre-rendered image tiles.

Image tiles weren’t designed or capable to scale. The fact that we rounded off interactions to the nearest zoom level and fetched image tiles with railroad, street, country, place names was constrained and not good enough. Time traveling to the past and assessing network results would reveal images fetched at various zoom levels –

In order to truly appreciate what Vector tiles can do for us, we need to understand image tiles completely. How about we recreate the past by drawing maps with ‘Image tiles’?

In the previous post; Data Visualization with Maps, we had seen how to create maps with GeoJSON. How about we overlay raster images to display more information. This exercise will not only help in supplementing GeoJSON/TopoJSON based high-level maps, it will also help in understanding the tech which served us a decade ago. Let’s go!

Like Natural Earth, Open Street Maps (OSM) is another data source for geospatial information. In addition to providing raw data, OSM also provides raster and vector tile servers. Since our objective is to consume raster images, our focus will be on raster tile services. Now, We could either set up our custom tile servers which would wrap OSM data or use their pre-built tile services. For the purposes of our demo, let’s use one of their tile servers — tile.openstreetmap.org

The final version which you will be building is at the Github Repo. To follow along, clone or download it.

Since we had already got our hands dirty with d3.js, it might be a good opportunity to switch gears and try another visualization library — leaflet, a lightweight library for building interactive maps. Leaflet offers a configuration based approach to create solutions with maps rather than an imperative style seen in d3.js. Straight out of the box, leaflet offers layers, interaction features, custom controls and also hooks to customize defaults.

Our action items:

– Consume TopoJSON to draw a map

– Overlay raster images to display more details

– Fill landmass colors correlating with population metrics.

Leaflet can be consumed with either a package manager or by including relevant JS & CSS references in the HTML. For brevity, let’s refer the hosted version –

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css">
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

Markup

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

CSS

html, body, #map {
height: 100%;
}

JS

First order of business is to initialize the map with Leaflet.

let map = L.map('map', {
minZoom: 2,
maxZoom: 10,
zoomControl: false
}).setView([0, 0], 3);

Now that the map is initialized and the map reference is retained with a variable “map”, adding layers is a breeze. In no particular order, add tile servers images as one layer and add TopoJSON layers as another layer. Why do we need to add 2 layers?. Well, the raster images form a layer to provide detailed imagery and the TopoJSON layer is used to appropriately color code countries based on population.

Select a tile server from the list of servers offered by OSM and add it to ‘map’ Leaflet map reference as a layer.

let tileLayer = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
noWrap: true,
maxZoom: 18,
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>',
id: 'mapbox.streets'
}).addTo(map);

You should see a map which fetches raster images at every zoom level.

Next step is to add our TopoJSON to the map as a layer. This would enable us to color code countries.

let xhr = new XMLHttpRequest();
xhr.open('GET', "src/data/50m.json");
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status === 200) {
let world = JSON.parse(xhr.responseText),
worldGeoJSON = topojson.feature(world, world.objects.countries).features,
geoJsonLayer = L.geoJson(worldGeoJSON);
}
}).addTo(map);
}
};
xhr.send();

You will now see 2 layers — a layer which displays raster images stitched together and another layer which displays country outlines read from TopoJSON.

But, wait a minute, what are those lines from east to west? It so happens that not all libraries understand the GeoJSON from ‘topojson-client’. This is a known issue for which Mike Bostock’s suggestion is to either a) Customize TopoJSON so that it plays nice with different libraries or b) Use GeoJSON.

Since GeoJSON is still the standards and the file size for our layout is not huge, using GeoJSON is a valid choice. If you are one of those people who cannot stand to change course, be my guest — feel free to use a TopoJSON adapted to spherical format.

let xhr = new XMLHttpRequest();
xhr.open('GET', "src/data/world.geo.json");
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status === 200) {
let world = JSON.parse(xhr.responseText),
geoJsonLayer = L.geoJson(world).addTo(map);
}
};
xhr.send();

We are in the final piece of the puzzle — color coding country outlines and adding interaction. As Leaflet offers hooks to custom style features, we return relevant colors correlating to the relative population density.

let geoJsonLayer = L.geoJson(world, {
style: function (feature) {
let country = feature.properties.name;
total = populationById[country] && populationById[country]["total"];

return {
fillColor: (total ? getColor(total) : getColor(0)),
fillOpacity: 0.8,
weight: 1,
color: 'grey'
};
},
onEachFeature: function (feature, layer) {
layer.on({
'mousemove': function (e) {
//Handle mousemove event
},
'mouseout': function (e) {
//Handle mouseout event
},
'click': function (e) {
//Handle click event
}
});
}
}).addTo(map);

Just like how we added custom styles, we can also add interactions by attaching listeners on the layer.

This, ladies and gentleman was the state of Maps back in 2010. Every zoom level initiated a network request to retrieve raster images and the resulting images were carefully stitched to provide meaningful map solutions. The strategy did not obviously see the next decade but it formed the foundations to build better maps.

References:
http://googleblog.blogspot.com/2010/12/next-generation-of-mobile-maps.html
http://googlemobile.blogspot.com/2010/12/under-hood-of-google-maps-50-for.html
http://leafletjs.com/examples/quick-start/

Originally published at javascriptstore.com on November 8, 2017.

--

--

Viswesh Subramanian
JavaScript Store

Full stack JS developer. Software generalist for the most part.