From e86d51a961dc56e66c05ec6f7c84e7266d279c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Wed, 15 Jan 2020 16:54:17 +0100 Subject: [PATCH] feat: bing Maps layer --- package.json | 1 + src/Map.css | 7 +++ src/Map.js | 21 ++++++--- src/index.js | 2 +- src/layers/BingLayer.js | 96 +++++++++++++++++++++++++++++++++-------- src/layers/Layer.js | 7 ++- src/utils/geo.js | 29 ++++++++----- src/utils/jsonp.js | 74 ------------------------------- yarn.lock | 5 +++ 9 files changed, 133 insertions(+), 109 deletions(-) delete mode 100644 src/utils/jsonp.js diff --git a/package.json b/package.json index cacec071..a35959b9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@turf/area": "^6.0.1", "@turf/bbox": "^6.0.1", "@turf/circle": "^6.0.1", + "fetch-jsonp": "^1.1.3", "lodash.throttle": "^4.1.1", "mapbox-gl": "^1.6.1", "mapboxgl-spiderifier": "^1.0.9", diff --git a/src/Map.css b/src/Map.css index 1c77f7bc..0adfecb5 100644 --- a/src/Map.css +++ b/src/Map.css @@ -2,6 +2,12 @@ box-shadow: 0 3px 14px rgba(0,0,0,0.4); } +#dhis2-map-container .bing-maps-logo { + position: absolute; + bottom: 0; + left: 0; +} + #dhis2-map-container .mapboxgl-popup em { font-style: normal; font-weight: bold; @@ -25,3 +31,4 @@ font-weight: bold; padding-right: 5px; } + diff --git a/src/Map.js b/src/Map.js index 8c186a38..1543686b 100644 --- a/src/Map.js +++ b/src/Map.js @@ -1,4 +1,4 @@ -import mapboxgl from 'mapbox-gl' +import { Map, AttributionControl, Popup } from 'mapbox-gl' import 'mapbox-gl/dist/mapbox-gl.css' import { Evented } from 'mapbox-gl' import getControl from './controls' @@ -8,7 +8,7 @@ import { getBoundsFromLayers } from './utils/geometry' import syncMaps from './utils/sync' import './Map.css' -export class Map extends Evented { +export class MapGL extends Evented { // Returns true if the layer type is supported static hasLayerSupport(type) { return !!layerTypes[type] @@ -17,7 +17,7 @@ export class Map extends Evented { constructor(el) { super() - this._mapgl = new mapboxgl.Map({ + this._mapgl = new Map({ container: el, style: { version: 8, @@ -26,8 +26,12 @@ export class Map extends Evented { glyphs: 'http://fonts.openmaptiles.org/{fontstack}/{range}.pbf', // TODO: Host ourseleves }, maxZoom: 18, + attributionControl: false, }) + this._attributionControl = new AttributionControl() + this._mapgl.addControl(this._attributionControl) + this._mapgl.on('load', evt => this.fire('ready', this)) this._mapgl.on('click', evt => this.onClick(evt)) this._mapgl.on('contextmenu', evt => this.onContextMenu(evt)) @@ -37,6 +41,8 @@ export class Map extends Evented { this._layers = [] this._controls = {} + + // console.log('AttributionControl', this._attributionControl); } fitBounds(bounds) { @@ -261,7 +267,7 @@ export class Map extends Evented { } openPopup(content, lnglat, onClose, offset) { - this._popup = new mapboxgl.Popup({ + this._popup = new Popup({ offset: offset, maxWidth: 'auto', }) @@ -281,6 +287,11 @@ export class Map extends Evented { } } + // Only called within the API + _updateAttributions() { + this._attributionControl._updateAttributions() + } + _createClickEvent(evt) { const { lngLat, originalEvent } = evt const type = 'click' @@ -295,4 +306,4 @@ export class Map extends Evented { } } -export default Map +export default MapGL diff --git a/src/index.js b/src/index.js index b9ab0eb5..8b86ee93 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,6 @@ * Wrapper around Mapbox GL JS for DHIS2 Maps */ -import { Map } from './Map' +import Map from './Map' export default Map diff --git a/src/layers/BingLayer.js b/src/layers/BingLayer.js index 4ad4f0b1..143a0f62 100644 --- a/src/layers/BingLayer.js +++ b/src/layers/BingLayer.js @@ -1,5 +1,6 @@ +import fetchJsonp from 'fetch-jsonp' import Layer from './Layer' -import { fetchJsonp } from '../utils/jsonp' +import { bboxIntersect } from '../utils/geo' const key = 'AotYGLQC0RDcofHC5pWLaW7k854n-6T9mTunsev9LEFwVqGaVnG8b4KERNY9PeKA' // TODO: Don't push! @@ -8,19 +9,27 @@ const key = 'AotYGLQC0RDcofHC5pWLaW7k854n-6T9mTunsev9LEFwVqGaVnG8b4KERNY9PeKA' / // https://github.com/mapbox/mapbox-gl-js/issues/4137 // https://github.com/mapbox/mapbox-gl-native/issues/4653 // https://github.com/digidem/leaflet-bing-layer -// TODO: Support for different locales // mkt={culture} +// https://github.com/shramov/leaflet-plugins/blob/master/layer/tile/Bing.md class BingLayer extends Layer { async createSource() { - const { imageUrl, imageUrlSubdomains } = await this.loadMetaData() - const tiles = imageUrlSubdomains.map(subdomain => - imageUrl.replace('{subdomain}', subdomain) - ) + const { + imageUrl, + imageUrlSubdomains, + imageryProviders, + brandLogoUri, + } = await this.loadMetaData() + + this._brandLogoUri = brandLogoUri + this._imageryProviders = imageryProviders + + console.log(imageUrl) this.setSource(this.getId(), { type: 'raster', - tiles, - tileSize: 256, - attribution: '', + tiles: imageUrlSubdomains.map( + subdomain => imageUrl.replace('{subdomain}', subdomain) // + '&dpi=d2&device=mobile' // TODO + ), + tileSize: 256, // default is 512 }) } @@ -34,17 +43,34 @@ class BingLayer extends Layer { async addTo(map) { await this.createSource() + this.createLayer() super.addTo(map) + + this.getMapGL().on('moveend', this.updateAttribution) + this.updateAttribution() + this.addBingMapsLogo() } - // TODO: Called before map is added - setIndex = () => {} + onRemove() { + const mapgl = this.getMapGL() + + mapgl.off('moveend', this.updateAttribution) + + if (this._brandLogoImg) { + mapgl.getContainer().removeChild(this._brandLogoImg) + } + } async loadMetaData() { const { style = 'Road' } = this.options - const metaDataUrl = `http://dev.virtualearth.net/REST/V1/Imagery/Metadata/${style}?output=json&include=ImageryProviders&key=${key}` + + // https://docs.microsoft.com/en-us/bingmaps/rest-services/common-parameters-and-types/supported-culture-codes + const culture = 'en-GB' + + // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata + const metaDataUrl = `http://dev.virtualearth.net/REST/V1/Imagery/Metadata/${style}?output=json&include=ImageryProviders&culture=${culture}&key=${key}` return fetchJsonp(metaDataUrl, { jsonpCallback: 'jsonp' }) .then(response => response.json()) @@ -60,17 +86,51 @@ class BingLayer extends Layer { ) } - const resource = metaData.resourceSets[0].resources[0] - const { imageUrl, imageryProviders, imageUrlSubdomains } = resource + const { brandLogoUri, resourceSets } = metaData return { - imageUrl, - imageryProviders, - imageUrlSubdomains, + brandLogoUri, + ...resourceSets[0].resources[0], } } - updateAttribution() {} + addBingMapsLogo() { + const container = this.getMap().getContainer() + const img = document.createElement('img') + + img.src = this._brandLogoUri + img.className = 'bing-maps-logo' + + container.appendChild(img) + + this._brandLogoImg = img + } + + getAttribution() { + const mapgl = this.getMapGL() + const [lngLat1, lngLat2] = mapgl.getBounds().toArray() + const mapBbox = [...lngLat1.reverse(), ...lngLat2.reverse()] + const mapZoom = mapgl.getZoom() < 1 ? 1 : mapgl.getZoom() + + const providers = this._imageryProviders.filter(({ coverageAreas }) => + coverageAreas.some( + ({ bbox, zoomMin, zoomMax }) => + bboxIntersect(bbox, mapBbox) && + mapZoom >= zoomMin && + mapZoom <= zoomMax + ) + ) + + return providers.map(p => p.attribution).join(', ') + } + + updateAttribution = () => { + const source = this.getMapGL().getSource(this.getId()) + + source.attribution = this.getAttribution() + + this.getMap()._updateAttributions() + } } export default BingLayer diff --git a/src/layers/Layer.js b/src/layers/Layer.js index d44dbaf8..02914fce 100644 --- a/src/layers/Layer.js +++ b/src/layers/Layer.js @@ -179,7 +179,12 @@ class Layer extends Evented { setIndex(index = 0) { this.options.index = index - this.getMap().orderLayers() + + const map = this.getMap() + + if (map) { + map.orderLayers() + } } getIndex() { diff --git a/src/utils/geo.js b/src/utils/geo.js index c99aac32..cd691252 100644 --- a/src/utils/geo.js +++ b/src/utils/geo.js @@ -1,21 +1,30 @@ const earthRadius = 6378137 +const tile2lon = (x, z) => (x / Math.pow(2, z)) * 360 - 180 + +const tile2lat = (y, z) => { + const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z) + return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))) +} + // Returns resolution in meters at zoom export const getZoomResolution = zoom => (2 * Math.PI * earthRadius) / 256 / Math.pow(2, zoom) // Returns lng/lat bounds for a tile export const getTileBBox = (x, y, z) => { - var e = tile2lon(x + 1, z) - var w = tile2lon(x, z) - var s = tile2lat(y + 1, z) - var n = tile2lat(y, z) + const e = tile2lon(x + 1, z) + const w = tile2lon(x, z) + const s = tile2lat(y + 1, z) + const n = tile2lat(y, z) return [w, s, e, n].join(',') } -const tile2lon = (x, z) => (x / Math.pow(2, z)) * 360 - 180 - -const tile2lat = (y, z) => { - const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z) - return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))) -} +// Returns true if two bbox'es intersects +export const bboxIntersect = (bbox1, bbox2) => + !( + bbox1[0] > bbox2[2] || + bbox1[2] < bbox2[0] || + bbox1[3] < bbox2[1] || + bbox1[1] > bbox2[3] + ) diff --git a/src/utils/jsonp.js b/src/utils/jsonp.js deleted file mode 100644 index 6e7c89a5..00000000 --- a/src/utils/jsonp.js +++ /dev/null @@ -1,74 +0,0 @@ -// Inspired by https://github.com/digidem/leaflet-bing-layer - -const defaultOptions = { - timeout: 5000, - jsonpCallback: 'callback', - jsonpCallbackFunction: null, -} - -const generateCallbackFunction = () => - `jsonp_${Date.now()}_${Math.ceil(Math.random() * 100000)}` - -// Known issue: Will throw 'Uncaught ReferenceError: callback_*** is not defined' error if request timeout -const clearFunction = functionName => { - // IE8 throws an exception when you try to delete a property on window - // http://stackoverflow.com/a/1824228/751089 - try { - delete window[functionName] - } catch (e) { - window[functionName] = undefined - } -} - -const removeScript = scriptId => { - const script = document.getElementById(scriptId) - document.getElementsByTagName('head')[0].removeChild(script) -} - -export const fetchJsonp = (url, options = {}) => { - const timeout = - options.timeout != null ? options.timeout : defaultOptions.timeout - const jsonpCallback = - options.jsonpCallback != null - ? options.jsonpCallback - : defaultOptions.jsonpCallback - let timeoutId - - return new Promise((resolve, reject) => { - const callbackFunction = - options.jsonpCallbackFunction || generateCallbackFunction() - - window[callbackFunction] = response => { - resolve({ - ok: true, - json: () => Promise.resolve(response), - }) - - if (timeoutId) { - clearTimeout(timeoutId) - } - - removeScript(jsonpCallback + '_' + callbackFunction) - clearFunction(callbackFunction) - } - - url += url.indexOf('?') === -1 ? '?' : '&' - - const jsonpScript = document.createElement('script') - - jsonpScript.setAttribute( - 'src', - url + jsonpCallback + '=' + callbackFunction - ) - jsonpScript.id = jsonpCallback + '_' + callbackFunction - - document.getElementsByTagName('head')[0].appendChild(jsonpScript) - - timeoutId = setTimeout(() => { - reject(new Error('JSONP request to ' + url + ' timed out')) - - clearFunction(callbackFunction) - removeScript(jsonpCallback + '_' + callbackFunction) - }, timeout) - }) -} diff --git a/yarn.lock b/yarn.lock index fd13363a..b74449f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2663,6 +2663,11 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fetch-jsonp@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fetch-jsonp/-/fetch-jsonp-1.1.3.tgz#9eb9e585ba08aaf700563538d17bbebbcd5a3db2" + integrity sha1-nrnlhboIqvcAVjU40Xu+u81aPbI= + figures@^1.5.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"