@loaders.gl/mvt
Version:
Loader for Mapbox Vector Tiles
186 lines • 6.7 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { DataSource } from '@loaders.gl/loader-utils';
import { ImageLoader, getBinaryImageMetadata } from '@loaders.gl/images';
import { MVTLoader, TileJSONLoader } from '@loaders.gl/mvt';
import { MVTFormat } from "./mvt-format.js";
/** Creates an MVTTileSource */
export const MVTSource = {
...MVTFormat,
version: '0.0.0',
type: 'mvt',
fromUrl: true,
fromBlob: false,
defaultOptions: {
mvt: {
// TODO - add options here
}
},
testURL: (url) => true,
createDataSource(url, options) {
return new MVTTileSource(url, options);
}
};
/**
* 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 {
metadataUrl = null;
schema = 'tms';
metadata;
extension;
mimeType = null;
constructor(url, options) {
super(url, options, MVTSource.defaultOptions);
this.metadataUrl = options.mvt?.metadataUrl || `${this.url}/tilejson.json`;
this.extension = options.mvt?.extension || '.png';
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.options.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 = {
mvt: {
shape: 'geojson-table',
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));
}
//# sourceMappingURL=mvt-source.js.map