maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
252 lines (224 loc) • 10.9 kB
text/typescript
import Protobuf from 'pbf';
import {VectorTile} from '@mapbox/vector-tile';
import {type ExpiryData, getArrayBuffer} from '../util/ajax';
import {WorkerTile} from './worker_tile';
import {BoundedLRUCache} from '../tile/tile_cache';
import {extend} from '../util/util';
import {RequestPerformance} from '../util/performance';
import {VectorTileOverzoomed, sliceVectorTileLayer, toVirtualVectorTile} from './vector_tile_overzoomed';
import {MLTVectorTile} from './vector_tile_mlt';
import type {
WorkerSource,
WorkerTileParameters,
TileParameters,
WorkerTileResult
} from '../source/worker_source';
import type {IActor} from '../util/actor';
import type {StyleLayer} from '../style/style_layer';
import type {StyleLayerIndex} from '../style/style_layer_index';
import type {VectorTileLayerLike, VectorTileLike} from '@maplibre/vt-pbf';
export type LoadVectorTileResult = {
vectorTile: VectorTileLike;
rawData: ArrayBufferLike;
resourceTiming?: Array<PerformanceResourceTiming>;
} & ExpiryData;
type FetchingState = {
rawTileData: ArrayBufferLike;
cacheControl: ExpiryData;
resourceTiming: any;
};
export type AbortVectorData = () => void;
export type LoadVectorData = (params: WorkerTileParameters, abortController: AbortController) => Promise<LoadVectorTileResult | null>;
/**
* The {@link WorkerSource} implementation that supports {@link VectorTileSource}.
* This class is designed to be easily reused to support custom source types
* for data formats that can be parsed/converted into an in-memory VectorTile
* representation. To do so, override its `loadVectorTile` method.
*/
export class VectorTileWorkerSource implements WorkerSource {
actor: IActor;
layerIndex: StyleLayerIndex;
availableImages: Array<string>;
fetching: {[_: string]: FetchingState };
loading: {[_: string]: WorkerTile};
loaded: {[_: string]: WorkerTile};
overzoomedTileResultCache: BoundedLRUCache<string, LoadVectorTileResult>;
/**
* @param loadVectorData - Optional method for custom loading of a VectorTile
* object based on parameters passed from the main-thread Source. See
* {@link VectorTileWorkerSource.loadTile}. The default implementation simply
* loads the pbf at `params.url`.
*/
constructor(actor: IActor, layerIndex: StyleLayerIndex, availableImages: Array<string>) {
this.actor = actor;
this.layerIndex = layerIndex;
this.availableImages = availableImages;
this.fetching = {};
this.loading = {};
this.loaded = {};
this.overzoomedTileResultCache = new BoundedLRUCache<string, LoadVectorTileResult>(1000);
}
/**
* Loads a vector tile
*/
async loadVectorTile(params: WorkerTileParameters, abortController: AbortController): Promise<LoadVectorTileResult> {
const response = await getArrayBuffer(params.request, abortController);
try {
const vectorTile = params.encoding !== 'mlt'
? new VectorTile(new Protobuf(response.data))
: new MLTVectorTile(response.data);
return {
vectorTile,
rawData: response.data,
cacheControl: response.cacheControl,
expires: response.expires
};
} catch (ex) {
const bytes = new Uint8Array(response.data);
const isGzipped = bytes[0] === 0x1f && bytes[1] === 0x8b;
let errorMessage = `Unable to parse the tile at ${params.request.url}, `;
if (isGzipped) {
errorMessage += 'please make sure the data is not gzipped and that you have configured the relevant header in the server';
} else {
errorMessage += `got error: ${ex.message}`;
}
throw new Error(errorMessage);
}
}
/**
* Implements {@link WorkerSource.loadTile}. Delegates to
* {@link VectorTileWorkerSource.loadVectorData} (which by default expects
* a `params.url` property) for fetching and producing a VectorTile object.
*/
async loadTile(params: WorkerTileParameters): Promise<WorkerTileResult | null> {
const {uid: tileUid, overzoomParameters} = params;
if (overzoomParameters) {
params.request = overzoomParameters.overzoomRequest;
}
const perf = (params && params.request && params.request.collectResourceTiming) ?
new RequestPerformance(params.request) : false;
const workerTile = new WorkerTile(params);
this.loading[tileUid] = workerTile;
const abortController = new AbortController();
workerTile.abort = abortController;
try {
const response = await this.loadVectorTile(params, abortController);
delete this.loading[tileUid];
if (!response) {
return null;
}
if (overzoomParameters) {
const overzoomTile = this._getOverzoomTile(params, response.vectorTile);
response.rawData = overzoomTile.rawData;
response.vectorTile = overzoomTile.vectorTile;
}
const rawTileData = response.rawData;
const cacheControl = {} as ExpiryData;
if (response.expires) cacheControl.expires = response.expires;
if (response.cacheControl) cacheControl.cacheControl = response.cacheControl;
const resourceTiming = {} as {resourceTiming: any};
if (perf) {
const resourceTimingData = perf.finish();
// it's necessary to eval the result of getEntriesByName() here via parse/stringify
// late evaluation in the main thread causes TypeError: illegal invocation
if (resourceTimingData)
resourceTiming.resourceTiming = JSON.parse(JSON.stringify(resourceTimingData));
}
workerTile.vectorTile = response.vectorTile;
const parsePromise = workerTile.parse(response.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity);
this.loaded[tileUid] = workerTile;
// keep the original fetching state so that reload tile can pick it up if the original parse is cancelled by reloads' parse
this.fetching[tileUid] = {rawTileData, cacheControl, resourceTiming};
try {
const result = await parsePromise;
// Transferring a copy of rawTileData because the worker needs to retain its copy.
return extend({rawTileData: rawTileData.slice(0), encoding: params.encoding}, result, cacheControl, resourceTiming);
} finally {
delete this.fetching[tileUid];
}
} catch (err) {
delete this.loading[tileUid];
workerTile.status = 'done';
this.loaded[tileUid] = workerTile;
throw err;
}
}
/**
* If we are seeking a tile deeper than the source's max available canonical tile, get the overzoomed tile
* @param params - the worker tile parameters
* @param maxZoomVectorTile - the original vector tile at the source's max available canonical zoom
* @returns the overzoomed tile and its raw data
*/
private _getOverzoomTile(params: WorkerTileParameters, maxZoomVectorTile: VectorTileLike): LoadVectorTileResult {
const {tileID, source, overzoomParameters} = params;
const {maxZoomTileID} = overzoomParameters;
const cacheKey = `${maxZoomTileID.key}_${tileID.key}`;
const cachedOverzoomTile = this.overzoomedTileResultCache.get(cacheKey);
if (cachedOverzoomTile) {
return cachedOverzoomTile;
}
const overzoomedVectorTile = new VectorTileOverzoomed();
const layerFamilies: Record<string, StyleLayer[][]> = this.layerIndex.familiesBySource[source];
for (const sourceLayerId in layerFamilies) {
const sourceLayer: VectorTileLayerLike = maxZoomVectorTile.layers[sourceLayerId];
if (!sourceLayer) {
continue;
}
const slicedTileLayer = sliceVectorTileLayer(sourceLayer, maxZoomTileID, tileID.canonical);
if (slicedTileLayer.length > 0) {
overzoomedVectorTile.addLayer(slicedTileLayer);
}
}
const overzoomedVectorTileResult = toVirtualVectorTile(overzoomedVectorTile);
this.overzoomedTileResultCache.set(cacheKey, overzoomedVectorTileResult);
return overzoomedVectorTileResult;
}
/**
* Implements {@link WorkerSource.reloadTile}.
*/
async reloadTile(params: WorkerTileParameters): Promise<WorkerTileResult> {
const uid = params.uid;
if (!this.loaded || !this.loaded[uid]) {
throw new Error('Should not be trying to reload a tile that was never loaded or has been removed');
}
const workerTile = this.loaded[uid];
workerTile.showCollisionBoxes = params.showCollisionBoxes;
if (workerTile.status === 'parsing') {
const result = await workerTile.parse(workerTile.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity);
// if we have cancelled the original parse, make sure to pass the rawTileData from the original fetch
let parseResult: WorkerTileResult;
if (this.fetching[uid]) {
const {rawTileData, cacheControl, resourceTiming} = this.fetching[uid];
delete this.fetching[uid];
parseResult = extend({rawTileData: rawTileData.slice(0), encoding: params.encoding}, result, cacheControl, resourceTiming);
} else {
parseResult = result;
}
return parseResult;
}
// if there was no vector tile data on the initial load, don't try and re-parse tile
if (workerTile.status === 'done' && workerTile.vectorTile) {
// this seems like a missing case where cache control is lost? see #3309
return workerTile.parse(workerTile.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity);
}
}
/**
* Implements {@link WorkerSource.abortTile}.
*/
async abortTile(params: TileParameters): Promise<void> {
const loading = this.loading;
const uid = params.uid;
if (loading && loading[uid] && loading[uid].abort) {
loading[uid].abort.abort();
delete loading[uid];
}
}
/**
* Implements {@link WorkerSource.removeTile}.
*/
async removeTile(params: TileParameters): Promise<void> {
if (this.loaded && this.loaded[params.uid]) {
delete this.loaded[params.uid];
}
}
}