├── CHANGELOG.md ├── README.md ├── index.html ├── index.js ├── leaflet-bing-layer.js ├── leaflet-bing-layer.min.js └── package.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [3.3.0] - 2018-03-14 7 | 8 | - ADDED: `options.style` to use [custom map styles](https://msdn.microsoft.com/en-us/library/mt823632.aspx) 9 | - ADDED: `AerialWithLabelsOnDemand` imagery set 10 | - ADDED: `minNativeZoom` and `maxNativeZoom` options 11 | - FIXED: imageryProviders error [#25](https://github.com/digidem/leaflet-bing-layer/pull/25) 12 | 13 | ## [v3.2.0] - 2017-08-09 14 | 15 | - CHANGED: Use `https` requests 16 | - ADDED: Additional imagery sets (`RoadOnDemand`, `CanvasLight`, `CanvasDark`, `CanvasGray`, `OrdnanceSurvey`) 17 | 18 | ## [v3.1.0] - 2016-04-29 19 | 20 | - ADDED: Use `https` for Bing API requests. 21 | 22 | ## [v3.0.1] - 2015-12-13 23 | 24 | - FIXED: options.BingMapsKey backwards compatability 25 | - FIXED: options.bingMapsKey was not working for getMetaData 26 | - FIXED: catch errors (and log to console) for jsonp 27 | 28 | ## [v3.0.0] - 2015-12-08 29 | 30 | - FIXED: **[BREAKING]** Export factory function on `L.tileLayer.bing` not `L.TileLayer.bing` 31 | - CHANGED: BingMapsKey is now passed on `options.bingMapsKey` (`options.BingMapsKey` will still work, but for convention this should start with a lowercase character) 32 | - IMPROVED: Package with browserify and require dependencies 33 | - IMPROVED: Throws error if invalid imagerySet is passed as option 34 | - ADDED: `getMetaData` method 35 | 36 | ## v2.0.2 - 2015-12-03 37 | 38 | Initial release 39 | 40 | [Unreleased]: https://github.com/digidem/leaflet-bing-layer/compare/v3.2.0...HEAD 41 | [v3.2.0]: https://github.com/digidem/leaflet-bing-layer/compare/v3.1.0...v3.2.0 42 | [v3.1.0]: https://github.com/digidem/leaflet-bing-layer/compare/v3.0.1...v3.1.0 43 | [v3.0.1]: https://github.com/digidem/leaflet-bing-layer/compare/v3.0.0...v3.0.1 44 | [v3.0.0]: https://github.com/digidem/leaflet-bing-layer/compare/v2.0.2...v3.0.0 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaflet-bing-layer 2 | 3 | Bing Maps Layer for Leaflet v1.0.0 4 | 5 | 6 | ### L.TileLayer.Bing(options|BingMapsKey) 7 | 8 | Create a new Bing Maps Layer. Depends on [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which needs a polyfill for [older browsers](http://caniuse.com/#feat=promises). 9 | 10 | ### Parameters 11 | 12 | | parameter | type | description | 13 | | ----------------------------- | -------------- | ----------------------------------------------------------------------------------------------------- | 14 | | `options` | string\|object | A valid [Bing Maps Key](https://msdn.microsoft.com/en-us/library/ff428642.aspx) or an `options` object. `options` inherits from [L.TileLayer options](http://mourner.github.io/Leaflet/reference.html#tilelayer-options) (e.g. you can use `minZoom` and `opacity` and etc) | 15 | | `options.bingMapsKey` | string | A valid Bing Maps Key [_required_] | 16 | | `[options.imagerySet]` | string | _optional:_ [Imagery Type](https://msdn.microsoft.com/en-us/library/ff701716.aspx) [_default=Aerial_]
- `Aerial` - Aerial imagery
- `AerialWithLabels` - Aerial imagery with a road overlay
- `AerialWithLabelsOnDemand` - Aerial imagery with on-demand road overlay.
- `CanvasDark` - A dark version of the road maps.
- `CanvasLight` - A lighter version of the road maps which also has some of the details such as hill shading disabled.
- `CanvasGray` - A grayscale version of the road maps.
- `Road` - Roads without additional imagery. Uses the legacy static tile service.
- `RoadOnDemand` - Roads without additional imagery. Uses the dynamic tile service.
- `OrdnanceSurvey` - Ordnance Survey imagery. This imagery is visible only for the London area.
**[Not supported](https://social.msdn.microsoft.com/Forums/en-US/3d80d4a6-f4c9-4926-a336-e0d545b1ef3c/is-it-possible-to-retrieve-birdseye-map-tiles-using-rest-services?forum=bingmapsservices)**: `Birdseye` and `BirdseyeWithLabels` | 17 | | `[options.culture]` | string | _optional:_ Language for labels, [see options](https://msdn.microsoft.com/en-us/library/hh441729.aspx) [_default=en_US_] | 18 | | `[options.style]` | string | _optional:_ Use a [custom map style](https://msdn.microsoft.com/en-us/library/mt823632.aspx) - only works with the `AerialWithLabelsOnDemand` and `RoadOnDemand` imagerySet options. | 19 | 20 | Other options are passed through to a [Leaflet TileLayer](http://leafletjs.com/reference-1.3.0.html#tilelayer-l-tilelayer) 21 | 22 | ### Methods 23 | 24 | | Method | Returns | Description | 25 | | ---------- | -------------- | ------------- | 26 | | `getMetaData( latlng, zoom)` | `Promise` | Get the [Bing Imagery metadata](https://msdn.microsoft.com/en-us/library/ff701712.aspx) for a specific [`LatLng`](http://leafletjs.com/reference.html#latlng) and zoom level. `latLng` or `zoom` are optional *if* the layer is attached to a map, they default to current map center and zoom. Returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves to the metadata JSON from Bing | 27 | 28 | ### Example 29 | 30 | ```js 31 | var map = L.map('map').setView([51.505, -0.09], 13) 32 | L.tileLayer.bing(MyBingMapsKey).addTo(map) 33 | ``` 34 | 35 | [Live Example](http://digidem.github.io/leaflet-bing-layer/) see [index.html](index.html) 36 | 37 | ### License 38 | 39 | MIT 40 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Leaflet Bing Maps Layer 7 | 8 | 9 | 10 | 22 | 23 | 24 | 25 |
26 | 27 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var L = require('leaflet') 2 | var fetchJsonp = require('fetch-jsonp') 3 | var bboxIntersect = require('bbox-intersect') 4 | 5 | /** 6 | * Converts tile xyz coordinates to Quadkey 7 | * @param {Number} x 8 | * @param {Number} y 9 | * @param {Number} z 10 | * @return {Number} Quadkey 11 | */ 12 | function toQuadKey (x, y, z) { 13 | var index = '' 14 | for (var i = z; i > 0; i--) { 15 | var b = 0 16 | var mask = 1 << (i - 1) 17 | if ((x & mask) !== 0) b++ 18 | if ((y & mask) !== 0) b += 2 19 | index += b.toString() 20 | } 21 | return index 22 | } 23 | 24 | /** 25 | * Converts Leaflet BBoxString to Bing BBox 26 | * @param {String} bboxString 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' 27 | * @return {Array} [south_lat, west_lng, north_lat, east_lng] 28 | */ 29 | function toBingBBox (bboxString) { 30 | var bbox = bboxString.split(',') 31 | return [bbox[1], bbox[0], bbox[3], bbox[2]] 32 | } 33 | 34 | var VALID_IMAGERY_SETS = [ 35 | 'Aerial', 36 | 'AerialWithLabels', 37 | 'AerialWithLabelsOnDemand', 38 | 'Road', 39 | 'RoadOnDemand', 40 | 'CanvasLight', 41 | 'CanvasDark', 42 | 'CanvasGray', 43 | 'OrdnanceSurvey' 44 | ] 45 | 46 | var DYNAMIC_IMAGERY_SETS = [ 47 | 'AerialWithLabelsOnDemand', 48 | 'RoadOnDemand' 49 | ] 50 | 51 | /** 52 | * Create a new Bing Maps layer. 53 | * @param {string|object} options Either a [Bing Maps Key](https://msdn.microsoft.com/en-us/library/ff428642.aspx) or an options object 54 | * @param {string} options.BingMapsKey A valid Bing Maps Key (required) 55 | * @param {string} [options.imagerySet=Aerial] Type of imagery, see https://msdn.microsoft.com/en-us/library/ff701716.aspx 56 | * @param {string} [options.culture='en-US'] Language for labels, see https://msdn.microsoft.com/en-us/library/hh441729.aspx 57 | * @return {L.TileLayer} A Leaflet TileLayer to add to your map 58 | * 59 | * Create a basic map 60 | * @example 61 | * var map = L.map('map').setView([51.505, -0.09], 13) 62 | * L.TileLayer.Bing(MyBingMapsKey).addTo(map) 63 | */ 64 | L.TileLayer.Bing = L.TileLayer.extend({ 65 | options: { 66 | bingMapsKey: null, // Required 67 | imagerySet: 'Aerial', 68 | culture: 'en-US', 69 | minZoom: 1, 70 | minNativeZoom: 1, 71 | maxNativeZoom: 19 72 | }, 73 | 74 | statics: { 75 | METADATA_URL: 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/{imagerySet}?key={bingMapsKey}&include=ImageryProviders&uriScheme=https&c={culture}', 76 | POINT_METADATA_URL: 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/{imagerySet}/{lat},{lng}?zl={z}&key={bingMapsKey}&uriScheme=https&c={culture}' 77 | }, 78 | 79 | initialize: function (options) { 80 | if (typeof options === 'string') { 81 | options = { bingMapsKey: options } 82 | } 83 | if (options && options.BingMapsKey) { 84 | options.bingMapsKey = options.BingMapsKey 85 | console.warn('use options.bingMapsKey instead of options.BingMapsKey') 86 | } 87 | if (!options || !options.bingMapsKey) { 88 | throw new Error('Must supply options.BingMapsKey') 89 | } 90 | options = L.setOptions(this, options) 91 | if (VALID_IMAGERY_SETS.indexOf(options.imagerySet) < 0) { 92 | throw new Error("'" + options.imagerySet + "' is an invalid imagerySet, see https://github.com/digidem/leaflet-bing-layer#parameters") 93 | } 94 | if (options && options.style && DYNAMIC_IMAGERY_SETS.indexOf(options.imagerySet) < 0) { 95 | console.warn('Dynamic styles will only work with these imagerySet choices: ' + DYNAMIC_IMAGERY_SETS.join(', ')) 96 | } 97 | 98 | var metaDataUrl = L.Util.template(L.TileLayer.Bing.METADATA_URL, { 99 | bingMapsKey: this.options.bingMapsKey, 100 | imagerySet: this.options.imagerySet, 101 | culture: this.options.culture 102 | }) 103 | 104 | this._imageryProviders = [] 105 | this._attributions = [] 106 | 107 | // Keep a reference to the promise so we can use it later 108 | this._fetch = fetchJsonp(metaDataUrl, {jsonpCallback: 'jsonp'}) 109 | .then(function (response) { 110 | return response.json() 111 | }) 112 | .then(this._metaDataOnLoad.bind(this)) 113 | .catch(console.error.bind(console)) 114 | 115 | // for https://github.com/Leaflet/Leaflet/issues/137 116 | if (!L.Browser.android) { 117 | this.on('tileunload', this._onTileRemove) 118 | } 119 | }, 120 | 121 | createTile: function (coords, done) { 122 | var tile = document.createElement('img') 123 | 124 | L.DomEvent.on(tile, 'load', L.bind(this._tileOnLoad, this, done, tile)) 125 | L.DomEvent.on(tile, 'error', L.bind(this._tileOnError, this, done, tile)) 126 | 127 | if (this.options.crossOrigin) { 128 | tile.crossOrigin = '' 129 | } 130 | 131 | /* 132 | Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons 133 | http://www.w3.org/TR/WCAG20-TECHS/H67 134 | */ 135 | tile.alt = '' 136 | 137 | // Don't create closure if we don't have to 138 | if (this._url) { 139 | tile.src = this.getTileUrl(coords) 140 | } else { 141 | this._fetch.then(function () { 142 | tile.src = this.getTileUrl(coords) 143 | }.bind(this)).catch(function (e) { 144 | console.error(e) 145 | done(e) 146 | }) 147 | } 148 | 149 | return tile 150 | }, 151 | 152 | getTileUrl: function (coords) { 153 | var quadkey = toQuadKey(coords.x, coords.y, coords.z) 154 | var url = L.Util.template(this._url, { 155 | quadkey: quadkey, 156 | subdomain: this._getSubdomain(coords), 157 | culture: this.options.culture 158 | }) 159 | if (typeof this.options.style === 'string') { 160 | url += '&st=' + this.options.style 161 | } 162 | return url 163 | }, 164 | 165 | // Update the attribution control every time the map is moved 166 | onAdd: function (map) { 167 | map.on('moveend', this._updateAttribution, this) 168 | L.TileLayer.prototype.onAdd.call(this, map) 169 | this._attributions.forEach(function (attribution) { 170 | map.attributionControl.addAttribution(attribution) 171 | }) 172 | }, 173 | 174 | // Clean up events and remove attributions from attribution control 175 | onRemove: function (map) { 176 | map.off('moveend', this._updateAttribution, this) 177 | this._attributions.forEach(function (attribution) { 178 | map.attributionControl.removeAttribution(attribution) 179 | }) 180 | L.TileLayer.prototype.onRemove.call(this, map) 181 | }, 182 | 183 | /** 184 | * Get the [Bing Imagery metadata](https://msdn.microsoft.com/en-us/library/ff701712.aspx) 185 | * for a specific [`LatLng`](http://leafletjs.com/reference.html#latlng) 186 | * and zoom level. If either `latlng` or `zoom` is omitted and the layer is attached 187 | * to a map, the map center and current map zoom are used. 188 | * @param {L.LatLng} latlng 189 | * @param {Number} zoom 190 | * @return {Promise} Resolves to the JSON metadata 191 | */ 192 | getMetaData: function (latlng, zoom) { 193 | if (!this._map && (!latlng || !zoom)) { 194 | return Promise.reject(new Error('If layer is not attached to map, you must provide LatLng and zoom')) 195 | } 196 | latlng = latlng || this._map.getCenter() 197 | zoom = zoom || this._map.getZoom() 198 | var PointMetaDataUrl = L.Util.template(L.TileLayer.Bing.POINT_METADATA_URL, { 199 | bingMapsKey: this.options.bingMapsKey, 200 | imagerySet: this.options.imagerySet, 201 | z: zoom, 202 | lat: latlng.lat, 203 | lng: latlng.lng 204 | }) 205 | return fetchJsonp(PointMetaDataUrl, {jsonpCallback: 'jsonp'}) 206 | .then(function (response) { 207 | return response.json() 208 | }) 209 | .catch(console.error.bind(console)) 210 | }, 211 | 212 | _metaDataOnLoad: function (metaData) { 213 | if (metaData.statusCode !== 200) { 214 | throw new Error('Bing Imagery Metadata error: \n' + JSON.stringify(metaData, null, ' ')) 215 | } 216 | var resource = metaData.resourceSets[0].resources[0] 217 | this._url = resource.imageUrl 218 | this._imageryProviders = resource.imageryProviders || [] 219 | this.options.subdomains = resource.imageUrlSubdomains 220 | this._updateAttribution() 221 | return Promise.resolve() 222 | }, 223 | 224 | /** 225 | * Update the attribution control of the map with the provider attributions 226 | * within the current map bounds 227 | */ 228 | _updateAttribution: function () { 229 | var map = this._map 230 | if (!map || !map.attributionControl) return 231 | var zoom = map.getZoom() 232 | var bbox = toBingBBox(map.getBounds().toBBoxString()) 233 | this._fetch.then(function () { 234 | var newAttributions = this._getAttributions(bbox, zoom) 235 | var prevAttributions = this._attributions 236 | // Add any new provider attributions in the current area to the attribution control 237 | newAttributions.forEach(function (attr) { 238 | if (prevAttributions.indexOf(attr) > -1) return 239 | map.attributionControl.addAttribution(attr) 240 | }) 241 | // Remove any attributions that are no longer in the current area from the attribution control 242 | prevAttributions.filter(function (attr) { 243 | if (newAttributions.indexOf(attr) > -1) return 244 | map.attributionControl.removeAttribution(attr) 245 | }) 246 | this._attributions = newAttributions 247 | }.bind(this)) 248 | }, 249 | 250 | /** 251 | * Returns an array of attributions for given bbox and zoom 252 | * @private 253 | * @param {Array} bbox [west, south, east, north] 254 | * @param {Number} zoom 255 | * @return {Array} Array of attribution strings for each provider 256 | */ 257 | _getAttributions: function (bbox, zoom) { 258 | return this._imageryProviders.reduce(function (attributions, provider) { 259 | for (var i = 0; i < provider.coverageAreas.length; i++) { 260 | if (bboxIntersect(bbox, provider.coverageAreas[i].bbox) && 261 | zoom >= provider.coverageAreas[i].zoomMin && 262 | zoom <= provider.coverageAreas[i].zoomMax) { 263 | attributions.push(provider.attribution) 264 | return attributions 265 | } 266 | } 267 | return attributions 268 | }, []) 269 | } 270 | }) 271 | 272 | L.tileLayer.bing = function (options) { 273 | return new L.TileLayer.Bing(options) 274 | } 275 | 276 | module.exports = L.TileLayer.Bing 277 | -------------------------------------------------------------------------------- /leaflet-bing-layer.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0; i--) { 17 | var b = 0 18 | var mask = 1 << (i - 1) 19 | if ((x & mask) !== 0) b++ 20 | if ((y & mask) !== 0) b += 2 21 | index += b.toString() 22 | } 23 | return index 24 | } 25 | 26 | /** 27 | * Converts Leaflet BBoxString to Bing BBox 28 | * @param {String} bboxString 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' 29 | * @return {Array} [south_lat, west_lng, north_lat, east_lng] 30 | */ 31 | function toBingBBox (bboxString) { 32 | var bbox = bboxString.split(',') 33 | return [bbox[1], bbox[0], bbox[3], bbox[2]] 34 | } 35 | 36 | var VALID_IMAGERY_SETS = [ 37 | 'Aerial', 38 | 'AerialWithLabels', 39 | 'AerialWithLabelsOnDemand', 40 | 'Road', 41 | 'RoadOnDemand', 42 | 'CanvasLight', 43 | 'CanvasDark', 44 | 'CanvasGray', 45 | 'OrdnanceSurvey' 46 | ] 47 | 48 | var DYNAMIC_IMAGERY_SETS = [ 49 | 'AerialWithLabelsOnDemand', 50 | 'RoadOnDemand' 51 | ] 52 | 53 | /** 54 | * Create a new Bing Maps layer. 55 | * @param {string|object} options Either a [Bing Maps Key](https://msdn.microsoft.com/en-us/library/ff428642.aspx) or an options object 56 | * @param {string} options.BingMapsKey A valid Bing Maps Key (required) 57 | * @param {string} [options.imagerySet=Aerial] Type of imagery, see https://msdn.microsoft.com/en-us/library/ff701716.aspx 58 | * @param {string} [options.culture='en-US'] Language for labels, see https://msdn.microsoft.com/en-us/library/hh441729.aspx 59 | * @return {L.TileLayer} A Leaflet TileLayer to add to your map 60 | * 61 | * Create a basic map 62 | * @example 63 | * var map = L.map('map').setView([51.505, -0.09], 13) 64 | * L.TileLayer.Bing(MyBingMapsKey).addTo(map) 65 | */ 66 | L.TileLayer.Bing = L.TileLayer.extend({ 67 | options: { 68 | bingMapsKey: null, // Required 69 | imagerySet: 'Aerial', 70 | culture: 'en-US', 71 | minZoom: 1, 72 | minNativeZoom: 1, 73 | maxNativeZoom: 19 74 | }, 75 | 76 | statics: { 77 | METADATA_URL: 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/{imagerySet}?key={bingMapsKey}&include=ImageryProviders&uriScheme=https', 78 | POINT_METADATA_URL: 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/{imagerySet}/{lat},{lng}?zl={z}&key={bingMapsKey}&uriScheme=https' 79 | }, 80 | 81 | initialize: function (options) { 82 | if (typeof options === 'string') { 83 | options = { bingMapsKey: options } 84 | } 85 | if (options && options.BingMapsKey) { 86 | options.bingMapsKey = options.BingMapsKey 87 | console.warn('use options.bingMapsKey instead of options.BingMapsKey') 88 | } 89 | if (!options || !options.bingMapsKey) { 90 | throw new Error('Must supply options.BingMapsKey') 91 | } 92 | options = L.setOptions(this, options) 93 | if (VALID_IMAGERY_SETS.indexOf(options.imagerySet) < 0) { 94 | throw new Error("'" + options.imagerySet + "' is an invalid imagerySet, see https://github.com/digidem/leaflet-bing-layer#parameters") 95 | } 96 | if (options && options.style && DYNAMIC_IMAGERY_SETS.indexOf(options.imagerySet) < 0) { 97 | console.warn('Dynamic styles will only work with these imagerySet choices: ' + DYNAMIC_IMAGERY_SETS.join(', ')) 98 | } 99 | 100 | var metaDataUrl = L.Util.template(L.TileLayer.Bing.METADATA_URL, { 101 | bingMapsKey: this.options.bingMapsKey, 102 | imagerySet: this.options.imagerySet 103 | }) 104 | 105 | this._imageryProviders = [] 106 | this._attributions = [] 107 | 108 | // Keep a reference to the promise so we can use it later 109 | this._fetch = fetchJsonp(metaDataUrl, {jsonpCallback: 'jsonp'}) 110 | .then(function (response) { 111 | return response.json() 112 | }) 113 | .then(this._metaDataOnLoad.bind(this)) 114 | .catch(console.error.bind(console)) 115 | 116 | // for https://github.com/Leaflet/Leaflet/issues/137 117 | if (!L.Browser.android) { 118 | this.on('tileunload', this._onTileRemove) 119 | } 120 | }, 121 | 122 | createTile: function (coords, done) { 123 | var tile = document.createElement('img') 124 | 125 | L.DomEvent.on(tile, 'load', L.bind(this._tileOnLoad, this, done, tile)) 126 | L.DomEvent.on(tile, 'error', L.bind(this._tileOnError, this, done, tile)) 127 | 128 | if (this.options.crossOrigin) { 129 | tile.crossOrigin = '' 130 | } 131 | 132 | /* 133 | Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons 134 | http://www.w3.org/TR/WCAG20-TECHS/H67 135 | */ 136 | tile.alt = '' 137 | 138 | // Don't create closure if we don't have to 139 | if (this._url) { 140 | tile.src = this.getTileUrl(coords) 141 | } else { 142 | this._fetch.then(function () { 143 | tile.src = this.getTileUrl(coords) 144 | }.bind(this)).catch(function (e) { 145 | console.error(e) 146 | done(e) 147 | }) 148 | } 149 | 150 | return tile 151 | }, 152 | 153 | getTileUrl: function (coords) { 154 | var quadkey = toQuadKey(coords.x, coords.y, coords.z) 155 | var url = L.Util.template(this._url, { 156 | quadkey: quadkey, 157 | subdomain: this._getSubdomain(coords), 158 | culture: this.options.culture 159 | }) 160 | if (typeof this.options.style === 'string') { 161 | url += '&st=' + this.options.style 162 | } 163 | return url 164 | }, 165 | 166 | // Update the attribution control every time the map is moved 167 | onAdd: function (map) { 168 | map.on('moveend', this._updateAttribution, this) 169 | L.TileLayer.prototype.onAdd.call(this, map) 170 | this._attributions.forEach(function (attribution) { 171 | map.attributionControl.addAttribution(attribution) 172 | }) 173 | }, 174 | 175 | // Clean up events and remove attributions from attribution control 176 | onRemove: function (map) { 177 | map.off('moveend', this._updateAttribution, this) 178 | this._attributions.forEach(function (attribution) { 179 | map.attributionControl.removeAttribution(attribution) 180 | }) 181 | L.TileLayer.prototype.onRemove.call(this, map) 182 | }, 183 | 184 | /** 185 | * Get the [Bing Imagery metadata](https://msdn.microsoft.com/en-us/library/ff701712.aspx) 186 | * for a specific [`LatLng`](http://leafletjs.com/reference.html#latlng) 187 | * and zoom level. If either `latlng` or `zoom` is omitted and the layer is attached 188 | * to a map, the map center and current map zoom are used. 189 | * @param {L.LatLng} latlng 190 | * @param {Number} zoom 191 | * @return {Promise} Resolves to the JSON metadata 192 | */ 193 | getMetaData: function (latlng, zoom) { 194 | if (!this._map && (!latlng || !zoom)) { 195 | return Promise.reject(new Error('If layer is not attached to map, you must provide LatLng and zoom')) 196 | } 197 | latlng = latlng || this._map.getCenter() 198 | zoom = zoom || this._map.getZoom() 199 | var PointMetaDataUrl = L.Util.template(L.TileLayer.Bing.POINT_METADATA_URL, { 200 | bingMapsKey: this.options.bingMapsKey, 201 | imagerySet: this.options.imagerySet, 202 | z: zoom, 203 | lat: latlng.lat, 204 | lng: latlng.lng 205 | }) 206 | return fetchJsonp(PointMetaDataUrl, {jsonpCallback: 'jsonp'}) 207 | .then(function (response) { 208 | return response.json() 209 | }) 210 | .catch(console.error.bind(console)) 211 | }, 212 | 213 | _metaDataOnLoad: function (metaData) { 214 | if (metaData.statusCode !== 200) { 215 | throw new Error('Bing Imagery Metadata error: \n' + JSON.stringify(metaData, null, ' ')) 216 | } 217 | var resource = metaData.resourceSets[0].resources[0] 218 | this._url = resource.imageUrl 219 | this._imageryProviders = resource.imageryProviders || [] 220 | this.options.subdomains = resource.imageUrlSubdomains 221 | this._updateAttribution() 222 | return Promise.resolve() 223 | }, 224 | 225 | /** 226 | * Update the attribution control of the map with the provider attributions 227 | * within the current map bounds 228 | */ 229 | _updateAttribution: function () { 230 | var map = this._map 231 | if (!map || !map.attributionControl) return 232 | var zoom = map.getZoom() 233 | var bbox = toBingBBox(map.getBounds().toBBoxString()) 234 | this._fetch.then(function () { 235 | var newAttributions = this._getAttributions(bbox, zoom) 236 | var prevAttributions = this._attributions 237 | // Add any new provider attributions in the current area to the attribution control 238 | newAttributions.forEach(function (attr) { 239 | if (prevAttributions.indexOf(attr) > -1) return 240 | map.attributionControl.addAttribution(attr) 241 | }) 242 | // Remove any attributions that are no longer in the current area from the attribution control 243 | prevAttributions.filter(function (attr) { 244 | if (newAttributions.indexOf(attr) > -1) return 245 | map.attributionControl.removeAttribution(attr) 246 | }) 247 | this._attributions = newAttributions 248 | }.bind(this)) 249 | }, 250 | 251 | /** 252 | * Returns an array of attributions for given bbox and zoom 253 | * @private 254 | * @param {Array} bbox [west, south, east, north] 255 | * @param {Number} zoom 256 | * @return {Array} Array of attribution strings for each provider 257 | */ 258 | _getAttributions: function (bbox, zoom) { 259 | return this._imageryProviders.reduce(function (attributions, provider) { 260 | for (var i = 0; i < provider.coverageAreas.length; i++) { 261 | if (bboxIntersect(bbox, provider.coverageAreas[i].bbox) && 262 | zoom >= provider.coverageAreas[i].zoomMin && 263 | zoom <= provider.coverageAreas[i].zoomMax) { 264 | attributions.push(provider.attribution) 265 | return attributions 266 | } 267 | } 268 | return attributions 269 | }, []) 270 | } 271 | }) 272 | 273 | L.tileLayer.bing = function (options) { 274 | return new L.TileLayer.Bing(options) 275 | } 276 | 277 | module.exports = L.TileLayer.Bing 278 | 279 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 280 | },{"bbox-intersect":2,"fetch-jsonp":3}],2:[function(require,module,exports){ 281 | module.exports = function(bbox1, bbox2){ 282 | if(!( 283 | bbox1[0] > bbox2[2] || 284 | bbox1[2] < bbox2[0] || 285 | bbox1[3] < bbox2[1] || 286 | bbox1[1] > bbox2[3] 287 | )){ 288 | return true; 289 | } else { 290 | return false; 291 | } 292 | } 293 | },{}],3:[function(require,module,exports){ 294 | (function (global, factory) { 295 | if (typeof define === 'function' && define.amd) { 296 | define(['exports', 'module'], factory); 297 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { 298 | factory(exports, module); 299 | } else { 300 | var mod = { 301 | exports: {} 302 | }; 303 | factory(mod.exports, mod); 304 | global.fetchJsonp = mod.exports; 305 | } 306 | })(this, function (exports, module) { 307 | 'use strict'; 308 | 309 | var defaultOptions = { 310 | timeout: 5000, 311 | jsonpCallback: 'callback', 312 | jsonpCallbackFunction: null 313 | }; 314 | 315 | function generateCallbackFunction() { 316 | return 'jsonp_' + Date.now() + '_' + Math.ceil(Math.random() * 100000); 317 | } 318 | 319 | // Known issue: Will throw 'Uncaught ReferenceError: callback_*** is not defined' error if request timeout 320 | function clearFunction(functionName) { 321 | // IE8 throws an exception when you try to delete a property on window 322 | // http://stackoverflow.com/a/1824228/751089 323 | try { 324 | delete window[functionName]; 325 | } catch (e) { 326 | window[functionName] = undefined; 327 | } 328 | } 329 | 330 | function removeScript(scriptId) { 331 | var script = document.getElementById(scriptId); 332 | document.getElementsByTagName('head')[0].removeChild(script); 333 | } 334 | 335 | var fetchJsonp = function fetchJsonp(url) { 336 | var options = arguments[1] === undefined ? {} : arguments[1]; 337 | 338 | var timeout = options.timeout != null ? options.timeout : defaultOptions.timeout; 339 | var jsonpCallback = options.jsonpCallback != null ? options.jsonpCallback : defaultOptions.jsonpCallback; 340 | 341 | var timeoutId = undefined; 342 | 343 | return new Promise(function (resolve, reject) { 344 | var callbackFunction = options.jsonpCallbackFunction || generateCallbackFunction(); 345 | 346 | window[callbackFunction] = function (response) { 347 | resolve({ 348 | ok: true, 349 | // keep consistent with fetch API 350 | json: function json() { 351 | return Promise.resolve(response); 352 | } 353 | }); 354 | 355 | if (timeoutId) clearTimeout(timeoutId); 356 | 357 | removeScript(jsonpCallback + '_' + callbackFunction); 358 | 359 | clearFunction(callbackFunction); 360 | }; 361 | 362 | // Check if the user set their own params, and if not add a ? to start a list of params 363 | url += url.indexOf('?') === -1 ? '?' : '&'; 364 | 365 | var jsonpScript = document.createElement('script'); 366 | jsonpScript.setAttribute('src', url + jsonpCallback + '=' + callbackFunction); 367 | jsonpScript.id = jsonpCallback + '_' + callbackFunction; 368 | document.getElementsByTagName('head')[0].appendChild(jsonpScript); 369 | 370 | timeoutId = setTimeout(function () { 371 | reject(new Error('JSONP request to ' + url + ' timed out')); 372 | 373 | clearFunction(callbackFunction); 374 | removeScript(jsonpCallback + '_' + callbackFunction); 375 | }, timeout); 376 | }); 377 | }; 378 | 379 | // export as global function 380 | /* 381 | let local; 382 | if (typeof global !== 'undefined') { 383 | local = global; 384 | } else if (typeof self !== 'undefined') { 385 | local = self; 386 | } else { 387 | try { 388 | local = Function('return this')(); 389 | } catch (e) { 390 | throw new Error('polyfill failed because global object is unavailable in this environment'); 391 | } 392 | } 393 | 394 | local.fetchJsonp = fetchJsonp; 395 | */ 396 | 397 | module.exports = fetchJsonp; 398 | }); 399 | },{}]},{},[1]); 400 | -------------------------------------------------------------------------------- /leaflet-bing-layer.min.js: -------------------------------------------------------------------------------- 1 | !function t(e,n,i){function o(a,s){if(!n[a]){if(!e[a]){var u="function"==typeof require&&require;if(!s&&u)return u(a,!0);if(r)return r(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[a]={exports:{}};e[a][0].call(c.exports,function(t){var n=e[a][1][t];return o(n?n:t)},c,c.exports,t,e,n,i)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;a0;o--){var r=0,a=1<-1||t.attributionControl.addAttribution(e)}),o.filter(function(e){i.indexOf(e)>-1||t.attributionControl.removeAttribution(e)}),this._attributions=i}.bind(this))}},_getAttributions:function(t,e){return this._imageryProviders.reduce(function(n,i){for(var o=0;o=i.coverageAreas[o].zoomMin&&e<=i.coverageAreas[o].zoomMax)return n.push(i.attribution),n;return n},[])}}),r.tileLayer.bing=function(t){return new r.TileLayer.Bing(t)},e.exports=r.TileLayer.Bing}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"bbox-intersect":2,"fetch-jsonp":3}],2:[function(t,e,n){e.exports=function(t,e){return t[0]>e[2]||t[2]e[3]?!1:!0}},{}],3:[function(t,e,n){!function(t,i){if("function"==typeof define&&define.amd)define(["exports","module"],i);else if("undefined"!=typeof n&&"undefined"!=typeof e)i(n,e);else{var o={exports:{}};i(o.exports,o),t.fetchJsonp=o.exports}}(this,function(t,e){"use strict";function n(){return"jsonp_"+Date.now()+"_"+Math.ceil(1e5*Math.random())}function i(t){try{delete window[t]}catch(e){window[t]=void 0}}function o(t){var e=document.getElementById(t);document.getElementsByTagName("head")[0].removeChild(e)}var r={timeout:5e3,jsonpCallback:"callback",jsonpCallbackFunction:null},a=function(t){var e=void 0===arguments[1]?{}:arguments[1],a=null!=e.timeout?e.timeout:r.timeout,s=null!=e.jsonpCallback?e.jsonpCallback:r.jsonpCallback,u=void 0;return new Promise(function(r,l){var c=e.jsonpCallbackFunction||n();window[c]=function(t){r({ok:!0,json:function(){return Promise.resolve(t)}}),u&&clearTimeout(u),o(s+"_"+c),i(c)},t+=-1===t.indexOf("?")?"?":"&";var d=document.createElement("script");d.setAttribute("src",t+s+"="+c),d.id=s+"_"+c,document.getElementsByTagName("head")[0].appendChild(d),u=setTimeout(function(){l(new Error("JSONP request to "+t+" timed out")),i(c),o(s+"_"+c)},a)})};e.exports=a})},{}]},{},[1]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-bing-layer", 3 | "version": "3.3.0", 4 | "description": "Bing Maps Layer for Leaflet v1.0.0", 5 | "main": "index.js", 6 | "browserify": { 7 | "transform": [ 8 | "browserify-shim" 9 | ] 10 | }, 11 | "browserify-shim": { 12 | "leaflet": "global:L" 13 | }, 14 | "scripts": { 15 | "build": "browserify index.js > leaflet-bing-layer.js", 16 | "postbuild": "uglifyjs leaflet-bing-layer.js -cm -o leaflet-bing-layer.min.js", 17 | "preversion": "npm test && npm run build", 18 | "lint": "standard index.js", 19 | "start": "budo index.js:leaflet-bing-layer.js --live", 20 | "test": "npm run lint" 21 | }, 22 | "keywords": [ 23 | "leaflet", 24 | "bing" 25 | ], 26 | "author": "Gregor MacLennan", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/gmaclennan/leaflet-bing-layer.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/gmaclennan/leaflet-bing-layer/issues" 34 | }, 35 | "homepage": "https://github.com/gmaclennan/leaflet-bing-layer", 36 | "dependencies": { 37 | "bbox-intersect": "^0.1.1", 38 | "browserify-shim": "^3.8.11", 39 | "fetch-jsonp": "^1.0.0", 40 | "leaflet": "^1.0.0-beta.2" 41 | }, 42 | "devDependencies": { 43 | "browserify": "^12.0.1", 44 | "budo": "^7.0.2", 45 | "standard": "^5.4.1" 46 | } 47 | } 48 | --------------------------------------------------------------------------------