Crowded Google Maps

Maps are a very useful tool to display information, and Google Maps can be used to display different types of data in a unique solution, allowing you to appreciate them at a glance.

Some times ago, I was asked to build a complex map with a large number of addresses and the ability to allow users to interact with them.

In short, my job consisted of:

  • put many markers (around 2000) in the map
  • allow users to filter them according to some parameters (and updating the map in same time)
  • use different shapes and colors to represent each kind of parameters.

Basic Google Maps APIs allow to do all this, I only added a small library to aggregate the nearest marker icons in order to maintain the map legible.

I couldn’t use my original data for this article, so I retrieved from the web the list of the around 400 municipalities of Latium (Italy) with some geographical data to replicate the same process I did in my real work (demo data have been downloaded from Istat site).

Although the data are not the same ones used originally (and despite their number is lower), the end result is absolutely identical.

Requirements

First of all, you need the Google Maps Javascript API

You’ve simply to link them to your page adding your API Key as specified in the API docs:

All Maps API applications should load the Maps API using an API key. Using an API key enables you to monitor your application’s Maps API usage, and ensures that Google can contact you about your application if necessary. If your application’s Maps API usage exceeds the Usage Limits, you must load the Maps API using an API key in order to purchase additional quota.

Beside Google Maps, I’ve added to the page the Marker Clusterer Plus library.

Marker Clusterer is part of the Google Maps Utility Library, an “Open source project to be a central repository of utility libraries that can be used with the Google Maps API JavaScript v3”. We will use Marker Clusterer to aggregate markers icons on the map.

About the demo

To perform the demo, I’ve prepared a json file that contains 378 records of all the municipalities of Latium, Italy. In the real world, of course, the file would be produced dynamically from a database.

The json file has the following structure (each record represents a municipality):

[
{
“province_code”: “RM”,
“denomination”: “Roma”,
“chief_town”: 1,
“altitude_zone”: 5,
“mountain_zone”: “P”,
“area”: 1287.7586,
“population”: 2617175,
“lat”: 41.9027835,
“lng”: 12.496365500000024
}
]

I have used the `lat` and `lng` fields to put a marker in the map, the `altitude` and `mountain` zones as filtering keys, and all the rest to displayed as additional info on selecting single markers. In addition, the `chief_town` boolean field was used to change icon shape.

Building the interface

The next step is to build an interface (very minimalist in this demo) to allow users to interact with the map.

Each municipality is represented by a marker icon which color depends on its altitude or mountain zone. Users can choose the way markers are colored by the “Color markers according to” radio buttons.

Whenever a filter is added, or when a coloring method is chosen or even when the “aggregate markers” checkbox is selected, the markers are redrawn.
Changing the coloring method also updates the small legend at the bottom of the form.

Marker icons

The easiest way to manage icons shape and color is using SVG paths.
Google Maps Markers supports the display of vector paths using SVG path notation.

It could seem tricky, but all you need is to build your icon using Illustrator, Sketch or your preferred editor.

I’ve drawn my icons in Sketch, and then I’ve exported them in SVG.

Then, I’ve opened the SVG file with an editor. You can see how each icon consists in a unique `path` object with a `d` attribute: its value is the code to use for Markers vector paths.

I also need to use icons as image in my page (in this demo I’ve used the “asterisk marker” in the legend), so I changed icons to symbols and added the whole SVG code to my page, as you can see below (`d` values have been shortened for convenience):

<svg style=”display:none”>
<symbol id=”std_marker” viewBox=”0 0 18 30">
<path d=”M18,9.69 C18,16.98 …”></path>
</symbol>
<symbol id=”asterisk_marker” viewBox=”24 0 18 30">
<path d=”M33,0 C28.11,0 24,4 …”></path>
</symbol>
</svg>

Now I can use icons in my HTML code as SVG symbols and, in the same time, I’ll be able to grab the vector path with a few javascript.

Starting the engine

