├── 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 | --------------------------------------------------------------------------------