├── .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 | [](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 |
--------------------------------------------------------------------------------