Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@mapmeld
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mapmeld/8943913 to your computer and use it in GitHub Desktop.
Save mapmeld/8943913 to your computer and use it in GitHub Desktop.
Vector tiles that hide undersized features
// 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) {
var geoid = feature.properties["geoid10"];
var partOfBigFeature = false;
if(typeof summarizedByGeoId[geoid] != "undefined" && summarizedByGeoId[geoid] === true){
// already added a full-size item with this id
partOfBigFeature = true;
}
var bounds = getLLBounds(feature.geometry.coordinates);
var maxGap = Math.max( bounds[2]-bounds[0], bounds[3]-bounds[1] );
if(partOfBigFeature || (maxGap > 7 * Math.pow(2, -1 * map.getZoom()))){
if(typeof summarizedByGeoId[geoid] != "undefined"){
// array of summary parts already added to the map
for(var s=0;s<summarizedByGeoId[geoid].length;s++){
var circle = summarizedByGeoId[geoid][s];
summaryLayer.removeLayer(circle);
}
}
// visible feature exists for this ID
summarizedByGeoId[geoid] = true;
this.addTileData(features[i], tilePoint);
}
else{
// add summary layer if it doesn't exist yet
if(!summaryLayer){
summaryLayer = L.featureGroup().addTo(map);
map.on("zoomstart", function(){
// when zoom ends, new tiles are loaded and objects which need summary circles change
summaryLayer.clearLayers();
summarizedByGeoId = { };
});
}
// assume feature is too small to display as original vector
var circle_props = featureStyle(feature.properties);
circle_props.radius = 3;
var circle = L.circleMarker( new L.LatLng((bounds[3]+bounds[1])/2, (bounds[2]+bounds[0])/2), circle_props)
.addTo(summaryLayer);
// add array of partials if it doesn't exist yet
if(typeof summarizedByGeoId[geoid] == "undefined"){
summarizedByGeoId[geoid] = [];
}
// store circles in case a large part appears
summarizedByGeoId[geoid].push(circle);
}
}
}
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;
}
incomingLayer.feature = L.GeoJSON.asFeature(geojson);
// 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 {
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);
}
});
// display summary circles in place of tiny features
var summaryLayer = null;
var summarizedByGeoId = { };
// remember highlighted features between zooms and stages
var highlightedGeoIds = [ ];
function featureStyle(props){
var standardStyle = {
clickable: true,
color: "#000",
fillColor: "#00D",
weight: 1,
opacity: 0.2,
fillOpacity: 0.2
};
if(highlightedGeoIds.indexOf(props["geoid10"]) > -1){
standardStyle.fillColor = "#F00";
}
return standardStyle;
}
function getLLBounds(coords, bounds){
if(typeof bounds == "undefined" || !bounds){
bounds = [ 180, 90, -180, -90 ];
}
if(coords.length){
if(typeof coords[0] == "object"){
// more coordinates inside each point
for(var i=0;i<coords.length;i++){
bounds = getLLBounds(coords[i], bounds);
}
}
else{
// is a coordinate
bounds[0] = Math.min(bounds[0], coords[0]);
bounds[1] = Math.min(bounds[1], coords[1]);
bounds[2] = Math.max(bounds[2], coords[0]);
bounds[3] = Math.max(bounds[3], coords[1]);
}
}
return bounds;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment