Skip to content

Instantly share code, notes, and snippets.

@rclark
Last active March 18, 2020 03:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save rclark/6705915 to your computer and use it in GitHub Desktop.
Save rclark/6705915 to your computer and use it in GitHub Desktop.
Composite Leaflet Layer

L.Composite

A Leaflet layer that brings together an L.TileLayer and an L.GeoJSON layer.

The problem:

Making an interactive web map becomes difficult if your dataset is very large. Instead of using the browser to render data as vector features, we often instead pre-render images and display those pre-rendered images instead of the data itself. This allows for quick maps, and for pretty visualization, but no interaction.

When you load massive amounts of vector data into the browser, the page takes forever to load and then the performance is terrible.

How this tries to solve it

You generate a "composite" layer by specifying:

  • the URL template that connects to a tile set and will be used to build an L.TileLayer,
  • the URL for some GeoJSON,
  • options for the layers.

The L.TileLayer will show up immediately, giving your page a quick, pretty face. It will also make an AJAX request for the GeoJSON right away.

Once the GeoJSON is returned, JSTS is used to create a spatial index of the GeoJSON features. Now, as your cursor moves across the map, the index is searched and the features under your cursor are added to an L.GeoJSON layer on the map.

The L.GeoJSON layer never has more than a few features in it at a time, and so the page remains performant, but you can do things like hover effects, popups, etc.

It has a couple of dependencies

  • JSTS for building a spatial index and doing intersections
  • jQuery for making AJAX requests
  • Leaflet for mapping

This example...

Loads a pretty large polygon dataset from Github representing rock types across the state of Arizona. Once they're loaded, you'll see a "hover" event where the polygon under your cursor is highlighted.

You can also find the code at https://github.com/rclark/L.Composite

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.css" />
<!--[if lte IE 8]>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.ie.css" />
<![endif]-->
<style type="text/css">
html, body, #map {
height: 100%;
width: 100%;
margin: 0px;
}
.composite-data-layer path:hover {
stroke-opacity: 1;
stroke-width: 3px;
stroke: red;
}
</style>
<!--
L.Composite dependencies:
- [Leaflet](http://leafletjs.com)
- [JSTS](https://github.com/bjornharrtell/jsts)
- [jQuery](http://jquery.com)
-->
<script src="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.js"></script>
<script src="https://rawgithub.com/bjornharrtell/jsts/master/lib/javascript.util.js"></script>
<script src="https://rawgithub.com/bjornharrtell/jsts/master/lib/jsts.js"></script>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script src="L.Composite.js"></script>
</head>
<body>
<div id="map"></div>
<script>
var map = L.map("map", {
center: [34, -111],
zoom: 6
});
L.composite({
tileUrl: 'http://{s}.tiles.mapbox.com/v3/rclark.map-d4ubicoz/{z}/{x}/{y}.png',
geojsonUrl: 'https://api.github.com/repos/azgs/geologic-map-of-arizona/git/blobs/410a8abcc1fddd4142952d21e395b6f0bbd1259e'
}).addTo(map);
</script>
</body>
</html>
L.Composite = L.Class.extend({
includes: L.Mixin.Events,
initialize: function (options) {
// options should include:
// - tileUrl: A URL template for this layer's visible representation
// - tileOptions (optional): Any [config options for the L.TileLayer](http://leafletjs.com/reference.html#tilelayer-options)
// - geojsonUrl: A URL that retrieves GeoJSON data for this layer as a FeatureCollection,
// or a [Github blob API call](http://developer.github.com/v3/git/blobs/#get-a-blob)
// - geojsonOptions (optional): [Config options for the L.GeoJSON layer](http://leafletjs.com/reference.html#geojson-options)
L.Util.setOptions(this, options);
var tileUrl = options.tileUrl || '',
tileOptions = options.tileOptions || {},
geojsonUrl = options.geojsonUrl || '',
defaultStyle = { opacity: 0, fillOpacity: 0 },
geojsonOptions = options.geojsonOptions || {};
if (typeof geojsonOptions.style !== 'function') {
geojsonOptions.style = L.Util.extend(defaultStyle, geojsonOptions.style);
}
// Setup the tile and geojson layers
var geojsonLayer = this._geojsonLayer = L.geoJson(null, geojsonOptions);
this._tileLayer = L.tileLayer(tileUrl, tileOptions);
this._data = {type: 'FeatureCollection', features: []};
// When the data is loaded, parse it, triggering the index to build
this.on('dataLoaded', this.parseData, this);
this.once('dataRefreshed', function (event) {
// This is a hack so that you can use CSS on the GeoJSON layer's SVG elements
var layers = geojsonLayer.getLayers();
if (layers.length > 0) {
layers[0]._container.parentNode.classList.add('composite-data-layer');
}
});
// Make an AJAX request for some GeoJSON
if (geojsonUrl !== '' && $) {
$.ajax({
url: geojsonUrl,
dataType: 'json',
success: L.bind(this.dataRecieved, this)
});
}
},
dataRecieved: function (data, status, xhr) {
// Got data. Make sure its a valid GeoJSON FeatureCollection
var geojson = {type: "FeatureCollection", features: []};
// If the response is from a Github API request, decode the base64 content
if (data.hasOwnProperty('content') && data.url === this.options.geojsonUrl) {
var content = data.content.replace(/\s/g, '');
data = atob(content);
geojson = JSON.parse(data);
} else if (data.type === 'FeatureCollection') {
geojson = data;
}
// Signal that the data is ready
this.fire('dataLoaded', {data: geojson});
},
parseData: function (event) {
// Builds a spatial index from GeoJSON data
var reader = new jsts.io.GeoJSONReader(),
data = this._data = reader.read(event.data),
index = this._index = new jsts.index.strtree.STRtree();
data.features.forEach(function (feature) {
var envelope = feature.geometry.getEnvelopeInternal();
index.insert(envelope, feature);
});
// Indicate that the index is ready
this._indexReady = true;
this.fire('indexReady');
},
highlightFeature: function (event) {
// Adjust the contents of the L.GeoJSON layer. Expects a [Leaflet Mouse Event](http://leafletjs.com/reference.html#mouse-event)
var feature = this._intersect(event.latlng),
writer = new jsts.io.GeoJSONWriter(),
features = null;
function jstsToGeoJSON(jstsFeature) {
// Convert a jsts feature to GeoJSON
return L.Util.extend({}, jstsFeature, {
geometry: writer.write(jstsFeature.geometry)
});
}
if (feature.type === 'Feature') {
features = jstsToGeoJSON(feature);
} else if (feature.length > 0) {
features = feature.map(jstsToGeoJSON);
}
if (features) {
this._geojsonLayer.clearLayers();
this._geojsonLayer.addData(features);
this.fire('dataRefreshed');
}
},
_intersect: function (latlng, singleFeature) {
// Finds data that intersects the passed in L.LatLng. Parameters are:
// - latlng: and L.LatLng of the point that you want to find intersecting features for
// - singleFeature (optional): Boolean. Do you want to get back only the specific feature that
// intersects the point? Or are a small set of features with overlapping envelopes okay?
if (this._indexReady) {
var point = this._latlngToPoint(latlng),
matches = this._index.query(point.getEnvelopeInternal())
if (singleFeature) {
for (var i = 0; i < matches.length; i++) {
if (matches[i].geometry.intersects(point)) { // this is expensive.
return matches[i];
}
}
} else {
return matches;
}
}
return [];
},
_latlngToPoint: function (latlng) {
// Converts a L.LatLng to a jsts Point
var geometryFactory = new jsts.geom.GeometryFactory(),
coord = new jsts.geom.Coordinate(latlng.lng, latlng.lat);
return geometryFactory.createPoint(coord);
},
onAdd: function (map) {
// Add the sub-layers to the map
this._tileLayer.addTo(map);
this._geojsonLayer.addTo(map);
// Adjust the GeoJSON layer's contents whenever the mouse moves
map.on('mousemove', this.highlightFeature, this);
},
addTo: function (map) {
this.onAdd(map);
},
onRemove: function (map) {
map.removeLayer(this._tileLayer);
map.removeLayer(this._geojsonLayer);
map.off('mousemove', this.highlightFeature);
},
});
L.composite = function (options) {
return new L.Composite(options);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment