318 lines
11 KiB
JavaScript
318 lines
11 KiB
JavaScript
/**
|
|
* @module ol/source/Google
|
|
*/
|
|
|
|
import ViewHint from '../ViewHint.js';
|
|
import {getBottomLeft, getTopRight} from '../extent.js';
|
|
import {toLonLat} from '../proj.js';
|
|
import {createXYZ, extentFromProjection} from '../tilegrid.js';
|
|
import TileImage from './TileImage.js';
|
|
|
|
const createSessionUrl = 'https://tile.googleapis.com/v1/createSession';
|
|
const tileUrl = 'https://tile.googleapis.com/v1/2dtiles';
|
|
const attributionUrl = 'https://tile.googleapis.com/tile/v1/viewport';
|
|
const maxZoom = 22;
|
|
|
|
/**
|
|
* @typedef {Object} Options
|
|
* @property {string} key Google Map Tiles API key. Get yours at https://developers.google.com/maps/documentation/tile/get-api-key.
|
|
* @property {string} [mapType='roadmap'] The type of [base map](https://developers.google.com/maps/documentation/tile/session_tokens#required_fields).
|
|
* @property {string} [language='en-US'] An [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) for information displayed on the tiles.
|
|
* @property {string} [region='US'] A [Common Locale Data Repository](https://cldr.unicode.org/) (CLDR) region identifier that represents the user location.
|
|
* @property {string} [imageFormat] The image format used for the map tiles (e.g. `'jpeg'`, or `'png'`).
|
|
* @property {string} [scale] Scale for map elements (`'scaleFactor1x'`, `'scaleFactor2x'`, or `'scaleFactor4x'`).
|
|
* @property {boolean} [highDpi=false] Use high-resolution tiles.
|
|
* @property {Array<string>} [layerTypes] The layer types added to the map (e.g. `'layerRoadmap'`, `'layerStreetview'`, or `'layerTraffic'`).
|
|
* @property {boolean} [overlay=false] Display only the `layerTypes` and not the underlying `mapType` (only works if `layerTypes` is provided).
|
|
* @property {Array<Object>} [styles] [Custom styles](https://developers.google.com/maps/documentation/tile/style-reference) applied to the map.
|
|
* @property {boolean} [attributionsCollapsible=true] Allow the attributions to be collapsed.
|
|
* @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} [cacheSize] Initial tile cache size. Will auto-grow to hold at least the number of tiles in the viewport.
|
|
* @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 {Array<string>} [apiOptions] An array of values specifying additional options to apply.
|
|
* @property {boolean} [wrapX=true] 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} SessionTokenRequest
|
|
* @property {string} mapType The map type.
|
|
* @property {string} language The language.
|
|
* @property {string} region The region.
|
|
* @property {string} [imageFormat] The image format.
|
|
* @property {string} [scale] The scale.
|
|
* @property {boolean} [highDpi] Use high resolution tiles.
|
|
* @property {Array<string>} [layerTypes] The layer types.
|
|
* @property {boolean} [overlay] The overlay.
|
|
* @property {Array<Object>} [styles] The styles.
|
|
* @property {Array<string>} [apiOptions] An array of values specifying additional options to apply.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} SessionTokenResponse
|
|
* @property {string} session The session token.
|
|
* @property {string} expiry The session token expiry (seconds since the epoch as a string).
|
|
* @property {number} tileWidth The tile width.
|
|
* @property {number} tileHeight The tile height.
|
|
* @property {string} imageFormat The image format.
|
|
*/
|
|
|
|
/**
|
|
* @classdesc
|
|
* A tile layer source that renders tiles from the Google [Map Tiles API](https://developers.google.com/maps/documentation/tile/overview).
|
|
* The constructor takes options that are passed to the request to create a session token. Refer to the
|
|
* [documentation](https://developers.google.com/maps/documentation/tile/session_tokens#required_fields)
|
|
* for additional details.
|
|
* @api
|
|
*/
|
|
class Google extends TileImage {
|
|
/**
|
|
* @param {Options} options Google Maps options.
|
|
*/
|
|
constructor(options) {
|
|
const highDpi = !!options.highDpi;
|
|
|
|
super({
|
|
attributionsCollapsible: options.attributionsCollapsible,
|
|
cacheSize: options.cacheSize,
|
|
crossOrigin: 'anonymous',
|
|
interpolate: options.interpolate,
|
|
projection: 'EPSG:3857',
|
|
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
|
|
state: 'loading',
|
|
tileLoadFunction: options.tileLoadFunction,
|
|
tilePixelRatio: highDpi ? 2 : 1,
|
|
wrapX: options.wrapX !== undefined ? options.wrapX : true,
|
|
transition: options.transition,
|
|
zDirection: options.zDirection,
|
|
});
|
|
|
|
/**
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this.apiKey_ = options.key;
|
|
|
|
/**
|
|
* @type {Error|null}
|
|
* @private
|
|
*/
|
|
this.error_ = null;
|
|
|
|
/**
|
|
* @type {SessionTokenRequest}
|
|
*/
|
|
const sessionTokenRequest = {
|
|
mapType: options.mapType || 'roadmap',
|
|
language: options.language || 'en-US',
|
|
region: options.region || 'US',
|
|
};
|
|
if (options.imageFormat) {
|
|
sessionTokenRequest.imageFormat = options.imageFormat;
|
|
}
|
|
if (options.scale) {
|
|
sessionTokenRequest.scale = options.scale;
|
|
}
|
|
if (highDpi) {
|
|
sessionTokenRequest.highDpi = true;
|
|
}
|
|
if (options.layerTypes) {
|
|
sessionTokenRequest.layerTypes = options.layerTypes;
|
|
}
|
|
if (options.styles) {
|
|
sessionTokenRequest.styles = options.styles;
|
|
}
|
|
if (options.overlay === true) {
|
|
sessionTokenRequest.overlay = true;
|
|
}
|
|
if (options.apiOptions) {
|
|
sessionTokenRequest.apiOptions = options.apiOptions;
|
|
}
|
|
|
|
/**
|
|
* @type {SessionTokenRequest}
|
|
* @private
|
|
*/
|
|
this.sessionTokenRequest_ = sessionTokenRequest;
|
|
|
|
/**
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this.sessionTokenValue_;
|
|
|
|
/**
|
|
* @type {ReturnType<typeof setTimeout>}
|
|
* @private
|
|
*/
|
|
this.sessionRefreshId_;
|
|
|
|
/**
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this.previousViewportAttribution_;
|
|
|
|
/**
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this.previousViewportExtent_;
|
|
|
|
this.createSession_();
|
|
}
|
|
|
|
/**
|
|
* @return {Error|null} A source loading error. When the source state is `error`, use this function
|
|
* to get more information about the error. To debug a faulty configuration, you may want to use
|
|
* a listener like
|
|
* ```js
|
|
* source.on('change', () => {
|
|
* if (source.getState() === 'error') {
|
|
* console.error(source.getError());
|
|
* }
|
|
* });
|
|
* ```
|
|
*/
|
|
getError() {
|
|
return this.error_;
|
|
}
|
|
|
|
/**
|
|
* Exposed here so it can be overridden in the tests.
|
|
* @param {string} url The URL.
|
|
* @param {RequestInit} config The config.
|
|
* @return {Promise<Response>} A promise that resolves with the response.
|
|
*/
|
|
fetchSessionToken(url, config) {
|
|
return fetch(url, config);
|
|
}
|
|
|
|
/**
|
|
* Get or renew a session token for use with tile requests.
|
|
* @private
|
|
*/
|
|
async createSession_() {
|
|
const url = createSessionUrl + '?key=' + this.apiKey_;
|
|
const config = {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(this.sessionTokenRequest_),
|
|
};
|
|
|
|
const response = await this.fetchSessionToken(url, config);
|
|
if (!response.ok) {
|
|
try {
|
|
const body = await response.json();
|
|
this.error_ = new Error(body.error.message);
|
|
} catch {
|
|
this.error_ = new Error('Error fetching session token');
|
|
}
|
|
this.setState('error');
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* @type {SessionTokenResponse}
|
|
*/
|
|
const sessionTokenResponse = await response.json();
|
|
|
|
const tilePixelRatio = this.getTilePixelRatio(1);
|
|
const tileSize = [
|
|
sessionTokenResponse.tileWidth / tilePixelRatio,
|
|
sessionTokenResponse.tileHeight / tilePixelRatio,
|
|
];
|
|
|
|
this.tileGrid = createXYZ({
|
|
extent: extentFromProjection(this.getProjection()),
|
|
maxZoom: maxZoom,
|
|
tileSize: tileSize,
|
|
});
|
|
|
|
const session = sessionTokenResponse.session;
|
|
this.sessionTokenValue_ = session;
|
|
const key = this.apiKey_;
|
|
this.tileUrlFunction = function (tileCoord, pixelRatio, projection) {
|
|
const z = tileCoord[0];
|
|
const x = tileCoord[1];
|
|
const y = tileCoord[2];
|
|
const url = `${tileUrl}/${z}/${x}/${y}?session=${session}&key=${key}`;
|
|
return url;
|
|
};
|
|
|
|
const expiry = parseInt(sessionTokenResponse.expiry, 10) * 1000;
|
|
const timeout = Math.max(expiry - Date.now() - 60 * 1000, 1);
|
|
this.sessionRefreshId_ = setTimeout(() => this.createSession_(), timeout);
|
|
|
|
this.setAttributions(this.fetchAttributions_.bind(this));
|
|
// even if the state is already ready, we want the change event
|
|
this.setState('ready');
|
|
}
|
|
|
|
/**
|
|
* @param {import('../Map.js').FrameState} frameState The frame state.
|
|
* @return {Promise<string>} The attributions.
|
|
* @private
|
|
*/
|
|
async fetchAttributions_(frameState) {
|
|
if (
|
|
frameState.viewHints[ViewHint.ANIMATING] ||
|
|
frameState.viewHints[ViewHint.INTERACTING] ||
|
|
frameState.animate
|
|
) {
|
|
return this.previousViewportAttribution_;
|
|
}
|
|
const [west, south] = toLonLat(
|
|
getBottomLeft(frameState.extent),
|
|
frameState.viewState.projection,
|
|
);
|
|
const [east, north] = toLonLat(
|
|
getTopRight(frameState.extent),
|
|
frameState.viewState.projection,
|
|
);
|
|
const tileGrid = this.getTileGrid();
|
|
const zoom = tileGrid.getZForResolution(
|
|
frameState.viewState.resolution,
|
|
this.zDirection,
|
|
);
|
|
const viewportExtent = `zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`;
|
|
// check if the extent or zoom has actually changed to avoid unnecessary requests
|
|
if (this.previousViewportExtent_ == viewportExtent) {
|
|
return this.previousViewportAttribution_;
|
|
}
|
|
this.previousViewportExtent_ = viewportExtent;
|
|
const session = this.sessionTokenValue_;
|
|
const key = this.apiKey_;
|
|
const url = `${attributionUrl}?session=${session}&key=${key}&${viewportExtent}`;
|
|
this.previousViewportAttribution_ = await fetch(url)
|
|
.then((response) => response.json())
|
|
.then((json) => json.copyright);
|
|
|
|
return this.previousViewportAttribution_;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
disposeInternal() {
|
|
clearTimeout(this.sessionRefreshId_);
|
|
super.disposeInternal();
|
|
}
|
|
}
|
|
|
|
export default Google;
|