├── LICENSE
├── README.md
├── examples
├── canvas-boundary-edit.html
├── canvas-boundary-providers.html
└── canvas-boundary.html
└── src
└── BoundaryCanvas.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Alexander Parshin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | BoundaryCanvas is a plugin for [Leaflet](http://leaflet.cloudmade.com/) mapping library to draw tiled raster layers with arbitrary boundary.
2 | HTML5 Canvas is used for rendering. Works with both Leaflet 0.7.x and 1.0beta versions.
3 |
4 | ## Demos
5 |
6 | * [Draw boundary of a raster layer yourself](http://aparshin.github.com/leaflet-boundary-canvas/examples/canvas-boundary-edit.html)
7 | * [Add boundary to popular base layers](http://aparshin.github.com/leaflet-boundary-canvas/examples/canvas-boundary-providers.html)
8 | * [A multipolygon with holes as a border](http://aparshin.github.com/leaflet-boundary-canvas/examples/canvas-boundary.html)
9 |
10 | ## Usage
11 |
12 | ```javascript
13 | var osm = new L.TileLayer.BoundaryCanvas(tileLayerUrl, options);
14 | map.addLayer(osm);
15 | ```
16 |
17 | where
18 | * `tileLayerUrl` - URL similar to `L.TileLayer`
19 | * `options` - all `L.TileLayer` options and additional options described below.
20 |
21 | ## Options
22 |
23 | `boundary` option can be
24 | * GeoJSON object (only `Polygon` and `MultiPolygon` geometries will be used)
25 | * `LatLng[]` - simple polygon (depricated)
26 | * `LatLng[][]` - polygon with holes (depricated)
27 | * `LatLng[][][]` - multipolygon (depricated)
28 |
29 | All rings of boundary should be without self-intersections or intersections with other rings. Zero-winding fill algorithm is used in HTML5 Canvas, so holes should have opposite direction to exterior ring.
30 |
31 | `crossOrigin` option (Boolean) should be set if you want to request CORS enabled images. It is not required for the plugin itself, but can be usefull for potential plugin extensions.
32 |
33 | `trackAttribution` option (Boolean) can be set to show layer's attribution only when map boundary intersects layer's geometry. Additional calculations are required after each map movement (critical for complex boundaries).
34 |
35 | ## Contruction from Other Layers
36 |
37 | There is a helper function to construct `L.TileLayer.BoundaryCanvas` based on already created `L.TileLayer` layer:
38 |
39 | ```javascript
40 | var boundaryLayer = L.TileLayer.BoundaryCanvas.createFromLayer(tileLayer, options);
41 | ```
42 |
43 | where
44 | * `tileLayer` - instance of `L.TileLayer`
45 | * `options` - `L.TileLayer.BoundaryCanvas` options (including `boundary`)
46 |
47 | This helper returns new `L.TileLayer.BoundaryCanvas` layer. It is based only on options of original layer and doesn't work for all the `L.TileLayer` descendant classes.
48 |
49 | ## Code Example
50 |
51 | ```javascript
52 | var latLngGeom = ...; //Define real geometry here
53 | var map = L.map('map').setView([55.7, 38], 7),
54 | osmUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png',
55 | osmAttribution = 'Map data © 2012 OpenStreetMap contributors';
56 |
57 | var osm = L.TileLayer.boundaryCanvas(osmUrl, {
58 | boundary: latLngGeom,
59 | attribution: osmAttribution
60 | }).addTo(map);
61 | ```
62 |
--------------------------------------------------------------------------------
/examples/canvas-boundary-edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet raster boundary plugin example (using drawing plugin)
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
36 |
37 |
38 |
39 |
40 |
41 | Draw polygons or rectangles to see parts of the map
42 |
43 |
44 |
45 |
46 |
47 |
83 |
84 |
--------------------------------------------------------------------------------
/examples/canvas-boundary-providers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet boundary canvas plugin example (popular baselayers)
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
45 |
46 |
--------------------------------------------------------------------------------
/examples/canvas-boundary.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet boundary canvas plugin example (multipolygon with holes)
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
43 |
44 |
--------------------------------------------------------------------------------
/src/BoundaryCanvas.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | 'use strict';
4 |
5 | var isRingBbox = function (ring, bbox) {
6 | if (ring.length !== 4) {
7 | return false;
8 | }
9 |
10 | var p, sumX = 0, sumY = 0;
11 |
12 | for (p = 0; p < 4; p++) {
13 | if ((ring[p].x !== bbox.min.x && ring[p].x !== bbox.max.x) ||
14 | (ring[p].y !== bbox.min.y && ring[p].y !== bbox.max.y)) {
15 | return false;
16 | }
17 |
18 | sumX += ring[p].x;
19 | sumY += ring[p].y;
20 |
21 | //bins[Number(ring[p].x === bbox.min.x) + 2 * Number(ring[p].y === bbox.min.y)] = 1;
22 | }
23 |
24 | //check that we have all 4 vertex of bbox in our geometry
25 | return sumX === 2*(bbox.min.x + bbox.max.x) && sumY === 2*(bbox.min.y + bbox.max.y);
26 | };
27 |
28 | var ExtendMethods = {
29 | _toMercGeometry: function(b, isGeoJSON) {
30 | var res = [];
31 | var c, r, p,
32 | mercComponent,
33 | mercRing,
34 | coords;
35 |
36 | if (!isGeoJSON) {
37 | if (!(b[0] instanceof Array)) {
38 | b = [[b]];
39 | } else if (!(b[0][0] instanceof Array)) {
40 | b = [b];
41 | }
42 | }
43 |
44 | for (c = 0; c < b.length; c++) {
45 | mercComponent = [];
46 | for (r = 0; r < b[c].length; r++) {
47 | mercRing = [];
48 | for (p = 0; p < b[c][r].length; p++) {
49 | coords = isGeoJSON ? L.latLng(b[c][r][p][1], b[c][r][p][0]) : b[c][r][p];
50 | mercRing.push(this._map.project(coords, 0));
51 | }
52 | mercComponent.push(mercRing);
53 | }
54 | res.push(mercComponent);
55 | }
56 |
57 | return res;
58 | },
59 |
60 | //lazy calculation of layer's boundary in map's projection. Bounding box is also calculated
61 | _getOriginalMercBoundary: function () {
62 | if (this._mercBoundary) {
63 | return this._mercBoundary;
64 | }
65 |
66 | var compomentBbox, c;
67 |
68 | if (L.Util.isArray(this.options.boundary)) { //Depricated: just array of coordinates
69 | this._mercBoundary = this._toMercGeometry(this.options.boundary);
70 | } else { //GeoJSON
71 | this._mercBoundary = [];
72 | var processGeoJSONObject = function(obj) {
73 | if (obj.type === 'GeometryCollection') {
74 | obj.geometries.forEach(processGeoJSONObject);
75 | } else if (obj.type === 'Feature') {
76 | processGeoJSONObject(obj.geometry);
77 | } else if (obj.type === 'FeatureCollection') {
78 | obj.features.forEach(processGeoJSONObject);
79 | } else if (obj.type === 'Polygon') {
80 | this._mercBoundary = this._mercBoundary.concat(this._toMercGeometry([obj.coordinates], true));
81 | } else if (obj.type === 'MultiPolygon') {
82 | this._mercBoundary = this._mercBoundary.concat(this._toMercGeometry(obj.coordinates, true));
83 | }
84 | }.bind(this);
85 | processGeoJSONObject(this.options.boundary);
86 | }
87 |
88 | this._mercBbox = new L.Bounds();
89 | for (c = 0; c < this._mercBoundary.length; c++) {
90 | compomentBbox = new L.Bounds(this._mercBoundary[c][0]);
91 | this._mercBbox.extend(compomentBbox.min);
92 | this._mercBbox.extend(compomentBbox.max);
93 | }
94 |
95 | return this._mercBoundary;
96 | },
97 |
98 | _getClippedGeometry: function(geom, bounds) {
99 | var clippedGeom = [],
100 | clippedComponent,
101 | clippedExternalRing,
102 | clippedHoleRing,
103 | iC, iR;
104 |
105 | for (iC = 0; iC < geom.length; iC++) {
106 | clippedComponent = [];
107 | clippedExternalRing = L.PolyUtil.clipPolygon(geom[iC][0], bounds);
108 | if (clippedExternalRing.length === 0) {
109 | continue;
110 | }
111 |
112 | clippedComponent.push(clippedExternalRing);
113 |
114 | for (iR = 1; iR < geom[iC].length; iR++) {
115 | clippedHoleRing = L.PolyUtil.clipPolygon(geom[iC][iR], bounds);
116 | if (clippedHoleRing.length > 0) {
117 | clippedComponent.push(clippedHoleRing);
118 | }
119 | }
120 | clippedGeom.push(clippedComponent);
121 | }
122 |
123 | if (clippedGeom.length === 0) { //we are outside of all multipolygon components
124 | return {isOut: true};
125 | }
126 |
127 | for (iC = 0; iC < clippedGeom.length; iC++) {
128 | if (isRingBbox(clippedGeom[iC][0], bounds)) {
129 | //inside exterior rings and no holes
130 | if (clippedGeom[iC].length === 1) {
131 | return {isIn: true};
132 | }
133 | } else { //intersects exterior ring
134 | return {geometry: clippedGeom};
135 | }
136 |
137 | for (iR = 1; iR < clippedGeom[iC].length; iR++) {
138 | //inside exterior ring, but have intersection with a hole
139 | if (!isRingBbox(clippedGeom[iC][iR], bounds)) {
140 | return {geometry: clippedGeom};
141 | }
142 | }
143 | }
144 |
145 | //we are inside all holes in geometry
146 | return {isOut: true};
147 | },
148 |
149 | // Calculates intersection of original boundary geometry and tile boundary.
150 | // Uses quadtree as cache to speed-up intersection.
151 | // Return
152 | // {isOut: true} if no intersection,
153 | // {isIn: true} if tile is fully inside layer's boundary
154 | // {geometry: } otherwise
155 | _getTileGeometry: function (x, y, z, skipIntersectionCheck) {
156 | if ( !this.options.boundary) {
157 | return {isIn: true};
158 | }
159 |
160 | var cacheID = x + ":" + y + ":" + z,
161 | zCoeff = Math.pow(2, z),
162 | parentState,
163 | cache = this._boundaryCache;
164 |
165 | if (cache[cacheID]) {
166 | return cache[cacheID];
167 | }
168 |
169 | var mercBoundary = this._getOriginalMercBoundary(),
170 | ts = this.options.tileSize,
171 | tileBbox = new L.Bounds(new L.Point(x * ts / zCoeff, y * ts / zCoeff), new L.Point((x + 1) * ts / zCoeff, (y + 1) * ts / zCoeff));
172 |
173 | //fast check intersection
174 | if (!skipIntersectionCheck && !tileBbox.intersects(this._mercBbox)) {
175 | return {isOut: true};
176 | }
177 |
178 | if (z === 0) {
179 | cache[cacheID] = {geometry: mercBoundary};
180 | return cache[cacheID];
181 | }
182 |
183 | parentState = this._getTileGeometry(Math.floor(x / 2), Math.floor(y / 2), z - 1, true);
184 |
185 | if (parentState.isOut || parentState.isIn) {
186 | return parentState;
187 | }
188 |
189 | cache[cacheID] = this._getClippedGeometry(parentState.geometry, tileBbox);
190 | return cache[cacheID];
191 | },
192 |
193 | _drawTileInternal: function (canvas, tilePoint, url, callback) {
194 | var zoom = this._getZoomForUrl(),
195 | state = this._getTileGeometry(tilePoint.x, tilePoint.y, zoom);
196 |
197 | if (state.isOut) {
198 | callback();
199 | return;
200 | }
201 |
202 | var ts = this.options.tileSize,
203 | tileX = ts * tilePoint.x,
204 | tileY = ts * tilePoint.y,
205 | zCoeff = Math.pow(2, zoom),
206 | ctx = canvas.getContext('2d'),
207 | imageObj = new Image(),
208 | _this = this;
209 |
210 | var setPattern = function () {
211 | var c, r, p,
212 | pattern,
213 | geom;
214 |
215 | if (!state.isIn) {
216 | geom = state.geometry;
217 | ctx.beginPath();
218 |
219 | for (c = 0; c < geom.length; c++) {
220 | for (r = 0; r < geom[c].length; r++) {
221 | if (geom[c][r].length === 0) {
222 | continue;
223 | }
224 |
225 | ctx.moveTo(geom[c][r][0].x * zCoeff - tileX, geom[c][r][0].y * zCoeff - tileY);
226 | for (p = 1; p < geom[c][r].length; p++) {
227 | ctx.lineTo(geom[c][r][p].x * zCoeff - tileX, geom[c][r][p].y * zCoeff - tileY);
228 | }
229 | }
230 | }
231 | ctx.clip();
232 | }
233 |
234 | pattern = ctx.createPattern(imageObj, "repeat");
235 | ctx.beginPath();
236 | ctx.rect(0, 0, canvas.width, canvas.height);
237 | ctx.fillStyle = pattern;
238 | ctx.fill();
239 | callback();
240 | };
241 |
242 | if (this.options.crossOrigin) {
243 | imageObj.crossOrigin = '';
244 | }
245 |
246 | imageObj.onload = function () {
247 | //TODO: implement correct image loading cancelation
248 | canvas.complete = true; //HACK: emulate HTMLImageElement property to make happy L.TileLayer
249 | setTimeout(setPattern, 0); //IE9 bug - black tiles appear randomly if call setPattern() without timeout
250 | }
251 |
252 | imageObj.src = url;
253 | },
254 |
255 | onAdd: function(map) {
256 | (L.TileLayer.Canvas || L.TileLayer).prototype.onAdd.call(this, map);
257 |
258 | if (this.options.trackAttribution) {
259 | map.on('moveend', this._updateAttribution, this);
260 | this._updateAttribution();
261 | }
262 | },
263 |
264 | onRemove: function(map) {
265 | (L.TileLayer.Canvas || L.TileLayer).prototype.onRemove.call(this, map);
266 |
267 | if (this.options.trackAttribution) {
268 | map.off('moveend', this._updateAttribution, this);
269 | if (!this._attributionRemoved) {
270 | var attribution = L.TileLayer.BoundaryCanvas.prototype.getAttribution.call(this);
271 | map.attributionControl.removeAttribution(attribution);
272 | }
273 | }
274 | },
275 |
276 | _updateAttribution: function() {
277 | var geom = this._getOriginalMercBoundary(),
278 | mapBounds = this._map.getBounds(),
279 | mercBounds = L.bounds(this._map.project(mapBounds.getSouthWest(), 0), this._map.project(mapBounds.getNorthEast(), 0)),
280 | state = this._getClippedGeometry(geom, mercBounds);
281 |
282 | if (this._attributionRemoved !== !!state.isOut) {
283 | var attribution = L.TileLayer.BoundaryCanvas.prototype.getAttribution.call(this);
284 | this._map.attributionControl[state.isOut ? 'removeAttribution' : 'addAttribution'](attribution);
285 | this._attributionRemoved = !!state.isOut;
286 | }
287 | }
288 | };
289 |
290 | if (L.version >= '0.8') {
291 | L.TileLayer.BoundaryCanvas = L.TileLayer.extend({
292 | options: {
293 | // all rings of boundary should be without self-intersections or intersections with other rings
294 | // zero-winding fill algorithm is used in canvas, so holes should have opposite direction to exterior ring
295 | // boundary can be
296 | // LatLng[] - simple polygon
297 | // LatLng[][] - polygon with holes
298 | // LatLng[][][] - multipolygon
299 | boundary: null
300 | },
301 | includes: ExtendMethods,
302 | initialize: function(url, options) {
303 | L.TileLayer.prototype.initialize.call(this, url, options);
304 | this._boundaryCache = {}; //cache index "x:y:z"
305 | this._mercBoundary = null;
306 | this._mercBbox = null;
307 |
308 | if (this.options.trackAttribution) {
309 | this._attributionRemoved = true;
310 | this.getAttribution = null;
311 | }
312 | },
313 | createTile: function(coords, done){
314 | var tile = document.createElement('canvas'),
315 | url = this.getTileUrl(coords);
316 | tile.width = tile.height = this.options.tileSize;
317 | this._drawTileInternal(tile, coords, url, L.bind(done, null, null, tile));
318 |
319 | return tile;
320 | }
321 | })
322 | } else {
323 | L.TileLayer.BoundaryCanvas = L.TileLayer.Canvas.extend({
324 | options: {
325 | // all rings of boundary should be without self-intersections or intersections with other rings
326 | // zero-winding fill algorithm is used in canvas, so holes should have opposite direction to exterior ring
327 | // boundary can be
328 | // LatLng[] - simple polygon
329 | // LatLng[][] - polygon with holes
330 | // LatLng[][][] - multipolygon
331 | boundary: null
332 | },
333 | includes: ExtendMethods,
334 | initialize: function (url, options) {
335 | L.Util.setOptions(this, options);
336 | L.Util.setOptions(this, {async: true}); //image loading is always async
337 | this._url = url;
338 | this._boundaryCache = {}; //cache index "x:y:z"
339 | this._mercBoundary = null;
340 | this._mercBbox = null;
341 |
342 | if (this.options.trackAttribution) {
343 | this._attributionRemoved = true;
344 | this.getAttribution = null;
345 | }
346 | },
347 |
348 | drawTile: function(canvas, tilePoint) {
349 | var adjustedTilePoint = L.extend({}, tilePoint),
350 | url;
351 |
352 | this._adjustTilePoint(adjustedTilePoint);
353 | url = this.getTileUrl(adjustedTilePoint);
354 | this._drawTileInternal(canvas, tilePoint, url, L.bind(this.tileDrawn, this, canvas));
355 |
356 | //Leaflet v0.7.x bugfix (L.Tile.Canvas doesn't support maxNativeZoom option)
357 | if (this._getTileSize() !== this.options.tileSize) {
358 | canvas.style.width = canvas.style.height = this._getTileSize() + 'px';
359 | }
360 | }
361 | });
362 | }
363 |
364 | L.TileLayer.boundaryCanvas = function (url, options) {
365 | return new L.TileLayer.BoundaryCanvas(url, options);
366 | };
367 |
368 | L.TileLayer.BoundaryCanvas.createFromLayer = function (layer, options) {
369 | return new L.TileLayer.BoundaryCanvas(layer._url, L.extend({}, layer.options, options));
370 | };
371 |
372 | })();
373 |
--------------------------------------------------------------------------------