pmtiles
Version:
PMTiles archive decoder for browsers
1 lines • 43.4 kB
Source Map (JSON)
{"version":3,"sources":["/Users/bdon/workspace/protomaps/PMTiles/js/dist/cjs/index.cjs","../../src/index.ts","../../src/adapters.ts"],"names":["leafletRasterLayer","__name","source","options","loaded","mimeType","cls","coord","done","el","controller","signal","header","arr","blob","imageUrl","e","key","tile","v3compat","v4","requestParameters","arg2","abortController","result","err","_Protocol","params","__async","pmtilesUrl","instance","PMTiles","data"],"mappings":"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","file":"/Users/bdon/workspace/protomaps/PMTiles/js/dist/cjs/index.cjs","sourcesContent":[null,"import { decompressSync } from \"fflate\";\nexport * from \"./adapters\";\n\n/** @hidden */\nexport interface BufferPosition {\n buf: Uint8Array;\n pos: number;\n}\n\nfunction toNum(low: number, high: number): number {\n return (high >>> 0) * 0x100000000 + (low >>> 0);\n}\n\nfunction readVarintRemainder(l: number, p: BufferPosition): number {\n const buf = p.buf;\n let b = buf[p.pos++];\n let h = (b & 0x70) >> 4;\n if (b < 0x80) return toNum(l, h);\n b = buf[p.pos++];\n h |= (b & 0x7f) << 3;\n if (b < 0x80) return toNum(l, h);\n b = buf[p.pos++];\n h |= (b & 0x7f) << 10;\n if (b < 0x80) return toNum(l, h);\n b = buf[p.pos++];\n h |= (b & 0x7f) << 17;\n if (b < 0x80) return toNum(l, h);\n b = buf[p.pos++];\n h |= (b & 0x7f) << 24;\n if (b < 0x80) return toNum(l, h);\n b = buf[p.pos++];\n h |= (b & 0x01) << 31;\n if (b < 0x80) return toNum(l, h);\n throw new Error(\"Expected varint not more than 10 bytes\");\n}\n\n/** @hidden */\nexport function readVarint(p: BufferPosition): number {\n const buf = p.buf;\n let b = buf[p.pos++];\n let val = b & 0x7f;\n if (b < 0x80) return val;\n b = buf[p.pos++];\n val |= (b & 0x7f) << 7;\n if (b < 0x80) return val;\n b = buf[p.pos++];\n val |= (b & 0x7f) << 14;\n if (b < 0x80) return val;\n b = buf[p.pos++];\n val |= (b & 0x7f) << 21;\n if (b < 0x80) return val;\n b = buf[p.pos];\n val |= (b & 0x0f) << 28;\n\n return readVarintRemainder(val, p);\n}\n\nfunction rotate(\n n: number,\n x: number,\n y: number,\n rx: number,\n ry: number\n): [number, number] {\n if (ry === 0) {\n if (rx !== 0) {\n return [n - 1 - y, n - 1 - x];\n }\n return [y, x];\n }\n return [x, y];\n}\n\n/**\n * Convert Z,X,Y to a Hilbert TileID.\n */\nexport function zxyToTileId(z: number, x: number, y: number): number {\n if (z > 26) {\n throw new Error(\"Tile zoom level exceeds max safe number limit (26)\");\n }\n if (x >= 1 << z || y >= 1 << z) {\n throw new Error(\"tile x/y outside zoom level bounds\");\n }\n let acc = ((1 << z) * (1 << z) - 1) / 3;\n let a = z - 1;\n let [tx, ty] = [x, y];\n for (let s = 1 << a; s > 0; s >>= 1) {\n const rx = tx & s;\n const ry = ty & s;\n acc += ((3 * rx) ^ ry) * (1 << a);\n [tx, ty] = rotate(s, tx, ty, rx, ry);\n a--;\n }\n return acc;\n}\n\nfunction tileIdToZ(i: number): number {\n const c = 3 * i + 1;\n if (c < 0x100000000) {\n return 31 - Math.clz32(c);\n }\n return 63 - Math.clz32(c / 0x100000000);\n}\n\n/**\n * Convert a Hilbert TileID to Z,X,Y.\n */\nexport function tileIdToZxy(i: number): [number, number, number] {\n const z = tileIdToZ(i) >> 1;\n if (z > 26)\n throw new Error(\"Tile zoom level exceeds max safe number limit (26)\");\n const acc = ((1 << z) * (1 << z) - 1) / 3;\n\n let t = i - acc;\n let x = 0;\n let y = 0;\n const n = 1 << z;\n for (let s = 1; s < n; s <<= 1) {\n const rx = s & (t / 2);\n const ry = s & (t ^ rx);\n [x, y] = rotate(s, x, y, rx, ry);\n t = t / 2;\n x += rx;\n y += ry;\n }\n return [z, x, y];\n}\n\n/**\n * PMTiles v3 directory entry.\n */\nexport interface Entry {\n tileId: number;\n offset: number;\n length: number;\n runLength: number;\n}\n\ninterface MetadataLike {\n attribution?: string;\n name?: string;\n version?: string;\n // biome-ignore lint: TileJSON spec\n vector_layers?: string;\n description?: string;\n}\n\n/**\n * Enum representing a compression algorithm used.\n * 0 = unknown compression, for if you must use a different or unspecified algorithm.\n * 1 = no compression.\n */\nexport enum Compression {\n Unknown = 0,\n None = 1,\n Gzip = 2,\n Brotli = 3,\n Zstd = 4,\n}\n\n/**\n * Provide a decompression implementation that acts on `buf` and returns decompressed data.\n *\n * Should use the native DecompressionStream on browsers, zlib on node.\n * Should throw if the compression algorithm is not supported.\n */\nexport type DecompressFunc = (\n buf: ArrayBuffer,\n compression: Compression\n) => Promise<ArrayBuffer>;\n\nasync function defaultDecompress(\n buf: ArrayBuffer,\n compression: Compression\n): Promise<ArrayBuffer> {\n if (compression === Compression.None || compression === Compression.Unknown) {\n return buf;\n }\n if (compression === Compression.Gzip) {\n // biome-ignore lint: needed to detect DecompressionStream in browser+node+cloudflare workers\n if (typeof (globalThis as any).DecompressionStream === \"undefined\") {\n return decompressSync(new Uint8Array(buf));\n }\n const stream = new Response(buf).body;\n if (!stream) {\n throw new Error(\"Failed to read response stream\");\n }\n const result: ReadableStream<Uint8Array> = stream.pipeThrough(\n // biome-ignore lint: needed to detect DecompressionStream in browser+node+cloudflare workers\n new (globalThis as any).DecompressionStream(\"gzip\")\n );\n return new Response(result).arrayBuffer();\n }\n throw new Error(\"Compression method not supported\");\n}\n\n/**\n * Describe the type of tiles stored in the archive.\n * 0 is unknown/other, 1 is \"MVT\" vector tiles.\n */\nexport enum TileType {\n Unknown = 0,\n Mvt = 1,\n Png = 2,\n Jpeg = 3,\n Webp = 4,\n Avif = 5,\n Mlt = 6,\n}\n\nexport function tileTypeExt(t: TileType): string {\n if (t === TileType.Mvt) return \".mvt\";\n if (t === TileType.Png) return \".png\";\n if (t === TileType.Jpeg) return \".jpg\";\n if (t === TileType.Webp) return \".webp\";\n if (t === TileType.Avif) return \".avif\";\n if (t === TileType.Mlt) return \".mlt\";\n return \"\";\n}\n\nconst HEADER_SIZE_BYTES = 127;\n\n/**\n * PMTiles v3 header storing basic archive-level information.\n */\nexport interface Header {\n specVersion: number;\n rootDirectoryOffset: number;\n rootDirectoryLength: number;\n jsonMetadataOffset: number;\n jsonMetadataLength: number;\n leafDirectoryOffset: number;\n leafDirectoryLength?: number;\n tileDataOffset: number;\n tileDataLength?: number;\n numAddressedTiles: number;\n numTileEntries: number;\n numTileContents: number;\n clustered: boolean;\n internalCompression: Compression;\n tileCompression: Compression;\n tileType: TileType;\n minZoom: number;\n maxZoom: number;\n minLon: number;\n minLat: number;\n maxLon: number;\n maxLat: number;\n centerZoom: number;\n centerLon: number;\n centerLat: number;\n etag?: string;\n}\n\n/**\n * Low-level function for looking up a TileID or leaf directory inside a directory.\n */\nexport function findTile(entries: Entry[], tileId: number): Entry | null {\n let m = 0;\n let n = entries.length - 1;\n while (m <= n) {\n const k = (n + m) >> 1;\n const cmp = tileId - entries[k].tileId;\n if (cmp > 0) {\n m = k + 1;\n } else if (cmp < 0) {\n n = k - 1;\n } else {\n return entries[k];\n }\n }\n\n // at this point, m > n\n if (n >= 0) {\n if (entries[n].runLength === 0) {\n return entries[n];\n }\n if (tileId - entries[n].tileId < entries[n].runLength) {\n return entries[n];\n }\n }\n return null;\n}\n\nexport interface RangeResponse {\n data: ArrayBuffer;\n etag?: string;\n expires?: string;\n cacheControl?: string;\n}\n\n/**\n * Interface for retrieving an archive from remote or local storage.\n */\nexport interface Source {\n getBytes: (\n offset: number,\n length: number,\n signal?: AbortSignal,\n etag?: string\n ) => Promise<RangeResponse>;\n\n /**\n * Return a unique string key for the archive e.g. a URL.\n */\n getKey: () => string;\n}\n\n/**\n * Use the Browser's File API, which is different from the NodeJS file API.\n * see https://developer.mozilla.org/en-US/docs/Web/API/File_API\n */\nexport class FileSource implements Source {\n file: File;\n\n constructor(file: File) {\n this.file = file;\n }\n\n getKey() {\n return this.file.name;\n }\n\n async getBytes(offset: number, length: number): Promise<RangeResponse> {\n const blob = this.file.slice(offset, offset + length);\n const a = await blob.arrayBuffer();\n return { data: a };\n }\n}\n\n/**\n * Uses the browser Fetch API to make tile requests via HTTP.\n *\n * This method does not send conditional request headers If-Match because of CORS.\n * Instead, it detects ETag mismatches via the response ETag or the 416 response code.\n *\n * This also works around browser and storage-specific edge cases.\n */\nexport class FetchSource implements Source {\n url: string;\n\n /**\n * A [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object, specfying custom [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) set for all requests to the remote archive.\n *\n * This should be used instead of maplibre's [transformRequest](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#example) for PMTiles archives.\n */\n customHeaders: Headers;\n credentials: \"same-origin\" | \"include\" | undefined;\n /** @hidden */\n mustReload: boolean;\n /** @hidden */\n chromeWindowsNoCache: boolean;\n\n constructor(\n url: string,\n customHeaders: Headers = new Headers(),\n credentials: \"same-origin\" | \"include\" | undefined = undefined\n ) {\n this.url = url;\n this.customHeaders = customHeaders;\n this.credentials = credentials;\n this.mustReload = false;\n let userAgent = \"\";\n if (\"navigator\" in globalThis) {\n //biome-ignore lint: cf workers\n userAgent = (globalThis as any).navigator?.userAgent ?? \"\";\n }\n const isWindows = userAgent.indexOf(\"Windows\") > -1;\n const isChromiumBased = /Chrome|Chromium|Edg|OPR|Brave/.test(userAgent);\n this.chromeWindowsNoCache = false;\n if (isWindows && isChromiumBased) {\n this.chromeWindowsNoCache = true;\n }\n }\n\n getKey() {\n return this.url;\n }\n\n /**\n * Mutate the custom [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) set for all requests to the remote archive.\n */\n setHeaders(customHeaders: Headers) {\n this.customHeaders = customHeaders;\n }\n\n async getBytes(\n offset: number,\n length: number,\n passedSignal?: AbortSignal,\n etag?: string\n ): Promise<RangeResponse> {\n let controller: AbortController | undefined;\n let signal: AbortSignal | undefined;\n if (passedSignal) {\n signal = passedSignal;\n } else {\n controller = new AbortController();\n signal = controller.signal;\n }\n\n const requestHeaders = new Headers(this.customHeaders);\n requestHeaders.set(\"range\", `bytes=${offset}-${offset + length - 1}`);\n\n // we don't send if match because:\n // * it disables browser caching completely (Chromium)\n // * it requires a preflight request for every tile request\n // * it requires CORS configuration becasue If-Match is not a CORs-safelisted header\n // CORs configuration should expose ETag.\n // if any etag mismatch is detected, we need to ignore the browser cache\n let cache: \"no-store\" | \"reload\" | undefined;\n if (this.mustReload) {\n cache = \"reload\";\n } else if (this.chromeWindowsNoCache) {\n cache = \"no-store\";\n }\n\n let resp = await fetch(this.url, {\n signal: signal,\n cache: cache,\n headers: requestHeaders,\n credentials: this.credentials,\n });\n\n // handle edge case where the archive is < 16384 kb total.\n if (offset === 0 && resp.status === 416) {\n const contentRange = resp.headers.get(\"Content-Range\");\n if (!contentRange || !contentRange.startsWith(\"bytes */\")) {\n throw new Error(\"Missing content-length on 416 response\");\n }\n const actualLength = +contentRange.substr(8);\n requestHeaders.set(\"range\", `bytes=0-${actualLength - 1}`);\n resp = await fetch(this.url, {\n signal: signal,\n cache: \"reload\",\n headers: requestHeaders,\n credentials: this.credentials,\n });\n }\n\n // if it's a weak etag, it's not useful for us, so ignore it.\n let newEtag = resp.headers.get(\"Etag\");\n if (newEtag?.startsWith(\"W/\")) {\n newEtag = null;\n }\n\n // some storage systems are misbehaved (Cloudflare R2)\n if (resp.status === 416 || (etag && newEtag && newEtag !== etag)) {\n this.mustReload = true;\n throw new EtagMismatch(\n `Server returned non-matching ETag ${etag} after one retry. Check browser extensions and servers for issues that may affect correct ETag headers.`\n );\n }\n\n if (resp.status >= 300) {\n throw new Error(`Bad response code: ${resp.status}`);\n }\n\n // some well-behaved backends, e.g. DigitalOcean CDN, respond with 200 instead of 206\n // but we also need to detect no support for Byte Serving which is returning the whole file\n const contentLength = resp.headers.get(\"Content-Length\");\n if (resp.status === 200 && (!contentLength || +contentLength > length)) {\n if (controller) controller.abort();\n throw new Error(\n \"Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving.\"\n );\n }\n\n const a = await resp.arrayBuffer();\n return {\n data: a,\n etag: newEtag || undefined,\n cacheControl: resp.headers.get(\"Cache-Control\") || undefined,\n expires: resp.headers.get(\"Expires\") || undefined,\n };\n }\n}\n\n/** @hidden */\nexport function getUint64(v: DataView, offset: number): number {\n const wh = v.getUint32(offset + 4, true);\n const wl = v.getUint32(offset + 0, true);\n return wh * 2 ** 32 + wl;\n}\n\n/**\n * Parse raw header bytes into a Header object.\n */\nexport function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header {\n const v = new DataView(bytes);\n const specVersion = v.getUint8(7);\n if (specVersion > 3) {\n throw new Error(\n `Archive is spec version ${specVersion} but this library supports up to spec version 3`\n );\n }\n\n return {\n specVersion: specVersion,\n rootDirectoryOffset: getUint64(v, 8),\n rootDirectoryLength: getUint64(v, 16),\n jsonMetadataOffset: getUint64(v, 24),\n jsonMetadataLength: getUint64(v, 32),\n leafDirectoryOffset: getUint64(v, 40),\n leafDirectoryLength: getUint64(v, 48),\n tileDataOffset: getUint64(v, 56),\n tileDataLength: getUint64(v, 64),\n numAddressedTiles: getUint64(v, 72),\n numTileEntries: getUint64(v, 80),\n numTileContents: getUint64(v, 88),\n clustered: v.getUint8(96) === 1,\n internalCompression: v.getUint8(97),\n tileCompression: v.getUint8(98),\n tileType: v.getUint8(99),\n minZoom: v.getUint8(100),\n maxZoom: v.getUint8(101),\n minLon: v.getInt32(102, true) / 10000000,\n minLat: v.getInt32(106, true) / 10000000,\n maxLon: v.getInt32(110, true) / 10000000,\n maxLat: v.getInt32(114, true) / 10000000,\n centerZoom: v.getUint8(118),\n centerLon: v.getInt32(119, true) / 10000000,\n centerLat: v.getInt32(123, true) / 10000000,\n etag: etag,\n };\n}\n\nfunction deserializeIndex(buffer: ArrayBuffer): Entry[] {\n const p = { buf: new Uint8Array(buffer), pos: 0 };\n const numEntries = readVarint(p);\n\n const entries: Entry[] = [];\n\n let lastId = 0;\n for (let i = 0; i < numEntries; i++) {\n const v = readVarint(p);\n entries.push({ tileId: lastId + v, offset: 0, length: 0, runLength: 1 });\n lastId += v;\n }\n\n for (let i = 0; i < numEntries; i++) {\n entries[i].runLength = readVarint(p);\n }\n\n for (let i = 0; i < numEntries; i++) {\n entries[i].length = readVarint(p);\n }\n\n for (let i = 0; i < numEntries; i++) {\n const v = readVarint(p);\n if (v === 0 && i > 0) {\n entries[i].offset = entries[i - 1].offset + entries[i - 1].length;\n } else {\n entries[i].offset = v - 1;\n }\n }\n\n return entries;\n}\n\n/**\n * Error thrown when a response for PMTiles over HTTP does not match previous, cached parts of the archive.\n * The default PMTiles implementation will catch this error once internally and retry a request.\n */\nexport class EtagMismatch extends Error {}\n\n/**\n * Interface for caches of parts (headers, directories) of a PMTiles archive.\n */\nexport interface Cache {\n getHeader: (source: Source) => Promise<Header>;\n getDirectory: (\n source: Source,\n offset: number,\n length: number,\n header: Header\n ) => Promise<Entry[]>;\n invalidate: (source: Source) => Promise<void>;\n}\n\nasync function getHeaderAndRoot(\n source: Source,\n decompress: DecompressFunc\n): Promise<[Header, [string, number, Entry[] | ArrayBuffer]?]> {\n const resp = await source.getBytes(0, 16384);\n\n const v = new DataView(resp.data);\n if (v.getUint16(0, true) !== 0x4d50) {\n throw new Error(\"Wrong magic number for PMTiles archive\");\n }\n\n const headerData = resp.data.slice(0, HEADER_SIZE_BYTES);\n\n const header = bytesToHeader(headerData, resp.etag);\n\n // optimistically set the root directory\n // TODO check root bounds\n const rootDirData = resp.data.slice(\n header.rootDirectoryOffset,\n header.rootDirectoryOffset + header.rootDirectoryLength\n );\n const dirKey = `${source.getKey()}|${header.etag || \"\"}|${\n header.rootDirectoryOffset\n }|${header.rootDirectoryLength}`;\n\n const rootDir = deserializeIndex(\n await decompress(rootDirData, header.internalCompression)\n );\n return [header, [dirKey, rootDir.length, rootDir]];\n}\n\nasync function getDirectory(\n source: Source,\n decompress: DecompressFunc,\n offset: number,\n length: number,\n header: Header\n): Promise<Entry[]> {\n const resp = await source.getBytes(offset, length, undefined, header.etag);\n const data = await decompress(resp.data, header.internalCompression);\n const directory = deserializeIndex(data);\n if (directory.length === 0) {\n throw new Error(\"Empty directory is invalid\");\n }\n\n return directory;\n}\n\ninterface ResolvedValue {\n lastUsed: number;\n data: Header | Entry[] | ArrayBuffer;\n}\n\n/**\n * A cache for parts of a PMTiles archive where promises are never shared between requests.\n *\n * Runtimes such as Cloudflare Workers cannot share promises between different requests.\n *\n * Only caches headers and directories, not individual tile contents.\n */\nexport class ResolvedValueCache {\n cache: Map<string, ResolvedValue>;\n maxCacheEntries: number;\n counter: number;\n decompress: DecompressFunc;\n\n constructor(\n maxCacheEntries = 100,\n prefetch = true, // deprecated\n decompress: DecompressFunc = defaultDecompress\n ) {\n this.cache = new Map<string, ResolvedValue>();\n this.maxCacheEntries = maxCacheEntries;\n this.counter = 1;\n this.decompress = decompress;\n }\n\n async getHeader(source: Source): Promise<Header> {\n const cacheKey = source.getKey();\n const cacheValue = this.cache.get(cacheKey);\n if (cacheValue) {\n cacheValue.lastUsed = this.counter++;\n const data = cacheValue.data;\n return data as Header;\n }\n\n const res = await getHeaderAndRoot(source, this.decompress);\n if (res[1]) {\n this.cache.set(res[1][0], {\n lastUsed: this.counter++,\n data: res[1][2],\n });\n }\n\n this.cache.set(cacheKey, {\n lastUsed: this.counter++,\n data: res[0],\n });\n this.prune();\n return res[0];\n }\n\n async getDirectory(\n source: Source,\n offset: number,\n length: number,\n header: Header\n ): Promise<Entry[]> {\n const cacheKey = `${source.getKey()}|${\n header.etag || \"\"\n }|${offset}|${length}`;\n const cacheValue = this.cache.get(cacheKey);\n if (cacheValue) {\n cacheValue.lastUsed = this.counter++;\n const data = cacheValue.data;\n return data as Entry[];\n }\n\n const directory = await getDirectory(\n source,\n this.decompress,\n offset,\n length,\n header\n );\n this.cache.set(cacheKey, {\n lastUsed: this.counter++,\n data: directory,\n });\n this.prune();\n return directory;\n }\n\n prune() {\n if (this.cache.size > this.maxCacheEntries) {\n let minUsed = Infinity;\n let minKey = undefined;\n this.cache.forEach((cacheValue: ResolvedValue, key: string) => {\n if (cacheValue.lastUsed < minUsed) {\n minUsed = cacheValue.lastUsed;\n minKey = key;\n }\n });\n if (minKey) {\n this.cache.delete(minKey);\n }\n }\n }\n\n async invalidate(source: Source) {\n this.cache.delete(source.getKey());\n }\n}\n\ninterface SharedPromiseCacheValue {\n lastUsed: number;\n data: Promise<Header | Entry[] | ArrayBuffer>;\n}\n\n/**\n * A cache for parts of a PMTiles archive where promises can be shared between requests.\n *\n * Only caches headers and directories, not individual tile contents.\n */\nexport class SharedPromiseCache {\n cache: Map<string, SharedPromiseCacheValue>;\n invalidations: Map<string, Promise<void>>;\n maxCacheEntries: number;\n counter: number;\n decompress: DecompressFunc;\n\n constructor(\n maxCacheEntries = 100,\n prefetch = true, // deprecated\n decompress: DecompressFunc = defaultDecompress\n ) {\n this.cache = new Map<string, SharedPromiseCacheValue>();\n this.invalidations = new Map<string, Promise<void>>();\n this.maxCacheEntries = maxCacheEntries;\n this.counter = 1;\n this.decompress = decompress;\n }\n\n async getHeader(source: Source): Promise<Header> {\n const cacheKey = source.getKey();\n const cacheValue = this.cache.get(cacheKey);\n if (cacheValue) {\n cacheValue.lastUsed = this.counter++;\n const data = await cacheValue.data;\n return data as Header;\n }\n\n const p = new Promise<Header>((resolve, reject) => {\n getHeaderAndRoot(source, this.decompress)\n .then((res) => {\n if (res[1]) {\n this.cache.set(res[1][0], {\n lastUsed: this.counter++,\n data: Promise.resolve(res[1][2]),\n });\n }\n resolve(res[0]);\n this.prune();\n })\n .catch((e) => {\n reject(e);\n });\n });\n this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });\n return p;\n }\n\n async getDirectory(\n source: Source,\n offset: number,\n length: number,\n header: Header\n ): Promise<Entry[]> {\n const cacheKey = `${source.getKey()}|${\n header.etag || \"\"\n }|${offset}|${length}`;\n const cacheValue = this.cache.get(cacheKey);\n if (cacheValue) {\n cacheValue.lastUsed = this.counter++;\n const data = await cacheValue.data;\n return data as Entry[];\n }\n\n const p = new Promise<Entry[]>((resolve, reject) => {\n getDirectory(source, this.decompress, offset, length, header)\n .then((directory) => {\n resolve(directory);\n this.prune();\n })\n .catch((e) => {\n reject(e);\n });\n });\n this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });\n return p;\n }\n\n prune() {\n if (this.cache.size >= this.maxCacheEntries) {\n let minUsed = Infinity;\n let minKey = undefined;\n this.cache.forEach((cacheValue: SharedPromiseCacheValue, key: string) => {\n if (cacheValue.lastUsed < minUsed) {\n minUsed = cacheValue.lastUsed;\n minKey = key;\n }\n });\n if (minKey) {\n this.cache.delete(minKey);\n }\n }\n }\n\n async invalidate(source: Source) {\n const key = source.getKey();\n if (this.invalidations.get(key)) {\n return await this.invalidations.get(key);\n }\n this.cache.delete(source.getKey());\n const p = new Promise<void>((resolve, reject) => {\n this.getHeader(source)\n .then((h) => {\n resolve();\n this.invalidations.delete(key);\n })\n .catch((e) => {\n reject(e);\n });\n });\n this.invalidations.set(key, p);\n }\n}\n\n/**\n * Main class encapsulating PMTiles decoding logic.\n *\n * if `source` is a string, creates a FetchSource using that string as the URL to a remote PMTiles.\n * if no `cache` is passed, use a SharedPromiseCache.\n * if no `decompress` is passed, default to the browser DecompressionStream API with a fallback to `fflate`.\n */\n// biome-ignore lint: that's just how its capitalized\nexport class PMTiles {\n source: Source;\n cache: Cache;\n decompress: DecompressFunc;\n\n constructor(\n source: Source | string,\n cache?: Cache,\n decompress?: DecompressFunc\n ) {\n if (typeof source === \"string\") {\n this.source = new FetchSource(source);\n } else {\n this.source = source;\n }\n if (decompress) {\n this.decompress = decompress;\n } else {\n this.decompress = defaultDecompress;\n }\n if (cache) {\n this.cache = cache;\n } else {\n this.cache = new SharedPromiseCache();\n }\n }\n\n /**\n * Return the header of the archive,\n * including information such as tile type, min/max zoom, bounds, and summary statistics.\n */\n async getHeader() {\n return await this.cache.getHeader(this.source);\n }\n\n /** @hidden */\n async getZxyAttempt(\n z: number,\n x: number,\n y: number,\n signal?: AbortSignal\n ): Promise<RangeResponse | undefined> {\n const tileId = zxyToTileId(z, x, y);\n const header = await this.cache.getHeader(this.source);\n\n if (z < header.minZoom || z > header.maxZoom) {\n return undefined;\n }\n\n let dO = header.rootDirectoryOffset;\n let dL = header.rootDirectoryLength;\n for (let depth = 0; depth <= 3; depth++) {\n const directory = await this.cache.getDirectory(\n this.source,\n dO,\n dL,\n header\n );\n const entry = findTile(directory, tileId);\n if (entry) {\n if (entry.runLength > 0) {\n const resp = await this.source.getBytes(\n header.tileDataOffset + entry.offset,\n entry.length,\n signal,\n header.etag\n );\n return {\n data: await this.decompress(resp.data, header.tileCompression),\n cacheControl: resp.cacheControl,\n expires: resp.expires,\n };\n }\n dO = header.leafDirectoryOffset + entry.offset;\n dL = entry.length;\n } else {\n // TODO: We should in fact return a valid RangeResponse\n // with empty data, but filled in cache control / expires headers\n return undefined;\n }\n }\n throw new Error(\"Maximum directory depth exceeded\");\n }\n\n /**\n * Primary method to get a single tile's bytes from an archive.\n *\n * Returns undefined if the tile does not exist in the archive.\n */\n async getZxy(\n z: number,\n x: number,\n y: number,\n signal?: AbortSignal\n ): Promise<RangeResponse | undefined> {\n try {\n return await this.getZxyAttempt(z, x, y, signal);\n } catch (e) {\n if (e instanceof EtagMismatch) {\n this.cache.invalidate(this.source);\n return await this.getZxyAttempt(z, x, y, signal);\n }\n throw e;\n }\n }\n\n /** @hidden */\n async getMetadataAttempt(): Promise<unknown> {\n const header = await this.cache.getHeader(this.source);\n\n const resp = await this.source.getBytes(\n header.jsonMetadataOffset,\n header.jsonMetadataLength,\n undefined,\n header.etag\n );\n const decompressed = await this.decompress(\n resp.data,\n header.internalCompression\n );\n const dec = new TextDecoder(\"utf-8\");\n return JSON.parse(dec.decode(decompressed));\n }\n\n /**\n * Return the arbitrary JSON metadata of the archive.\n */\n async getMetadata(): Promise<unknown> {\n try {\n return await this.getMetadataAttempt();\n } catch (e) {\n if (e instanceof EtagMismatch) {\n this.cache.invalidate(this.source);\n return await this.getMetadataAttempt();\n }\n throw e;\n }\n }\n\n /**\n * Construct a [TileJSON](https://github.com/mapbox/tilejson-spec) object.\n *\n * baseTilesUrl is the desired tiles URL, excluding the suffix `/{z}/{x}/{y}.{ext}`.\n * For example, if the desired URL is `http://example.com/tileset/{z}/{x}/{y}.mvt`,\n * the baseTilesUrl should be `https://example.com/tileset`.\n */\n async getTileJson(baseTilesUrl: string): Promise<unknown> {\n const header = await this.getHeader();\n const metadata = (await this.getMetadata()) as MetadataLike;\n const ext = tileTypeExt(header.tileType);\n\n return {\n tilejson: \"3.0.0\",\n scheme: \"xyz\",\n tiles: [`${baseTilesUrl}/{z}/{x}/{y}${ext}`],\n // biome-ignore lint: TileJSON spec\n vector_layers: metadata.vector_layers,\n attribution: metadata.attribution,\n description: metadata.description,\n name: metadata.name,\n version: metadata.version,\n bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],\n center: [header.centerLon, header.centerLat, header.centerZoom],\n minzoom: header.minZoom,\n maxzoom: header.maxZoom,\n };\n }\n}\n","// biome-ignore lint: needed for Leaflet + IIFE to work\ndeclare const L: any;\n// biome-ignore lint: needed for window.URL to disambiguate from cloudflare workers\ndeclare const window: any;\ndeclare const document: DocumentLike;\n\nimport type { Coords } from \"leaflet\";\nimport { PMTiles, TileType } from \"./index\";\n\ninterface DocumentLike {\n // biome-ignore lint: we don't want to bring in the entire document type\n createElement: (s: string) => any;\n}\n\n// biome-ignore lint: we don't want to bring in the entire document type\ntype DoneCallback = (error?: Error, tile?: any) => void;\n\n/**\n * Add a raster PMTiles as a layer to a Leaflet map.\n *\n * For vector tiles see https://github.com/protomaps/protomaps-leaflet\n */\nexport const leafletRasterLayer = (source: PMTiles, options: unknown) => {\n let loaded = false;\n let mimeType = \"\";\n const cls = L.GridLayer.extend({\n createTile: (coord: Coords, done: DoneCallback) => {\n const el = document.createElement(\"img\");\n const controller = new AbortController();\n const signal = controller.signal;\n el.cancel = () => {\n controller.abort();\n };\n if (!loaded) {\n source.getHeader().then((header) => {\n if (\n header.tileType === TileType.Mvt ||\n header.tileType === TileType.Mlt\n ) {\n console.error(\n \"Error: archive contains vector tiles, but leafletRasterLayer is for displaying raster tiles. See https://github.com/protomaps/PMTiles/tree/main/js for details.\"\n );\n } else if (header.tileType === 2) {\n mimeType = \"image/png\";\n } else if (header.tileType === 3) {\n mimeType = \"image/jpeg\";\n } else if (header.tileType === 4) {\n mimeType = \"image/webp\";\n } else if (header.tileType === 5) {\n mimeType = \"image/avif\";\n }\n });\n loaded = true;\n }\n source\n .getZxy(coord.z, coord.x, coord.y, signal)\n .then((arr) => {\n if (arr) {\n const blob = new Blob([arr.data], { type: mimeType });\n const imageUrl = window.URL.createObjectURL(blob);\n el.src = imageUrl;\n } else {\n el.style.display = \"none\";\n }\n el.cancel = undefined;\n done(undefined, el);\n })\n .catch((e) => {\n if (e.name !== \"AbortError\") {\n throw e;\n }\n });\n return el;\n },\n\n _removeTile: function (key: string) {\n const tile = this._tiles[key];\n if (!tile) {\n return;\n }\n\n if (tile.el.cancel) tile.el.cancel();\n if (tile.el.src) window.URL.revokeObjectURL(tile.el.src);\n\n tile.el.width = 0;\n tile.el.height = 0;\n tile.el.deleted = true;\n L.DomUtil.remove(tile.el);\n delete this._tiles[key];\n this.fire(\"tileunload\", {\n tile: tile.el,\n coords: this._keyToTileCoords(key),\n });\n },\n });\n return new cls(options);\n};\n\ntype GetResourceResponse<T> = ExpiryData & {\n data: T;\n};\ntype AddProtocolAction = (\n requestParameters: RequestParameters,\n abortController: AbortController\n) => Promise<GetResourceResponse<unknown>>;\n\ntype ExpiryData = {\n cacheControl?: string | null;\n expires?: string | null; // MapLibre can be a Date object\n};\n\n// copied from MapLibre /util/ajax.ts\ntype RequestParameters = {\n url: string;\n headers?: unknown;\n method?: \"GET\" | \"POST\" | \"PUT\";\n body?: string;\n type?: \"string\" | \"json\" | \"arrayBuffer\" | \"image\";\n credentials?: \"same-origin\" | \"include\";\n collectResourceTiming?: boolean;\n};\n\n// for legacy maplibre-3 interop\ntype ResponseCallbackV3 = (\n error?: Error | undefined,\n data?: unknown | undefined,\n cacheControl?: string | undefined,\n expires?: string | undefined\n) => void;\n\ntype V3OrV4Protocol = <\n T extends AbortController | ResponseCallbackV3,\n R = T extends AbortController\n ? Promise<GetResourceResponse<unknown>>\n : { cancel: () => void },\n>(\n requestParameters: RequestParameters,\n arg2: T\n) => R;\n\nconst v3compat =\n (v4: AddProtocolAction): V3OrV4Protocol =>\n (requestParameters, arg2) => {\n if (arg2 instanceof AbortController) {\n // biome-ignore lint: overloading return type not handled by compiler\n return v4(requestParameters, arg2) as any;\n }\n const abortController = new AbortController();\n v4(requestParameters, abortController)\n .then(\n (result) => {\n return arg2(\n undefined,\n result.data,\n result.cacheControl || \"\",\n result.expires || \"\"\n );\n },\n (err) => {\n return arg2(err);\n }\n )\n .catch((e) => {\n return arg2(e);\n });\n return { cancel: () => abortController.abort() };\n };\n\n/**\n * MapLibre GL JS protocol. Must be added once globally.\n */\nexport class Protocol {\n /** @hidden */\n tiles: Map<string, PMTiles>;\n metadata: boolean;\n errorOnMissingTile: boolean;\n\n /**\n * Initialize the MapLibre PMTiles protocol.\n *\n * * metadata: also load the metadata section of the PMTiles. required for some \"inspect\" functionality\n * and to automatically populate the map attribution. Requires an extra HTTP request.\n * * errorOnMissingTile: When a vector MVT tile is missing from the archive, raise an error instead of\n * returning the empty array. Not recommended. This is only to reproduce the behavior of ZXY tile APIs\n * which some applications depend on when overzooming.\n */\n constructor(options?: { metadata?: boolean; errorOnMissingTile?: boolean }) {\n this.tiles = new Map<string, PMTiles>();\n this.metadata = options?.metadata || false;\n this.errorOnMissingTile = options?.errorOnMissingTile || false;\n }\n\n /**\n * Add a {@link PMTiles} instance to the global protocol instance.\n *\n * For remote fetch sources, references in MapLibre styles like pmtiles://http://...\n * will resolve to the same instance if the URLs match.\n */\n add(p: PMTiles) {\n this.tiles.set(p.source.getKey(), p);\n }\n\n /**\n * Fetch a {@link PMTiles} instance by URL, for remote PMTiles instances.\n */\n get(url: string) {\n return this.tiles.get(url);\n }\n\n /** @hidden */\n tilev4 = async (\n params: RequestParameters,\n abortController: AbortController\n ) => {\n if (params.type === \"json\") {\n const pmtilesUrl = params.url.substr(10);\n let instance = this.tiles.get(pmtilesUrl);\n if (!instance) {\n instance = new PMTiles(pmtilesUrl);\n this.tiles.set(pmtilesUrl, instance);\n }\n\n if (this.metadata) {\n const data = await instance.getTileJson(params.url);\n abortController.signal.throwIfAborted();\n return { data };\n }\n\n const h = await instance.getHeader();\n abortController.signal.throwIfAborted();\n\n if (h.minLon >= h.maxLon || h.minLat >= h.maxLat) {\n console.error(\n `Bounds of PMTiles archive ${h.minLon},${h.minLat},${h.maxLon},${h.maxLat} are not valid.`\n );\n }\n\n return {\n data: {\n tiles: [`${params.url}/{z}/{x}/{y}`],\n minzoom: h.minZoom,\n maxzoom: h.maxZoom,\n bounds: [h.minLon, h.minLat, h.maxLon, h.maxLat],\n },\n };\n }\n const re = new RegExp(/pmtiles:\\/\\/(.+)\\/(\\d+)\\/(\\d+)\\/(\\d+)/);\n const result = params.url.match(re);\n if (!result) {\n throw new Error(\"Invalid PMTiles protocol URL\");\n }\n const pmtilesUrl = result[1];\n\n let instance = this.tiles.get(pmtilesUrl);\n if (!instance) {\n instance = new PMTiles(pmtilesUrl);\n this.tiles.set(pmtilesUrl, instance);\n }\n const z = result[2];\n const x = result[3];\n const y = result[4];\n\n const resp = await instance?.getZxy(+z, +x, +y, abortController.signal);\n abortController.signal.throwIfAborted();\n if (resp) {\n return {\n data: new Uint8Array(resp.data),\n cacheControl: resp.cacheControl,\n expires: resp.expires,\n };\n }\n const header = await instance.getHeader();\n if (header.tileType === TileType.Mvt || header.tileType === TileType.Mlt) {\n if (this.errorOnMissingTile) {\n throw new Error(\"Tile not found.\");\n }\n return { data: new Uint8Array() };\n }\n return { data: null };\n };\n\n tile = v3compat(this.tilev4);\n}\n"]}