|
|
/**
|
|
|
* @module ol/source/BingMaps
|
|
|
*/
|
|
|
|
|
|
import TileImage from './TileImage.js';
|
|
|
import {applyTransform, intersects} from '../extent.js';
|
|
|
import {createFromTileUrlFunctions} from '../tileurlfunction.js';
|
|
|
import {createOrUpdate} from '../tilecoord.js';
|
|
|
import {createXYZ, extentFromProjection} from '../tilegrid.js';
|
|
|
import {get as getProjection, getTransformFromProjections} from '../proj.js';
|
|
|
import {jsonp as requestJSONP} from '../net.js';
|
|
|
|
|
|
/**
|
|
|
* @param {import('../tilecoord.js').TileCoord} tileCoord Tile coord.
|
|
|
* @return {string} Quad key.
|
|
|
*/
|
|
|
export function quadKey(tileCoord) {
|
|
|
const z = tileCoord[0];
|
|
|
const digits = new Array(z);
|
|
|
let mask = 1 << (z - 1);
|
|
|
let i, charCode;
|
|
|
for (i = 0; i < z; ++i) {
|
|
|
// 48 is charCode for 0 - '0'.charCodeAt(0)
|
|
|
charCode = 48;
|
|
|
if (tileCoord[1] & mask) {
|
|
|
charCode += 1;
|
|
|
}
|
|
|
if (tileCoord[2] & mask) {
|
|
|
charCode += 2;
|
|
|
}
|
|
|
digits[i] = String.fromCharCode(charCode);
|
|
|
mask >>= 1;
|
|
|
}
|
|
|
return digits.join('');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* The attribution containing a link to the Microsoft® Bing™ Maps Platform APIs’
|
|
|
* Terms Of Use.
|
|
|
* @const
|
|
|
* @type {string}
|
|
|
*/
|
|
|
const TOS_ATTRIBUTION =
|
|
|
'<a class="ol-attribution-bing-tos" ' +
|
|
|
'href="https://www.microsoft.com/maps/product/terms.html" target="_blank">' +
|
|
|
'Terms of Use</a>';
|
|
|
|
|
|
/**
|
|
|
* @typedef {Object} Options
|
|
|
* @property {number} [cacheSize] Initial tile cache size. Will auto-grow to hold at least the number of tiles in the viewport.
|
|
|
* @property {boolean} [hidpi=false] If `true` hidpi tiles will be requested.
|
|
|
* @property {string} [culture='en-us'] Culture code.
|
|
|
* @property {string} key Bing Maps API key. Get yours at https://www.bingmapsportal.com/.
|
|
|
* @property {string} imagerySet Type of imagery.
|
|
|
* @property {boolean} [interpolate=true] Use interpolated values when resampling. By default,
|
|
|
* linear interpolation is used when resampling. Set to false to use the nearest neighbor instead.
|
|
|
* @property {number} [maxZoom=21] Max zoom. Default is what's advertized by the BingMaps service.
|
|
|
* @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
|
|
|
* Higher values can increase reprojection performance, but decrease precision.
|
|
|
* @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is
|
|
|
* ```js
|
|
|
* function(imageTile, src) {
|
|
|
* imageTile.getImage().src = src;
|
|
|
* };
|
|
|
* ```
|
|
|
* @property {boolean} [wrapX=true] Whether to wrap the world horizontally.
|
|
|
* @property {number} [transition] Duration of the opacity transition for rendering.
|
|
|
* To disable the opacity transition, pass `transition: 0`.
|
|
|
* @property {number|import("../array.js").NearestDirectionFunction} [zDirection=0]
|
|
|
* Choose whether to use tiles with a higher or lower zoom level when between integer
|
|
|
* zoom levels. See {@link module:ol/tilegrid/TileGrid~TileGrid#getZForResolution}.
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @typedef {Object} BingMapsImageryMetadataResponse
|
|
|
* @property {number} statusCode The response status code
|
|
|
* @property {string} statusDescription The response status description
|
|
|
* @property {string} authenticationResultCode The authentication result code
|
|
|
* @property {Array<ResourceSet>} resourceSets The array of resource sets
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @typedef {Object} ResourceSet
|
|
|
* @property {Array<Resource>} resources Resources.
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @typedef {Object} Resource
|
|
|
* @property {number} imageHeight The image height
|
|
|
* @property {number} imageWidth The image width
|
|
|
* @property {number} zoomMin The minimum zoom level
|
|
|
* @property {number} zoomMax The maximum zoom level
|
|
|
* @property {string} imageUrl The image URL
|
|
|
* @property {Array<string>} imageUrlSubdomains The image URL subdomains for rotation
|
|
|
* @property {Array<ImageryProvider>} [imageryProviders] The array of ImageryProviders
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @typedef {Object} ImageryProvider
|
|
|
* @property {Array<CoverageArea>} coverageAreas The coverage areas
|
|
|
* @property {string} [attribution] The attribution
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @typedef {Object} CoverageArea
|
|
|
* @property {number} zoomMin The minimum zoom
|
|
|
* @property {number} zoomMax The maximum zoom
|
|
|
* @property {Array<number>} bbox The coverage bounding box
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* @classdesc
|
|
|
* Layer source for Bing Maps tile data.
|
|
|
* @api
|
|
|
*/
|
|
|
class BingMaps extends TileImage {
|
|
|
/**
|
|
|
* @param {Options} options Bing Maps options.
|
|
|
*/
|
|
|
constructor(options) {
|
|
|
const hidpi = options.hidpi !== undefined ? options.hidpi : false;
|
|
|
|
|
|
super({
|
|
|
cacheSize: options.cacheSize,
|
|
|
crossOrigin: 'anonymous',
|
|
|
interpolate: options.interpolate,
|
|
|
opaque: true,
|
|
|
projection: getProjection('EPSG:3857'),
|
|
|
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
|
|
|
state: 'loading',
|
|
|
tileLoadFunction: options.tileLoadFunction,
|
|
|
tilePixelRatio: hidpi ? 2 : 1,
|
|
|
wrapX: options.wrapX !== undefined ? options.wrapX : true,
|
|
|
transition: options.transition,
|
|
|
zDirection: options.zDirection,
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @private
|
|
|
* @type {boolean}
|
|
|
*/
|
|
|
this.hidpi_ = hidpi;
|
|
|
|
|
|
/**
|
|
|
* @private
|
|
|
* @type {string}
|
|
|
*/
|
|
|
this.culture_ = options.culture !== undefined ? options.culture : 'en-us';
|
|
|
|
|
|
/**
|
|
|
* @private
|
|
|
* @type {number}
|
|
|
*/
|
|
|
this.maxZoom_ = options.maxZoom !== undefined ? options.maxZoom : -1;
|
|
|
|
|
|
/**
|
|
|
* @private
|
|
|
* @type {string}
|
|
|
*/
|
|
|
this.apiKey_ = options.key;
|
|
|
|
|
|
/**
|
|
|
* @private
|
|
|
* @type {string}
|
|
|
*/
|
|
|
this.imagerySet_ = options.imagerySet;
|
|
|
|
|
|
const url =
|
|
|
'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/' +
|
|
|
this.imagerySet_ +
|
|
|
'?uriScheme=https&include=ImageryProviders&key=' +
|
|
|
this.apiKey_ +
|
|
|
'&c=' +
|
|
|
this.culture_;
|
|
|
|
|
|
requestJSONP(
|
|
|
url,
|
|
|
this.handleImageryMetadataResponse.bind(this),
|
|
|
undefined,
|
|
|
'jsonp'
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Get the api key used for this source.
|
|
|
*
|
|
|
* @return {string} The api key.
|
|
|
* @api
|
|
|
*/
|
|
|
getApiKey() {
|
|
|
return this.apiKey_;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Get the imagery set associated with this source.
|
|
|
*
|
|
|
* @return {string} The imagery set.
|
|
|
* @api
|
|
|
*/
|
|
|
getImagerySet() {
|
|
|
return this.imagerySet_;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param {BingMapsImageryMetadataResponse} response Response.
|
|
|
*/
|
|
|
handleImageryMetadataResponse(response) {
|
|
|
if (
|
|
|
response.statusCode != 200 ||
|
|
|
response.statusDescription != 'OK' ||
|
|
|
response.authenticationResultCode != 'ValidCredentials' ||
|
|
|
response.resourceSets.length != 1 ||
|
|
|
response.resourceSets[0].resources.length != 1
|
|
|
) {
|
|
|
this.setState('error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const resource = response.resourceSets[0].resources[0];
|
|
|
const maxZoom = this.maxZoom_ == -1 ? resource.zoomMax : this.maxZoom_;
|
|
|
|
|
|
const sourceProjection = this.getProjection();
|
|
|
const extent = extentFromProjection(sourceProjection);
|
|
|
const scale = this.hidpi_ ? 2 : 1;
|
|
|
const tileSize =
|
|
|
resource.imageWidth == resource.imageHeight
|
|
|
? resource.imageWidth / scale
|
|
|
: [resource.imageWidth / scale, resource.imageHeight / scale];
|
|
|
|
|
|
const tileGrid = createXYZ({
|
|
|
extent: extent,
|
|
|
minZoom: resource.zoomMin,
|
|
|
maxZoom: maxZoom,
|
|
|
tileSize: tileSize,
|
|
|
});
|
|
|
this.tileGrid = tileGrid;
|
|
|
|
|
|
const culture = this.culture_;
|
|
|
const hidpi = this.hidpi_;
|
|
|
this.tileUrlFunction = createFromTileUrlFunctions(
|
|
|
resource.imageUrlSubdomains.map(function (subdomain) {
|
|
|
/** @type {import('../tilecoord.js').TileCoord} */
|
|
|
const quadKeyTileCoord = [0, 0, 0];
|
|
|
const imageUrl = resource.imageUrl
|
|
|
.replace('{subdomain}', subdomain)
|
|
|
.replace('{culture}', culture);
|
|
|
return (
|
|
|
/**
|
|
|
* @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
|
|
|
* @param {number} pixelRatio Pixel ratio.
|
|
|
* @param {import("../proj/Projection.js").default} projection Projection.
|
|
|
* @return {string|undefined} Tile URL.
|
|
|
*/
|
|
|
function (tileCoord, pixelRatio, projection) {
|
|
|
if (!tileCoord) {
|
|
|
return undefined;
|
|
|
} else {
|
|
|
createOrUpdate(
|
|
|
tileCoord[0],
|
|
|
tileCoord[1],
|
|
|
tileCoord[2],
|
|
|
quadKeyTileCoord
|
|
|
);
|
|
|
let url = imageUrl;
|
|
|
if (hidpi) {
|
|
|
url += '&dpi=d1&device=mobile';
|
|
|
}
|
|
|
return url.replace('{quadkey}', quadKey(quadKeyTileCoord));
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
})
|
|
|
);
|
|
|
|
|
|
if (resource.imageryProviders) {
|
|
|
const transform = getTransformFromProjections(
|
|
|
getProjection('EPSG:4326'),
|
|
|
this.getProjection()
|
|
|
);
|
|
|
|
|
|
this.setAttributions(
|
|
|
function (frameState) {
|
|
|
const attributions = [];
|
|
|
const viewState = frameState.viewState;
|
|
|
const tileGrid = this.getTileGrid();
|
|
|
const z = tileGrid.getZForResolution(
|
|
|
viewState.resolution,
|
|
|
this.zDirection
|
|
|
);
|
|
|
const tileCoord = tileGrid.getTileCoordForCoordAndZ(
|
|
|
viewState.center,
|
|
|
z
|
|
|
);
|
|
|
const zoom = tileCoord[0];
|
|
|
resource.imageryProviders.map(function (imageryProvider) {
|
|
|
let intersecting = false;
|
|
|
const coverageAreas = imageryProvider.coverageAreas;
|
|
|
for (let i = 0, ii = coverageAreas.length; i < ii; ++i) {
|
|
|
const coverageArea = coverageAreas[i];
|
|
|
if (
|
|
|
zoom >= coverageArea.zoomMin &&
|
|
|
zoom <= coverageArea.zoomMax
|
|
|
) {
|
|
|
const bbox = coverageArea.bbox;
|
|
|
const epsg4326Extent = [bbox[1], bbox[0], bbox[3], bbox[2]];
|
|
|
const extent = applyTransform(epsg4326Extent, transform);
|
|
|
if (intersects(extent, frameState.extent)) {
|
|
|
intersecting = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if (intersecting) {
|
|
|
attributions.push(imageryProvider.attribution);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
attributions.push(TOS_ATTRIBUTION);
|
|
|
return attributions;
|
|
|
}.bind(this)
|
|
|
);
|
|
|
}
|
|
|
|
|
|
this.setState('ready');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export default BingMaps;
|