* export DecompressFunc type
* add typedoc comments
This commit is contained in:
Brandon Liu
2024-02-25 18:29:48 +08:00
committed by GitHub
parent 264b1d0db8
commit 3451540a3a
7 changed files with 105 additions and 15 deletions

View File

@@ -1,3 +1,6 @@
3.0.4
* export DecompressFunc type
3.0.3 3.0.3
* Deprecate `prefetch`-ing the first 16 kb as an option, always true * Deprecate `prefetch`-ing the first 16 kb as an option, always true

View File

@@ -15,6 +15,11 @@ interface DocumentLike {
// biome-ignore lint: we don't want to bring in the entire document type // biome-ignore lint: we don't want to bring in the entire document type
type DoneCallback = (error?: Error, tile?: any) => void; type DoneCallback = (error?: Error, tile?: any) => void;
/**
* Add a raster PMTiles as a layer to a Leaflet map.
*
* For vector tiles see https://github.com/protomaps/protomaps-leaflet
*/
export const leafletRasterLayer = (source: PMTiles, options: unknown) => { export const leafletRasterLayer = (source: PMTiles, options: unknown) => {
let loaded = false; let loaded = false;
let mimeType = ""; let mimeType = "";
@@ -155,6 +160,9 @@ const v3compat =
return { cancel: () => abortController.abort() }; return { cancel: () => abortController.abort() };
}; };
/**
* MapLibre GL JS protocol. Must be added once globally.
*/
export class Protocol { export class Protocol {
tiles: Map<string, PMTiles>; tiles: Map<string, PMTiles>;

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.0/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.0/dist/leaflet.js"></script>
<script src="https://unpkg.com/pmtiles@3.0.3/dist/pmtiles.js"></script> <script src="https://unpkg.com/pmtiles@3.0.4/dist/pmtiles.js"></script>
<style> <style>
body, #map { body, #map {
height:100vh; height:100vh;

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css" crossorigin="anonymous"> <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css" crossorigin="anonymous">
<script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js" crossorigin="anonymous"></script> <script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/pmtiles@3.0.3/dist/pmtiles.js"></script> <script src="https://unpkg.com/pmtiles@3.0.4/dist/pmtiles.js"></script>
<style> <style>
body { body {
margin: 0; margin: 0;

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css" crossorigin="anonymous"> <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css" crossorigin="anonymous">
<script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js" crossorigin="anonymous"></script> <script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/pmtiles@3.0.3/dist/pmtiles.js"></script> <script src="https://unpkg.com/pmtiles@3.0.4/dist/pmtiles.js"></script>
<style> <style>
body { body {
margin: 0; margin: 0;

View File

@@ -94,6 +94,9 @@ const tzValues: number[] = [
93824992236885, 375299968947541, 1501199875790165, 93824992236885, 375299968947541, 1501199875790165,
]; ];
/**
* Convert Z,X,Y to a Hilbert TileID.
*/
export function zxyToTileId(z: number, x: number, y: number): number { export function zxyToTileId(z: number, x: number, y: number): number {
if (z > 26) { if (z > 26) {
throw Error("Tile zoom level exceeds max safe number limit (26)"); throw Error("Tile zoom level exceeds max safe number limit (26)");
@@ -119,6 +122,9 @@ export function zxyToTileId(z: number, x: number, y: number): number {
return acc + d; return acc + d;
} }
/**
* Convert a Hilbert TileID to Z,X,Y.
*/
export function tileIdToZxy(i: number): [number, number, number] { export function tileIdToZxy(i: number): [number, number, number] {
let acc = 0; let acc = 0;
const z = 0; const z = 0;
@@ -134,6 +140,9 @@ export function tileIdToZxy(i: number): [number, number, number] {
throw Error("Tile zoom level exceeds max safe number limit (26)"); throw Error("Tile zoom level exceeds max safe number limit (26)");
} }
/**
* PMTiles v3 directory entry.
*/
export interface Entry { export interface Entry {
tileId: number; tileId: number;
offset: number; offset: number;
@@ -141,6 +150,11 @@ export interface Entry {
runLength: number; runLength: number;
} }
/**
* Enum representing a compression algorithm used.
* 0 = unknown compression, for if you must use a different or unspecified algorithm.
* 1 = no compression.
*/
export enum Compression { export enum Compression {
Unknown = 0, Unknown = 0,
None = 1, None = 1,
@@ -149,7 +163,13 @@ export enum Compression {
Zstd = 4, Zstd = 4,
} }
type DecompressFunc = ( /**
* Provide a decompression implementation that acts on `buf` and returns decompressed data.
*
* Should use the native DecompressionStream on browsers, zlib on node.
* Should throw if the compression algorithm is not supported.
*/
export type DecompressFunc = (
buf: ArrayBuffer, buf: ArrayBuffer,
compression: Compression compression: Compression
) => Promise<ArrayBuffer>; ) => Promise<ArrayBuffer>;
@@ -179,6 +199,10 @@ async function defaultDecompress(
throw Error("Compression method not supported"); throw Error("Compression method not supported");
} }
/**
* Describe the type of tiles stored in the archive.
* 0 is unknown/other, 1 is "MVT" vector tiles.
*/
export enum TileType { export enum TileType {
Unknown = 0, Unknown = 0,
Mvt = 1, Mvt = 1,
@@ -190,6 +214,9 @@ export enum TileType {
const HEADER_SIZE_BYTES = 127; const HEADER_SIZE_BYTES = 127;
/**
* PMTiles v3 header storing basic archive-level information.
*/
export interface Header { export interface Header {
specVersion: number; specVersion: number;
rootDirectoryOffset: number; rootDirectoryOffset: number;
@@ -219,6 +246,9 @@ export interface Header {
etag?: string; etag?: string;
} }
/**
* Low-level function for looking up a TileID or leaf directory inside a directory.
*/
export function findTile(entries: Entry[], tileId: number): Entry | null { export function findTile(entries: Entry[], tileId: number): Entry | null {
let m = 0; let m = 0;
let n = entries.length - 1; let n = entries.length - 1;
@@ -253,8 +283,9 @@ export interface RangeResponse {
cacheControl?: string; cacheControl?: string;
} }
// In the future this may need to change /**
// to support ReadableStream to pass to native DecompressionStream API * Interface for retrieving an archive from remote or local storage.
*/
export interface Source { export interface Source {
getBytes: ( getBytes: (
offset: number, offset: number,
@@ -263,11 +294,16 @@ export interface Source {
etag?: string etag?: string
) => Promise<RangeResponse>; ) => Promise<RangeResponse>;
/**
* Return a unique string key for the archive e.g. a URL.
*/
getKey: () => string; getKey: () => string;
} }
// uses the Browser's File API, which is different from the NodeJS file API. /**
// see https://developer.mozilla.org/en-US/docs/Web/API/File_API * Use the Browser's File API, which is different from the NodeJS file API.
* see https://developer.mozilla.org/en-US/docs/Web/API/File_API
*/
export class FileSource implements Source { export class FileSource implements Source {
file: File; file: File;
@@ -286,6 +322,12 @@ export class FileSource implements Source {
} }
} }
/**
* Uses the browser Fetch API to make tile requests via HTTP.
*
* This method does not send conditional request headers If-Match because of CORS.
* Instead, it detects ETag mismatches via the response ETag or the 416 response code.
*/
export class FetchSource implements Source { export class FetchSource implements Source {
url: string; url: string;
customHeaders: Headers; customHeaders: Headers;
@@ -399,6 +441,9 @@ export function getUint64(v: DataView, offset: number): number {
return wh * 2 ** 32 + wl; return wh * 2 ** 32 + wl;
} }
/**
* Parse raw header bytes into a Header object.
*/
export function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header { export function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header {
const v = new DataView(bytes); const v = new DataView(bytes);
const specVersion = v.getUint8(7); const specVersion = v.getUint8(7);
@@ -488,8 +533,15 @@ function detectVersion(a: ArrayBuffer): number {
return 3; return 3;
} }
/**
* Error thrown when a response for PMTiles over HTTP does not match previous, cached parts of the archive.
* The default PMTiles implementation will catch this error once internally and retry a request.
*/
export class EtagMismatch extends Error {} export class EtagMismatch extends Error {}
/**
* Interface for caches of parts (headers, directories) of a PMTiles archive.
*/
export interface Cache { export interface Cache {
getHeader: (source: Source) => Promise<Header>; getHeader: (source: Source) => Promise<Header>;
getDirectory: ( getDirectory: (
@@ -565,6 +617,13 @@ interface ResolvedValue {
data: Header | Entry[] | ArrayBuffer; data: Header | Entry[] | ArrayBuffer;
} }
/**
* A cache for parts of a PMTiles archive where promises are never shared between requests.
*
* Runtimes such as Cloudflare Workers cannot share promises between different requests.
*
* Only caches headers and directories, not individual tile contents.
*/
export class ResolvedValueCache { export class ResolvedValueCache {
cache: Map<string, ResolvedValue>; cache: Map<string, ResolvedValue>;
maxCacheEntries: number; maxCacheEntries: number;
@@ -691,10 +750,11 @@ interface SharedPromiseCacheValue {
data: Promise<Header | Entry[] | ArrayBuffer>; data: Promise<Header | Entry[] | ArrayBuffer>;
} }
// a "dumb" bag of bytes. /**
// only caches headers and directories * A cache for parts of a PMTiles archive where promises can be shared between requests.
// deduplicates simultaneous responses *
// (estimates) the maximum size of the cache. * Only caches headers and directories, not individual tile contents.
*/
export class SharedPromiseCache { export class SharedPromiseCache {
cache: Map<string, SharedPromiseCacheValue>; cache: Map<string, SharedPromiseCacheValue>;
invalidations: Map<string, Promise<void>>; invalidations: Map<string, Promise<void>>;
@@ -843,8 +903,13 @@ export class SharedPromiseCache {
} }
} }
// if source is a string, create a browser FetchSource(). /**
// if no cache is passed, create a browser SharedPromiseCache where simultaneous tile requests share intermediate requests. * Main class encapsulating PMTiles decoding logic.
*
* if `source` is a string, creates a FetchSource using that string as the URL to a remote PMTiles.
* if no `cache` is passed, use a SharedPromiseCache.
* if no `decompress` is passed, default to the browser DecompressionStream API with a fallback to `fflate`.
*/
// biome-ignore lint: that's just how its capitalized // biome-ignore lint: that's just how its capitalized
export class PMTiles { export class PMTiles {
source: Source; source: Source;
@@ -873,10 +938,15 @@ export class PMTiles {
} }
} }
/**
* Return the header of the archive,
* including information such as tile type, min/max zoom, bounds, and summary statistics.
*/
async getHeader() { async getHeader() {
return await this.cache.getHeader(this.source); return await this.cache.getHeader(this.source);
} }
/** @hidden */
async getZxyAttempt( async getZxyAttempt(
z: number, z: number,
x: number, x: number,
@@ -930,6 +1000,11 @@ export class PMTiles {
throw Error("Maximum directory depth exceeded"); throw Error("Maximum directory depth exceeded");
} }
/**
* Primary method to get a single tile bytes from an archive.
*
* Returns undefined if the tile does not exist in the archive.
*/
async getZxy( async getZxy(
z: number, z: number,
x: number, x: number,
@@ -947,6 +1022,7 @@ export class PMTiles {
} }
} }
/** @hidden */
async getMetadataAttempt(): Promise<unknown> { async getMetadataAttempt(): Promise<unknown> {
const header = await this.cache.getHeader(this.source); const header = await this.cache.getHeader(this.source);
@@ -964,6 +1040,9 @@ export class PMTiles {
return JSON.parse(dec.decode(decompressed)); return JSON.parse(dec.decode(decompressed));
} }
/**
* Return the arbitrary JSON metadata of the archive.
*/
async getMetadata(): Promise<unknown> { async getMetadata(): Promise<unknown> {
try { try {
return await this.getMetadataAttempt(); return await this.getMetadataAttempt();

View File

@@ -1,6 +1,6 @@
{ {
"name": "pmtiles", "name": "pmtiles",
"version": "3.0.3", "version": "3.0.4",
"description": "PMTiles archive decoder for browsers", "description": "PMTiles archive decoder for browsers",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",