When the page is loaded, a set of little tasks is immediately performed:

  • Some variables are defined:
  • the altitude and mountain zones objects (which are used both to decode some json values and to build the content of the “filter by…” select fields in the form
  • a list of colors, in hex format, to be used for markers coloring
  • references to some element in the page that will be used later
  • a `buildLegend` function is instanced. It is used to build the legend every time users change the “Color marker according by…” value
  • an overlay `div` is added over the page. Its scope is to avoid access to the form and to add a message for users until data are loaded.

Now we can load our json file..

Loading data

There are some different options to load data, it depends, mainly, by the way you can access them.

If your data are located in the same domain of your script, the best way is to to perform an `ajax` request, but if you must call data from a different origin (as in my case) you must use CORS or JSONP.

CORS is a mechanism to enable client-side cross-origin requests and is a W3C Recommendation since January 2014; it is well supported by most modern browser but have some issues with legacy ones (see caniuse.com for more details). For more info, take a look at Using CORS article on HTML5 Rocks.
JSONP (JSON with padding) is an older technique that allows to bypass the same-origin policy. It is extremely easy and compatible with all browsers, and since this demo has no particular requirements, I decided to use it.

The idea on which JSONP is based is very simple. If you perform a `XMLHttpRequest`, you will get an object (typically XML or JSON) which will be passed to a callback function that will do all the work.
The problem starts when the request is addressed to an external domain: the same-domain policy, for security reasons, blocks all this kinds of request.

But we know that we can load a javascript file from any domain thru the `script` tag, and this is the way JSONP loads data.

Instead of calling a `XMLHttpRequest`, your script must perform a DOM injection and call a JS script which contains a call to a function (the callback) you have already instanced. The function pass an unique argument: the json data object, that, in this way, can bypass the same-domain restriction.

Here is what it happens:

First we perform the DOM injection:

var head = document.getElementsByTagName(“head”)[0],
script = document.createElement(“script”);

script.type = “text/javascript”;
script.src = “http://your.remote.domain/jsonp_file.js";
head.appendChild(script);

The JSONP file contains the callback function call:

my_callback([item1, item2, …, itemN]);

Note that in the real world the JSONP file would be build dynamically passing some parameters to the remote server (typically, a *callback* parameter is appended to the URL), but in this demo we don’t need any variable to be added.

As soon as the remote file is loaded, the callback function (that we have just instanced in our script) is invoked, just like in any typical AJAX call.

All this can become very tricky in situations where you need many JSONP calls or high control on performance and loading errors, but in cases like this, I think it is an extremely simple and practical solution.

Putting it all together

Let’s recap what’s going on: some variables and functions have been instanced, a “Loading data… please wait” message has been shown, some data has been called thru JSONP and, after them all have been downloaded, a callback has been invoked.

In the demo, the callback function is `mapData_callback`, and it will do almost all the work.

First, it set up the map: using the `Geocoder` function, a new map is initialized centered at Rome, Italy.

// this function receives the json data and performs all tasks
mapData_callback = function(map_data) {
// loading Google Maps
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { ‘address’: ‘Rome, Italy’}, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {

// do something

} else {
alert(“Can’t draw map: \n” + status);
}
}); // end geocoder.geocode
}; // end mapData_callback

If all goes well, and the map can be drawn (`google.maps.GeocoderStatus.OK`), we can begin doing all our stuff.
Again, first of all some variables are instanced:

  • `mapOptions`: some settings for the map, for a complete list of all available options take a look at the google.maps.MapOptions object specification
  • `mcOptions`: options for Marker Clusterer (we’ll cover them later)
  • `mc` and `markers`: default values for marker clusters and standard gmaps markers
  • `infoWindow`: a Google Maps InfoWindow object, it will be show on markers click
  • `standard_marker_shape` and `chief_towns_marker_shape`: vector shapes for markers icons, they are retrieved from the SVG symbols at the top of the page with the `getAttribute` method:
document.getElementById(‘std_marker’).querySelector(‘path’).getAttribute(‘d’)
  • `form_fields`: reference to all the fields in the form (note that I always use the term “form” to denote the set of fields used in the application, but there isn’t a real `form` element in the page)
  • `map_wrapper`: the map container

Finally, the `map` variable is instanced and the map is generated:

map = new google.maps.Map(map_wrapper, mapOptions)

Now, the map (without markers) is displayed, and we can remove the loading message. Next, we can start working on markers.

All markers tasks are managed thru the `addMarkers` function. This function is attached as listener to all form fields (which we have previously referenced to the `form_fields` variable). Every time their value changes, the `addMarkers` function is invoked.

//adding a listener to add fields in the form to make them call the addMarkers function on change
for( i = 0; i < form_fields.length; i++ ) {
form_fields[i].addEventListener(‘change’, addMarkers);
}
addMarkers(); // first run

Removing previous markers

Before adding markers, we must clear any previous one.
We can find two kind of markers: the standard Google Maps and the Marker Clusterer ones, since they have different ways to be deleted, we must remove markers in two ways.

Marker Clusterer, referenced to the `mc` variable, has a specific method to remove markers. if `mc` is not null (this means that Marker Clusterer has been activated) we can use the `removeMarkers` method to clear the map.

