// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { DataSource, resolvePath } from '@loaders.gl/loader-utils'; import { ImageLoader, getBinaryImageMetadata } from '@loaders.gl/images'; import { MVTLoader, TileJSONLoader } from '@loaders.gl/mvt'; /** Creates an MVTTileSource */ export const MVTSource = { name: 'MVT', id: 'mvt', module: 'mvt', version: '0.0.0', extensions: ['mvt'], mimeTypes: ['application/octet-stream'], options: { mvt: { // TODO - add options here } }, type: 'mvt', fromUrl: true, fromBlob: false, testURL: (url) => true, createDataSource(url, props) { return new MVTTileSource(url, props); } }; /** * MVT data source for Mapbox Vector Tiles v1. */ /** * A PMTiles data source * @note Can be either a raster or vector tile source depending on the contents of the PMTiles file. */ export class MVTTileSource extends DataSource { props; url; metadataUrl = null; data; schema = 'tms'; metadata; extension; mimeType = null; constructor(url, props) { super(props); this.props = props; this.url = resolvePath(url); this.metadataUrl = props.mvt?.metadataUrl || `${this.url}/tilejson.json`; this.extension = props.mvt?.extension || '.png'; this.data = this.url; this.getTileData = this.getTileData.bind(this); this.metadata = this.getMetadata(); if (isURLTemplate(this.url)) { this.schema = 'template'; } } // @ts-ignore - Metadata type misalignment async getMetadata() { if (!this.metadataUrl) { return null; } let response; try { // Annoyingly, on CORS errors, fetch doesn't use the response status/ok mechanism but instead throws // CORS errors are common when requesting an unavailable sub resource such as a metadata file or an unavailable tile) response = await this.fetch(this.metadataUrl); } catch (error) { // eslint-disable-next-line no-console console.error(error.message); return null; } if (!response.ok) { // eslint-disable-next-line no-console console.error(response.statusText); return null; } const tileJSON = await response.text(); const metadata = TileJSONLoader.parseTextSync?.(tileJSON) || null; // TODO add metadata attributions // metadata.attributions = [...this.props.attributions, ...(metadata.attributions || [])]; // if (metadata?.mimeType) { // this.mimeType = metadata?.tileMIMEType; // } return metadata; } getTileMIMEType() { return this.mimeType; } async getTile(parameters) { const { x, y, z } = parameters; const tileUrl = this.getTileURL(x, y, z); const response = await this.fetch(tileUrl); if (!response.ok) { return null; } const arrayBuffer = await response.arrayBuffer(); return arrayBuffer; } // Tile Source interface implementation: deck.gl compatible API // TODO - currently only handles image tiles, not vector tiles async getTileData(parameters) { const { x, y, z } = parameters.index; // const metadata = await this.metadata; // mimeType = metadata?.tileMIMEType || 'application/vnd.mapbox-vector-tile'; const arrayBuffer = await this.getTile({ x, y, z, layers: [] }); if (arrayBuffer === null) { return null; } const imageMetadata = getBinaryImageMetadata(arrayBuffer); this.mimeType = this.mimeType || imageMetadata?.mimeType || 'application/vnd.mapbox-vector-tile'; switch (this.mimeType) { case 'application/vnd.mapbox-vector-tile': return await this._parseVectorTile(arrayBuffer, { x, y, z, layers: [] }); default: return await this._parseImageTile(arrayBuffer); } } // ImageTileSource interface implementation async getImageTile(tileParams) { const arrayBuffer = await this.getTile(tileParams); return arrayBuffer ? this._parseImageTile(arrayBuffer) : null; } async _parseImageTile(arrayBuffer) { return await ImageLoader.parse(arrayBuffer, this.loadOptions); } // VectorTileSource interface implementation async getVectorTile(tileParams) { const arrayBuffer = await this.getTile(tileParams); return arrayBuffer ? this._parseVectorTile(arrayBuffer, tileParams) : null; } async _parseVectorTile(arrayBuffer, tileParams) { const loadOptions = { shape: 'geojson-table', mvt: { coordinates: 'wgs84', tileIndex: { x: tileParams.x, y: tileParams.y, z: tileParams.z }, ...this.loadOptions?.mvt }, ...this.loadOptions }; return await MVTLoader.parse(arrayBuffer, loadOptions); } getMetadataUrl() { return this.metadataUrl; } getTileURL(x, y, z) { switch (this.schema) { case 'xyz': return `${this.url}/${x}/${y}/${z}${this.extension}`; case 'tms': return `${this.url}/${z}/${x}/${y}${this.extension}`; case 'template': return getURLFromTemplate(this.url, x, y, z, '0'); default: throw new Error(this.schema); } } } export function isURLTemplate(s) { return /(?=.*{z})(?=.*{x})(?=.*({y}|{-y}))|(?=.*{x})(?=.*({y}|{-y})(?=.*{z}))/.test(s); } const xRegex = new RegExp('{x}', 'g'); const yRegex = new RegExp('{y}', 'g'); const zRegex = new RegExp('{z}', 'g'); /** * Get a URL from a URL template * @note copied from deck.gl/modules/geo-layers/src/tileset-2d/utils.ts * @param template - URL template * @param x - tile x coordinate * @param y - tile y coordinate * @param z - tile z coordinate * @param id - tile id * @returns URL */ export function getURLFromTemplate(template, x, y, z, id = '0') { if (Array.isArray(template)) { const i = stringHash(id) % template.length; template = template[i]; } let url = template; url = url.replace(xRegex, String(x)); url = url.replace(yRegex, String(y)); url = url.replace(zRegex, String(z)); // Back-compatible support for {-y} if (Number.isInteger(y) && Number.isInteger(z)) { url = url.replace(/\{-y\}/g, String(Math.pow(2, z) - y - 1)); } return url; } function stringHash(s) { return Math.abs(s.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)); }