From c07c73481b4b1f75051d74be4461600d7cede0d1 Mon Sep 17 00:00:00 2001 From: Logan Houp Date: Sat, 11 Mar 2023 12:35:07 -0500 Subject: [PATCH] add prettierrc config file --- js/.prettierrc.json | 1 + js/index.ts | 1532 +++++++++++++++++++++--------------------- js/package-lock.json | 22 + js/package.json | 1 + js/v2.ts | 556 +++++++-------- 5 files changed, 1068 insertions(+), 1044 deletions(-) create mode 100644 js/.prettierrc.json diff --git a/js/.prettierrc.json b/js/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/js/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/js/index.ts b/js/index.ts index 18338d2..787d841 100644 --- a/js/index.ts +++ b/js/index.ts @@ -3,667 +3,667 @@ import v2 from "./v2"; export * from "./adapters"; export interface BufferPosition { - buf: Uint8Array; - pos: number; + buf: Uint8Array; + pos: number; } function toNum(low: number, high: number): number { - return (high >>> 0) * 0x100000000 + (low >>> 0); + return (high >>> 0) * 0x100000000 + (low >>> 0); } function readVarintRemainder(l: number, p: BufferPosition): number { - const buf = p.buf; - let h, b; - b = buf[p.pos++]; - h = (b & 0x70) >> 4; - if (b < 0x80) return toNum(l, h); - b = buf[p.pos++]; - h |= (b & 0x7f) << 3; - if (b < 0x80) return toNum(l, h); - b = buf[p.pos++]; - h |= (b & 0x7f) << 10; - if (b < 0x80) return toNum(l, h); - b = buf[p.pos++]; - h |= (b & 0x7f) << 17; - if (b < 0x80) return toNum(l, h); - b = buf[p.pos++]; - h |= (b & 0x7f) << 24; - if (b < 0x80) return toNum(l, h); - b = buf[p.pos++]; - h |= (b & 0x01) << 31; - if (b < 0x80) return toNum(l, h); - throw new Error("Expected varint not more than 10 bytes"); + const buf = p.buf; + let h, b; + b = buf[p.pos++]; + h = (b & 0x70) >> 4; + if (b < 0x80) return toNum(l, h); + b = buf[p.pos++]; + h |= (b & 0x7f) << 3; + if (b < 0x80) return toNum(l, h); + b = buf[p.pos++]; + h |= (b & 0x7f) << 10; + if (b < 0x80) return toNum(l, h); + b = buf[p.pos++]; + h |= (b & 0x7f) << 17; + if (b < 0x80) return toNum(l, h); + b = buf[p.pos++]; + h |= (b & 0x7f) << 24; + if (b < 0x80) return toNum(l, h); + b = buf[p.pos++]; + h |= (b & 0x01) << 31; + if (b < 0x80) return toNum(l, h); + throw new Error("Expected varint not more than 10 bytes"); } export function readVarint(p: BufferPosition): number { - const buf = p.buf; - let val, b; + const buf = p.buf; + let val, b; - b = buf[p.pos++]; - val = b & 0x7f; - if (b < 0x80) return val; - b = buf[p.pos++]; - val |= (b & 0x7f) << 7; - if (b < 0x80) return val; - b = buf[p.pos++]; - val |= (b & 0x7f) << 14; - if (b < 0x80) return val; - b = buf[p.pos++]; - val |= (b & 0x7f) << 21; - if (b < 0x80) return val; - b = buf[p.pos]; - val |= (b & 0x0f) << 28; + b = buf[p.pos++]; + val = b & 0x7f; + if (b < 0x80) return val; + b = buf[p.pos++]; + val |= (b & 0x7f) << 7; + if (b < 0x80) return val; + b = buf[p.pos++]; + val |= (b & 0x7f) << 14; + if (b < 0x80) return val; + b = buf[p.pos++]; + val |= (b & 0x7f) << 21; + if (b < 0x80) return val; + b = buf[p.pos]; + val |= (b & 0x0f) << 28; - return readVarintRemainder(val, p); + return readVarintRemainder(val, p); } function rotate(n: number, xy: number[], rx: number, ry: number): void { - if (ry == 0) { - if (rx == 1) { - xy[0] = n - 1 - xy[0]; - xy[1] = n - 1 - xy[1]; - } - const t = xy[0]; - xy[0] = xy[1]; - xy[1] = t; - } + if (ry == 0) { + if (rx == 1) { + xy[0] = n - 1 - xy[0]; + xy[1] = n - 1 - xy[1]; + } + const t = xy[0]; + xy[0] = xy[1]; + xy[1] = t; + } } function idOnLevel(z: number, pos: number): [number, number, number] { - const n = Math.pow(2, z); - let rx = pos; - let ry = pos; - let t = pos; - const xy = [0, 0]; - let s = 1; - while (s < n) { - rx = 1 & (t / 2); - ry = 1 & (t ^ rx); - rotate(s, xy, rx, ry); - xy[0] += s * rx; - xy[1] += s * ry; - t = t / 4; - s *= 2; - } - return [z, xy[0], xy[1]]; + const n = Math.pow(2, z); + let rx = pos; + let ry = pos; + let t = pos; + const xy = [0, 0]; + let s = 1; + while (s < n) { + rx = 1 & (t / 2); + ry = 1 & (t ^ rx); + rotate(s, xy, rx, ry); + xy[0] += s * rx; + xy[1] += s * ry; + t = t / 4; + s *= 2; + } + return [z, xy[0], xy[1]]; } export function zxyToTileId(z: number, x: number, y: number): number { - if (z > 26) { - throw Error("Tile zoom level exceeds max safe number limit (26)"); - } - if (x > Math.pow(2, z) - 1 || y > Math.pow(2, z) - 1) { - throw Error("tile x/y outside zoom level bounds"); - } + if (z > 26) { + throw Error("Tile zoom level exceeds max safe number limit (26)"); + } + if (x > Math.pow(2, z) - 1 || y > Math.pow(2, z) - 1) { + throw Error("tile x/y outside zoom level bounds"); + } - let acc = 0; - let tz = 0; - while (tz < z) { - acc += Math.pow(2, tz) * Math.pow(2, tz); - tz++; - } - const n = Math.pow(2, z); - let rx = 0; - let ry = 0; - let d = 0; - const xy = [x, y]; - let s = n / 2; - while (s > 0) { - rx = (xy[0] & s) > 0 ? 1 : 0; - ry = (xy[1] & s) > 0 ? 1 : 0; - d += s * s * ((3 * rx) ^ ry); - rotate(s, xy, rx, ry); - s = s / 2; - } - return acc + d; + let acc = 0; + let tz = 0; + while (tz < z) { + acc += Math.pow(2, tz) * Math.pow(2, tz); + tz++; + } + const n = Math.pow(2, z); + let rx = 0; + let ry = 0; + let d = 0; + const xy = [x, y]; + let s = n / 2; + while (s > 0) { + rx = (xy[0] & s) > 0 ? 1 : 0; + ry = (xy[1] & s) > 0 ? 1 : 0; + d += s * s * ((3 * rx) ^ ry); + rotate(s, xy, rx, ry); + s = s / 2; + } + return acc + d; } export function tileIdToZxy(i: number): [number, number, number] { - let acc = 0; - let z = 0; + let acc = 0; + let z = 0; - for (let z = 0; z < 27; z++) { - const num_tiles = (0x1 << z) * (0x1 << z); - if (acc + num_tiles > i) { - return idOnLevel(z, i - acc); - } - acc += num_tiles; - } + for (let z = 0; z < 27; z++) { + const num_tiles = (0x1 << z) * (0x1 << z); + if (acc + num_tiles > i) { + return idOnLevel(z, i - acc); + } + acc += num_tiles; + } - throw Error("Tile zoom level exceeds max safe number limit (26)"); + throw Error("Tile zoom level exceeds max safe number limit (26)"); } export interface Entry { - tileId: number; - offset: number; - length: number; - runLength: number; + tileId: number; + offset: number; + length: number; + runLength: number; } export enum Compression { - Unknown = 0, - None = 1, - Gzip = 2, - Brotli = 3, - Zstd = 4, + Unknown = 0, + None = 1, + Gzip = 2, + Brotli = 3, + Zstd = 4, } type DecompressFunc = ( - buf: ArrayBuffer, - compression: Compression + buf: ArrayBuffer, + compression: Compression ) => Promise; async function fflateDecompress( - buf: ArrayBuffer, - compression: Compression + buf: ArrayBuffer, + compression: Compression ): Promise { - if (compression === Compression.None || compression === Compression.Unknown) { - return buf; - } else if (compression === Compression.Gzip) { - return decompressSync(new Uint8Array(buf)); - } else { - throw Error("Compression method not supported"); - } + if (compression === Compression.None || compression === Compression.Unknown) { + return buf; + } else if (compression === Compression.Gzip) { + return decompressSync(new Uint8Array(buf)); + } else { + throw Error("Compression method not supported"); + } } export enum TileType { - Unknown = 0, - Mvt = 1, - Png = 2, - Jpeg = 3, - Webp = 4, + Unknown = 0, + Mvt = 1, + Png = 2, + Jpeg = 3, + Webp = 4, } const HEADER_SIZE_BYTES = 127; export interface Header { - specVersion: number; - rootDirectoryOffset: number; - rootDirectoryLength: number; - jsonMetadataOffset: number; - jsonMetadataLength: number; - leafDirectoryOffset: number; - leafDirectoryLength?: number; - tileDataOffset: number; - tileDataLength?: number; - numAddressedTiles: number; - numTileEntries: number; - numTileContents: number; - clustered: boolean; - internalCompression: Compression; - tileCompression: Compression; - tileType: TileType; - minZoom: number; - maxZoom: number; - minLon: number; - minLat: number; - maxLon: number; - maxLat: number; - centerZoom: number; - centerLon: number; - centerLat: number; - etag?: string; + specVersion: number; + rootDirectoryOffset: number; + rootDirectoryLength: number; + jsonMetadataOffset: number; + jsonMetadataLength: number; + leafDirectoryOffset: number; + leafDirectoryLength?: number; + tileDataOffset: number; + tileDataLength?: number; + numAddressedTiles: number; + numTileEntries: number; + numTileContents: number; + clustered: boolean; + internalCompression: Compression; + tileCompression: Compression; + tileType: TileType; + minZoom: number; + maxZoom: number; + minLon: number; + minLat: number; + maxLon: number; + maxLat: number; + centerZoom: number; + centerLon: number; + centerLat: number; + etag?: string; } export function findTile(entries: Entry[], tileId: number): Entry | null { - let m = 0; - let n = entries.length - 1; - while (m <= n) { - const k = (n + m) >> 1; - const cmp = tileId - entries[k].tileId; - if (cmp > 0) { - m = k + 1; - } else if (cmp < 0) { - n = k - 1; - } else { - return entries[k]; - } - } + let m = 0; + let n = entries.length - 1; + while (m <= n) { + const k = (n + m) >> 1; + const cmp = tileId - entries[k].tileId; + if (cmp > 0) { + m = k + 1; + } else if (cmp < 0) { + n = k - 1; + } else { + return entries[k]; + } + } - // at this point, m > n - if (n >= 0) { - if (entries[n].runLength === 0) { - return entries[n]; - } - if (tileId - entries[n].tileId < entries[n].runLength) { - return entries[n]; - } - } - return null; + // at this point, m > n + if (n >= 0) { + if (entries[n].runLength === 0) { + return entries[n]; + } + if (tileId - entries[n].tileId < entries[n].runLength) { + return entries[n]; + } + } + return null; } export interface RangeResponse { - data: ArrayBuffer; - etag?: string; - expires?: string; - cacheControl?: string; + data: ArrayBuffer; + etag?: string; + expires?: string; + cacheControl?: string; } // In the future this may need to change // to support ReadableStream to pass to native DecompressionStream API export interface Source { - getBytes: ( - offset: number, - length: number, - signal?: AbortSignal - ) => Promise; + getBytes: ( + offset: number, + length: number, + signal?: AbortSignal + ) => Promise; - getKey: () => string; + getKey: () => string; } export class FileAPISource implements Source { - file: File; + file: File; - constructor(file: File) { - this.file = file; - } + constructor(file: File) { + this.file = file; + } - getKey() { - return this.file.name; - } + getKey() { + return this.file.name; + } - async getBytes(offset: number, length: number): Promise { - const blob = this.file.slice(offset, offset + length); - const a = await blob.arrayBuffer(); - return { data: a }; - } + async getBytes(offset: number, length: number): Promise { + const blob = this.file.slice(offset, offset + length); + const a = await blob.arrayBuffer(); + return { data: a }; + } } export class FetchSource implements Source { - url: string; + url: string; - constructor(url: string) { - this.url = url; - } + constructor(url: string) { + this.url = url; + } - getKey() { - return this.url; - } + getKey() { + return this.url; + } - async getBytes( - offset: number, - length: number, - signal?: AbortSignal - ): Promise { - let controller; - if (!signal) { - // TODO check this works or assert 206 - controller = new AbortController(); - signal = controller.signal; - } + async getBytes( + offset: number, + length: number, + signal?: AbortSignal + ): Promise { + let controller; + if (!signal) { + // TODO check this works or assert 206 + controller = new AbortController(); + signal = controller.signal; + } - let resp = await fetch(this.url, { - signal: signal, - headers: { Range: "bytes=" + offset + "-" + (offset + length - 1) }, - }); + let resp = await fetch(this.url, { + signal: signal, + headers: { Range: "bytes=" + offset + "-" + (offset + length - 1) }, + }); - // TODO: can return 416 with offset > 0 if content changed, which will have a blank etag. - // See https://github.com/protomaps/PMTiles/issues/90 + // TODO: can return 416 with offset > 0 if content changed, which will have a blank etag. + // See https://github.com/protomaps/PMTiles/issues/90 - if (resp.status === 416 && offset === 0) { - // some HTTP servers don't accept ranges beyond the end of the resource. - // Retry with the exact length - const content_range = resp.headers.get("Content-Range"); - if (!content_range || !content_range.startsWith("bytes */")) { - throw Error("Missing content-length on 416 response"); - } - const actual_length = +content_range.substr(8); - resp = await fetch(this.url, { - signal: signal, - headers: { Range: "bytes=0-" + (actual_length - 1) }, - }); - } + if (resp.status === 416 && offset === 0) { + // some HTTP servers don't accept ranges beyond the end of the resource. + // Retry with the exact length + const content_range = resp.headers.get("Content-Range"); + if (!content_range || !content_range.startsWith("bytes */")) { + throw Error("Missing content-length on 416 response"); + } + const actual_length = +content_range.substr(8); + resp = await fetch(this.url, { + signal: signal, + headers: { Range: "bytes=0-" + (actual_length - 1) }, + }); + } - if (resp.status >= 300) { - throw Error("Bad response code: " + resp.status); - } + if (resp.status >= 300) { + throw Error("Bad response code: " + resp.status); + } - const content_length = resp.headers.get("Content-Length"); + const content_length = resp.headers.get("Content-Length"); - // some well-behaved backends, e.g. DigitalOcean CDN, respond with 200 instead of 206 - // but we also need to detect no support for Byte Serving which is returning the whole file - if (resp.status === 200 && (!content_length || +content_length > length)) { - if (controller) controller.abort(); - throw Error( - "Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving." - ); - } + // some well-behaved backends, e.g. DigitalOcean CDN, respond with 200 instead of 206 + // but we also need to detect no support for Byte Serving which is returning the whole file + if (resp.status === 200 && (!content_length || +content_length > length)) { + if (controller) controller.abort(); + throw Error( + "Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving." + ); + } - const a = await resp.arrayBuffer(); - return { - data: a, - etag: resp.headers.get("ETag") || undefined, - cacheControl: resp.headers.get("Cache-Control") || undefined, - expires: resp.headers.get("Expires") || undefined, - }; - } + const a = await resp.arrayBuffer(); + return { + data: a, + etag: resp.headers.get("ETag") || undefined, + cacheControl: resp.headers.get("Cache-Control") || undefined, + expires: resp.headers.get("Expires") || undefined, + }; + } } export function getUint64(v: DataView, offset: number): number { - const wh = v.getUint32(offset + 4, true); - const wl = v.getUint32(offset + 0, true); - return wh * Math.pow(2, 32) + wl; + const wh = v.getUint32(offset + 4, true); + const wl = v.getUint32(offset + 0, true); + return wh * Math.pow(2, 32) + wl; } export function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header { - const v = new DataView(bytes); - const spec_version = v.getUint8(7); - if (spec_version > 3) { - throw Error( - `Archive is spec version ${spec_version} but this library supports up to spec version 3` - ); - } + const v = new DataView(bytes); + const spec_version = v.getUint8(7); + if (spec_version > 3) { + throw Error( + `Archive is spec version ${spec_version} but this library supports up to spec version 3` + ); + } - return { - specVersion: spec_version, - rootDirectoryOffset: getUint64(v, 8), - rootDirectoryLength: getUint64(v, 16), - jsonMetadataOffset: getUint64(v, 24), - jsonMetadataLength: getUint64(v, 32), - leafDirectoryOffset: getUint64(v, 40), - leafDirectoryLength: getUint64(v, 48), - tileDataOffset: getUint64(v, 56), - tileDataLength: getUint64(v, 64), - numAddressedTiles: getUint64(v, 72), - numTileEntries: getUint64(v, 80), - numTileContents: getUint64(v, 88), - clustered: v.getUint8(96) === 1, - internalCompression: v.getUint8(97), - tileCompression: v.getUint8(98), - tileType: v.getUint8(99), - minZoom: v.getUint8(100), - maxZoom: v.getUint8(101), - minLon: v.getInt32(102, true) / 10000000, - minLat: v.getInt32(106, true) / 10000000, - maxLon: v.getInt32(110, true) / 10000000, - maxLat: v.getInt32(114, true) / 10000000, - centerZoom: v.getUint8(118), - centerLon: v.getInt32(119, true) / 10000000, - centerLat: v.getInt32(123, true) / 10000000, - etag: etag, - }; + return { + specVersion: spec_version, + rootDirectoryOffset: getUint64(v, 8), + rootDirectoryLength: getUint64(v, 16), + jsonMetadataOffset: getUint64(v, 24), + jsonMetadataLength: getUint64(v, 32), + leafDirectoryOffset: getUint64(v, 40), + leafDirectoryLength: getUint64(v, 48), + tileDataOffset: getUint64(v, 56), + tileDataLength: getUint64(v, 64), + numAddressedTiles: getUint64(v, 72), + numTileEntries: getUint64(v, 80), + numTileContents: getUint64(v, 88), + clustered: v.getUint8(96) === 1, + internalCompression: v.getUint8(97), + tileCompression: v.getUint8(98), + tileType: v.getUint8(99), + minZoom: v.getUint8(100), + maxZoom: v.getUint8(101), + minLon: v.getInt32(102, true) / 10000000, + minLat: v.getInt32(106, true) / 10000000, + maxLon: v.getInt32(110, true) / 10000000, + maxLat: v.getInt32(114, true) / 10000000, + centerZoom: v.getUint8(118), + centerLon: v.getInt32(119, true) / 10000000, + centerLat: v.getInt32(123, true) / 10000000, + etag: etag, + }; } function deserializeIndex(buffer: ArrayBuffer): Entry[] { - const p = { buf: new Uint8Array(buffer), pos: 0 }; - const numEntries = readVarint(p); + const p = { buf: new Uint8Array(buffer), pos: 0 }; + const numEntries = readVarint(p); - const entries: Entry[] = []; + const entries: Entry[] = []; - let lastId = 0; - for (let i = 0; i < numEntries; i++) { - const v = readVarint(p); - entries.push({ tileId: lastId + v, offset: 0, length: 0, runLength: 1 }); - lastId += v; - } + let lastId = 0; + for (let i = 0; i < numEntries; i++) { + const v = readVarint(p); + entries.push({ tileId: lastId + v, offset: 0, length: 0, runLength: 1 }); + lastId += v; + } - for (let i = 0; i < numEntries; i++) { - entries[i].runLength = readVarint(p); - } + for (let i = 0; i < numEntries; i++) { + entries[i].runLength = readVarint(p); + } - for (let i = 0; i < numEntries; i++) { - entries[i].length = readVarint(p); - } + for (let i = 0; i < numEntries; i++) { + entries[i].length = readVarint(p); + } - for (let i = 0; i < numEntries; i++) { - const v = readVarint(p); - if (v === 0 && i > 0) { - entries[i].offset = entries[i - 1].offset + entries[i - 1].length; - } else { - entries[i].offset = v - 1; - } - } + for (let i = 0; i < numEntries; i++) { + const v = readVarint(p); + if (v === 0 && i > 0) { + entries[i].offset = entries[i - 1].offset + entries[i - 1].length; + } else { + entries[i].offset = v - 1; + } + } - return entries; + return entries; } function detectVersion(a: ArrayBuffer): number { - const v = new DataView(a); - if (v.getUint16(2, true) === 2) { - console.warn( - "PMTiles spec version 2 has been deprecated; please see github.com/protomaps/PMTiles for tools to upgrade" - ); - return 2; - } else if (v.getUint16(2, true) === 1) { - console.warn( - "PMTiles spec version 1 has been deprecated; please see github.com/protomaps/PMTiles for tools to upgrade" - ); - return 1; - } - return 3; + const v = new DataView(a); + if (v.getUint16(2, true) === 2) { + console.warn( + "PMTiles spec version 2 has been deprecated; please see github.com/protomaps/PMTiles for tools to upgrade" + ); + return 2; + } else if (v.getUint16(2, true) === 1) { + console.warn( + "PMTiles spec version 1 has been deprecated; please see github.com/protomaps/PMTiles for tools to upgrade" + ); + return 1; + } + return 3; } export class EtagMismatch extends Error {} export interface Cache { - getHeader: (source: Source, current_etag?: string) => Promise
; - getDirectory: ( - source: Source, - offset: number, - length: number, - header: Header - ) => Promise; - getArrayBuffer: ( - source: Source, - offset: number, - length: number, - header: Header - ) => Promise; - invalidate: (source: Source, current_etag: string) => Promise; + getHeader: (source: Source, current_etag?: string) => Promise
; + getDirectory: ( + source: Source, + offset: number, + length: number, + header: Header + ) => Promise; + getArrayBuffer: ( + source: Source, + offset: number, + length: number, + header: Header + ) => Promise; + invalidate: (source: Source, current_etag: string) => Promise; } async function getHeaderAndRoot( - source: Source, - decompress: DecompressFunc, - prefetch: boolean, - current_etag?: string + source: Source, + decompress: DecompressFunc, + prefetch: boolean, + current_etag?: string ): Promise<[Header, [string, number, Entry[] | ArrayBuffer]?]> { - const resp = await source.getBytes(0, 16384); + const resp = await source.getBytes(0, 16384); - const v = new DataView(resp.data); - if (v.getUint16(0, true) !== 0x4d50) { - throw new Error("Wrong magic number for PMTiles archive"); - } + const v = new DataView(resp.data); + if (v.getUint16(0, true) !== 0x4d50) { + throw new Error("Wrong magic number for PMTiles archive"); + } - // V2 COMPATIBILITY - if (detectVersion(resp.data) < 3) { - return [await v2.getHeader(source)]; - } + // V2 COMPATIBILITY + if (detectVersion(resp.data) < 3) { + return [await v2.getHeader(source)]; + } - const headerData = resp.data.slice(0, HEADER_SIZE_BYTES); + const headerData = resp.data.slice(0, HEADER_SIZE_BYTES); - let resp_etag = resp.etag; - if (current_etag && resp.etag != current_etag) { - console.warn( - "ETag conflict detected; your HTTP server might not support content-based ETag headers. ETags disabled for " + - source.getKey() - ); - resp_etag = undefined; - } + let resp_etag = resp.etag; + if (current_etag && resp.etag != current_etag) { + console.warn( + "ETag conflict detected; your HTTP server might not support content-based ETag headers. ETags disabled for " + + source.getKey() + ); + resp_etag = undefined; + } - const header = bytesToHeader(headerData, resp_etag); + const header = bytesToHeader(headerData, resp_etag); - // optimistically set the root directory - // TODO check root bounds - if (prefetch) { - const rootDirData = resp.data.slice( - header.rootDirectoryOffset, - header.rootDirectoryOffset + header.rootDirectoryLength - ); - const dirKey = - source.getKey() + - "|" + - (header.etag || "") + - "|" + - header.rootDirectoryOffset + - "|" + - header.rootDirectoryLength; + // optimistically set the root directory + // TODO check root bounds + if (prefetch) { + const rootDirData = resp.data.slice( + header.rootDirectoryOffset, + header.rootDirectoryOffset + header.rootDirectoryLength + ); + const dirKey = + source.getKey() + + "|" + + (header.etag || "") + + "|" + + header.rootDirectoryOffset + + "|" + + header.rootDirectoryLength; - const rootDir = deserializeIndex( - await decompress(rootDirData, header.internalCompression) - ); - return [header, [dirKey, rootDir.length, rootDir]]; - } + const rootDir = deserializeIndex( + await decompress(rootDirData, header.internalCompression) + ); + return [header, [dirKey, rootDir.length, rootDir]]; + } - return [header, undefined]; + return [header, undefined]; } async function getDirectory( - source: Source, - decompress: DecompressFunc, - offset: number, - length: number, - header: Header + source: Source, + decompress: DecompressFunc, + offset: number, + length: number, + header: Header ): Promise { - const resp = await source.getBytes(offset, length); + const resp = await source.getBytes(offset, length); - if (header.etag && header.etag !== resp.etag) { - throw new EtagMismatch(resp.etag); - } + if (header.etag && header.etag !== resp.etag) { + throw new EtagMismatch(resp.etag); + } - const data = await decompress(resp.data, header.internalCompression); - const directory = deserializeIndex(data); - if (directory.length === 0) { - throw new Error("Empty directory is invalid"); - } + const data = await decompress(resp.data, header.internalCompression); + const directory = deserializeIndex(data); + if (directory.length === 0) { + throw new Error("Empty directory is invalid"); + } - return directory; + return directory; } interface ResolvedValue { - lastUsed: number; - data: Header | Entry[] | ArrayBuffer; + lastUsed: number; + data: Header | Entry[] | ArrayBuffer; } export class ResolvedValueCache { - cache: Map; - maxCacheEntries: number; - counter: number; - prefetch: boolean; - decompress: DecompressFunc; + cache: Map; + maxCacheEntries: number; + counter: number; + prefetch: boolean; + decompress: DecompressFunc; - constructor( - maxCacheEntries = 100, - prefetch = true, - decompress: DecompressFunc = fflateDecompress - ) { - this.cache = new Map(); - this.maxCacheEntries = maxCacheEntries; - this.counter = 1; - this.prefetch = prefetch; - this.decompress = decompress; - } + constructor( + maxCacheEntries = 100, + prefetch = true, + decompress: DecompressFunc = fflateDecompress + ) { + this.cache = new Map(); + this.maxCacheEntries = maxCacheEntries; + this.counter = 1; + this.prefetch = prefetch; + this.decompress = decompress; + } - async getHeader(source: Source, current_etag?: string): Promise
{ - const cacheKey = source.getKey(); - if (this.cache.has(cacheKey)) { - this.cache.get(cacheKey)!.lastUsed = this.counter++; - const data = this.cache.get(cacheKey)!.data; - return data as Header; - } + async getHeader(source: Source, current_etag?: string): Promise
{ + const cacheKey = source.getKey(); + if (this.cache.has(cacheKey)) { + this.cache.get(cacheKey)!.lastUsed = this.counter++; + const data = this.cache.get(cacheKey)!.data; + return data as Header; + } - const res = await getHeaderAndRoot( - source, - this.decompress, - this.prefetch, - current_etag - ); - if (res[1]) { - this.cache.set(res[1][0], { - lastUsed: this.counter++, - data: res[1][2], - }); - } + const res = await getHeaderAndRoot( + source, + this.decompress, + this.prefetch, + current_etag + ); + if (res[1]) { + this.cache.set(res[1][0], { + lastUsed: this.counter++, + data: res[1][2], + }); + } - this.cache.set(cacheKey, { - lastUsed: this.counter++, - data: res[0], - }); - this.prune(); - return res[0]; - } + this.cache.set(cacheKey, { + lastUsed: this.counter++, + data: res[0], + }); + this.prune(); + return res[0]; + } - async getDirectory( - source: Source, - offset: number, - length: number, - header: Header - ): Promise { - const cacheKey = - source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; - if (this.cache.has(cacheKey)) { - this.cache.get(cacheKey)!.lastUsed = this.counter++; - const data = this.cache.get(cacheKey)!.data; - return data as Entry[]; - } + async getDirectory( + source: Source, + offset: number, + length: number, + header: Header + ): Promise { + const cacheKey = + source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; + if (this.cache.has(cacheKey)) { + this.cache.get(cacheKey)!.lastUsed = this.counter++; + const data = this.cache.get(cacheKey)!.data; + return data as Entry[]; + } - const directory = await getDirectory( - source, - this.decompress, - offset, - length, - header - ); - this.cache.set(cacheKey, { - lastUsed: this.counter++, - data: directory, - }); - this.prune(); - return directory; - } + const directory = await getDirectory( + source, + this.decompress, + offset, + length, + header + ); + this.cache.set(cacheKey, { + lastUsed: this.counter++, + data: directory, + }); + this.prune(); + return directory; + } - // for v2 backwards compatibility - async getArrayBuffer( - source: Source, - offset: number, - length: number, - header: Header - ): Promise { - const cacheKey = - source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; - if (this.cache.has(cacheKey)) { - this.cache.get(cacheKey)!.lastUsed = this.counter++; - const data = await this.cache.get(cacheKey)!.data; - return data as ArrayBuffer; - } + // for v2 backwards compatibility + async getArrayBuffer( + source: Source, + offset: number, + length: number, + header: Header + ): Promise { + const cacheKey = + source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; + if (this.cache.has(cacheKey)) { + this.cache.get(cacheKey)!.lastUsed = this.counter++; + const data = await this.cache.get(cacheKey)!.data; + return data as ArrayBuffer; + } - const resp = await source.getBytes(offset, length); - if (header.etag && header.etag !== resp.etag) { - throw new EtagMismatch(header.etag); - } - this.cache.set(cacheKey, { - lastUsed: this.counter++, - data: resp.data, - }); - this.prune(); - return resp.data; - } + const resp = await source.getBytes(offset, length); + if (header.etag && header.etag !== resp.etag) { + throw new EtagMismatch(header.etag); + } + this.cache.set(cacheKey, { + lastUsed: this.counter++, + data: resp.data, + }); + this.prune(); + return resp.data; + } - prune() { - if (this.cache.size > this.maxCacheEntries) { - let minUsed = Infinity; - let minKey = undefined; - this.cache.forEach((cache_value: ResolvedValue, key: string) => { - if (cache_value.lastUsed < minUsed) { - minUsed = cache_value.lastUsed; - minKey = key; - } - }); - if (minKey) { - this.cache.delete(minKey); - } - } - } + prune() { + if (this.cache.size > this.maxCacheEntries) { + let minUsed = Infinity; + let minKey = undefined; + this.cache.forEach((cache_value: ResolvedValue, key: string) => { + if (cache_value.lastUsed < minUsed) { + minUsed = cache_value.lastUsed; + minKey = key; + } + }); + if (minKey) { + this.cache.delete(minKey); + } + } + } - async invalidate(source: Source, current_etag: string) { - this.cache.delete(source.getKey()); - await this.getHeader(source, current_etag); - } + async invalidate(source: Source, current_etag: string) { + this.cache.delete(source.getKey()); + await this.getHeader(source, current_etag); + } } interface SharedPromiseCacheValue { - lastUsed: number; - data: Promise
; + lastUsed: number; + data: Promise
; } // a "dumb" bag of bytes. @@ -671,272 +671,272 @@ interface SharedPromiseCacheValue { // deduplicates simultaneous responses // (estimates) the maximum size of the cache. export class SharedPromiseCache { - cache: Map; - maxCacheEntries: number; - counter: number; - prefetch: boolean; - decompress: DecompressFunc; + cache: Map; + maxCacheEntries: number; + counter: number; + prefetch: boolean; + decompress: DecompressFunc; - constructor( - maxCacheEntries = 100, - prefetch = true, - decompress: DecompressFunc = fflateDecompress - ) { - this.cache = new Map(); - this.maxCacheEntries = maxCacheEntries; - this.counter = 1; - this.prefetch = prefetch; - this.decompress = decompress; - } + constructor( + maxCacheEntries = 100, + prefetch = true, + decompress: DecompressFunc = fflateDecompress + ) { + this.cache = new Map(); + this.maxCacheEntries = maxCacheEntries; + this.counter = 1; + this.prefetch = prefetch; + this.decompress = decompress; + } - async getHeader(source: Source, current_etag?: string): Promise
{ - const cacheKey = source.getKey(); - if (this.cache.has(cacheKey)) { - this.cache.get(cacheKey)!.lastUsed = this.counter++; - const data = await this.cache.get(cacheKey)!.data; - return data as Header; - } + async getHeader(source: Source, current_etag?: string): Promise
{ + const cacheKey = source.getKey(); + if (this.cache.has(cacheKey)) { + this.cache.get(cacheKey)!.lastUsed = this.counter++; + const data = await this.cache.get(cacheKey)!.data; + return data as Header; + } - const p = new Promise
((resolve, reject) => { - getHeaderAndRoot(source, this.decompress, this.prefetch, current_etag) - .then((res) => { - if (res[1]) { - this.cache.set(res[1][0], { - lastUsed: this.counter++, - data: Promise.resolve(res[1][2]), - }); - } - resolve(res[0]); - this.prune(); - }) - .catch((e) => { - reject(e); - }); - }); - this.cache.set(cacheKey, { lastUsed: this.counter++, data: p }); - return p; - } + const p = new Promise
((resolve, reject) => { + getHeaderAndRoot(source, this.decompress, this.prefetch, current_etag) + .then((res) => { + if (res[1]) { + this.cache.set(res[1][0], { + lastUsed: this.counter++, + data: Promise.resolve(res[1][2]), + }); + } + resolve(res[0]); + this.prune(); + }) + .catch((e) => { + reject(e); + }); + }); + this.cache.set(cacheKey, { lastUsed: this.counter++, data: p }); + return p; + } - async getDirectory( - source: Source, - offset: number, - length: number, - header: Header - ): Promise { - const cacheKey = - source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; - if (this.cache.has(cacheKey)) { - this.cache.get(cacheKey)!.lastUsed = this.counter++; - const data = await this.cache.get(cacheKey)!.data; - return data as Entry[]; - } + async getDirectory( + source: Source, + offset: number, + length: number, + header: Header + ): Promise { + const cacheKey = + source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; + if (this.cache.has(cacheKey)) { + this.cache.get(cacheKey)!.lastUsed = this.counter++; + const data = await this.cache.get(cacheKey)!.data; + return data as Entry[]; + } - const p = new Promise((resolve, reject) => { - getDirectory(source, this.decompress, offset, length, header) - .then((directory) => { - resolve(directory); - this.prune(); - }) - .catch((e) => { - reject(e); - }); - }); - this.cache.set(cacheKey, { lastUsed: this.counter++, data: p }); - return p; - } + const p = new Promise((resolve, reject) => { + getDirectory(source, this.decompress, offset, length, header) + .then((directory) => { + resolve(directory); + this.prune(); + }) + .catch((e) => { + reject(e); + }); + }); + this.cache.set(cacheKey, { lastUsed: this.counter++, data: p }); + return p; + } - // for v2 backwards compatibility - async getArrayBuffer( - source: Source, - offset: number, - length: number, - header: Header - ): Promise { - const cacheKey = - source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; - if (this.cache.has(cacheKey)) { - this.cache.get(cacheKey)!.lastUsed = this.counter++; - const data = await this.cache.get(cacheKey)!.data; - return data as ArrayBuffer; - } + // for v2 backwards compatibility + async getArrayBuffer( + source: Source, + offset: number, + length: number, + header: Header + ): Promise { + const cacheKey = + source.getKey() + "|" + (header.etag || "") + "|" + offset + "|" + length; + if (this.cache.has(cacheKey)) { + this.cache.get(cacheKey)!.lastUsed = this.counter++; + const data = await this.cache.get(cacheKey)!.data; + return data as ArrayBuffer; + } - const p = new Promise((resolve, reject) => { - source - .getBytes(offset, length) - .then((resp) => { - if (header.etag && header.etag !== resp.etag) { - throw new EtagMismatch(resp.etag); - } - resolve(resp.data); - if (this.cache.has(cacheKey)) { - } - this.prune(); - }) - .catch((e) => { - reject(e); - }); - }); - this.cache.set(cacheKey, { lastUsed: this.counter++, data: p }); - return p; - } + const p = new Promise((resolve, reject) => { + source + .getBytes(offset, length) + .then((resp) => { + if (header.etag && header.etag !== resp.etag) { + throw new EtagMismatch(resp.etag); + } + resolve(resp.data); + if (this.cache.has(cacheKey)) { + } + this.prune(); + }) + .catch((e) => { + reject(e); + }); + }); + this.cache.set(cacheKey, { lastUsed: this.counter++, data: p }); + return p; + } - prune() { - if (this.cache.size >= this.maxCacheEntries) { - let minUsed = Infinity; - let minKey = undefined; - this.cache.forEach( - (cache_value: SharedPromiseCacheValue, key: string) => { - if (cache_value.lastUsed < minUsed) { - minUsed = cache_value.lastUsed; - minKey = key; - } - } - ); - if (minKey) { - this.cache.delete(minKey); - } - } - } + prune() { + if (this.cache.size >= this.maxCacheEntries) { + let minUsed = Infinity; + let minKey = undefined; + this.cache.forEach( + (cache_value: SharedPromiseCacheValue, key: string) => { + if (cache_value.lastUsed < minUsed) { + minUsed = cache_value.lastUsed; + minKey = key; + } + } + ); + if (minKey) { + this.cache.delete(minKey); + } + } + } - async invalidate(source: Source, current_etag: string) { - this.cache.delete(source.getKey()); - await this.getHeader(source, current_etag); - } + async invalidate(source: Source, current_etag: string) { + this.cache.delete(source.getKey()); + await this.getHeader(source, current_etag); + } } export class PMTiles { - source: Source; - cache: Cache; - decompress: DecompressFunc; + source: Source; + cache: Cache; + decompress: DecompressFunc; - constructor( - source: Source | string, - cache?: Cache, - decompress?: DecompressFunc - ) { - if (typeof source === "string") { - this.source = new FetchSource(source); - } else { - this.source = source; - } - if (decompress) { - this.decompress = decompress; - } else { - this.decompress = fflateDecompress; - } - if (cache) { - this.cache = cache; - } else { - this.cache = new SharedPromiseCache(); - } - } + constructor( + source: Source | string, + cache?: Cache, + decompress?: DecompressFunc + ) { + if (typeof source === "string") { + this.source = new FetchSource(source); + } else { + this.source = source; + } + if (decompress) { + this.decompress = decompress; + } else { + this.decompress = fflateDecompress; + } + if (cache) { + this.cache = cache; + } else { + this.cache = new SharedPromiseCache(); + } + } - async getHeader() { - return await this.cache.getHeader(this.source); - } + async getHeader() { + return await this.cache.getHeader(this.source); + } - async getZxyAttempt( - z: number, - x: number, - y: number, - signal?: AbortSignal - ): Promise { - const tile_id = zxyToTileId(z, x, y); - const header = await this.cache.getHeader(this.source); + async getZxyAttempt( + z: number, + x: number, + y: number, + signal?: AbortSignal + ): Promise { + 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); - } + // 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; - } + 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) { - const resp = await this.source.getBytes( - header.tileDataOffset + entry.offset, - entry.length, - signal - ); - if (header.etag && header.etag !== resp.etag) { - throw new EtagMismatch(resp.etag); - } - return { - data: await this.decompress(resp.data, header.tileCompression), - cacheControl: resp.cacheControl, - expires: resp.expires, - }; - } else { - d_o = header.leafDirectoryOffset + entry.offset; - d_l = entry.length; - } - } else { - // TODO: We should in fact return a valid RangeResponse - // with empty data, but filled in cache control / expires headers - return undefined; - } - } - throw Error("Maximum directory depth exceeded"); - } + 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) { + const resp = await this.source.getBytes( + header.tileDataOffset + entry.offset, + entry.length, + signal + ); + if (header.etag && header.etag !== resp.etag) { + throw new EtagMismatch(resp.etag); + } + return { + data: await this.decompress(resp.data, header.tileCompression), + cacheControl: resp.cacheControl, + expires: resp.expires, + }; + } else { + d_o = header.leafDirectoryOffset + entry.offset; + d_l = entry.length; + } + } else { + // TODO: We should in fact return a valid RangeResponse + // with empty data, but filled in cache control / expires headers + return undefined; + } + } + throw Error("Maximum directory depth exceeded"); + } - async getZxy( - z: number, - x: number, - y: number, - signal?: AbortSignal - ): Promise { - try { - return await this.getZxyAttempt(z, x, y, signal); - } catch (e) { - if (e instanceof EtagMismatch) { - this.cache.invalidate(this.source, e.message); - return await this.getZxyAttempt(z, x, y, signal); - } else { - throw e; - } - } - } + async getZxy( + z: number, + x: number, + y: number, + signal?: AbortSignal + ): Promise { + try { + return await this.getZxyAttempt(z, x, y, signal); + } catch (e) { + if (e instanceof EtagMismatch) { + this.cache.invalidate(this.source, e.message); + return await this.getZxyAttempt(z, x, y, signal); + } else { + throw e; + } + } + } - async getMetadataAttempt(): Promise { - const header = await this.cache.getHeader(this.source); + async getMetadataAttempt(): Promise { + const header = await this.cache.getHeader(this.source); - const resp = await this.source.getBytes( - header.jsonMetadataOffset, - header.jsonMetadataLength - ); - if (header.etag && header.etag !== resp.etag) { - throw new EtagMismatch(resp.etag); - } - const decompressed = await this.decompress( - resp.data, - header.internalCompression - ); - const dec = new TextDecoder("utf-8"); - return JSON.parse(dec.decode(decompressed)); - } + const resp = await this.source.getBytes( + header.jsonMetadataOffset, + header.jsonMetadataLength + ); + if (header.etag && header.etag !== resp.etag) { + throw new EtagMismatch(resp.etag); + } + const decompressed = await this.decompress( + resp.data, + header.internalCompression + ); + const dec = new TextDecoder("utf-8"); + return JSON.parse(dec.decode(decompressed)); + } - async getMetadata(): Promise { - try { - return await this.getMetadataAttempt(); - } catch (e) { - if (e instanceof EtagMismatch) { - this.cache.invalidate(this.source, e.message); - return await this.getMetadataAttempt(); - } else { - throw e; - } - } - } + async getMetadata(): Promise { + try { + return await this.getMetadataAttempt(); + } catch (e) { + if (e instanceof EtagMismatch) { + this.cache.invalidate(this.source, e.message); + return await this.getMetadataAttempt(); + } else { + throw e; + } + } + } } diff --git a/js/package-lock.json b/js/package-lock.json index f6b7398..87bc55a 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -15,6 +15,7 @@ "@types/node": "^18.11.9", "esbuild": "^0.15.11", "esbuild-runner": "^2.2.1", + "prettier": "^2.8.4", "typescript": "^4.5.5" } }, @@ -440,6 +441,21 @@ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.3.tgz", "integrity": "sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==" }, + "node_modules/prettier": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -691,6 +707,12 @@ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.3.tgz", "integrity": "sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==" }, + "prettier": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/js/package.json b/js/package.json index adc4c5e..afe9e46 100644 --- a/js/package.json +++ b/js/package.json @@ -27,6 +27,7 @@ "@types/node": "^18.11.9", "esbuild": "^0.15.11", "esbuild-runner": "^2.2.1", + "prettier": "^2.8.4", "typescript": "^4.5.5" }, "dependencies": { diff --git a/js/v2.ts b/js/v2.ts index e628f25..ea5a959 100644 --- a/js/v2.ts +++ b/js/v2.ts @@ -1,360 +1,360 @@ import { - Source, - Header, - Cache, - RangeResponse, - Compression, - TileType, + Source, + Header, + Cache, + RangeResponse, + Compression, + TileType, } from "./index"; import { decompressSync } from "fflate"; export const shift = (n: number, shift: number) => { - return n * Math.pow(2, shift); + return n * Math.pow(2, shift); }; export const unshift = (n: number, shift: number) => { - return Math.floor(n / Math.pow(2, shift)); + return Math.floor(n / Math.pow(2, shift)); }; export const getUint24 = (view: DataView, pos: number) => { - return shift(view.getUint16(pos + 1, true), 8) + view.getUint8(pos); + return shift(view.getUint16(pos + 1, true), 8) + view.getUint8(pos); }; export const getUint48 = (view: DataView, pos: number) => { - return shift(view.getUint32(pos + 2, true), 16) + view.getUint16(pos, true); + return shift(view.getUint32(pos + 2, true), 16) + view.getUint16(pos, true); }; interface Zxy { - z: number; - x: number; - y: number; + z: number; + x: number; + y: number; } export interface EntryV2 { - z: number; - x: number; - y: number; - offset: number; - length: number; - is_dir: boolean; + z: number; + x: number; + y: number; + offset: number; + length: number; + is_dir: boolean; } const compare = ( - tz: number, - tx: number, - ty: number, - view: DataView, - i: number + tz: number, + tx: number, + ty: number, + view: DataView, + i: number ) => { - if (tz != view.getUint8(i)) return tz - view.getUint8(i); - const x = getUint24(view, i + 1); - if (tx != x) return tx - x; - const y = getUint24(view, i + 4); - if (ty != y) return ty - y; - return 0; + if (tz != view.getUint8(i)) return tz - view.getUint8(i); + const x = getUint24(view, i + 1); + if (tx != x) return tx - x; + const y = getUint24(view, i + 4); + if (ty != y) return ty - y; + return 0; }; export const queryLeafdir = ( - view: DataView, - z: number, - x: number, - y: number + view: DataView, + z: number, + x: number, + y: number ): EntryV2 | null => { - const offset_len = queryView(view, z | 0x80, x, y); - if (offset_len) { - return { - z: z, - x: x, - y: y, - offset: offset_len[0], - length: offset_len[1], - is_dir: true, - }; - } - return null; + const offset_len = queryView(view, z | 0x80, x, y); + if (offset_len) { + return { + z: z, + x: x, + y: y, + offset: offset_len[0], + length: offset_len[1], + is_dir: true, + }; + } + return null; }; export const queryTile = (view: DataView, z: number, x: number, y: number) => { - const offset_len = queryView(view, z, x, y); - if (offset_len) { - return { - z: z, - x: x, - y: y, - offset: offset_len[0], - length: offset_len[1], - is_dir: false, - }; - } - return null; + const offset_len = queryView(view, z, x, y); + if (offset_len) { + return { + z: z, + x: x, + y: y, + offset: offset_len[0], + length: offset_len[1], + is_dir: false, + }; + } + return null; }; const queryView = ( - view: DataView, - z: number, - x: number, - y: number + view: DataView, + z: number, + x: number, + y: number ): [number, number] | null => { - let m = 0; - let n = view.byteLength / 17 - 1; - while (m <= n) { - const k = (n + m) >> 1; - const cmp = compare(z, x, y, view, k * 17); - if (cmp > 0) { - m = k + 1; - } else if (cmp < 0) { - n = k - 1; - } else { - return [getUint48(view, k * 17 + 7), view.getUint32(k * 17 + 13, true)]; - } - } - return null; + let m = 0; + let n = view.byteLength / 17 - 1; + while (m <= n) { + const k = (n + m) >> 1; + const cmp = compare(z, x, y, view, k * 17); + if (cmp > 0) { + m = k + 1; + } else if (cmp < 0) { + n = k - 1; + } else { + return [getUint48(view, k * 17 + 7), view.getUint32(k * 17 + 13, true)]; + } + } + return null; }; const entrySort = (a: EntryV2, b: EntryV2): number => { - if (a.is_dir && !b.is_dir) { - return 1; - } - if (!a.is_dir && b.is_dir) { - return -1; - } - if (a.z !== b.z) { - return a.z - b.z; - } - if (a.x !== b.x) { - return a.x - b.x; - } - return a.y - b.y; + if (a.is_dir && !b.is_dir) { + return 1; + } + if (!a.is_dir && b.is_dir) { + return -1; + } + if (a.z !== b.z) { + return a.z - b.z; + } + if (a.x !== b.x) { + return a.x - b.x; + } + return a.y - b.y; }; export const parseEntry = (dataview: DataView, i: number): EntryV2 => { - const z_raw = dataview.getUint8(i * 17); - const z = z_raw & 127; - return { - z: z, - x: getUint24(dataview, i * 17 + 1), - y: getUint24(dataview, i * 17 + 4), - offset: getUint48(dataview, i * 17 + 7), - length: dataview.getUint32(i * 17 + 13, true), - is_dir: z_raw >> 7 === 1, - }; + const z_raw = dataview.getUint8(i * 17); + const z = z_raw & 127; + return { + z: z, + x: getUint24(dataview, i * 17 + 1), + y: getUint24(dataview, i * 17 + 4), + offset: getUint48(dataview, i * 17 + 7), + length: dataview.getUint32(i * 17 + 13, true), + is_dir: z_raw >> 7 === 1, + }; }; export const sortDir = (a: ArrayBuffer): ArrayBuffer => { - const entries: EntryV2[] = []; - const view = new DataView(a); - for (let i = 0; i < view.byteLength / 17; i++) { - entries.push(parseEntry(view, i)); - } - return createDirectory(entries); + const entries: EntryV2[] = []; + const view = new DataView(a); + for (let i = 0; i < view.byteLength / 17; i++) { + entries.push(parseEntry(view, i)); + } + return createDirectory(entries); }; export const createDirectory = (entries: EntryV2[]): ArrayBuffer => { - entries.sort(entrySort); + entries.sort(entrySort); - const buffer = new ArrayBuffer(17 * entries.length); - const arr = new Uint8Array(buffer); - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - let z = entry.z; - if (entry.is_dir) z = z | 0x80; - arr[i * 17] = z; + const buffer = new ArrayBuffer(17 * entries.length); + const arr = new Uint8Array(buffer); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + let z = entry.z; + if (entry.is_dir) z = z | 0x80; + arr[i * 17] = z; - arr[i * 17 + 1] = entry.x & 0xff; - arr[i * 17 + 2] = (entry.x >> 8) & 0xff; - arr[i * 17 + 3] = (entry.x >> 16) & 0xff; + arr[i * 17 + 1] = entry.x & 0xff; + arr[i * 17 + 2] = (entry.x >> 8) & 0xff; + arr[i * 17 + 3] = (entry.x >> 16) & 0xff; - arr[i * 17 + 4] = entry.y & 0xff; - arr[i * 17 + 5] = (entry.y >> 8) & 0xff; - arr[i * 17 + 6] = (entry.y >> 16) & 0xff; + arr[i * 17 + 4] = entry.y & 0xff; + arr[i * 17 + 5] = (entry.y >> 8) & 0xff; + arr[i * 17 + 6] = (entry.y >> 16) & 0xff; - arr[i * 17 + 7] = entry.offset & 0xff; - arr[i * 17 + 8] = unshift(entry.offset, 8) & 0xff; - arr[i * 17 + 9] = unshift(entry.offset, 16) & 0xff; - arr[i * 17 + 10] = unshift(entry.offset, 24) & 0xff; - arr[i * 17 + 11] = unshift(entry.offset, 32) & 0xff; - arr[i * 17 + 12] = unshift(entry.offset, 48) & 0xff; + arr[i * 17 + 7] = entry.offset & 0xff; + arr[i * 17 + 8] = unshift(entry.offset, 8) & 0xff; + arr[i * 17 + 9] = unshift(entry.offset, 16) & 0xff; + arr[i * 17 + 10] = unshift(entry.offset, 24) & 0xff; + arr[i * 17 + 11] = unshift(entry.offset, 32) & 0xff; + arr[i * 17 + 12] = unshift(entry.offset, 48) & 0xff; - arr[i * 17 + 13] = entry.length & 0xff; - arr[i * 17 + 14] = (entry.length >> 8) & 0xff; - arr[i * 17 + 15] = (entry.length >> 16) & 0xff; - arr[i * 17 + 16] = (entry.length >> 24) & 0xff; - } - return buffer; + arr[i * 17 + 13] = entry.length & 0xff; + arr[i * 17 + 14] = (entry.length >> 8) & 0xff; + arr[i * 17 + 15] = (entry.length >> 16) & 0xff; + arr[i * 17 + 16] = (entry.length >> 24) & 0xff; + } + return buffer; }; export const deriveLeaf = (view: DataView, tile: Zxy): Zxy | null => { - if (view.byteLength < 17) return null; - const numEntries = view.byteLength / 17; - const entry = parseEntry(view, numEntries - 1); - if (entry.is_dir) { - const leaf_level = entry.z; - const level_diff = tile.z - leaf_level; - const leaf_x = Math.trunc(tile.x / (1 << level_diff)); - const leaf_y = Math.trunc(tile.y / (1 << level_diff)); - return { z: leaf_level, x: leaf_x, y: leaf_y }; - } - return null; + if (view.byteLength < 17) return null; + const numEntries = view.byteLength / 17; + const entry = parseEntry(view, numEntries - 1); + if (entry.is_dir) { + const leaf_level = entry.z; + const level_diff = tile.z - leaf_level; + const leaf_x = Math.trunc(tile.x / (1 << level_diff)); + const leaf_y = Math.trunc(tile.y / (1 << level_diff)); + return { z: leaf_level, x: leaf_x, y: leaf_y }; + } + return null; }; async function getHeader(source: Source): Promise
{ - const resp = await source.getBytes(0, 512000); + const resp = await source.getBytes(0, 512000); - const dataview = new DataView(resp.data); + const dataview = new DataView(resp.data); - const json_size = dataview.getUint32(4, true); - const root_entries = dataview.getUint16(8, true); + const json_size = dataview.getUint32(4, true); + const root_entries = dataview.getUint16(8, true); - const dec = new TextDecoder("utf-8"); - const json_metadata = JSON.parse( - dec.decode(new DataView(resp.data, 10, json_size)) - ); - let tile_compression = Compression.Unknown; - if (json_metadata.compression === "gzip") { - tile_compression = Compression.Gzip; - } + const dec = new TextDecoder("utf-8"); + const json_metadata = JSON.parse( + dec.decode(new DataView(resp.data, 10, json_size)) + ); + let tile_compression = Compression.Unknown; + if (json_metadata.compression === "gzip") { + tile_compression = Compression.Gzip; + } - let minzoom = 0; - if ("minzoom" in json_metadata) { - minzoom = +json_metadata.minzoom; - } + let minzoom = 0; + if ("minzoom" in json_metadata) { + minzoom = +json_metadata.minzoom; + } - let maxzoom = 0; - if ("maxzoom" in json_metadata) { - maxzoom = +json_metadata.maxzoom; - } + let maxzoom = 0; + if ("maxzoom" in json_metadata) { + maxzoom = +json_metadata.maxzoom; + } - let center_lon = 0; - let center_lat = 0; - let center_zoom = 0; - let min_lon = -180.0; - let min_lat = -85.0; - let max_lon = 180.0; - let max_lat = 85.0; + let center_lon = 0; + let center_lat = 0; + let center_zoom = 0; + let min_lon = -180.0; + let min_lat = -85.0; + let max_lon = 180.0; + let max_lat = 85.0; - if (json_metadata.bounds) { - const split = json_metadata.bounds.split(","); - min_lon = +split[0]; - min_lat = +split[1]; - max_lon = +split[2]; - max_lat = +split[3]; - } + if (json_metadata.bounds) { + const split = json_metadata.bounds.split(","); + min_lon = +split[0]; + min_lat = +split[1]; + max_lon = +split[2]; + max_lat = +split[3]; + } - if (json_metadata.center) { - const split = json_metadata.center.split(","); - center_lon = +split[0]; - center_lat = +split[1]; - center_zoom = +split[2]; - } + if (json_metadata.center) { + const split = json_metadata.center.split(","); + center_lon = +split[0]; + center_lat = +split[1]; + center_zoom = +split[2]; + } - const header = { - specVersion: dataview.getUint16(2, true), - rootDirectoryOffset: 10 + json_size, - rootDirectoryLength: root_entries * 17, - jsonMetadataOffset: 10, - jsonMetadataLength: json_size, - leafDirectoryOffset: 0, - leafDirectoryLength: undefined, - tileDataOffset: 0, - tileDataLength: undefined, - numAddressedTiles: 0, - numTileEntries: 0, - numTileContents: 0, - clustered: false, - internalCompression: Compression.None, - tileCompression: tile_compression, - tileType: TileType.Mvt, - minZoom: minzoom, - maxZoom: maxzoom, - minLon: min_lon, - minLat: min_lat, - maxLon: max_lon, - maxLat: max_lat, - centerZoom: center_zoom, - centerLon: center_lon, - centerLat: center_lat, - etag: resp.etag, - }; - return header; + const header = { + specVersion: dataview.getUint16(2, true), + rootDirectoryOffset: 10 + json_size, + rootDirectoryLength: root_entries * 17, + jsonMetadataOffset: 10, + jsonMetadataLength: json_size, + leafDirectoryOffset: 0, + leafDirectoryLength: undefined, + tileDataOffset: 0, + tileDataLength: undefined, + numAddressedTiles: 0, + numTileEntries: 0, + numTileContents: 0, + clustered: false, + internalCompression: Compression.None, + tileCompression: tile_compression, + tileType: TileType.Mvt, + minZoom: minzoom, + maxZoom: maxzoom, + minLon: min_lon, + minLat: min_lat, + maxLon: max_lon, + maxLat: max_lat, + centerZoom: center_zoom, + centerLon: center_lon, + centerLat: center_lat, + etag: resp.etag, + }; + return header; } async function getZxy( - header: Header, - source: Source, - cache: Cache, - z: number, - x: number, - y: number, - signal?: AbortSignal + header: Header, + source: Source, + cache: Cache, + z: number, + x: number, + y: number, + signal?: AbortSignal ): Promise { - let root_dir = await cache.getArrayBuffer( - source, - header.rootDirectoryOffset, - header.rootDirectoryLength, - header - ); - if (header.specVersion === 1) { - root_dir = sortDir(root_dir); - } + let root_dir = await cache.getArrayBuffer( + source, + header.rootDirectoryOffset, + header.rootDirectoryLength, + header + ); + if (header.specVersion === 1) { + root_dir = sortDir(root_dir); + } - const entry = queryTile(new DataView(root_dir), z, x, y); - if (entry) { - const resp = await source.getBytes(entry.offset, entry.length, signal); - let tile_data = resp.data; + const entry = queryTile(new DataView(root_dir), z, x, y); + if (entry) { + const resp = await source.getBytes(entry.offset, entry.length, signal); + let tile_data = resp.data; - const view = new DataView(tile_data); - if (view.getUint8(0) == 0x1f && view.getUint8(1) == 0x8b) { - tile_data = decompressSync(new Uint8Array(tile_data)); - } + const view = new DataView(tile_data); + if (view.getUint8(0) == 0x1f && view.getUint8(1) == 0x8b) { + tile_data = decompressSync(new Uint8Array(tile_data)); + } - return { - data: tile_data, - }; - } - const leafcoords = deriveLeaf(new DataView(root_dir), { z: z, x: x, y: y }); + return { + data: tile_data, + }; + } + const leafcoords = deriveLeaf(new DataView(root_dir), { z: z, x: x, y: y }); - if (leafcoords) { - const leafdir_entry = queryLeafdir( - new DataView(root_dir), - leafcoords.z, - leafcoords.x, - leafcoords.y - ); - if (leafdir_entry) { - let leaf_dir = await cache.getArrayBuffer( - source, - leafdir_entry.offset, - leafdir_entry.length, - header - ); + if (leafcoords) { + const leafdir_entry = queryLeafdir( + new DataView(root_dir), + leafcoords.z, + leafcoords.x, + leafcoords.y + ); + if (leafdir_entry) { + let leaf_dir = await cache.getArrayBuffer( + source, + leafdir_entry.offset, + leafdir_entry.length, + header + ); - if (header.specVersion === 1) { - leaf_dir = sortDir(leaf_dir); - } - const tile_entry = queryTile(new DataView(leaf_dir), z, x, y); - if (tile_entry) { - const resp = await source.getBytes( - tile_entry.offset, - tile_entry.length, - signal - ); - let tile_data = resp.data; + if (header.specVersion === 1) { + leaf_dir = sortDir(leaf_dir); + } + const tile_entry = queryTile(new DataView(leaf_dir), z, x, y); + if (tile_entry) { + const resp = await source.getBytes( + tile_entry.offset, + tile_entry.length, + signal + ); + let tile_data = resp.data; - const view = new DataView(tile_data); - if (view.getUint8(0) == 0x1f && view.getUint8(1) == 0x8b) { - tile_data = decompressSync(new Uint8Array(tile_data)); - } - return { - data: tile_data, - }; - } - } - } + const view = new DataView(tile_data); + if (view.getUint8(0) == 0x1f && view.getUint8(1) == 0x8b) { + tile_data = decompressSync(new Uint8Array(tile_data)); + } + return { + data: tile_data, + }; + } + } + } - return undefined; + return undefined; } export default { - getHeader: getHeader, - getZxy: getZxy, + getHeader: getHeader, + getZxy: getZxy, };