UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

252 lines (224 loc) 10.9 kB
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]; } } }