UNPKG

protomaps-leaflet

Version:

Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).

292 lines (265 loc) 7.63 kB
import Point from "@mapbox/point-geometry"; import { VectorTile } from "@mapbox/vector-tile"; import Protobuf from "pbf"; import { PMTiles } from "pmtiles"; export type JsonValue = | boolean | number | string | null | JsonArray | JsonObject; export interface JsonObject { [key: string]: JsonValue; } export interface JsonArray extends Array<JsonValue> {} export enum GeomType { Point = 1, Line = 2, Polygon = 3, } export interface Bbox { minX: number; minY: number; maxX: number; maxY: number; } export interface Feature { readonly props: JsonObject; readonly bbox: Bbox; readonly geomType: GeomType; readonly geom: Point[][]; readonly numVertices: number; } export interface Zxy { readonly z: number; readonly x: number; readonly y: number; } export function toIndex(c: Zxy): string { return `${c.x}:${c.y}:${c.z}`; } export interface TileSource { get(c: Zxy, tileSize: number): Promise<Map<string, Feature[]>>; } interface ZoomAbort { z: number; controller: AbortController; } // reimplement loadGeometry with a scalefactor // so the general tile rendering case does not need rescaling. const loadGeomAndBbox = (pbf: Protobuf, geometry: number, scale: number) => { pbf.pos = geometry; const end = pbf.readVarint() + pbf.pos; let cmd = 1; let length = 0; let x = 0; let y = 0; let x1 = Infinity; let x2 = -Infinity; let y1 = Infinity; let y2 = -Infinity; const lines: Point[][] = []; let line: Point[] = []; while (pbf.pos < end) { if (length <= 0) { const cmdLen = pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; } length--; if (cmd === 1 || cmd === 2) { x += pbf.readSVarint() * scale; y += pbf.readSVarint() * scale; if (x < x1) x1 = x; if (x > x2) x2 = x; if (y < y1) y1 = y; if (y > y2) y2 = y; if (cmd === 1) { if (line.length > 0) lines.push(line); line = []; } line.push(new Point(x, y)); } else if (cmd === 7) { if (line) line.push(line[0].clone()); } else throw new Error(`unknown command ${cmd}`); } if (line) lines.push(line); return { geom: lines, bbox: { minX: x1, minY: y1, maxX: x2, maxY: y2 } }; }; function parseTile( buffer: ArrayBuffer, tileSize: number, ): Map<string, Feature[]> { const v = new VectorTile(new Protobuf(buffer)); const result = new Map<string, Feature[]>(); for (const [key, value] of Object.entries(v.layers)) { const features = []; // biome-ignore lint: need to use private fields of vector-tile const layer = value as any; for (let i = 0; i < layer.length; i++) { const loaded = loadGeomAndBbox( layer.feature(i)._pbf, layer.feature(i)._geometry, tileSize / layer.extent, ); let numVertices = 0; for (const part of loaded.geom) numVertices += part.length; features.push({ id: layer.feature(i).id, geomType: layer.feature(i).type, geom: loaded.geom, numVertices: numVertices, bbox: loaded.bbox, props: layer.feature(i).properties, }); } result.set(key, features); } return result; } export class PmtilesSource implements TileSource { p: PMTiles; zoomaborts: ZoomAbort[]; shouldCancelZooms: boolean; constructor(url: string | PMTiles, shouldCancelZooms: boolean) { if (typeof url === "string") { this.p = new PMTiles(url); } else { this.p = url; } this.zoomaborts = []; this.shouldCancelZooms = shouldCancelZooms; } public async get(c: Zxy, tileSize: number): Promise<Map<string, Feature[]>> { if (this.shouldCancelZooms) { this.zoomaborts = this.zoomaborts.filter((za) => { if (za.z !== c.z) { za.controller.abort(); return false; } return true; }); } const controller = new AbortController(); this.zoomaborts.push({ z: c.z, controller: controller }); const signal = controller.signal; const result = await this.p.getZxy(c.z, c.x, c.y, signal); if (result) { return parseTile(result.data, tileSize); } return new Map<string, Feature[]>(); } } export class ZxySource implements TileSource { url: string; zoomaborts: ZoomAbort[]; shouldCancelZooms: boolean; constructor(url: string, shouldCancelZooms: boolean) { this.url = url; this.zoomaborts = []; this.shouldCancelZooms = shouldCancelZooms; } public async get(c: Zxy, tileSize: number): Promise<Map<string, Feature[]>> { if (this.shouldCancelZooms) { this.zoomaborts = this.zoomaborts.filter((za) => { if (za.z !== c.z) { za.controller.abort(); return false; } return true; }); } const url = this.url .replace("{z}", c.z.toString()) .replace("{x}", c.x.toString()) .replace("{y}", c.y.toString()); const controller = new AbortController(); this.zoomaborts.push({ z: c.z, controller: controller }); const signal = controller.signal; return new Promise((resolve, reject) => { fetch(url, { signal: signal }) .then((resp) => { return resp.arrayBuffer(); }) .then((buffer) => { const result = parseTile(buffer, tileSize); resolve(result); }) .catch((e) => { reject(e); }); }); } } interface CacheEntry { used: number; data: Map<string, Feature[]>; } interface PromiseOptions { resolve: (result: Map<string, Feature[]>) => void; reject: (e: Error) => void; } export class TileCache { source: TileSource; cache: Map<string, CacheEntry>; inflight: Map<string, PromiseOptions[]>; tileSize: number; constructor(source: TileSource, tileSize: number) { this.source = source; this.cache = new Map<string, CacheEntry>(); this.inflight = new Map<string, PromiseOptions[]>(); this.tileSize = tileSize; } public async get(c: Zxy): Promise<Map<string, Feature[]>> { const idx = toIndex(c); return new Promise((resolve, reject) => { const entry = this.cache.get(idx); if (entry) { entry.used = performance.now(); resolve(entry.data); } else { const ifentry = this.inflight.get(idx); if (ifentry) { ifentry.push({ resolve: resolve, reject: reject }); } else { this.inflight.set(idx, []); this.source .get(c, this.tileSize) .then((tile) => { this.cache.set(idx, { used: performance.now(), data: tile }); const ifentry2 = this.inflight.get(idx); if (ifentry2) { for (const f of ifentry2) { f.resolve(tile); } } this.inflight.delete(idx); resolve(tile); if (this.cache.size >= 64) { let minUsed = +Infinity; let minKey = undefined; this.cache.forEach((value, key) => { if (value.used < minUsed) { minUsed = value.used; minKey = key; } }); if (minKey) this.cache.delete(minKey); } }) .catch((e) => { const ifentry2 = this.inflight.get(idx); if (ifentry2) { for (const f of ifentry2) { f.reject(e); } } this.inflight.delete(idx); reject(e); }); } } }); } }