UNPKG

@maticoapp/deck.gl-pmtiles

Version:

This repo provides a typed Deck.gl layer and loader for PMTiles data. PMTiles data provides a serverless and compact way to store tile geospatial data. Combined with Deck.gl's rendering, this provides a flexible and powerful way to cheaply manage geospati

224 lines (190 loc) 6.63 kB
import { type TileLayerProps, MVTLayer } from "@deck.gl/geo-layers/typed"; import { type DefaultProps } from "@deck.gl/core/typed"; import { GeoJsonLayer, type GeoJsonLayerProps } from "@deck.gl/layers/typed"; import { findTile, PMTiles, zxyToTileId } from "pmtiles"; import type { BinaryFeatures } from "@loaders.gl/schema"; import type { Feature } from "geojson"; import type {Loader} from '@loaders.gl/loader-utils'; import { PMTWorkerLoader } from "../pmt-loader"; // from @deck.gl/geo-layers/src/mvt-layer/mvt-layer export type TileJson = { tilejson: string; tiles: string[]; // eslint-disable-next-line camelcase vector_layers: any[]; attribution?: string; scheme?: string; maxzoom?: number; minzoom?: number; version?: string; }; /** Props added by the MVTLayer */ export type _MVTLayerProps = { /** Called if `data` is a TileJSON URL when it is successfully fetched. */ onDataLoad?: ((tilejson: TileJson | null) => void) | null; /** Needed for highlighting a feature split across two or more tiles. */ uniqueIdProperty?: string; /** A feature with ID corresponding to the supplied value will be highlighted. */ highlightedFeatureId?: string | null; /** * Use tile data in binary format. * * @default true */ binary?: boolean; /** * Loaders used to transform tiles into `data` property passed to `renderSubLayers`. * * @default [MVTWorkerLoader] from `@loaders.gl/mvt` */ loaders?: Loader[]; }; // From @deck.gl/geo-layers/typed/tile-layer/types export type GeoBoundingBox = {west: number; north: number; east: number; south: number}; export type NonGeoBoundingBox = {left: number; top: number; right: number; bottom: number}; export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox; export type TileIndex = {x: number; y: number; z: number}; export type TileLoadProps = { index: TileIndex; id: string; bbox: TileBoundingBox; url?: string | null; signal?: AbortSignal; userData?: Record<string, any>; zoom?: number; }; export type ParsedPmTile = Feature[] | BinaryFeatures; export type ExtraProps = { raster?: boolean; }; export type _PMTLayerProps = _MVTLayerProps & ExtraProps; export type PmtLayerProps<DataT extends Feature = Feature> = _PMTLayerProps & GeoJsonLayerProps<DataT> & TileLayerProps<ParsedPmTile>; const defaultProps: DefaultProps<PmtLayerProps> = { ...GeoJsonLayer.defaultProps, onDataLoad: { type: "function", value: null, optional: true, compare: false }, uniqueIdProperty: "", highlightedFeatureId: null, binary: true, raster: false, loaders: [PMTWorkerLoader] }; type ZxyOffset = { offset: number; length: number }; export class DeckglPmtiles extends PMTiles { async getZxyOffset( z: number, x: number, y: number, signal?: AbortSignal ): Promise<ZxyOffset | undefined> { const tile_id = zxyToTileId(z, x, y); const header = await this.cache.getHeader(this.source); // V2 COMPATIBILITY // if (header.specVersion < 3) { // return v2.getZxy(header, this.source, this.cache, z, x, y, signal); // } if (z < header.minZoom || z > header.maxZoom) { return undefined; } let d_o = header.rootDirectoryOffset; let d_l = header.rootDirectoryLength; for (let depth = 0; depth <= 3; depth++) { const directory = await this.cache.getDirectory( this.source, d_o, d_l, header ); const entry = findTile(directory, tile_id); if (entry) { if (entry.runLength > 0) { return { offset: entry.offset, length: entry.length, }; } else { d_o = header.leafDirectoryOffset + entry.offset; d_l = entry.length; } } else { return undefined; } } throw Error("Maximum directory depth exceeded"); } } export class PMTLayer< DataT extends Feature = Feature, ExtraProps = {} > extends MVTLayer<DataT, ExtraProps> { static layerName = "PMTilesLayer"; static defaultProps = defaultProps; initializeState(): void { super.initializeState(); // GlobeView doesn't work well with binary data const binary = this.context.viewport.resolution !== undefined ? false : this.props.binary; // @ts-ignore const raster = this.props.raster; (this as any)._updateTileData = async (): Promise<void> => { const data = this.props.data; // @ts-ignore const raster = this.props.raster; const pmtiles = new DeckglPmtiles(data as string); const header = await pmtiles.getHeader(); this.setState({ data, pmtiles, raster, header }); }; this.setState({ binary, raster, data: null, }); } getTileData(loadProps: TileLoadProps, iter?: number): Promise<ParsedPmTile> { const { index, signal } = loadProps; const { data, binary, raster, pmtiles, header } = this.state; const { x, y, z } = index; let loadOptions = this.getLoadOptions(); const { fetch } = this.props; return pmtiles .getZxyOffset(z, x, y, signal) .then((entry: Awaited<ZxyOffset | undefined>) => { if (!entry) { return new Promise((resolve) => resolve(null)); } const tileOffset = entry.offset + header.tileDataOffset; const tileLength = entry.length; loadOptions = { ...loadOptions, mimeType: "application/x-protobuf", pmt: { workerUrl: "https://unpkg.com/@maticoapp/deck.gl-pmtiles@latest/dist/pmt-worker.js", coordinates: this.context.viewport.resolution ? "wgs84" : "local", tileIndex: index, raster: raster, tileCompression: header.tileCompression, ...loadOptions?.pmt, }, gis: binary ? { format: "binary" } : {}, fetch: { headers: { Range: `bytes=${tileOffset}-${tileOffset + tileLength - 1}`, }, }, }; return fetch(data, { propName: "data", layer: this, loadOptions, signal, }); }); } } export default PMTLayer; // code adapted from // Deckgl MVT Layer (MIT) https://github.com/visgl/deck.gl/blob/master/modules/geo-layers/src/mvt-layer/mvt-layer.ts // @jtmiclat/deck.gl-pmtiles (MIT) https://github.com/jtmiclat/deck.gl-pmtiles