├── .gitignore ├── LICENCE ├── README.md ├── TileLayer.GeoJSON.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Glen Robertson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are 5 | permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of 8 | conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | of conditions and the following disclaimer in the documentation and/or other materials 12 | provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 21 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet GeoJSON Tile Layer 2 | [![CDNJS](https://img.shields.io/cdnjs/v/leaflet-tilelayer-geojson.svg)](https://cdnjs.com/libraries/leaflet-tilelayer-geojson) 3 | 4 | Renders GeoJSON tiles on an L.GeoJSON layer. 5 | 6 | ## Docs 7 | 8 | ### Usage example 9 | The following example shows how to render a GeoJSON Tile Layer for a tile endpoint at http://tile.example.com/{z}/{x}/{y}.json. 10 | 11 | var style = { 12 | "clickable": true, 13 | "color": "#00D", 14 | "fillColor": "#00D", 15 | "weight": 1.0, 16 | "opacity": 0.3, 17 | "fillOpacity": 0.2 18 | }; 19 | var hoverStyle = { 20 | "fillOpacity": 0.5 21 | }; 22 | 23 | var geojsonURL = 'http://tile.example.com/{z}/{x}/{y}.json'; 24 | var geojsonTileLayer = new L.TileLayer.GeoJSON(geojsonURL, { 25 | clipTiles: true, 26 | unique: function (feature) { 27 | return feature.id; 28 | } 29 | }, { 30 | style: style, 31 | onEachFeature: function (feature, layer) { 32 | if (feature.properties) { 33 | var popupString = ''; 39 | layer.bindPopup(popupString); 40 | } 41 | if (!(layer instanceof L.Point)) { 42 | layer.on('mouseover', function () { 43 | layer.setStyle(hoverStyle); 44 | }); 45 | layer.on('mouseout', function () { 46 | layer.setStyle(style); 47 | }); 48 | } 49 | } 50 | } 51 | ); 52 | map.addLayer(geojsonTileLayer); 53 | 54 | ### Constructor 55 | L.TileLayer.GeoJSON( urlTemplate, options?, geojsonOptions? ) 56 | 57 | ### URL Template 58 | A string of the following form, that returns valid GeoJSON. 59 | 60 | 'http://{s}.somedomain.com/blabla/{z}/{x}/{y}.json' 61 | 62 | ### GeoJSONTileLayer options 63 | * `clipTiles (boolean) (default = false)`: If `true`, clips tile feature geometries to their tile boundaries using SVG clipping. 64 | * `unique (function)`: If set, the feature's are grouped into GeometryCollection GeoJSON objects. Each group is defined by the key returned by this function, with the feature object as the first argument. 65 | 66 | ### GeoJSON options 67 | Options that will be passed to the resulting L.GeoJSON layer: [http://leafletjs.com/reference.html#geojson-options](http://leafletjs.com/reference.html#geojson-options) 68 | 69 | ## Hosts 70 | 1. npm: [https://www.npmjs.com/package/leaflet-tilelayer-geojson](https://www.npmjs.com/package/leaflet-tilelayer-geojson) 71 | 1. cdnjs: [https://cdnjs.com/libraries/leaflet-tilelayer-geojson](https://cdnjs.com/libraries/leaflet-tilelayer-geojson) 72 | 73 | ## Contributors 74 | Thanks to the following people who contributed: https://github.com/glenrobertson/leaflet-tilelayer-geojson/graphs/contributors 75 | -------------------------------------------------------------------------------- /TileLayer.GeoJSON.js: -------------------------------------------------------------------------------- 1 | // Load data tiles from an AJAX data source 2 | L.TileLayer.Ajax = L.TileLayer.extend({ 3 | _requests: [], 4 | _addTile: function (tilePoint) { 5 | var tile = { datum: null, processed: false }; 6 | this._tiles[tilePoint.x + ':' + tilePoint.y] = tile; 7 | this._loadTile(tile, tilePoint); 8 | }, 9 | // XMLHttpRequest handler; closure over the XHR object, the layer, and the tile 10 | _xhrHandler: function (req, layer, tile, tilePoint) { 11 | return function () { 12 | if (req.readyState !== 4) { 13 | return; 14 | } 15 | var s = req.status; 16 | if ((s >= 200 && s < 300 && s != 204) || s === 304) { 17 | tile.datum = JSON.parse(req.responseText); 18 | layer._tileLoaded(tile, tilePoint); 19 | } else { 20 | layer._tileLoaded(tile, tilePoint); 21 | } 22 | }; 23 | }, 24 | // Load the requested tile via AJAX 25 | _loadTile: function (tile, tilePoint) { 26 | this._adjustTilePoint(tilePoint); 27 | var layer = this; 28 | var req = new XMLHttpRequest(); 29 | this._requests.push(req); 30 | req.onreadystatechange = this._xhrHandler(req, layer, tile, tilePoint); 31 | req.open('GET', this.getTileUrl(tilePoint), true); 32 | req.send(); 33 | }, 34 | _reset: function () { 35 | L.TileLayer.prototype._reset.apply(this, arguments); 36 | for (var i = 0; i < this._requests.length; i++) { 37 | this._requests[i].abort(); 38 | } 39 | this._requests = []; 40 | }, 41 | _update: function () { 42 | if (this._map && this._map._panTransition && this._map._panTransition._inProgress) { return; } 43 | if (this._tilesToLoad < 0) { this._tilesToLoad = 0; } 44 | L.TileLayer.prototype._update.apply(this, arguments); 45 | } 46 | }); 47 | 48 | 49 | L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({ 50 | // Store each GeometryCollection's layer by key, if options.unique function is present 51 | _keyLayers: {}, 52 | 53 | // Used to calculate svg path string for clip path elements 54 | _clipPathRectangles: {}, 55 | 56 | initialize: function (url, options, geojsonOptions) { 57 | L.TileLayer.Ajax.prototype.initialize.call(this, url, options); 58 | this.geojsonLayer = new L.GeoJSON(null, geojsonOptions); 59 | }, 60 | onAdd: function (map) { 61 | this._map = map; 62 | L.TileLayer.Ajax.prototype.onAdd.call(this, map); 63 | map.addLayer(this.geojsonLayer); 64 | }, 65 | onRemove: function (map) { 66 | map.removeLayer(this.geojsonLayer); 67 | L.TileLayer.Ajax.prototype.onRemove.call(this, map); 68 | }, 69 | _reset: function () { 70 | this.geojsonLayer.clearLayers(); 71 | this._keyLayers = {}; 72 | this._removeOldClipPaths(); 73 | L.TileLayer.Ajax.prototype._reset.apply(this, arguments); 74 | }, 75 | 76 | _getUniqueId: function() { 77 | return String(this._leaflet_id || ''); // jshint ignore:line 78 | }, 79 | 80 | // Remove clip path elements from other earlier zoom levels 81 | _removeOldClipPaths: function () { 82 | for (var clipPathId in this._clipPathRectangles) { 83 | var prefix = clipPathId.split('tileClipPath')[0]; 84 | if (this._getUniqueId() === prefix) { 85 | var clipPathZXY = clipPathId.split('_').slice(1); 86 | var zoom = parseInt(clipPathZXY[0], 10); 87 | if (zoom !== this._map.getZoom()) { 88 | var rectangle = this._clipPathRectangles[clipPathId]; 89 | this._map.removeLayer(rectangle); 90 | var clipPath = document.getElementById(clipPathId); 91 | if (clipPath !== null) { 92 | clipPath.parentNode.removeChild(clipPath); 93 | } 94 | delete this._clipPathRectangles[clipPathId]; 95 | } 96 | } 97 | } 98 | }, 99 | 100 | // Recurse LayerGroups and call func() on L.Path layer instances 101 | _recurseLayerUntilPath: function (func, layer) { 102 | if (layer instanceof L.Path) { 103 | func(layer); 104 | } 105 | else if (layer instanceof L.LayerGroup) { 106 | // Recurse each child layer 107 | layer.getLayers().forEach(this._recurseLayerUntilPath.bind(this, func), this); 108 | } 109 | }, 110 | 111 | _clipLayerToTileBoundary: function (layer, tilePoint) { 112 | // Only perform SVG clipping if the browser is using SVG 113 | if (!L.Path.SVG) { return; } 114 | if (!this._map) { return; } 115 | 116 | if (!this._map._pathRoot) { 117 | this._map._pathRoot = L.Path.prototype._createElement('svg'); 118 | this._map._panes.overlayPane.appendChild(this._map._pathRoot); 119 | } 120 | var svg = this._map._pathRoot; 121 | 122 | // create the defs container if it doesn't exist 123 | var defs = null; 124 | if (svg.getElementsByTagName('defs').length === 0) { 125 | defs = document.createElementNS(L.Path.SVG_NS, 'defs'); 126 | svg.insertBefore(defs, svg.firstChild); 127 | } 128 | else { 129 | defs = svg.getElementsByTagName('defs')[0]; 130 | } 131 | 132 | // Create the clipPath for the tile if it doesn't exist 133 | var clipPathId = this._getUniqueId() + 'tileClipPath_' + tilePoint.z + '_' + tilePoint.x + '_' + tilePoint.y; 134 | var clipPath = document.getElementById(clipPathId); 135 | if (clipPath === null) { 136 | clipPath = document.createElementNS(L.Path.SVG_NS, 'clipPath'); 137 | clipPath.id = clipPathId; 138 | 139 | // Create a hidden L.Rectangle to represent the tile's area 140 | var tileSize = this.options.tileSize, 141 | nwPoint = tilePoint.multiplyBy(tileSize), 142 | sePoint = nwPoint.add([tileSize, tileSize]), 143 | nw = this._map.unproject(nwPoint), 144 | se = this._map.unproject(sePoint); 145 | this._clipPathRectangles[clipPathId] = new L.Rectangle(new L.LatLngBounds([nw, se]), { 146 | opacity: 0, 147 | fillOpacity: 0, 148 | clickable: false, 149 | noClip: true 150 | }); 151 | this._map.addLayer(this._clipPathRectangles[clipPathId]); 152 | 153 | // Add a clip path element to the SVG defs element 154 | // With a path element that has the hidden rectangle's SVG path string 155 | var path = document.createElementNS(L.Path.SVG_NS, 'path'); 156 | var pathString = this._clipPathRectangles[clipPathId].getPathString(); 157 | path.setAttribute('d', pathString); 158 | clipPath.appendChild(path); 159 | defs.appendChild(clipPath); 160 | } 161 | 162 | // Add the clip-path attribute to reference the id of the tile clipPath 163 | this._recurseLayerUntilPath(function (pathLayer) { 164 | pathLayer._container.setAttribute('clip-path', 'url(' + window.location.href + '#' + clipPathId + ')'); 165 | }, layer); 166 | }, 167 | 168 | // Add a geojson object from a tile to the GeoJSON layer 169 | // * If the options.unique function is specified, merge geometries into GeometryCollections 170 | // grouped by the key returned by options.unique(feature) for each GeoJSON feature 171 | // * If options.clipTiles is set, and the browser is using SVG, perform SVG clipping on each 172 | // tile's GeometryCollection 173 | addTileData: function (geojson, tilePoint) { 174 | var features = L.Util.isArray(geojson) ? geojson : geojson.features, 175 | i, len, feature; 176 | 177 | if (features) { 178 | for (i = 0, len = features.length; i < len; i++) { 179 | // Only add this if geometry or geometries are set and not null 180 | feature = features[i]; 181 | if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { 182 | this.addTileData(features[i], tilePoint); 183 | } 184 | } 185 | return this; 186 | } 187 | 188 | var options = this.geojsonLayer.options; 189 | 190 | if (options.filter && !options.filter(geojson)) { return; } 191 | 192 | var parentLayer = this.geojsonLayer; 193 | var incomingLayer = null; 194 | if (this.options.unique && typeof(this.options.unique) === 'function') { 195 | var key = this.options.unique(geojson); 196 | 197 | // When creating the layer for a unique key, 198 | // Force the geojson to be a geometry collection 199 | if (!(key in this._keyLayers && geojson.geometry.type !== 'GeometryCollection')) { 200 | geojson.geometry = { 201 | type: 'GeometryCollection', 202 | geometries: [geojson.geometry] 203 | }; 204 | } 205 | 206 | // Transform the geojson into a new Layer 207 | try { 208 | incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng); 209 | } 210 | // Ignore GeoJSON objects that could not be parsed 211 | catch (e) { 212 | return this; 213 | } 214 | 215 | incomingLayer.feature = L.GeoJSON.asFeature(geojson); 216 | // Add the incoming Layer to existing key's GeometryCollection 217 | if (key in this._keyLayers) { 218 | parentLayer = this._keyLayers[key]; 219 | parentLayer.feature.geometry.geometries.push(geojson.geometry); 220 | } 221 | // Convert the incoming GeoJSON feature into a new GeometryCollection layer 222 | else { 223 | this._keyLayers[key] = incomingLayer; 224 | } 225 | } 226 | // Add the incoming geojson feature to the L.GeoJSON Layer 227 | else { 228 | // Transform the geojson into a new layer 229 | try { 230 | incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng); 231 | } 232 | // Ignore GeoJSON objects that could not be parsed 233 | catch (e) { 234 | return this; 235 | } 236 | incomingLayer.feature = L.GeoJSON.asFeature(geojson); 237 | } 238 | incomingLayer.defaultOptions = incomingLayer.options; 239 | 240 | this.geojsonLayer.resetStyle(incomingLayer); 241 | 242 | if (options.onEachFeature) { 243 | options.onEachFeature(geojson, incomingLayer); 244 | } 245 | parentLayer.addLayer(incomingLayer); 246 | 247 | // If options.clipTiles is set and the browser is using SVG 248 | // then clip the layer using SVG clipping 249 | if (this.options.clipTiles) { 250 | this._clipLayerToTileBoundary(incomingLayer, tilePoint); 251 | } 252 | return this; 253 | }, 254 | 255 | _tileLoaded: function (tile, tilePoint) { 256 | L.TileLayer.Ajax.prototype._tileLoaded.apply(this, arguments); 257 | if (tile.datum === null) { return null; } 258 | this.addTileData(tile.datum, tilePoint); 259 | } 260 | }); 261 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-tilelayer-geojson", 3 | "version": "1.0", 4 | "description": "Leaflet plugin to render GeoJSON data over tile layer", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/glenrobertson/leaflet-tilelayer-geojson.git" 8 | }, 9 | "keywords": [ 10 | "Leaflet", 11 | "leaflet.js", 12 | "tile", 13 | "tilelayer", 14 | "geojson" 15 | ], 16 | "author": "Glen Robertson", 17 | "license": "BSD-2-Clause-FreeBSD", 18 | "dependencies": { 19 | "leaflet": "~0.7.1" 20 | } 21 | } 22 | --------------------------------------------------------------------------------