re-prettier pmtiles.ts; add prettier task

This commit is contained in:
Brandon Liu
2022-02-17 14:18:27 +08:00
parent c4560be9a2
commit 802b5e46fd
2 changed files with 364 additions and 379 deletions

View File

@@ -15,7 +15,8 @@
"build-tsc": "tsc --declaration --outdir dist", "build-tsc": "tsc --declaration --outdir dist",
"build": "npm run build-iife && npm run build-esm && npm run build-tsc", "build": "npm run build-iife && npm run build-esm && npm run build-tsc",
"test": "node -r esbuild-runner/register pmtiles.test.ts", "test": "node -r esbuild-runner/register pmtiles.test.ts",
"tsc": "tsc --noEmit --watch" "tsc": "tsc --noEmit --watch",
"prettier": "prettier --write pmtiles.ts"
}, },
"homepage": "https://github.com/protomaps/pmtiles", "homepage": "https://github.com/protomaps/pmtiles",
"author": "Brandon Liu", "author": "Brandon Liu",

View File

@@ -1,476 +1,460 @@
declare var L: any; declare var L: any;
export const shift = (n: number, shift: number) => { 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) => { 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) => { 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) => { 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);
}; };
const compare = ( const compare = (
tz: number, tz: number,
tx: number, tx: number,
ty: number, ty: number,
view: DataView, view: DataView,
i: number i: number
) => { ) => {
if (tz != view.getUint8(i)) return tz - view.getUint8(i); if (tz != view.getUint8(i)) return tz - view.getUint8(i);
var x = getUint24(view, i + 1); var x = getUint24(view, i + 1);
if (tx != x) return tx - x; if (tx != x) return tx - x;
var y = getUint24(view, i + 4); var y = getUint24(view, i + 4);
if (ty != y) return ty - y; if (ty != y) return ty - y;
return 0; return 0;
}; };
export const queryLeafdir = ( export const queryLeafdir = (
view: DataView, view: DataView,
z: number, z: number,
x: number, x: number,
y: number y: number
): Entry | null => { ): Entry | null => {
let offset_len = queryView(view, z | 0x80, x, y); let offset_len = queryView(view, z | 0x80, x, y);
if (offset_len) { if (offset_len) {
return { return {
z: z, z: z,
x: x, x: x,
y: y, y: y,
offset: offset_len[0], offset: offset_len[0],
length: offset_len[1], length: offset_len[1],
is_dir: true, is_dir: true,
}; };
} }
return null; return null;
}; };
export const queryTile = (view: DataView, z: number, x: number, y: number) => { export const queryTile = (view: DataView, z: number, x: number, y: number) => {
let offset_len = queryView(view, z, x, y); let offset_len = queryView(view, z, x, y);
if (offset_len) { if (offset_len) {
return { return {
z: z, z: z,
x: x, x: x,
y: y, y: y,
offset: offset_len[0], offset: offset_len[0],
length: offset_len[1], length: offset_len[1],
is_dir: false, is_dir: false,
}; };
} }
return null; return null;
}; };
const queryView = ( const queryView = (
view: DataView, view: DataView,
z: number, z: number,
x: number, x: number,
y: number y: number
): [number, number] | null => { ): [number, number] | null => {
var m = 0; var m = 0;
var n = view.byteLength / 17 - 1; var n = view.byteLength / 17 - 1;
while (m <= n) { while (m <= n) {
var k = (n + m) >> 1; var k = (n + m) >> 1;
var cmp = compare(z, x, y, view, k * 17); var cmp = compare(z, x, y, view, k * 17);
if (cmp > 0) { if (cmp > 0) {
m = k + 1; m = k + 1;
} else if (cmp < 0) { } else if (cmp < 0) {
n = k - 1; n = k - 1;
} else { } else {
return [ return [getUint48(view, k * 17 + 7), view.getUint32(k * 17 + 13, true)];
getUint48(view, k * 17 + 7), }
view.getUint32(k * 17 + 13, true), }
]; return null;
}
}
return null;
}; };
const entrySort = (a: Entry, b: Entry): number => { const entrySort = (a: Entry, b: Entry): number => {
if (a.is_dir && !b.is_dir) { if (a.is_dir && !b.is_dir) {
return 1; return 1;
} }
if (!a.is_dir && b.is_dir) { if (!a.is_dir && b.is_dir) {
return -1; return -1;
} }
if (a.z !== b.z) { if (a.z !== b.z) {
return a.z - b.z; return a.z - b.z;
} }
if (a.x !== b.x) { if (a.x !== b.x) {
return a.x - b.x; return a.x - b.x;
} }
return a.y - b.y; return a.y - b.y;
}; };
export const parseEntry = (dataview: DataView, i: number): Entry => { export const parseEntry = (dataview: DataView, i: number): Entry => {
var z_raw = dataview.getUint8(i * 17); var z_raw = dataview.getUint8(i * 17);
var z = z_raw & 127; var z = z_raw & 127;
return { return {
z: z, z: z,
x: getUint24(dataview, i * 17 + 1), x: getUint24(dataview, i * 17 + 1),
y: getUint24(dataview, i * 17 + 4), y: getUint24(dataview, i * 17 + 4),
offset: getUint48(dataview, i * 17 + 7), offset: getUint48(dataview, i * 17 + 7),
length: dataview.getUint32(i * 17 + 13, true), length: dataview.getUint32(i * 17 + 13, true),
is_dir: z_raw >> 7 === 1, is_dir: z_raw >> 7 === 1,
}; };
}; };
export const sortDir = (dataview: DataView): ArrayBuffer => { export const sortDir = (dataview: DataView): ArrayBuffer => {
let entries: Entry[] = []; let entries: Entry[] = [];
for (var i = 0; i < dataview.byteLength / 17; i++) { for (var i = 0; i < dataview.byteLength / 17; i++) {
entries.push(parseEntry(dataview, i)); entries.push(parseEntry(dataview, i));
} }
return createDirectory(entries); return createDirectory(entries);
}; };
export const createDirectory = (entries: Entry[]): ArrayBuffer => { export const createDirectory = (entries: Entry[]): ArrayBuffer => {
entries.sort(entrySort); entries.sort(entrySort);
let buffer = new ArrayBuffer(17 * entries.length); let buffer = new ArrayBuffer(17 * entries.length);
let arr = new Uint8Array(buffer); let arr = new Uint8Array(buffer);
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
var entry = entries[i]; var entry = entries[i];
var z = entry.z; var z = entry.z;
if (entry.is_dir) z = z | 0x80; if (entry.is_dir) z = z | 0x80;
arr[i * 17] = z; arr[i * 17] = z;
arr[i * 17 + 1] = entry.x & 0xff; arr[i * 17 + 1] = entry.x & 0xff;
arr[i * 17 + 2] = (entry.x >> 8) & 0xff; arr[i * 17 + 2] = (entry.x >> 8) & 0xff;
arr[i * 17 + 3] = (entry.x >> 16) & 0xff; arr[i * 17 + 3] = (entry.x >> 16) & 0xff;
arr[i * 17 + 4] = entry.y & 0xff; arr[i * 17 + 4] = entry.y & 0xff;
arr[i * 17 + 5] = (entry.y >> 8) & 0xff; arr[i * 17 + 5] = (entry.y >> 8) & 0xff;
arr[i * 17 + 6] = (entry.y >> 16) & 0xff; arr[i * 17 + 6] = (entry.y >> 16) & 0xff;
arr[i * 17 + 7] = entry.offset & 0xff; arr[i * 17 + 7] = entry.offset & 0xff;
arr[i * 17 + 8] = unshift(entry.offset, 8) & 0xff; arr[i * 17 + 8] = unshift(entry.offset, 8) & 0xff;
arr[i * 17 + 9] = unshift(entry.offset, 16) & 0xff; arr[i * 17 + 9] = unshift(entry.offset, 16) & 0xff;
arr[i * 17 + 10] = unshift(entry.offset, 24) & 0xff; arr[i * 17 + 10] = unshift(entry.offset, 24) & 0xff;
arr[i * 17 + 11] = unshift(entry.offset, 32) & 0xff; arr[i * 17 + 11] = unshift(entry.offset, 32) & 0xff;
arr[i * 17 + 12] = unshift(entry.offset, 48) & 0xff; arr[i * 17 + 12] = unshift(entry.offset, 48) & 0xff;
arr[i * 17 + 13] = entry.length & 0xff; arr[i * 17 + 13] = entry.length & 0xff;
arr[i * 17 + 14] = (entry.length >> 8) & 0xff; arr[i * 17 + 14] = (entry.length >> 8) & 0xff;
arr[i * 17 + 15] = (entry.length >> 16) & 0xff; arr[i * 17 + 15] = (entry.length >> 16) & 0xff;
arr[i * 17 + 16] = (entry.length >> 24) & 0xff; arr[i * 17 + 16] = (entry.length >> 24) & 0xff;
} }
return buffer; return buffer;
}; };
interface Zxy { interface Zxy {
z: number; z: number;
x: number; x: number;
y: number; y: number;
} }
// TODO: handle different leaf levels // TODO: handle different leaf levels
export const deriveLeaf = (tile: Zxy): Zxy => { export const deriveLeaf = (tile: Zxy): Zxy => {
let z7_tile_diff = tile.z - 7; let z7_tile_diff = tile.z - 7;
let z7_x = Math.trunc(tile.x / (1 << z7_tile_diff)); let z7_x = Math.trunc(tile.x / (1 << z7_tile_diff));
let z7_y = Math.trunc(tile.y / (1 << z7_tile_diff)); let z7_y = Math.trunc(tile.y / (1 << z7_tile_diff));
return { z: 7, x: z7_x, y: z7_y }; return { z: 7, x: z7_x, y: z7_y };
}; };
interface Header { interface Header {
version: number; version: number;
json_size: number; json_size: number;
root_entries: number; root_entries: number;
} }
interface Root { interface Root {
header: Header; header: Header;
buffer: ArrayBuffer; buffer: ArrayBuffer;
dir: DataView; dir: DataView;
// etag: string | null; // etag: string | null;
} }
export interface Entry { export interface Entry {
z: number; z: number;
x: number; x: number;
y: number; y: number;
offset: number; offset: number;
length: number; length: number;
is_dir: boolean; is_dir: boolean;
} }
interface CachedLeaf { interface CachedLeaf {
lastUsed: number; lastUsed: number;
buffer: Promise<ArrayBuffer>; buffer: Promise<ArrayBuffer>;
} }
export const parseHeader = (dataview: DataView): Header => { export const parseHeader = (dataview: DataView): Header => {
var magic = dataview.getUint16(0, true); var magic = dataview.getUint16(0, true);
if (magic !== 19792) { if (magic !== 19792) {
throw new Error('File header does not begin with "PM"'); throw new Error('File header does not begin with "PM"');
} }
var version = dataview.getUint16(2, true); var version = dataview.getUint16(2, true);
var json_size = dataview.getUint32(4, true); var json_size = dataview.getUint32(4, true);
var root_entries = dataview.getUint16(8, true); var root_entries = dataview.getUint16(8, true);
return { return {
version: version, version: version,
json_size: json_size, json_size: json_size,
root_entries: root_entries, root_entries: root_entries,
}; };
}; };
export class PMTiles { export class PMTiles {
root: Promise<Root> | null; root: Promise<Root> | null;
url: string; url: string;
leaves: Map<number, CachedLeaf>; leaves: Map<number, CachedLeaf>;
maxLeaves: number; maxLeaves: number;
constructor(url: string, maxLeaves: number = 64) { constructor(url: string, maxLeaves: number = 64) {
this.root = null; this.root = null;
this.url = url; this.url = url;
this.leaves = new Map<number, CachedLeaf>(); this.leaves = new Map<number, CachedLeaf>();
this.maxLeaves = maxLeaves; this.maxLeaves = maxLeaves;
} }
fetchRoot = async (url: string): Promise<Root> => { fetchRoot = async (url: string): Promise<Root> => {
const controller = new AbortController(); const controller = new AbortController();
const signal = controller.signal; const signal = controller.signal;
let resp = await fetch(url, { let resp = await fetch(url, {
signal: signal, signal: signal,
headers: { Range: "bytes=0-511999" }, headers: { Range: "bytes=0-511999" },
}); });
let contentLength = resp.headers.get("Content-Length"); let contentLength = resp.headers.get("Content-Length");
if (!contentLength || +contentLength !== 512000) { if (!contentLength || +contentLength !== 512000) {
console.error( console.error(
"Content-Length mismatch indicates byte serving not supported; aborting." "Content-Length mismatch indicates byte serving not supported; aborting."
); );
controller.abort(); controller.abort();
} }
let a = await resp.arrayBuffer(); let a = await resp.arrayBuffer();
let header = parseHeader(new DataView(a, 0, 10)); let header = parseHeader(new DataView(a, 0, 10));
var root_dir = new DataView( var root_dir = new DataView(
a, a,
10 + header.json_size, 10 + header.json_size,
17 * header.root_entries 17 * header.root_entries
); );
if (header.version === 1) { if (header.version === 1) {
console.log("Sorting pmtiles v1 directory"); console.log("Sorting pmtiles v1 directory");
root_dir = new DataView(sortDir(root_dir)); root_dir = new DataView(sortDir(root_dir));
} }
return { return {
buffer: a, buffer: a,
header: header, header: header,
dir: root_dir, dir: root_dir,
}; };
}; };
getRoot = async (): Promise<Root> => { getRoot = async (): Promise<Root> => {
if (this.root) return this.root; if (this.root) return this.root;
this.root = this.fetchRoot(this.url); this.root = this.fetchRoot(this.url);
return this.root; return this.root;
}; };
metadata = async (): Promise<any> => { metadata = async (): Promise<any> => {
let root = await this.getRoot(); let root = await this.getRoot();
let dec = new TextDecoder("utf-8"); let dec = new TextDecoder("utf-8");
let result = JSON.parse( let result = JSON.parse(
dec.decode(new DataView(root.buffer, 10, root.header.json_size)) dec.decode(new DataView(root.buffer, 10, root.header.json_size))
); );
if (result.compression) { if (result.compression) {
console.error( console.error(
`Archive has compression type: ${result.compression} and may not be readable directly by browsers.` `Archive has compression type: ${result.compression} and may not be readable directly by browsers.`
); );
} }
return result; return result;
}; };
fetchLeafdir = async ( fetchLeafdir = async (
version: number, version: number,
entry: Entry entry: Entry
): Promise<ArrayBuffer> => { ): Promise<ArrayBuffer> => {
let resp = await fetch(this.url, { let resp = await fetch(this.url, {
headers: { headers: {
Range: Range:
"bytes=" + "bytes=" + entry.offset + "-" + (entry.offset + entry.length - 1),
entry.offset + },
"-" + });
(entry.offset + entry.length - 1), var buf = await resp.arrayBuffer();
},
});
var buf = await resp.arrayBuffer();
if (version === 1) { if (version === 1) {
console.log("Sorting pmtiles v1 directory"); console.log("Sorting pmtiles v1 directory");
buf = sortDir(new DataView(buf)); buf = sortDir(new DataView(buf));
} }
return buf; return buf;
}; };
getLeafdir = async ( getLeafdir = async (version: number, entry: Entry): Promise<ArrayBuffer> => {
version: number, let leaf = this.leaves.get(entry.offset);
entry: Entry if (leaf) return await leaf.buffer;
): Promise<ArrayBuffer> => {
let leaf = this.leaves.get(entry.offset);
if (leaf) return await leaf.buffer;
var buf = this.fetchLeafdir(version, entry); var buf = this.fetchLeafdir(version, entry);
this.leaves.set(entry.offset, { this.leaves.set(entry.offset, {
lastUsed: performance.now(), lastUsed: performance.now(),
buffer: buf, buffer: buf,
}); });
if (this.leaves.size > this.maxLeaves) { if (this.leaves.size > this.maxLeaves) {
var minUsed = Infinity; var minUsed = Infinity;
var minKey = undefined; var minKey = undefined;
this.leaves.forEach((val, key) => { this.leaves.forEach((val, key) => {
if (val.lastUsed < minUsed) { if (val.lastUsed < minUsed) {
minUsed = val.lastUsed; minUsed = val.lastUsed;
minKey = key; minKey = key;
} }
}); });
if (minKey) this.leaves.delete(minKey); if (minKey) this.leaves.delete(minKey);
} }
return await buf; return await buf;
}; };
getZxy = async (z: number, x: number, y: number): Promise<Entry | null> => { getZxy = async (z: number, x: number, y: number): Promise<Entry | null> => {
let root = await this.getRoot(); let root = await this.getRoot();
let entry = queryTile(root.dir, z, x, y); let entry = queryTile(root.dir, z, x, y);
if (entry) return entry; if (entry) return entry;
let leafcoords = deriveLeaf({ z: z, x: x, y: y }); let leafcoords = deriveLeaf({ z: z, x: x, y: y });
let leafdir_entry = queryLeafdir( let leafdir_entry = queryLeafdir(
root.dir, root.dir,
leafcoords.z, leafcoords.z,
leafcoords.x, leafcoords.x,
leafcoords.y leafcoords.y
); );
if (leafdir_entry) { if (leafdir_entry) {
let leafdir = await this.getLeafdir( let leafdir = await this.getLeafdir(root.header.version, leafdir_entry);
root.header.version, return queryTile(new DataView(leafdir), z, x, y);
leafdir_entry }
); return null;
return queryTile(new DataView(leafdir), z, x, y); };
}
return null;
};
} }
export const leafletLayer = (source: PMTiles, options: any) => { export const leafletLayer = (source: PMTiles, options: any) => {
var cls = L.GridLayer.extend({ var cls = L.GridLayer.extend({
createTile: function (coord: any, done: any) { createTile: function (coord: any, done: any) {
var tile: any = document.createElement("img"); var tile: any = document.createElement("img");
source.getZxy(coord.z, coord.x, coord.y).then((result) => { source.getZxy(coord.z, coord.x, coord.y).then((result) => {
if (result === null) return; if (result === null) return;
const controller = new AbortController(); const controller = new AbortController();
const signal = controller.signal; const signal = controller.signal;
tile.cancel = () => { tile.cancel = () => {
controller.abort(); controller.abort();
}; };
fetch(source.url, { fetch(source.url, {
signal: signal, signal: signal,
headers: { headers: {
Range: Range:
"bytes=" + "bytes=" +
result.offset + result.offset +
"-" + "-" +
(result.offset + result.length - 1), (result.offset + result.length - 1),
}, },
}) })
.then((resp) => { .then((resp) => {
return resp.arrayBuffer(); return resp.arrayBuffer();
}) })
.then((buf) => { .then((buf) => {
var blob = new Blob([buf], { type: "image/png" }); var blob = new Blob([buf], { type: "image/png" });
var imageUrl = window.URL.createObjectURL(blob); var imageUrl = window.URL.createObjectURL(blob);
tile.src = imageUrl; tile.src = imageUrl;
tile.cancel = null; tile.cancel = null;
done(null, tile); done(null, tile);
}) })
.catch((error) => { .catch((error) => {
if (error.name !== "AbortError") throw error; if (error.name !== "AbortError") throw error;
}); });
}); });
return tile; return tile;
}, },
_removeTile: function (key: string) { _removeTile: function (key: string) {
var tile = this._tiles[key]; var tile = this._tiles[key];
if (!tile) { if (!tile) {
return; return;
} }
if (tile.el.cancel) tile.el.cancel(); if (tile.el.cancel) tile.el.cancel();
tile.el.width = 0; tile.el.width = 0;
tile.el.height = 0; tile.el.height = 0;
tile.el.deleted = true; tile.el.deleted = true;
L.DomUtil.remove(tile.el); L.DomUtil.remove(tile.el);
delete this._tiles[key]; delete this._tiles[key];
this.fire("tileunload", { this.fire("tileunload", {
tile: tile.el, tile: tile.el,
coords: this._keyToTileCoords(key), coords: this._keyToTileCoords(key),
}); });
}, },
}); });
return new cls(options); return new cls(options);
}; };
export const addProtocol = (maplibregl: any) => { export const addProtocol = (maplibregl: any) => {
let re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/); let re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
let pmtiles_instances = new Map<string, PMTiles>(); let pmtiles_instances = new Map<string, PMTiles>();
maplibregl.addProtocol("pmtiles", (params: any, callback: any) => { maplibregl.addProtocol("pmtiles", (params: any, callback: any) => {
let result = params.url.match(re); let result = params.url.match(re);
let pmtiles_url = result[1]; let pmtiles_url = result[1];
var instance = pmtiles_instances.get(pmtiles_url); var instance = pmtiles_instances.get(pmtiles_url);
if (!instance) { if (!instance) {
instance = new PMTiles(pmtiles_url); instance = new PMTiles(pmtiles_url);
pmtiles_instances.set(pmtiles_url, instance); pmtiles_instances.set(pmtiles_url, instance);
} }
let z = result[2]; let z = result[2];
let x = result[3]; let x = result[3];
let y = result[4]; let y = result[4];
var cancel = () => {}; var cancel = () => {};
instance.getZxy(+z, +x, +y).then((val) => { instance.getZxy(+z, +x, +y).then((val) => {
if (val) { if (val) {
let headers = { let headers = {
Range: Range: "bytes=" + val.offset + "-" + (val.offset + val.length - 1),
"bytes=" + };
val.offset + const controller = new AbortController();
"-" + const signal = controller.signal;
(val.offset + val.length - 1), cancel = () => {
}; controller.abort();
const controller = new AbortController(); };
const signal = controller.signal; fetch(pmtiles_url, { signal: signal, headers: headers })
cancel = () => { .then((resp) => {
controller.abort(); return resp.arrayBuffer();
}; })
fetch(pmtiles_url, { signal: signal, headers: headers }) .then((arr) => {
.then((resp) => { callback(null, arr, null, null);
return resp.arrayBuffer(); })
}) .catch((e) => {
.then((arr) => { callback(new Error("Canceled"), null, null, null);
callback(null, arr, null, null); });
}) } else {
.catch((e) => { callback(null, new Uint8Array(), null, null);
callback(new Error("Canceled"), null, null, null); }
}); });
} else { return {
callback(null, new Uint8Array(), null, null); cancel: () => {
} cancel();
}); },
return { };
cancel: () => { });
cancel(); return pmtiles_instances;
},
};
});
return pmtiles_instances;
}; };