nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

commit 680c1b49b4542d127dca14837487ba67bfbfa8dc
parent 5cf7f791036495e26f584b63ad4da05ba7bbb820
Author: Sergey Orlov <wladimirych@gmail.com>
Date:   Thu,  3 Dec 2020 12:13:24 +0100

Allow to cut tile layers with arbitrary polygon

Diffstat:
Meslint_rules/imports_webapp.js | 5+++--
Asrc/lib/layers-cutlines/index.js | 12++++++++++++
Asrc/lib/leaflet.layer.TileLayer.cutline/index.js | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/leaflet.layer.rasterize/TileLayer.js | 24+++++++++++++++++++++++-
4 files changed, 231 insertions(+), 3 deletions(-)

diff --git a/eslint_rules/imports_webapp.js b/eslint_rules/imports_webapp.js @@ -1,11 +1,12 @@ 'use strict'; +const filesWithSideEffects = ['src/lib/leaflet.layer.TileLayer.cutline/index.js']; module.exports = { rules: { 'import/no-unused-modules': [ 'error', - {missingExports: true, unusedExports: true, ignoreExports: ['src/index.js']}, + {missingExports: true, unusedExports: true, ignoreExports: filesWithSideEffects}, ], - 'import/no-unassigned-import': ['error', {allow: ['**/*.css']}], + 'import/no-unassigned-import': ['error', {allow: ['**/*.css', ...filesWithSideEffects]}], }, }; diff --git a/src/lib/layers-cutlines/index.js b/src/lib/layers-cutlines/index.js @@ -0,0 +1,12 @@ +function getCutline(cutlineName) { + return async function () { + return ( + await import( + /* webpackChunkName: "cutline-[request]" */ + `./${cutlineName}.json` + ) + ).cutline; + }; +} + +export {getCutline}; diff --git a/src/lib/leaflet.layer.TileLayer.cutline/index.js b/src/lib/leaflet.layer.TileLayer.cutline/index.js @@ -0,0 +1,193 @@ +import L from 'leaflet'; + +import urlViaCorsProxy from '~/lib/CORSProxy'; + +const origCreateTile = L.TileLayer.prototype.createTile; +const origIsValidTile = L.TileLayer.prototype._isValidTile; +const origOnAdd = L.TileLayer.prototype.onAdd; + +function coordsListBounds(coordsList) { + const bounds = L.latLngBounds(); + coordsList.forEach(([lon, lat]) => bounds.extend([lat, lon])); + return bounds; +} + +function latLngBoundsToBounds(latlLngBounds) { + return L.bounds( + L.point(latlLngBounds.getWest(), latlLngBounds.getSouth()), + L.point(latlLngBounds.getEast(), latlLngBounds.getNorth()) + ); +} + +function isCoordsListIntersectingBounds(coordsList, latLngBounds) { + const latLngBoundsAsBound = latLngBoundsToBounds(latLngBounds); + for (let i = 1; i < coordsList.length; i++) { + if (L.LineUtil.clipSegment(L.point(coordsList[i - 1]), L.point(coordsList[i]), latLngBoundsAsBound, i > 1)) { + return true; + } + } + return Boolean( + L.LineUtil.clipSegment(L.point(coordsList[coordsList.length - 1]), L.point(coordsList[0]), latLngBoundsAsBound) + ); +} + +function isPointInsidePolygon(polygon, latLng) { + let inside = false; + let prevNode = polygon[polygon.length - 1]; + for (let i = 0; i < polygon.length; i++) { + const node = polygon[i]; + if ( + node[0] !== prevNode[1] && + ((node[0] <= latLng.lng && latLng.lng < prevNode[0]) || + (prevNode[0] <= latLng.lng && latLng.lng < node[0])) && + latLng.lat < ((prevNode[1] - node[1]) * (latLng.lng - node[0])) / (prevNode[0] - node[0]) + node[1] + ) { + inside = !inside; + } + prevNode = node; + } + return inside; +} + +L.TileLayer.include({ + _drawTileClippedByCutline: function (coords, srcImg, destCanvas, done) { + const width = srcImg.naturalWidth; + const height = srcImg.naturalHeight; + destCanvas.width = width; + destCanvas.height = height; + + const zoomScale = 2 ** coords.z; + const tileScale = width / this.getTileSize().x; + const tileNwPoint = coords.scaleBy(this.getTileSize()); + const tileLatLngBounds = this._tileCoordsToBounds(coords); + const ctx = destCanvas.getContext('2d'); + ctx.beginPath(); + const projectedCutline = this.getProjectedCutline(); + for (let i = 0; i < projectedCutline.length; i++) { + const cutlineLatLngBounds = this._cutline.bounds[i]; + if (tileLatLngBounds.intersects(cutlineLatLngBounds)) { + const path = projectedCutline[i].map((point) => + point.multiplyBy(zoomScale).subtract(tileNwPoint).multiplyBy(tileScale) + ); + ctx.moveTo(path[0].x, path[0].y); + for (let j = 1; j < path.length; j++) { + ctx.lineTo(path[j].x, path[j].y); + } + ctx.closePath(); + } + } + ctx.clip(); + ctx.drawImage(srcImg, 0, 0); + destCanvas.complete = true; // HACK: emulate HTMLImageElement property to make L.TileLayer._abortLoading() happy + this._tileOnLoad(done, destCanvas); + }, + + onAdd: function (map) { + const result = origOnAdd.call(this, map); + if (this.options.cutline && !this._cutlinePromise) { + this._cutlinePromise = this._setCutline(this.options.cutline, this.options.cutlineApprox).then(() => { + this._updateProjectedCutline(); + this.redraw(); + }); + } + this._updateProjectedCutline(); + return result; + }, + + createTile: function (coords, done) { + if (this._cutline && !this._cutline.approx && this.isCutlineIntersectingTile(coords, true)) { + const img = document.createElement('img'); + img.crossOrigin = ''; + L.DomEvent.on(img, 'error', L.bind(this._tileOnError, this, done, img)); + + const tile = document.createElement('canvas'); + tile.setAttribute('role', 'presentation'); + + L.DomEvent.on(img, 'load', L.bind(this._drawTileClippedByCutline, this, coords, img, tile, done)); + let url = this.getTileUrl(coords); + if (this.options.noCors) { + url = urlViaCorsProxy(url); + } + img.src = url; + return tile; + } + return origCreateTile.call(this, coords, done); + }, + + isCutlineIntersectingTile: function (coords, onlyBorder) { + const tileLatLngBounds = this._tileCoordsToBounds(coords); + for (let i = 0; i < this._cutline.latlng.length; i++) { + const cutline = this._cutline.latlng[i]; + const cutlineLatLngBounds = this._cutline.bounds[i]; + if ( + cutlineLatLngBounds.overlaps(tileLatLngBounds) && + (isCoordsListIntersectingBounds(cutline, tileLatLngBounds) || + (!onlyBorder && isPointInsidePolygon(cutline, tileLatLngBounds.getNorthEast()))) + ) { + return true; + } + } + return false; + }, + + _isValidTile: function (coords) { + const isOrigValid = origIsValidTile.call(this, coords); + if (this._cutline && isOrigValid) { + return this.isCutlineIntersectingTile(coords, false); + } + return isOrigValid; + }, + + getProjectedCutline: function () { + const map = this._map; + function projectCoordsList(coordsList) { + return coordsList.map(([lng, lat]) => map.project([lat, lng], 0)); + } + + if (!this._cutline._projected || this._cutline._projectedWithMap !== map) { + this._cutline._projected = this._cutline.latlng.map(projectCoordsList); + this._cutline._projectedWithMap = map; + } + + return this._cutline._projected; + }, + + _setCutline: async function (cutline, approx) { + let cutlinePromise = cutline; + if (typeof cutlinePromise === 'function') { + cutlinePromise = cutlinePromise(); + } + if (!cutlinePromise.then) { + cutlinePromise = Promise.resolve(cutlinePromise); + } + let cutlineCoords; + try { + cutlineCoords = await cutlinePromise; + } catch (_) { + // will be handled as empty later + } + + if (cutlineCoords) { + this._cutline = { + latlng: cutlineCoords, + bounds: cutlineCoords.map(coordsListBounds), + approx: approx, + }; + } else { + this._cutline = null; + } + }, + + _updateProjectedCutline: function () { + const map = this._map; + if (!this._cutline || !map || (this._cutline._projected && this._cutline._projectedWithMap !== map)) { + return; + } + function projectCoordsList(coordsList) { + return coordsList.map(([lng, lat]) => map.project([lat, lng], 0)); + } + + this._cutline._projected = this._cutline.latlng.map(projectCoordsList); + this._cutline._projectedWithMap = map; + }, +}); diff --git a/src/lib/leaflet.layer.rasterize/TileLayer.js b/src/lib/leaflet.layer.rasterize/TileLayer.js @@ -79,4 +79,26 @@ const TileLayerGrabMixin = L.Util.extend({}, GridLayerGrabMixin, { // CanvasLayerGrabMixin has been deleted -L.TileLayer.include(TileLayerGrabMixin); +const TileLayerWithCutlineGrabMixin = L.Util.extend({}, TileLayerGrabMixin, { + waitTilesReadyToGrab: function() { + return TileLayerGrabMixin.waitTilesReadyToGrab.call(this).then(() => this._cutlinePromise); + }, + + tileImagePromiseFromCoords: function(coords, printOptions) { + const result = TileLayerGrabMixin.tileImagePromiseFromCoords.call(this, coords, printOptions); + if (!printOptions.rawData && this._cutlinePromise && this.isCutlineIntersectingTile(coords, true)) { + result.tilePromise = result.tilePromise.then((img) => { + if (!img) { + return img; + } + const clipped = document.createElement('canvas'); + return new Promise((resolve) => { + this._drawTileClippedByCutline(coords, img, clipped, () => resolve(clipped)); + }); + }); + } + return result; + } +}); + +L.TileLayer.include(TileLayerWithCutlineGrabMixin);