Skip to content

Instantly share code, notes, and snippets.

@glenrobertson
Last active August 2, 2021 07:11
Show Gist options
  • Save glenrobertson/6203331 to your computer and use it in GitHub Desktop.
Save glenrobertson/6203331 to your computer and use it in GitHub Desktop.
Leaflet GeoJSON Tile Layer Example
<!DOCTYPE html>
<html>
<head>
<title>US States</title>
<link rel="stylesheet" href="//leafletjs.com/dist/leaflet.css" />
<!--[if lte IE 8]>
<link rel="stylesheet" href="/static/leaflet/leaflet.ie.css" />
<![endif]-->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//leafletjs.com/dist/leaflet.js"></script>
<script src="TileLayer.GeoJSON.js"></script>
<style type="text/css">
html, body, #map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script type="text/javascript">
var map = new L.Map('map');
map.setView(new L.LatLng(37.749, -122.424), 13);
var style = {
"clickable": true,
"color": "#00D",
"fillColor": "#00D",
"weight": 1.0,
"opacity": 0.3,
"fillOpacity": 0.2
};
var hoverStyle = {
"fillOpacity": 0.5
};
var geojsonURL = 'http://tile.openstreetmap.us/vectiles-highroad/{z}/{x}/{y}.json';
var geojsonTileLayer = new L.TileLayer.GeoJSON(geojsonURL, {
clipTiles: true,
unique: function (feature) {
return feature.id;
}
}, {
style: style,
onEachFeature: function (feature, layer) {
if (feature.properties) {
var popupString = '<div class="popup">';
for (var k in feature.properties) {
var v = feature.properties[k];
popupString += k + ': ' + v + '<br />';
}
popupString += '</div>';
layer.bindPopup(popupString);
}
if (!(layer instanceof L.Point)) {
layer.on('mouseover', function () {
layer.setStyle(hoverStyle);
});
layer.on('mouseout', function () {
layer.setStyle(style);
});
}
}
}
);
map.addLayer(geojsonTileLayer);
</script>
</body>
</html>
// Load data tiles from an AJAX data source
L.TileLayer.Ajax = L.TileLayer.extend({
_requests: [],
_addTile: function (tilePoint) {
var tile = { datum: null, processed: false };
this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;
this._loadTile(tile, tilePoint);
},
// XMLHttpRequest handler; closure over the XHR object, the layer, and the tile
_xhrHandler: function (req, layer, tile, tilePoint) {
return function () {
if (req.readyState !== 4) {
return;
}
var s = req.status;
if ((s >= 200 && s < 300) || s === 304) {
tile.datum = JSON.parse(req.responseText);
layer._tileLoaded(tile, tilePoint);
} else {
layer._tileLoaded(tile, tilePoint);
}
};
},
// Load the requested tile via AJAX
_loadTile: function (tile, tilePoint) {
this._adjustTilePoint(tilePoint);
var layer = this;
var req = new XMLHttpRequest();
this._requests.push(req);
req.onreadystatechange = this._xhrHandler(req, layer, tile, tilePoint);
req.open('GET', this.getTileUrl(tilePoint), true);
req.send();
},
_reset: function () {
L.TileLayer.prototype._reset.apply(this, arguments);
for (var i in this._requests) {
this._requests[i].abort();
}
this._requests = [];
},
_update: function () {
if (this._map._panTransition && this._map._panTransition._inProgress) { return; }
if (this._tilesToLoad < 0) { this._tilesToLoad = 0; }
L.TileLayer.prototype._update.apply(this, arguments);
}
});
L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
// Store each GeometryCollection's layer by key, if options.unique function is present
_keyLayers: {},
// Used to calculate svg path string for clip path elements
_clipPathRectangles: {},
initialize: function (url, options, geojsonOptions) {
L.TileLayer.Ajax.prototype.initialize.call(this, url, options);
this.geojsonLayer = new L.GeoJSON(null, geojsonOptions);
},
onAdd: function (map) {
this._map = map;
L.TileLayer.Ajax.prototype.onAdd.call(this, map);
map.addLayer(this.geojsonLayer);
},
onRemove: function (map) {
map.removeLayer(this.geojsonLayer);
L.TileLayer.Ajax.prototype.onRemove.call(this, map);
},
_reset: function () {
this.geojsonLayer.clearLayers();
this._keyLayers = {};
this._removeOldClipPaths();
L.TileLayer.Ajax.prototype._reset.apply(this, arguments);
},
// Remove clip path elements from other earlier zoom levels
_removeOldClipPaths: function () {
for (var clipPathId in this._clipPathRectangles) {
var clipPathZXY = clipPathId.split('_').slice(1);
var zoom = parseInt(clipPathZXY[0], 10);
if (zoom !== this._map.getZoom()) {
var rectangle = this._clipPathRectangles[clipPathId];
this._map.removeLayer(rectangle);
var clipPath = document.getElementById(clipPathId);
if (clipPath !== null) {
clipPath.parentNode.removeChild(clipPath);
}
delete this._clipPathRectangles[clipPathId];
}
}
},
// Recurse LayerGroups and call func() on L.Path layer instances
_recurseLayerUntilPath: function (func, layer) {
if (layer instanceof L.Path) {
func(layer);
}
else if (layer instanceof L.LayerGroup) {
// Recurse each child layer
layer.getLayers().forEach(this._recurseLayerUntilPath.bind(this, func), this);
}
},
_clipLayerToTileBoundary: function (layer, tilePoint) {
// Only perform SVG clipping if the browser is using SVG
if (!L.Path.SVG) { return; }
var svg = this._map._pathRoot;
// create the defs container if it doesn't exist
var defs = null;
if (svg.getElementsByTagName('defs').length === 0) {
defs = document.createElementNS(L.Path.SVG_NS, 'defs');
svg.insertBefore(defs, svg.firstChild);
}
else {
defs = svg.getElementsByTagName('defs')[0];
}
// Create the clipPath for the tile if it doesn't exist
var clipPathId = 'tileClipPath_' + tilePoint.z + '_' + tilePoint.x + '_' + tilePoint.y;
var clipPath = document.getElementById(clipPathId);
if (clipPath === null) {
clipPath = document.createElementNS(L.Path.SVG_NS, 'clipPath');
clipPath.id = clipPathId;
// Create a hidden L.Rectangle to represent the tile's area
var tileSize = this.options.tileSize,
nwPoint = tilePoint.multiplyBy(tileSize),
sePoint = nwPoint.add([tileSize, tileSize]),
nw = this._map.unproject(nwPoint),
se = this._map.unproject(sePoint);
this._clipPathRectangles[clipPathId] = new L.Rectangle(new L.LatLngBounds([nw, se]), {
opacity: 0,
fillOpacity: 0,
clickable: false,
noClip: true
});
this._map.addLayer(this._clipPathRectangles[clipPathId]);
// Add a clip path element to the SVG defs element
// With a path element that has the hidden rectangle's SVG path string
var path = document.createElementNS(L.Path.SVG_NS, 'path');
var pathString = this._clipPathRectangles[clipPathId].getPathString();
path.setAttribute('d', pathString);
clipPath.appendChild(path);
defs.appendChild(clipPath);
}
// Add the clip-path attribute to reference the id of the tile clipPath
this._recurseLayerUntilPath(function (pathLayer) {
pathLayer._container.setAttribute('clip-path', 'url(#' + clipPathId + ')');
}, layer);
},
// Add a geojson object from a tile to the GeoJSON layer
// * If the options.unique function is specified, merge geometries into GeometryCollections
// grouped by the key returned by options.unique(feature) for each GeoJSON feature
// * If options.clipTiles is set, and the browser is using SVG, perform SVG clipping on each
// tile's GeometryCollection
addTileData: function (geojson, tilePoint) {
var features = L.Util.isArray(geojson) ? geojson : geojson.features,
i, len, feature;
if (features) {
for (i = 0, len = features.length; i < len; i++) {
// Only add this if geometry or geometries are set and not null
feature = features[i];
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
this.addTileData(features[i], tilePoint);
}
}
return this;
}
var options = this.geojsonLayer.options;
if (options.filter && !options.filter(geojson)) { return; }
var parentLayer = this.geojsonLayer;
var incomingLayer = null;
if (this.options.unique && typeof(this.options.unique) === 'function') {
var key = this.options.unique(geojson);
// When creating the layer for a unique key,
// Force the geojson to be a geometry collection
if (!(key in this._keyLayers && geojson.geometry.type !== 'GeometryCollection')) {
geojson.geometry = {
type: 'GeometryCollection',
geometries: [geojson.geometry]
};
}
// Transform the geojson into a new Layer
try {
incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng);
}
// Ignore GeoJSON objects that could not be parsed
catch (e) {
return this;
}
// Add the incoming Layer to existing key's GeometryCollection
if (key in this._keyLayers) {
parentLayer = this._keyLayers[key];
parentLayer.feature.geometry.geometries.push(geojson.geometry);
}
// Convert the incoming GeoJSON feature into a new GeometryCollection layer
else {
incomingLayer.feature = L.GeoJSON.asFeature(geojson);
this._keyLayers[key] = incomingLayer;
}
}
// Add the incoming geojson feature to the L.GeoJSON Layer
else {
// Transform the geojson into a new layer
try {
incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng);
}
// Ignore GeoJSON objects that could not be parsed
catch (e) {
return this;
}
incomingLayer.feature = L.GeoJSON.asFeature(geojson);
}
incomingLayer.defaultOptions = incomingLayer.options;
this.geojsonLayer.resetStyle(incomingLayer);
if (options.onEachFeature) {
options.onEachFeature(geojson, incomingLayer);
}
parentLayer.addLayer(incomingLayer);
// If options.clipTiles is set and the browser is using SVG
// then clip the layer using SVG clipping
if (this.options.clipTiles) {
this._clipLayerToTileBoundary(incomingLayer, tilePoint);
}
return this;
},
_tileLoaded: function (tile, tilePoint) {
L.TileLayer.Ajax.prototype._tileLoaded.apply(this, arguments);
if (tile.datum === null) { return null; }
this.addTileData(tile.datum, tilePoint);
}
});
@divya1c
Copy link

divya1c commented Feb 17, 2016

Hi!

This is a great piece of work. I am new to mapping, and was wondering if there is a demo for this code? I'd like to evaluate how fast this is, for my needs.

Thanks :)

@jpsilvashy
Copy link

@divya1c what more could you want than what he posted, that literally is a full example. Thanks glenrobertson, this is awesome!

@programscode
Copy link

not work

@StanleyKebite
Copy link

Doesn't work for me as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment