index.js (7145B)
1 import L from 'leaflet'; 2 3 import {urlViaCorsProxy} from '~/lib/CORSProxy'; 4 5 const origCreateTile = L.TileLayer.prototype.createTile; 6 const origIsValidTile = L.TileLayer.prototype._isValidTile; 7 const origOnAdd = L.TileLayer.prototype.onAdd; 8 9 function coordsListBounds(coordsList) { 10 const bounds = L.latLngBounds(); 11 coordsList.forEach(([lon, lat]) => bounds.extend([lat, lon])); 12 return bounds; 13 } 14 15 function latLngBoundsToBounds(latlLngBounds) { 16 return L.bounds( 17 L.point(latlLngBounds.getWest(), latlLngBounds.getSouth()), 18 L.point(latlLngBounds.getEast(), latlLngBounds.getNorth()) 19 ); 20 } 21 22 function isCoordsListIntersectingBounds(coordsList, latLngBounds) { 23 const latLngBoundsAsBound = latLngBoundsToBounds(latLngBounds); 24 for (let i = 1; i < coordsList.length; i++) { 25 if (L.LineUtil.clipSegment(L.point(coordsList[i - 1]), L.point(coordsList[i]), latLngBoundsAsBound, i > 1)) { 26 return true; 27 } 28 } 29 return Boolean( 30 L.LineUtil.clipSegment(L.point(coordsList[coordsList.length - 1]), L.point(coordsList[0]), latLngBoundsAsBound) 31 ); 32 } 33 34 function isPointInsidePolygon(polygon, latLng) { 35 let inside = false; 36 let prevNode = polygon[polygon.length - 1]; 37 for (let i = 0; i < polygon.length; i++) { 38 const node = polygon[i]; 39 if ( 40 node[0] !== prevNode[1] && 41 ((node[0] <= latLng.lng && latLng.lng < prevNode[0]) || 42 (prevNode[0] <= latLng.lng && latLng.lng < node[0])) && 43 latLng.lat < ((prevNode[1] - node[1]) * (latLng.lng - node[0])) / (prevNode[0] - node[0]) + node[1] 44 ) { 45 inside = !inside; 46 } 47 prevNode = node; 48 } 49 return inside; 50 } 51 52 L.TileLayer.include({ 53 _drawTileClippedByCutline: function (coords, srcImg, destCanvas, done) { 54 if (!this._map) { 55 return; 56 } 57 const width = srcImg.naturalWidth; 58 const height = srcImg.naturalHeight; 59 destCanvas.width = width; 60 destCanvas.height = height; 61 62 const zoomScale = 2 ** coords.z; 63 const tileScale = width / this.getTileSize().x; 64 const tileNwPoint = coords.scaleBy(this.getTileSize()); 65 const tileLatLngBounds = this._tileCoordsToBounds(coords); 66 const ctx = destCanvas.getContext('2d'); 67 ctx.beginPath(); 68 const projectedCutline = this.getProjectedCutline(); 69 for (let i = 0; i < projectedCutline.length; i++) { 70 const cutlineLatLngBounds = this._cutline.bounds[i]; 71 if (tileLatLngBounds.intersects(cutlineLatLngBounds)) { 72 const path = projectedCutline[i].map((point) => 73 point.multiplyBy(zoomScale).subtract(tileNwPoint).multiplyBy(tileScale) 74 ); 75 ctx.moveTo(path[0].x, path[0].y); 76 for (let j = 1; j < path.length; j++) { 77 ctx.lineTo(path[j].x, path[j].y); 78 } 79 ctx.closePath(); 80 } 81 } 82 ctx.clip(); 83 ctx.drawImage(srcImg, 0, 0); 84 destCanvas.complete = true; // HACK: emulate HTMLImageElement property to make L.TileLayer._abortLoading() happy 85 this._tileOnLoad(done, destCanvas); 86 }, 87 88 onAdd: function (map) { 89 const result = origOnAdd.call(this, map); 90 if (this.options.cutline && !this._cutlinePromise) { 91 this._cutlinePromise = this._setCutline(this.options.cutline, this.options.cutlineApprox).then(() => { 92 this._updateProjectedCutline(); 93 this.redraw(); 94 }); 95 } 96 this._updateProjectedCutline(); 97 return result; 98 }, 99 100 createTile: function (coords, done) { 101 if (this._cutline && !this._cutline.approx && this.isCutlineIntersectingTile(coords, true)) { 102 const img = document.createElement('img'); 103 img.crossOrigin = ''; 104 105 const tile = document.createElement('canvas'); 106 tile.setAttribute('role', 'presentation'); 107 108 L.DomEvent.on(img, 'load', L.bind(this._drawTileClippedByCutline, this, coords, img, tile, done)); 109 L.DomEvent.on(img, 'error', L.bind(this._tileOnError, this, done, tile)); 110 111 let url = this.getTileUrl(coords); 112 if (this.options.noCors) { 113 url = urlViaCorsProxy(url); 114 } 115 img.src = url; 116 return tile; 117 } 118 return origCreateTile.call(this, coords, done); 119 }, 120 121 isCutlineIntersectingTile: function (coords, onlyBorder) { 122 const tileLatLngBounds = this._tileCoordsToBounds(coords); 123 for (let i = 0; i < this._cutline.latlng.length; i++) { 124 const cutline = this._cutline.latlng[i]; 125 const cutlineLatLngBounds = this._cutline.bounds[i]; 126 if ( 127 cutlineLatLngBounds.overlaps(tileLatLngBounds) && 128 (isCoordsListIntersectingBounds(cutline, tileLatLngBounds) || 129 (!onlyBorder && isPointInsidePolygon(cutline, tileLatLngBounds.getNorthEast()))) 130 ) { 131 return true; 132 } 133 } 134 return false; 135 }, 136 137 _isValidTile: function (coords) { 138 const isOrigValid = origIsValidTile.call(this, coords); 139 if (this._cutline && isOrigValid) { 140 return this.isCutlineIntersectingTile(coords, false); 141 } 142 return isOrigValid; 143 }, 144 145 getProjectedCutline: function () { 146 const map = this._map; 147 function projectCoordsList(coordsList) { 148 return coordsList.map(([lng, lat]) => map.project([lat, lng], 0)); 149 } 150 151 if (!this._cutline._projected || this._cutline._projectedWithMap !== map) { 152 this._cutline._projected = this._cutline.latlng.map(projectCoordsList); 153 this._cutline._projectedWithMap = map; 154 } 155 156 return this._cutline._projected; 157 }, 158 159 _setCutline: async function (cutline, approx) { 160 let cutlinePromise = cutline; 161 if (typeof cutlinePromise === 'function') { 162 cutlinePromise = cutlinePromise(); 163 } 164 if (!cutlinePromise.then) { 165 cutlinePromise = Promise.resolve(cutlinePromise); 166 } 167 let cutlineCoords; 168 try { 169 cutlineCoords = await cutlinePromise; 170 } catch (_) { 171 // will be handled as empty later 172 } 173 174 if (cutlineCoords) { 175 this._cutline = { 176 latlng: cutlineCoords, 177 bounds: cutlineCoords.map(coordsListBounds), 178 approx: approx, 179 }; 180 } else { 181 this._cutline = null; 182 } 183 }, 184 185 _updateProjectedCutline: function () { 186 const map = this._map; 187 if (!this._cutline || !map || (this._cutline._projected && this._cutline._projectedWithMap !== map)) { 188 return; 189 } 190 function projectCoordsList(coordsList) { 191 return coordsList.map(([lng, lat]) => map.project([lat, lng], 0)); 192 } 193 194 this._cutline._projected = this._cutline.latlng.map(projectCoordsList); 195 this._cutline._projectedWithMap = map; 196 }, 197 });