Google Maps standard markers are referenced to the `markers` array. To remove them we must set the map location of each marker to `null`.

Then we can reinitialize the `mc` and `markers` variables to their default values.

// marker clusterer removing
if(mc !== null) {
mc.removeMarkers(markers, false);
}
// Google Maps markers removing
for(i = 0; i<markers.length; i++){
markers[i].setMap(null);
}
// markers variables reset
markers = []; // Google Maps
mc = null; //markerClusterer

Filtering data

The `addMarkers` function also contains a data filtering task. Since this function is called on field values changes, we must check every time if a data filter is requested.

We can use the filter method to quickly select only the data we need.

filter() calls a provided callback function once for each element in an array, and constructs a new array of all the values for which callback returns a true value.

In this demo, only two fields requires this task: the mountain and altitude zone filtering, the `filter()` callback check their values and return a new `filtered_data` array.

In addition, the number of filtered elements is printed at the bottom of the form.

// data filtering
var filtered_data = map_data.filter(function (row) {
var test = [],
mountain_zone_filter_value = mountain_zone_select.options[mountain_zone_select.selectedIndex].value,
altitude_zone_filter_value = altitude_zone_select.options[altitude_zone_select.selectedIndex].value;
if(mountain_zone_filter_value) {
test.push( row.mountain_zone === mountain_zone_filter_value );
}
if(altitude_zone_filter_value) {
test.push( row.altitude_zone === Number(altitude_zone_filter_value) );
}
return test.every(function (item) {return item !== false;});
});
// found items message
document.getElementById(‘found_items’).innerHTML = filtered_data.length;

Finally, we can add markers.

Adding Markers

We have now to cycle the `filtered_data` array and add a marker (Google Maps ones) for each element.

This task is always required, even if the user selected the “aggregate markers” options: the Marker Clusterer add-on requires a set of Google Maps markers to work.

We must create a `google.maps.Marker` object for each item of the array. Each marker has this options:

  • position: its location in the map
  • title: specifies the `title` attribute of the marker
  • icon: icon object definition. It contains the `path` option (the vector shape of the icon, that we already defined thru our SVG icons) and some other display setting (fill, stroke, scale, etc…). For a complete reference see the google.maps.Symbol object specification

The marker is then linked to the map (in this case using the `setMap` method) and added to the `markers` array:

// markers
var this_item, this_marker,
// markers: color criterion chosen by user
colorBy_item = document.querySelector(‘.marker_color_button:checked’).value,
// conversion to array of parameter object
//Object.keys returns an array of strings even if keys are numbers
color_category_array = Object.keys(parameters[colorBy_item])
;
for(i = 0; i < filtered_data.length; i++) {
this_item = filtered_data[i];

this_marker = new google.maps.Marker({
position: new google.maps.LatLng(this_item.lat, this_item.lng),
title: this_item.label,
icon: {
path: this_item.chief_town? chief_towns_marker_shape : standard_marker_shape ,
fillColor: colors[color_category_array.indexOf(String(this_item[colorBy_item]))],
fillOpacity: 1,
strokeColor: ‘#000’,
strokeWeight: 1,
scale: 1, //18x30px
anchor: new google.maps.Point(9,30)
}
});
this_marker.setMap(map);
makeinfowindowCallback(this_marker, this_item);
markers.push(this_marker);
}

Each marker has an `infoWindow` to display some information when it is clicked (See Marker Clusterer documentation for more info). Since the infoWindow is built inside a loop, we need a callback function (`makeinfowindowCallback`) to let it work properly (take a look at the MDN closures page).

Now, we have to aggregate markers (if user chose this option).
We only need to call the Marker Clusterer constructor with three parameters:

  • the Map object (`map`)
  • an array with all the previously generated markers (`markers`)
  • an object with all MC properties (the previously defined `mcOptions`).

Some notable things about `mcOptions`:

  • `maxZoom`: the maximum zoom level at which clustering is enabled, in other words you can decide the zoom level within which the markers are aggregated.
  • `url`: the URL of the cluster icon image file. For this demo I’ve drawn a SVG icon, but you can use any image format.
// Marker Clusterer
if(document.getElementById(‘aggregate’).checked) {
mc = new MarkerClusterer(map, markers, mcOptions);
}

The complete documentation can be found at the MarkerClustererPlus for Google Maps V3 page.

Play it yourself

The script works on all modern browser including IE10+. You’ll need some fix to make it work with oldest IE. Play it yourself at http://codepen.io/massimo-cassandro/pen/XJGgvg/