UNPKG

leaflet-overpass-layer

Version:

This is an simple way to bring OSM data as POI overlay on your leaflet Map

559 lines (443 loc) 14.1 kB
import L from 'leaflet'; import ClipperLib from 'js-clipper'; import { openDB } from 'idb'; import './OverPassLayer.css'; import './MinZoomIndicator'; const OverPassLayer = L.FeatureGroup.extend({ options: { debug: false, minZoom: 15, endPoint: 'https://overpass-api.de/api/', query: '(node({{bbox}})[organic];node({{bbox}})[second_hand];);out qt;', loadedBounds: [], markerIcon: null, timeout: 30 * 1000, // Milliseconds retryOnTimeout: false, noInitialRequest: false, cacheEnabled: false, cacheTTL: 1800, // Seconds beforeRequest() {}, afterRequest() {}, onSuccess(data) { for (let i = 0; i < data.elements.length; i++) { let pos; let marker; const e = data.elements[i]; if (e.id in this._ids) { continue; } this._ids[e.id] = true; if (e.type === 'node') { pos = L.latLng(e.lat, e.lon); } else { pos = L.latLng(e.center.lat, e.center.lon); } if (this.options.markerIcon) { marker = L.marker(pos, { icon: this.options.markerIcon }); } else { marker = L.circle(pos, 20, { stroke: false, fillColor: '#E54041', fillOpacity: 0.9 }); } const popupContent = this._getPoiPopupHTML(e.tags, e.id); const popup = L.popup().setContent(popupContent); marker.bindPopup(popup); this._markers.addLayer(marker); } }, onError() {}, onTimeout() {}, minZoomIndicatorEnabled: true, minZoomIndicatorOptions: { minZoomMessageNoLayer: 'No layer assigned', minZoomMessage: 'Current zoom Level: CURRENTZOOM. Data are visible at Level: MINZOOMLEVEL.' } }, initialize(options) { L.Util.setOptions(this, options); this._ids = {}; this._loadedBounds = options.loadedBounds || []; this._requestInProgress = false; if (this.options.cacheEnabled) { this._initCacheDb(); } }, async _initCacheDb() { this._cacheDb = await openDB('leaflet-overpass-layer', 1, { upgrade(db) { db.createObjectStore('cache', { autoIncrement: true }); } }); const layerAddedToMap = !!this._map; if (layerAddedToMap) { this._loadCachedItems(); } }, _getPoiPopupHTML(tags, id) { let row; const link = document.createElement('a'); const table = document.createElement('table'); const div = document.createElement('div'); link.href = `https://www.openstreetmap.org/edit?editor=id&node=${id}`; link.appendChild(document.createTextNode('Edit this entry in iD')); table.style.borderSpacing = '10px'; table.style.borderCollapse = 'separate'; for (const key in tags) { row = table.insertRow(0); row.insertCell(0).appendChild(document.createTextNode(key)); row.insertCell(1).appendChild(document.createTextNode(tags[key])); } div.appendChild(link); div.appendChild(table); return div; }, _buildRequestBox(bounds) { return L.rectangle(bounds, { bounds: bounds, color: '#204a87', stroke: false, fillOpacity: 0.1, clickable: false }); }, _addRequestBox(box) { return this._requestBoxes.addLayer(box); }, _getRequestBoxes() { return this._requestBoxes.getLayers(); }, _removeRequestBox(box) { this._requestBoxes.removeLayer(box); }, _removeRequestBoxes() { return this._requestBoxes.clearLayers(); }, _addResponseBox(box) { return this._responseBoxes.addLayer(box); }, _addResponseBoxes(requestBoxes) { this._removeRequestBoxes(); requestBoxes.forEach(box => { box.setStyle({ color: 'black', weight: 2 }); this._addResponseBox(box); }); }, _isFullyLoadedBounds(bounds, loadedBounds) { if (loadedBounds.length === 0) { return false; } const subjectClips = this._buildClipsFromBounds([bounds]); const knownClips = this._buildClipsFromBounds(loadedBounds); const clipper = new ClipperLib.Clipper(); const solutionPolyTree = new ClipperLib.PolyTree(); clipper.AddPaths(subjectClips, ClipperLib.PolyType.ptSubject, true); clipper.AddPaths(knownClips, ClipperLib.PolyType.ptClip, true); clipper.Execute( ClipperLib.ClipType.ctDifference, solutionPolyTree, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero ); const solutionExPolygons = ClipperLib.JS.PolyTreeToExPolygons( solutionPolyTree ); if (solutionExPolygons.length === 0) { return true; } else { return false; } }, _getLoadedBounds() { return this._loadedBounds; }, _addLoadedBounds(bounds) { this._loadedBounds.push(bounds); }, _buildClipsFromBounds(bounds) { return bounds.map(bound => [ { X: bound._southWest.lng * 1000000, Y: bound._southWest.lat * 1000000 }, { X: bound._southWest.lng * 1000000, Y: bound._northEast.lat * 1000000 }, { X: bound._northEast.lng * 1000000, Y: bound._northEast.lat * 1000000 }, { X: bound._northEast.lng * 1000000, Y: bound._southWest.lat * 1000000 } ]); }, _buildBoundsFromClips(clips) { return clips.map(clip => L.latLngBounds( L.latLng(clip[0].Y / 1000000, clip[0].X / 1000000).wrap(), L.latLng(clip[2].Y / 1000000, clip[2].X / 1000000).wrap() ) ); }, _buildOverpassQueryFromQueryAndBounds(query, bounds) { const sw = bounds._southWest; const ne = bounds._northEast; const coordinates = [sw.lat, sw.lng, ne.lat, ne.lng].join(','); query = query.replace(/(\/\/.*)/g, ''); query = query.replace(/(\{\{bbox\}\})/g, coordinates); return query; }, _buildOverpassUrlFromEndPointAndQuery(endPoint, query) { return `${endPoint}interpreter?data=[out:json];${query}`; }, _buildLargerBounds(bounds) { const width = Math.abs(bounds._northEast.lng - bounds._southWest.lng); const height = Math.abs(bounds._northEast.lat - bounds._southWest.lat); const biggestDimension = width > height ? width : height; bounds._southWest.lat -= biggestDimension / 2; bounds._southWest.lng -= biggestDimension / 2; bounds._northEast.lat += biggestDimension / 2; bounds._northEast.lng += biggestDimension / 2; return L.latLngBounds( L.latLng(bounds._southWest.lat, bounds._southWest.lng).wrap(), L.latLng(bounds._northEast.lat, bounds._northEast.lng).wrap() ); }, _setRequestInProgress(isInProgress) { this._requestInProgress = isInProgress; }, _isRequestInProgress() { return this._requestInProgress; }, _hasNextRequest() { if (this._nextRequest) { return true; } return false; }, _getNextRequest() { return this._nextRequest; }, _setNextRequest(nextRequest) { this._nextRequest = nextRequest; }, _removeNextRequest() { this._nextRequest = null; }, _prepareRequest() { if (this._map.getZoom() < this.options.minZoom) { return false; } const bounds = this._buildLargerBounds(this._map.getBounds()); const nextRequest = this._sendRequest.bind(this, bounds); if (this._isRequestInProgress()) { this._setNextRequest(nextRequest); } else { this._removeNextRequest(); nextRequest(); } }, _sendRequest(bounds) { const loadedBounds = this._getLoadedBounds(); if (this._isFullyLoadedBounds(bounds, loadedBounds)) { this._setRequestInProgress(false); return; } const requestBounds = this._buildLargerBounds(bounds); const url = this._buildOverpassUrlFromEndPointAndQuery( this.options.endPoint, this._buildOverpassQueryFromQueryAndBounds( this.options.query, requestBounds ) ); const request = new XMLHttpRequest(); const beforeRequestResult = this.options.beforeRequest.call(this); if (beforeRequestResult === false) { this.options.afterRequest.call(this); return; } this._setRequestInProgress(true); if (this.options.debug) { this._addRequestBox(this._buildRequestBox(requestBounds)); } request.open('GET', url, true); request.timeout = this.options.timeout; request.ontimeout = () => this._onRequestTimeout(request, url, requestBounds); request.onload = () => this._onRequestLoad(request, requestBounds); request.send(); }, _onRequestLoad(xhr, bounds) { if (xhr.status >= 200 && xhr.status < 400) { let result = JSON.parse(xhr.response); this.options.onSuccess.call(this, result); const cacheDbInitialized = typeof this._cacheDb !== 'undefined'; if (this.options.cacheEnabled && cacheDbInitialized) { let expireDate = new Date(); expireDate.setSeconds(expireDate.getSeconds() + this.options.cacheTTL); this._getCachedItems().then(cachedItems => { let existingCacheKey = undefined; cachedItems.forEach((cachedItem, key) => { const queryEquals = cachedItem.query === this.options.query; const northEast = L.latLng(cachedItem.bounds._northEast); const southWest = L.latLng(cachedItem.bounds._southWest); const cachedItemBounds = L.latLngBounds(northEast, southWest); if (queryEquals && bounds.equals(cachedItemBounds)) { existingCacheKey = key; } }); this._cacheDb.put( 'cache', { query: this.options.query, result: result, bounds: bounds, expires: expireDate }, existingCacheKey ); }); } this._onRequestLoadCallback(bounds); } else { this._onRequestErrorCallback(bounds); this.options.onError.call(this, xhr); } this._onRequestCompleteCallback(bounds); }, _onRequestTimeout(xhr, url, bounds) { this.options.onTimeout.call(this, xhr); if (this.options.retryOnTimeout) { this._sendRequest(url); } else { this._onRequestErrorCallback(bounds); this._onRequestCompleteCallback(bounds); } }, _onRequestLoadCallback(bounds) { this._addLoadedBounds(bounds); if (this.options.debug) { this._addResponseBoxes(this._getRequestBoxes()); } }, _onRequestErrorCallback(bounds) { if (this.options.debug) { this._removeRequestBox(this._buildRequestBox(bounds)); } }, _onRequestCompleteCallback() { this.options.afterRequest.call(this); if (this._hasNextRequest()) { const nextRequest = this._getNextRequest(); this._removeNextRequest(); nextRequest(); } else { this._setRequestInProgress(false); } }, async _getCachedItems() { const cacheDbInitialized = typeof this._cacheDb !== 'undefined'; if (!cacheDbInitialized) return; const keys = await this._cacheDb.getAllKeys('cache'); const items = await this._cacheDb.getAll('cache'); const itemsMap = new Map(); items.forEach((item, i) => { const key = keys[i]; itemsMap.set(key, item); }); return itemsMap; }, async _loadCachedItems() { const cacheDbInitialized = typeof this._cacheDb !== 'undefined'; if (!cacheDbInitialized || !this._map) return; const cachedItems = await this._getCachedItems(); cachedItems.forEach((cachedItem, key) => { if (new Date() > cachedItem.expires) { this._cacheDb.delete('cache', key); } else { if (cachedItem.query === this.options.query) { this.options.onSuccess.call(this, cachedItem.result); this._onRequestLoadCallback(cachedItem.bounds); } } }); }, onAdd(map) { this._map = map; if (this.options.minZoomIndicatorEnabled === true) { if (this._map.zoomIndicator) { this._zoomControl = this._map.zoomIndicator; this._zoomControl._addLayer(this); } else { this._zoomControl = new L.Control.MinZoomIndicator( this.options.minZoomIndicatorOptions ); this._map.addControl(this._zoomControl); this._zoomControl._addLayer(this); } } if (this.options.debug) { this._requestBoxes = L.featureGroup().addTo(this._map); this._responseBoxes = L.featureGroup().addTo(this._map); } this._markers = L.featureGroup().addTo(this._map); if (this.options.cacheEnabled) { const cacheDbInitialized = typeof this._cacheDb !== 'undefined'; if (cacheDbInitialized) { this._loadCachedItems(); } } if (!this.options.noInitialRequest) { this._prepareRequest(); } this._map.on('moveend', this._prepareRequest, this); }, onRemove(map) { L.LayerGroup.prototype.onRemove.call(this, map); if (this.options.minZoomIndicatorEnabled === true) { if (this._map.zoomIndicator) { this._zoomControl._removeLayer(this); this._map.removeControl(this._zoomControl); this._zoomControl = null; this._map.zoomIndicator = null; } } if (this.options.debug) { this._map.removeLayer(this._requestBoxes); this._map.removeLayer(this._responseBoxes); } this._resetData(); this._map.removeLayer(this._markers); this._map.off('moveend', this._prepareRequest, this); }, setQuery(query) { this.options.query = query; this._resetData(); this._prepareRequest(); }, _resetData() { this._ids = {}; this._loadedBounds = []; this._requestInProgress = false; this._markers.clearLayers(); if (this.options.debug) { this._requestBoxes.clearLayers(); this._responseBoxes.clearLayers(); } }, getData() { return this._data; } }); L.OverPassLayer = OverPassLayer; L.overpassLayer = options => new L.OverPassLayer(options); export default OverPassLayer;