mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 10:51:07 +00:00
Finish implementation of v2 compatibility shim in v2.ts
This commit is contained in:
560
js/index.ts
560
js/index.ts
@@ -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<DataView>;
|
|
||||||
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<DataView>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LRUCacheSource implements Source {
|
|
||||||
entries: Map<string, CacheEntry>;
|
|
||||||
maxEntries: number;
|
|
||||||
source: Source;
|
|
||||||
|
|
||||||
constructor(source: Source, maxEntries: number) {
|
|
||||||
this.source = source;
|
|
||||||
this.entries = new Map<string, CacheEntry>();
|
|
||||||
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<Root> {
|
|
||||||
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<Entry[]> {
|
|
||||||
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<any> {
|
|
||||||
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<DataView> {
|
|
||||||
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<DataView> {
|
|
||||||
return this.fetchLeafdir(version, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getZxy(z: number, x: number, y: number): Promise<Entry | null> {
|
|
||||||
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<string, PMTiles>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.tiles = new Map<string, PMTiles>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
323
js/v2.ts
323
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 = {
|
const header = {
|
||||||
specVersion: 2,
|
specVersion: dataview.getUint16(2, true),
|
||||||
rootDirectoryOffset: 0,
|
rootDirectoryOffset: 10 + json_size,
|
||||||
rootDirectoryLength: 0,
|
rootDirectoryLength: root_entries * 17,
|
||||||
jsonMetadataOffset: 0,
|
jsonMetadataOffset: 10,
|
||||||
jsonMetadataLength: 0,
|
jsonMetadataLength: json_size,
|
||||||
leafDirectoryOffset: 0,
|
leafDirectoryOffset: 0,
|
||||||
leafDirectoryLength: undefined,
|
leafDirectoryLength: undefined,
|
||||||
tileDataOffset: 512000,
|
tileDataOffset: 0,
|
||||||
tileDataLength: undefined,
|
tileDataLength: undefined,
|
||||||
numAddressedTiles: 0,
|
numAddressedTiles: 0,
|
||||||
numTileEntries: 0,
|
numTileEntries: 0,
|
||||||
numTileContents: 0,
|
numTileContents: 0,
|
||||||
clustered: false,
|
clustered: false,
|
||||||
internalCompression: 0,
|
internalCompression: Compression.Unknown,
|
||||||
tileCompression: 0,
|
tileCompression: 0,
|
||||||
tileType: 0,
|
tileType: 0,
|
||||||
minZoom: 0,
|
minZoom: +json_metadata.minzoom,
|
||||||
maxZoom: 0,
|
maxZoom: +json_metadata.maxzoom,
|
||||||
minLon: 0,
|
minLon: 0,
|
||||||
minLat: 0,
|
minLat: 0,
|
||||||
maxLon: 0,
|
maxLon: 0,
|
||||||
@@ -27,17 +250,83 @@ async function getHeaderAndRoot(a:ArrayBuffer, etag?:string): Promise<[Header, [
|
|||||||
centerZoom: 0,
|
centerZoom: 0,
|
||||||
centerLon: 0,
|
centerLon: 0,
|
||||||
centerLat: 0,
|
centerLat: 0,
|
||||||
etag: etag,
|
etag: resp.etag,
|
||||||
};
|
};
|
||||||
return [header, ["",0,new ArrayBuffer(0)]];
|
return [header, ["", 0, new ArrayBuffer(0)]];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getZxy(header:Header,cache:Cache): Promise<RangeResponse | undefined> {
|
async function getZxy(
|
||||||
return Promise.resolve(undefined);
|
header: Header,
|
||||||
|
source: Source,
|
||||||
|
cache: Cache,
|
||||||
|
z: number,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
): Promise<RangeResponse | undefined> {
|
||||||
|
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 {
|
export default {
|
||||||
getHeaderAndRoot: getHeaderAndRoot,
|
getHeaderAndRoot: getHeaderAndRoot,
|
||||||
getZxy: getZxy
|
getZxy: getZxy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
37
js/v3.ts
37
js/v3.ts
@@ -132,7 +132,7 @@ export interface Entry {
|
|||||||
|
|
||||||
const ENTRY_SIZE_BYTES = 32;
|
const ENTRY_SIZE_BYTES = 32;
|
||||||
|
|
||||||
enum Compression {
|
export enum Compression {
|
||||||
None = 0,
|
None = 0,
|
||||||
Gzip = 1,
|
Gzip = 1,
|
||||||
Brotli = 2,
|
Brotli = 2,
|
||||||
@@ -299,9 +299,6 @@ export class FetchSource implements Source {
|
|||||||
|
|
||||||
export function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header {
|
export function bytesToHeader(bytes: ArrayBuffer, etag?: string): Header {
|
||||||
const v = new DataView(bytes);
|
const v = new DataView(bytes);
|
||||||
if (v.getUint16(0, true) !== 0x4d50) {
|
|
||||||
throw new Error("Wrong magic number for PMTiles archive");
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
specVersion: 3,
|
specVersion: 3,
|
||||||
rootDirectoryOffset: Number(v.getBigUint64(3, true)),
|
rootDirectoryOffset: Number(v.getBigUint64(3, true)),
|
||||||
@@ -366,6 +363,14 @@ function deserializeIndex(buffer: ArrayBuffer): Entry[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectVersion(a: ArrayBuffer): number {
|
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;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +399,14 @@ async function getHeaderAndRoot(
|
|||||||
): Promise<[Header, [string, number, Entry[] | ArrayBuffer]?]> {
|
): Promise<[Header, [string, number, Entry[] | ArrayBuffer]?]> {
|
||||||
let resp = await source.getBytes(0, 16384);
|
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
|
// V2 COMPATIBILITY
|
||||||
if (detectVersion(resp.data) < 2) {
|
if (detectVersion(resp.data) < 3) {
|
||||||
return v2.getHeaderAndRoot(resp.data, resp.etag);
|
return v2.getHeaderAndRoot(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerData = resp.data.slice(0, HEADER_SIZE_BYTES);
|
const headerData = resp.data.slice(0, HEADER_SIZE_BYTES);
|
||||||
@@ -728,8 +738,12 @@ export class PMTiles {
|
|||||||
source: Source;
|
source: Source;
|
||||||
cache: Cache;
|
cache: Cache;
|
||||||
|
|
||||||
constructor(source: Source, cache?: Cache) {
|
constructor(source: Source | string, cache?: Cache) {
|
||||||
this.source = source;
|
if (typeof source === "string") {
|
||||||
|
this.source = new FetchSource(source);
|
||||||
|
} else {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
if (cache) {
|
if (cache) {
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
} else {
|
} else {
|
||||||
@@ -739,6 +753,11 @@ export class PMTiles {
|
|||||||
|
|
||||||
async root_entries() {
|
async root_entries() {
|
||||||
const header = await this.cache.getHeader(this.source);
|
const header = await this.cache.getHeader(this.source);
|
||||||
|
|
||||||
|
// V2 COMPATIBILITY
|
||||||
|
if (header.specVersion < 3) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
let d_o = header.rootDirectoryOffset;
|
let d_o = header.rootDirectoryOffset;
|
||||||
let d_l = header.rootDirectoryLength;
|
let d_l = header.rootDirectoryLength;
|
||||||
return await this.cache.getDirectory(this.source, d_o, d_l, header);
|
return await this.cache.getDirectory(this.source, d_o, d_l, header);
|
||||||
@@ -759,7 +778,7 @@ export class PMTiles {
|
|||||||
|
|
||||||
// V2 COMPATIBILITY
|
// V2 COMPATIBILITY
|
||||||
if (header.specVersion < 3) {
|
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) {
|
if (z < header.minZoom || z > header.maxZoom) {
|
||||||
|
|||||||
Reference in New Issue
Block a user