Leaflet and Google Maps

I’ve recently been developing an application that uses Leaflet to interactivity with a geographic map. One of the business requirements was to use Google Maps as a basemap, since it is pervasively used by our customers. A naive implementation used Leaflet’s tileLayer to render the tiles directly:

var map = L.map('map').setView([-29, 133], 4);
L.tileLayer('https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m3!1e0!2sm!3i349018013!3m9!2sen-US!3sUS!5e18!12m1!1e47!12m3!1e37!2m1!1ssmartmaps!4e0').addTo(map);

This approach, while simple, does not conform to the Google Maps API Terms of Service. Section 10.1.a indicates that:

No access to APIs or Content except through the Service. You will not access the Maps API(s) or the Content except through the Service. For example, you must not access map tiles or imagery through interfaces or channels (including undocumented Google interfaces) other than the Maps API(s).

To conform with the Terms of Service, I initially tried using leaflet-plugins, which creates a layer using the Google Maps Javascript API. However, when panning the map, the Google Map tiles lag compared to vector layers (e.g. polylines, polygons). This is a known, but unresolved issue and occurs because the Google Map setCenter method is asynchronous and thus will get out of sync with the Leaflet controlled elements.

I next used a wrapper around leaflet-plugins. The wrapper (1) hides the leaflet-plugins Google layer and (2) finds the right tile image in the DOM (rendered by leaflet-plugins) and renders it when it becomes available. Here is a simplified example:

var GoogleGridLayer = L.GridLayer.extend({
// googleLayer is a leaflet-plugins Google object
initialize: function(googleLayer) {
this.googleLayer = googleLayer;
},
onAdd: function(map) {
L.GridLayer.prototype.onAdd.call(this, map);
map.addLayer(this.googleLayer);
// Hide the leaflet-plugins layer
$(this.googleLayer._container).css("visibility", "hidden");
},
onRemove: function(map) {
L.GridLayer.prototype.onRemove.call(this, map);
map.removeLayer(this.googleLayer);
$(map._container).find("#" + this.googleLayer._container.id).remove();
},
createTile: function(coords, done) {
var img = L.DomUtil.create("img");
var googleLayer = this.googleLayer;
var interval = setInterval(function() {
var src;
var id = "#" + googleLayer._container.id;
var googleImg = $(id + " .gm-style img").filter(function(i, el) {
var src = $(el).attr("src");
return src.indexOf("!1i" + coords.z + "!") > 0
&& src.indexOf("!2i" + coords.x + "!") > 0
&& src.indexOf("!3i" + coords.y + "!") > 0;
});
if (googleImg.length) {
googleImg = googleImg.first();
src = googleImg.attr("src");
}
if (src) {
clearInterval(interval);
img.src = src;
done(null, img);
}
});
return img;
}
});

This approach seems to work reasonably well and is as responsive as the original tile layer implementation.

Other enhancements I added later were:

  • Caching the image src rather than having to poll the DOM on every create tile request
  • Adding support for Satellite and Hybrid maps (the latter requires a div with two nested images – one for the Satellite tile and one for the labels/roads tile)
  • Adding a timeout to polling – there are certain tile requests by Leaflet that aren’t fulfilled by the Google tile layer. It may be that Leaflet tries to load tiles past the viewport
  • Adding attribution (including logo and copyright) by moving some of the .gm-style elements in front of the GoogleGridLayer. The Google Map stores a reference to these elements and appropriately changes their content.

One of the disadvantages at this stage is that there is no easy method to access Street View coverage tiles without simulating a drag of the Pegman. It would be nice if the Google Maps Javascript API supports a function to turn on/off the Street View coverage tiles.


Posted

in

,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *