From 98311c1f12bc7d480184d667f0aec181b263368c Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Tue, 4 Oct 2022 21:07:49 +0800 Subject: [PATCH] Finish implementation of v2 compatibility shim in v2.ts --- js/index.ts | 560 ---------------------------------------------------- js/v2.ts | 325 ++++++++++++++++++++++++++++-- js/v3.ts | 37 +++- 3 files changed, 335 insertions(+), 587 deletions(-) delete mode 100644 js/index.ts diff --git a/js/index.ts b/js/index.ts deleted file mode 100644 index 2ff762e..0000000 --- a/js/index.ts +++ /dev/null @@ -1,560 +0,0 @@ -declare const L: any; - -import { decompressSync } from "fflate"; - -export const shift = (n: number, shift: number) => { - return n * Math.pow(2, shift); -}; - -export const unshift = (n: number, shift: number) => { - 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); -}; - -export const getUint48 = (view: DataView, pos: number) => { - return shift(view.getUint32(pos + 2, true), 16) + view.getUint16(pos, true); -}; - -interface Zxy { - z: number; - x: number; - y: number; -} - -interface Header { - version: number; - json_size: number; - root_entries: number; -} - -interface Root { - header: Header; - dir: DataView; - view: DataView; -} - -export interface Entry { - 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 -) => { - 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 -): Entry | 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 queryView = ( - 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; -}; - -export const queryLeafLevel = (view: DataView): number | null => { - if (view.byteLength < 17) return null; - const numEntries = view.byteLength / 17; - const entry = parseEntry(view, numEntries - 1); - if (entry.is_dir) return entry.z; - return null; -}; - -const entrySort = (a: Entry, b: Entry): 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; -}; - -export const parseEntry = (dataview: DataView, i: number): Entry => { - 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 = (dataview: DataView): DataView => { - const entries: Entry[] = []; - for (let i = 0; i < dataview.byteLength / 17; i++) { - entries.push(parseEntry(dataview, i)); - } - return createDirectory(entries); -}; - -export const createDirectory = (entries: Entry[]): DataView => { - 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; - - 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 + 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 new DataView(arr.buffer, arr.byteOffset, arr.length); -}; - -export const deriveLeaf = (root: Root, tile: Zxy): Zxy | null => { - const leaf_level = queryLeafLevel(root.dir); - if (leaf_level) { - 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; -}; - -export const parseHeader = (dataview: DataView): Header => { - const magic = dataview.getUint16(0, true); - if (magic !== 19792) { - throw new Error('File header does not begin with "PM"'); - } - const version = dataview.getUint16(2, true); - const json_size = dataview.getUint32(4, true); - const root_entries = dataview.getUint16(8, true); - return { - version: version, - json_size: json_size, - root_entries: root_entries, - }; -}; - -export interface Source { - getBytes: (offset: number, length: number) => Promise; - getKey: () => string; -} - -export class FileSource implements Source { - file: File; - - constructor(file: File) { - this.file = file; - } - - getKey() { - return this.file.name; - } - - async getBytes(offset: number, length: number) { - let blob = this.file.slice(offset, offset + length); - let a = await blob.arrayBuffer(); - return new DataView(a); - } -} - -export class FetchSource implements Source { - url: string; - - constructor(url: string) { - this.url = url; - } - - getKey() { - return this.url; - } - - async getBytes(offset: number, length: number) { - const controller = new AbortController(); - const signal = controller.signal; - const resp = await fetch(this.url, { - signal: signal, - headers: { Range: "bytes=" + offset + "-" + (offset + length - 1) }, - }); - const contentLength = resp.headers.get("Content-Length"); - if (!contentLength || +contentLength !== length) { - console.error( - "Content-Length mismatch indicates byte serving not supported; aborting." - ); - controller.abort(); - } - - const a = await resp.arrayBuffer(); - return new DataView(a); - } -} - -interface CacheEntry { - lastUsed: number; - buffer: Promise; -} - -export class LRUCacheSource implements Source { - entries: Map; - maxEntries: number; - source: Source; - - constructor(source: Source, maxEntries: number) { - this.source = source; - this.entries = new Map(); - this.maxEntries = maxEntries; - } - - getKey = () => { - return this.source.getKey(); - }; - - async getBytes(offset: number, length: number) { - let val = this.entries.get(offset + "-" + length); - if (val) { - val.lastUsed = performance.now(); - return val.buffer; - } - - let promise = this.source.getBytes(offset, length); - - this.entries.set(offset + "-" + length, { - lastUsed: performance.now(), - buffer: promise, - }); - - if (this.entries.size > this.maxEntries) { - let minUsed = Infinity; - let minKey = undefined; - this.entries.forEach((val, key) => { - if (val.lastUsed < minUsed) { - minUsed = val.lastUsed; - minKey = key; - } - }); - if (minKey) this.entries.delete(minKey); - } - - return promise; - } -} - -export class PMTiles { - source: Source; - - constructor(source: string | Source, maxLeaves = 64) { - if (typeof source === "string") { - this.source = new LRUCacheSource(new FetchSource(source), maxLeaves); - } else { - this.source = source; - } - } - - async fetchRoot(): Promise { - const v = await this.source.getBytes(0, 512000); - const header = parseHeader(new DataView(v.buffer, v.byteOffset, 10)); - - let root_dir = new DataView( - v.buffer, - 10 + header.json_size, - 17 * header.root_entries - ); - if (header.version === 1) { - console.warn("Sorting pmtiles v1 directory"); - root_dir = sortDir(root_dir); - } - - return { - header: header, - view: v, - dir: root_dir, - }; - } - - async root_entries(): Promise { - const root = await this.fetchRoot(); - let entries = []; - for (var i = 0; i < root.header.root_entries; i++) { - entries.push(parseEntry(root.dir, i)); - } - return entries; - } - - async metadata(): Promise { - const root = await this.fetchRoot(); - const dec = new TextDecoder("utf-8"); - const result = JSON.parse( - dec.decode( - new DataView( - root.view.buffer, - root.view.byteOffset + 10, - root.header.json_size - ) - ) - ); - if (result.compression) { - console.warn( - `Archive has compression type: ${result.compression} and may not be readable directly by browsers.` - ); - } - if (!result.bounds) { - console.warn( - `Archive is missing 'bounds' in metadata, required in v2 and above.` - ); - } - if (!result.minzoom) { - console.warn( - `Archive is missing 'minzoom' in metadata, required in v2 and above.` - ); - } - if (!result.maxzoom) { - console.warn( - `Archive is missing 'maxzoom' in metadata, required in v2 and above.` - ); - } - return result; - } - - async fetchLeafdir(version: number, entry: Entry): Promise { - let buf = await this.source.getBytes(entry.offset, entry.length); - - if (version === 1) { - console.warn("Sorting pmtiles v1 directory."); - buf = sortDir(buf); - } - - return buf; - } - - async getLeafdir(version: number, entry: Entry): Promise { - return this.fetchLeafdir(version, entry); - } - - async getZxy(z: number, x: number, y: number): Promise { - const root = await this.fetchRoot(); - const entry = queryTile( - new DataView(root.dir.buffer, root.dir.byteOffset, root.dir.byteLength), - z, - x, - y - ); - if (entry) return entry; - - const leafcoords = deriveLeaf(root, { z: z, x: x, y: y }); - if (leafcoords) { - const leafdir_entry = queryLeafdir( - new DataView(root.dir.buffer, root.dir.byteOffset, root.dir.byteLength), - leafcoords.z, - leafcoords.x, - leafcoords.y - ); - if (leafdir_entry) { - const leafdir = await this.getLeafdir( - root.header.version, - leafdir_entry - ); - return queryTile( - new DataView(leafdir.buffer, leafdir.byteOffset, leafdir.byteLength), - z, - x, - y - ); - } - } - return null; - } -} - -export const leafletLayer = (source: PMTiles, options: any) => { - const cls = L.GridLayer.extend({ - createTile: function (coord: any, done: any) { - const tile: any = document.createElement("img"); - source.getZxy(coord.z, coord.x, coord.y).then((result) => { - if (result === null) return; - - const controller = new AbortController(); - const signal = controller.signal; - tile.cancel = () => { - controller.abort(); - }; - - source.source - .getBytes(result.offset, result.length) - .then((buf) => { - const blob = new Blob([buf], { type: "image/png" }); - const imageUrl = window.URL.createObjectURL(blob); - tile.src = imageUrl; - tile.cancel = null; - done(null, tile); - }) - .catch((error) => { - if (error.name !== "AbortError") throw error; - }); - }); - return tile; - }, - - _removeTile: function (key: string) { - const tile = this._tiles[key]; - if (!tile) { - return; - } - - if (tile.el.cancel) tile.el.cancel(); - - tile.el.width = 0; - tile.el.height = 0; - tile.el.deleted = true; - L.DomUtil.remove(tile.el); - delete this._tiles[key]; - this.fire("tileunload", { - tile: tile.el, - coords: this._keyToTileCoords(key), - }); - }, - }); - return new cls(options); -}; - -export class ProtocolCache { - tiles: Map; - - constructor() { - this.tiles = new Map(); - } - - add(p: PMTiles) { - this.tiles.set(p.source.getKey(), p); - } - - get(url: string) { - return this.tiles.get(url); - } - - protocol = (params: any, callback: any) => { - const re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/); - const result = params.url.match(re); - const pmtiles_url = result[1]; - - let instance = this.tiles.get(pmtiles_url); - if (!instance) { - instance = new PMTiles(pmtiles_url); - this.tiles.set(pmtiles_url, instance); - } - const z = result[2]; - const x = result[3]; - const y = result[4]; - let cancel = () => {}; - - instance.getZxy(+z, +x, +y).then((val) => { - if (val) { - instance!.source - .getBytes(val.offset, val.length) - .then((arr) => { - let data = new Uint8Array(arr.buffer); - if (data[0] == 0x1f && data[1] == 0x8b) { - data = decompressSync(data); - } - callback(null, data, null, null); - }) - .catch((e) => { - callback(new Error("Canceled"), null, null, null); - }); - } else { - callback(null, new Uint8Array(), null, null); - } - }); - return { - cancel: () => { - cancel(); - }, - }; - }; -} diff --git a/js/v2.ts b/js/v2.ts index 8b06838..8b90d3e 100644 --- a/js/v2.ts +++ b/js/v2.ts @@ -1,25 +1,248 @@ -import { Header, Cache, RangeResponse } from './v3'; +import { Source, Header, Cache, RangeResponse, Compression } from "./v3"; +import { decompressSync } from "fflate"; + +export const shift = (n: number, shift: number) => { + return n * Math.pow(2, shift); +}; + +export const unshift = (n: number, shift: number) => { + 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); +}; + +export const getUint48 = (view: DataView, pos: number) => { + return shift(view.getUint32(pos + 2, true), 16) + view.getUint16(pos, true); +}; + +interface Zxy { + z: number; + x: number; + y: number; +} + +export interface EntryV2 { + 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 +) => { + 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 +): 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; +}; + +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 queryView = ( + 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; +}; + +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; +}; + +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, + }; +}; + +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); +}; + +export const createDirectory = (entries: EntryV2[]): ArrayBuffer => { + 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; + + 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 + 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; +}; + +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) { + let 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 getHeaderAndRoot( + source: Source +): Promise<[Header, [string, number, ArrayBuffer]]> { + let resp = await source.getBytes(0, 512000); + + const dataview = new DataView(resp.data); + + 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)) + ); + + // if (json_metadata.compression) { + + // } + if (!json_metadata.bounds) { + console.warn( + `Archive is missing 'bounds' in metadata, required in v2 and above.` + ); + } + if (!json_metadata.minzoom) { + console.warn( + `Archive is missing 'minzoom' in metadata, required in v2 and above.` + ); + } + if (!json_metadata.maxzoom) { + console.warn( + `Archive is missing 'maxzoom' in metadata, required in v2 and above.` + ); + } -async function getHeaderAndRoot(a:ArrayBuffer, etag?:string): Promise<[Header, [string, number, ArrayBuffer]]> { const header = { - specVersion: 2, - rootDirectoryOffset: 0, - rootDirectoryLength: 0, - jsonMetadataOffset: 0, - jsonMetadataLength: 0, + specVersion: dataview.getUint16(2, true), + rootDirectoryOffset: 10 + json_size, + rootDirectoryLength: root_entries * 17, + jsonMetadataOffset: 10, + jsonMetadataLength: json_size, leafDirectoryOffset: 0, leafDirectoryLength: undefined, - tileDataOffset: 512000, + tileDataOffset: 0, tileDataLength: undefined, numAddressedTiles: 0, numTileEntries: 0, numTileContents: 0, clustered: false, - internalCompression: 0, + internalCompression: Compression.Unknown, tileCompression: 0, tileType: 0, - minZoom: 0, - maxZoom: 0, + minZoom: +json_metadata.minzoom, + maxZoom: +json_metadata.maxzoom, minLon: 0, minLat: 0, maxLon: 0, @@ -27,17 +250,83 @@ async function getHeaderAndRoot(a:ArrayBuffer, etag?:string): Promise<[Header, [ centerZoom: 0, centerLon: 0, centerLat: 0, - etag: etag, - }; - return [header, ["",0,new ArrayBuffer(0)]]; + etag: resp.etag, + }; + return [header, ["", 0, new ArrayBuffer(0)]]; } -async function getZxy(header:Header,cache:Cache): Promise { - return Promise.resolve(undefined); +async function getZxy( + header: Header, + source: Source, + cache: Cache, + z: number, + x: number, + y: number +): Promise { + 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) { + let resp = await source.getBytes(entry.offset, entry.length); // TODO signal + let tile_data = resp.data; + + let 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 }); + + 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); + } + let tile_entry = queryTile(new DataView(leaf_dir), z, x, y); + if (tile_entry) { + let resp = await source.getBytes(tile_entry.offset, tile_entry.length); // TODO signal + let tile_data = resp.data; + + let 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; } export default { getHeaderAndRoot: getHeaderAndRoot, - getZxy: getZxy + getZxy: getZxy, }; - diff --git a/js/v3.ts b/js/v3.ts index 7abdf9b..ff3ea24 100644 --- a/js/v3.ts +++ b/js/v3.ts @@ -132,7 +132,7 @@ export interface Entry { const ENTRY_SIZE_BYTES = 32; -enum Compression { +export enum Compression { None = 0, Gzip = 1, Brotli = 2, @@ -299,9 +299,6 @@ export class FetchSource implements Source { export function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header { const v = new DataView(bytes); - if (v.getUint16(0, true) !== 0x4d50) { - throw new Error("Wrong magic number for PMTiles archive"); - } return { specVersion: 3, rootDirectoryOffset: Number(v.getBigUint64(3, true)), @@ -366,6 +363,14 @@ function deserializeIndex(buffer: ArrayBuffer): Entry[] { } 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;"); + return 2; + } else if (v.getUint16(2, true) === 1) { + console.warn("PMTiles spec version 1 has been deprecated;"); + return 1; + } return 3; } @@ -394,9 +399,14 @@ async function getHeaderAndRoot( ): Promise<[Header, [string, number, Entry[] | ArrayBuffer]?]> { let 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"); + } + // V2 COMPATIBILITY - if (detectVersion(resp.data) < 2) { - return v2.getHeaderAndRoot(resp.data, resp.etag); + if (detectVersion(resp.data) < 3) { + return v2.getHeaderAndRoot(source); } const headerData = resp.data.slice(0, HEADER_SIZE_BYTES); @@ -728,8 +738,12 @@ export class PMTiles { source: Source; cache: Cache; - constructor(source: Source, cache?: Cache) { - this.source = source; + constructor(source: Source | string, cache?: Cache) { + if (typeof source === "string") { + this.source = new FetchSource(source); + } else { + this.source = source; + } if (cache) { this.cache = cache; } else { @@ -739,6 +753,11 @@ export class PMTiles { async root_entries() { const header = await this.cache.getHeader(this.source); + + // V2 COMPATIBILITY + if (header.specVersion < 3) { + return []; + } let d_o = header.rootDirectoryOffset; let d_l = header.rootDirectoryLength; return await this.cache.getDirectory(this.source, d_o, d_l, header); @@ -759,7 +778,7 @@ export class PMTiles { // V2 COMPATIBILITY if (header.specVersion < 3) { - return v2.getZxy(header, this.cache); + return v2.getZxy(header, this.source, this.cache, z, x, y); } if (z < header.minZoom || z > header.maxZoom) {