UNPKG

protomaps-leaflet

Version:

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

269 lines (250 loc) 7.95 kB
import Point from "@mapbox/point-geometry"; import { PMTiles } from "pmtiles"; import { Bbox, Feature, PmtilesSource, TileCache, TileSource, Zxy, ZxySource, } from "./tilecache"; /* * PreparedTile * For a given display Z: * layers: map of names-> features with coordinates in CSS pixel units. * translate: how to get layers coordinates to global Z coordinates. * dataTile: the Z,X,Y of the data tile. * window? if present, use as bounding box or canvas clipping area. */ export interface PreparedTile { z: number; // the display zoom level that it is for origin: Point; // the top-left corner in global CSS pixel coordinates data: Map<string, Feature[]>; // return a map to Iterable scale: number; // over or underzooming scale dim: number; // the effective size of this tile on the zoom level dataTile: Zxy; // the key of the raw tile } export interface TileTransform { dataTile: Zxy; origin: Point; scale: number; dim: number; } // TODO make this lazy export const transformGeom = ( geom: Array<Array<Point>>, scale: number, translate: Point, ) => { const retval = []; for (const arr of geom) { const loop = []; for (const coord of arr) { loop.push(coord.clone().mult(scale).add(translate)); } retval.push(loop); } return retval; }; export const wrap = (val: number, z: number) => { const dim = 1 << z; if (val < 0) return dim + val; if (val >= dim) return val % dim; return val; }; /* * @class View * expresses relationship between canvas coordinates and data tiles. */ export class View { levelDiff: number; tileCache: TileCache; maxDataLevel: number; constructor(tileCache: TileCache, maxDataLevel: number, levelDiff: number) { this.tileCache = tileCache; this.maxDataLevel = maxDataLevel; this.levelDiff = levelDiff; } public dataTilesForBounds( displayZoom: number, bounds: Bbox, ): Array<TileTransform> { const fractional = 2 ** displayZoom / 2 ** Math.ceil(displayZoom); const needed = []; let scale = 1; const dim = this.tileCache.tileSize; if (displayZoom < this.levelDiff) { scale = (1 / (1 << (this.levelDiff - displayZoom))) * fractional; needed.push({ dataTile: { z: 0, x: 0, y: 0 }, origin: new Point(0, 0), scale: scale, dim: dim * scale, }); } else if (displayZoom <= this.levelDiff + this.maxDataLevel) { const f = 1 << this.levelDiff; const basetileSize = 256 * fractional; const dataZoom = Math.ceil(displayZoom) - this.levelDiff; const mintileX = Math.floor(bounds.minX / f / basetileSize); const mintileY = Math.floor(bounds.minY / f / basetileSize); const maxtileX = Math.floor(bounds.maxX / f / basetileSize); const maxtileY = Math.floor(bounds.maxY / f / basetileSize); for (let tx = mintileX; tx <= maxtileX; tx++) { for (let ty = mintileY; ty <= maxtileY; ty++) { const origin = new Point( tx * f * basetileSize, ty * f * basetileSize, ); needed.push({ dataTile: { z: dataZoom, x: wrap(tx, dataZoom), y: wrap(ty, dataZoom), }, origin: origin, scale: fractional, dim: dim * fractional, }); } } } else { const f = 1 << this.levelDiff; scale = (1 << (Math.ceil(displayZoom) - this.maxDataLevel - this.levelDiff)) * fractional; const mintileX = Math.floor(bounds.minX / f / 256 / scale); const mintileY = Math.floor(bounds.minY / f / 256 / scale); const maxtileX = Math.floor(bounds.maxX / f / 256 / scale); const maxtileY = Math.floor(bounds.maxY / f / 256 / scale); for (let tx = mintileX; tx <= maxtileX; tx++) { for (let ty = mintileY; ty <= maxtileY; ty++) { const origin = new Point(tx * f * 256 * scale, ty * f * 256 * scale); needed.push({ dataTile: { z: this.maxDataLevel, x: wrap(tx, this.maxDataLevel), y: wrap(ty, this.maxDataLevel), }, origin: origin, scale: scale, dim: dim * scale, }); } } } return needed; } public dataTileForDisplayTile(displayTile: Zxy): TileTransform { let dataTile: Zxy; let scale = 1; let dim = this.tileCache.tileSize; let origin: Point; if (displayTile.z < this.levelDiff) { dataTile = { z: 0, x: 0, y: 0 }; scale = 1 / (1 << (this.levelDiff - displayTile.z)); origin = new Point(0, 0); dim = dim * scale; } else if (displayTile.z <= this.levelDiff + this.maxDataLevel) { const f = 1 << this.levelDiff; dataTile = { z: displayTile.z - this.levelDiff, x: Math.floor(displayTile.x / f), y: Math.floor(displayTile.y / f), }; origin = new Point(dataTile.x * f * 256, dataTile.y * f * 256); } else { scale = 1 << (displayTile.z - this.maxDataLevel - this.levelDiff); const f = 1 << this.levelDiff; dataTile = { z: this.maxDataLevel, x: Math.floor(displayTile.x / f / scale), y: Math.floor(displayTile.y / f / scale), }; origin = new Point( dataTile.x * f * scale * 256, dataTile.y * f * scale * 256, ); dim = dim * scale; } return { dataTile: dataTile, scale: scale, origin: origin, dim: dim }; } public async getBbox( displayZoom: number, bounds: Bbox, ): Promise<Array<PreparedTile>> { const needed = this.dataTilesForBounds(displayZoom, bounds); const result = await Promise.all( needed.map((tt) => this.tileCache.get(tt.dataTile)), ); return result.map((data, i) => { const tt = needed[i]; return { data: data, z: displayZoom, dataTile: tt.dataTile, scale: tt.scale, dim: tt.dim, origin: tt.origin, }; }); } public async getDisplayTile(displayTile: Zxy): Promise<PreparedTile> { const tt = this.dataTileForDisplayTile(displayTile); const data = await this.tileCache.get(tt.dataTile); return { data: data, z: displayTile.z, dataTile: tt.dataTile, scale: tt.scale, origin: tt.origin, dim: tt.dim, }; } public queryFeatures( lng: number, lat: number, displayZoom: number, brushSize: number, ) { const roundedZoom = Math.round(displayZoom); const dataZoom = Math.min(roundedZoom - this.levelDiff, this.maxDataLevel); const brushSizeAtZoom = brushSize / (1 << (roundedZoom - dataZoom)); return this.tileCache.queryFeatures(lng, lat, dataZoom, brushSizeAtZoom); } } export interface SourceOptions { levelDiff?: number; maxDataZoom?: number; url?: PMTiles | string; sources?: Record<string, SourceOptions>; } export const sourcesToViews = (options: SourceOptions) => { const sourceToViews = (o: SourceOptions): View => { const levelDiff = o.levelDiff === undefined ? 1 : o.levelDiff; const maxDataZoom = o.maxDataZoom || 15; let source: TileSource; if (typeof o.url === "string") { if (new URL(o.url, "http://example.com").pathname.endsWith(".pmtiles")) { source = new PmtilesSource(o.url, true); } else { source = new ZxySource(o.url, true); } } else if (o.url) { source = new PmtilesSource(o.url, true); } else { throw new Error(`Invalid source ${o.url}`); } const cache = new TileCache(source, (256 * 1) << levelDiff); return new View(cache, maxDataZoom, levelDiff); }; const sources = new Map<string, View>(); if (options.sources) { for (const key in options.sources) { sources.set(key, sourceToViews(options.sources[key])); } } else { sources.set("", sourceToViews(options)); } return sources; };