// loaders.gl, MIT license /* eslint-disable camelcase */ import type {ImageType} from '@loaders.gl/images'; import {ImageLoader} from '@loaders.gl/images'; import type {TileSourceMetadata, GetTileParameters} from '../sources/tile-source'; import {TileSource} from '../sources/tile-source'; import {ImageServiceProps, getFetchFunction, mergeImageServiceProps} from '../sources/tile-service'; import type {WMTSCapabilities} from '../../wmts/wmts-types'; import {WMTSCapabilitiesLoader} from '../../../wmts-capabilities-loader'; // import {GMLLoader} from '../../../wip/wms-feature-info-loader'; import {WMSErrorLoader as WMTSErrorLoader} from '../../../wms-error-loader'; type WMTSCommonParameters = { /** In case the endpoint supports multiple services */ service?: 'WMTS'; /** In case the endpoint supports multiple WMTS versions */ version?: '1.0.0' | '1.3.0'; }; export type WMTSGetCapabilitiesParameters = WMTSCommonParameters & { /** Request type */ request?: 'GetCapabilities'; }; export type WMTSGetTileParameters = WMTSCommonParameters & { /** Request type */ request?: 'GetTile'; /** requested format for the return image */ format?: 'image/png'; /** Styling */ style?: string; /** Layer to render */ layer: string; /** Tiling "Schema" (e.g. 'google_maps') */ tileMatrixSet: string; /** Tile zoom level, named */ tileMatrix: string | number; /** tile x coordinate */ tileCol: number; /** tile y coordinate */ tileRow: number; }; export type WMTSGetFeatureInfoParameters = WMTSCommonParameters & { /** Request type */ request?: 'GetFeatureInfo'; /** Layer to render */ layer: string[]; /** Styling */ style?: string; /** x coordinate for the feature info request */ x: number; /** y coordinate for the feature info request */ y: number; /** list of layer to query (could be different from rendered layer) */ query_layer: string[]; /** MIME type of returned feature info */ info_format?: 'text/plain' | 'application/vnd.ogc.gml'; }; export type WMTSDescribeLayerParameters = WMTSCommonParameters & { /** Request type */ request?: 'DescribeLayer'; }; export type WMTSGetLegendGraphicParameters = WMTSCommonParameters & { /** Request type */ request?: 'GetLegendGraphic'; }; type WMTSServiceProps = { url: string; attribution: string; layer: string; matrixSet: string; style: string; format: string; requestFormat: string; }; /** * The WMTSService class provides * - provides type safe methods to form URLs to a WMTS service * - provides type safe methods to query and parse results (and errors) from a WMTS service * - implements the ImageService interface * @note Only the URL parameter conversion is supported. XML posts are not supported. */ export class WMTSService extends TileSource { static type: 'wms' = 'wms'; static testURL = (url: string): boolean => url.toLowerCase().includes('wms'); props: Required; fetch: (url: string, options?: RequestInit) => Promise; capabilities: WMTSCapabilities | null = null; /** A list of loaders used by the WMTSService methods */ readonly loaders = [ ImageLoader, WMTSErrorLoader, WMTSCapabilitiesLoader, GMLLoader ]; readonly requestEncoding = 'kvp'; /** Create a WMTSService */ constructor(props: ImageServiceProps) { super(); this.props = mergeImageServiceProps(props); this.fetch = getFetchFunction(this.props); this.props.loadOptions = { ...this.props.loadOptions, // We want error responses to throw exceptions, the WMTSErrorLoader can do this wms: {...this.props.loadOptions?.wms, throwOnError: true} }; } // TileSource implementation getMetadata(): Promise { return this.getCapabilities(); } getTile(parameters: GetTileParameters): Promise { const wmtsParameters: WMTSGetTileParameters = { layer: parameters.layer, tileMatrix: String(parameters.zoom), tileCol: parameters.x, tileRow: parameters.y }; return this.getTile_(wmtsParameters); } // WMTS Service API Stubs /** Get Capabilities */ async getCapabilities( wmtsParameters?: WMTSGetCapabilitiesParameters, vendorParameters?: Record ): Promise { const url = this.getCapabilitiesURL(wmtsParameters, vendorParameters); const response = await this.fetch(url); const arrayBuffer = await response.arrayBuffer(); this._checkResponse(response, arrayBuffer); const capabilities = await WMTSCapabilitiesLoader.parse(arrayBuffer, this.props.loadOptions); this.capabilities = capabilities; return capabilities; } /** Get a map image */ async getTile_( options: WMTSGetTileParameters, vendorParameters?: Record ): Promise { const url = this.getTileURL(options, vendorParameters); const response = await this.fetch(url); const arrayBuffer = await response.arrayBuffer(); this._checkResponse(response, arrayBuffer); try { return await ImageLoader.parse(arrayBuffer, this.props.loadOptions); } catch { throw this._parseError(arrayBuffer); } } /** Get Feature Info for a coordinate */ async getFeatureInfo( options: WMTSGetFeatureInfoParameters, vendorParameters?: Record ): Promise { const url = this.getFeatureInfoURL(options, vendorParameters); const response = await this.fetch(url); const arrayBuffer = await response.arrayBuffer(); this._checkResponse(response, arrayBuffer); return await GMLLoader.parse(arrayBuffer, this.props.loadOptions); } /** Get Feature Info for a coordinate */ async getFeatureInfoText( options: WMTSGetFeatureInfoParameters, vendorParameters?: Record ): Promise { options = {...options, info_format: 'text/plain'}; const url = this.getFeatureInfoURL(options, vendorParameters); const response = await this.fetch(url); const arrayBuffer = await response.arrayBuffer(); this._checkResponse(response, arrayBuffer); return new TextDecoder().decode(arrayBuffer); } /** Get an image with a semantic legend */ async getLegendGraphic( options: WMTSGetLegendGraphicParameters, vendorParameters?: Record ): Promise { const url = this.getLegendGraphicURL(options, vendorParameters); const response = await this.fetch(url); const arrayBuffer = await response.arrayBuffer(); this._checkResponse(response, arrayBuffer); try { return await ImageLoader.parse(arrayBuffer, this.props.loadOptions); } catch { throw this._parseError(arrayBuffer); } } // Typed URL creators // For applications that want full control of fetching and parsing /** Generate a URL for the GetCapabilities request */ getCapabilitiesURL( wmtsParameters?: WMTSGetCapabilitiesParameters, vendorParameters?: Record ): string { const options: Required = { service: 'WMTS', version: '1.0.0', request: 'GetCapabilities', ...wmtsParameters, ...vendorParameters }; const url = `${this.props.url}/WMTSCapabilities.xml`; return this._getWMTSUrl(options, vendorParameters); } /** Generate a URL for the GetTile request */ getTileURL( wmtsParameters: WMTSGetTileParameters, vendorParameters?: Record ): string { const options: Required = { service: 'WMTS', version: '1.0.0', request: 'GetTile', style: undefined, format: 'image/png', // tileMatrixSet // tileMatrix // tileCol // tileRow ...wmtsParameters, ...vendorParameters }; const {version, layer = 'default', style = 'default', tileMatrixSet = 'default', tileMatrix, tileCol, tileRow} = options; const extension = options.format.replace('image/', ''); const url = `${this.props.url}/tile/${version}/${layer}/${style}/${tileMatrixSet}/${tileMatrix}/${tileCol}/${tileRow}.${extension}`; return this._getWMTSUrl(options, vendorParameters); } /** Generate a URL for the GetFeatureInfo request */ getFeatureInfoURL( wmtsParameters: WMTSGetFeatureInfoParameters, vendorParameters?: Record ): string { const options: Required = { service: 'WMTS', version: '1.0.0', request: 'GetFeatureInfo', // layer: [], // bbox: [-77.87304, 40.78975, -77.85828, 40.80228], // width: 1200, // height: 900, // x: undefined!, // y: undefined!, // query_layer: [], // srs: 'EPSG:4326', // format: 'image/png', info_format: 'text/plain', style: undefined!, ...wmtsParameters, ...vendorParameters }; return this._getWMTSUrl(options, vendorParameters); } getLegendGraphicURL( wmtsParameters: WMTSGetLegendGraphicParameters, vendorParameters?: Record ): string { const options: Required = { service: 'WMTS', version: '1.0.0', request: 'GetLegendGraphic', ...wmtsParameters, ...vendorParameters }; return this._getWMTSUrl(options, vendorParameters); } // INTERNAL METHODS /** * @note case _getWMTSUrl may need to be overridden to handle certain backends? * */ protected _getWMTSUrl( options: Record, vendorParameters?: Record ): string { switch (this.requestEncoding) { case 'kvp': return this._getWMTSUrlKVP(options, vendorParameters); // case 'REST': // case 'SOAP': default: throw new Error(this.requestEncoding); } } /** * @note case _getWMTSUrl may need to be overridden to handle certain backends? * */ protected _getWMTSUrlKVP( options: Record, vendorParameters?: Record ): string { let url = this.props.url; let first = true; for (const [key, value] of Object.entries(options)) { url += first ? '?' : '&'; first = false; if (Array.isArray(value)) { url += `${key.toUpperCase()}=${value.join(',')}`; } else { url += `${key.toUpperCase()}=${value ? String(value) : ''}`; } } return encodeURI(url); } /** Checks for and parses a WMTS XML formatted ServiceError and throws an exception */ protected _checkResponse(response: Response, arrayBuffer: ArrayBuffer): void { const contentType = response.headers['content-type']; if (!response.ok || WMTSErrorLoader.mimeTypes.includes(contentType)) { const error = WMTSErrorLoader.parseSync(arrayBuffer, this.props.loadOptions); throw new Error(error); } } /** Error situation detected */ protected _parseError(arrayBuffer: ArrayBuffer): Error { const error = WMTSErrorLoader.parseSync(arrayBuffer, this.props.loadOptions); return new Error(error); } }