UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

209 lines (208 loc) 8.16 kB
import { Cache } from 'gis-tools/index.js'; import Source from './source.js'; const MAX_SIZE = 2_000_000; // ~2 MB const NODE_SIZE = 10; const DIR_SIZE = 1_365 * NODE_SIZE; // (13_650) -> 6 levels, the 6th level has both node and leaf (1+4+16+64+256+1024)*2 = 2_730 const ROOT_DIR_SIZE = DIR_SIZE * 6; // (81_900) -> 6 faces of 6 level directories + their leaves const DB_METADATA_SIZE = ROOT_DIR_SIZE + 20; // (81_920) -> 6 faces of 6 level directories + their leaves + 20 bytes for the header /** * # S2 Tiles Source * * A Tile caching mechanic that stores all tile data in a single file. Great for cloud storage. * * NOTE: This is most likely deprecated and may be removed in the future to be replaced * by S2PMTilesSource. */ export default class S2TilesSource extends Source { version = 1; rootDir = {}; dirCache = new Cache(15); /** @param mapID - the id of the map we are fetching data for */ async build(mapID) { // fetch the metadata const ab = (await this.getRange(`${this.path}?type=dir`, 0, DB_METADATA_SIZE, mapID)); if (ab === undefined || ab.byteLength !== DB_METADATA_SIZE) { // if the return is empty, we failed this.active = false; console.error(`Failed to extrapolate ${this.path} metadata`); return; } // prep a data view, store in header, build metadata const dv = new DataView(ab, 0, 20); if (dv.getUint16(0, true) !== 12883) { // the first two bytes are S and 2, we validate this.active = false; console.error(`Bad metadata from ${this.path}`); return; } // parse: grab the version this.version = dv.getUint16(2, true); // parse the JSON metadata length and offset const mL = dv.getUint32(4, true); const mO = getUint48(dv, 8); if (mL === 0 || mO === 0) { // if the metadata is empty, we failed this.active = false; console.error(`Failed to extrapolate ${this.path} metadata`); return; } // create root directories for (const face of [0, 1, 2, 3, 4, 5]) this.rootDir[face] = new DataView(ab, 20 + face * DIR_SIZE, DIR_SIZE); const metadata = (await this.getRange(`${this.path}?type=metadata`, mO, mL, mapID)); this._buildMetadata(metadata, mapID); } /** * Here, we use the memory mapped file directory tree system to find our data * @param mapID - the id of the map we are fetching data for * @param tile - the tile request * @param sourceName - the name of the source */ async _tileRequest(mapID, tile, sourceName) { const { type, encoding, session, size } = this; const { parent } = tile; const { face, zoom, i, j } = parent ?? tile; // pull in the correct face's directory const dir = this.rootDir[face]; // now we walk to the next directory as necessary const node = await this.#walk(mapID, dir, zoom, i, j); // [offset, length] if (node === undefined) { this._flush(mapID, tile, sourceName); return; } // we found the vector file, let's send the details off to the tile worker const data = (await this.getRange(`${this.path}?type=tile&enc=${encoding}`, node[0], node[1], mapID)); if (data !== undefined) { const worker = session.requestWorker(); worker.postMessage({ mapID, type, tile, sourceName, data, size }, [data]); } else { this._flush(mapID, tile, sourceName); } } /** * Walk the directory tree * @param mapID - the map id * @param dir - the current directory we are walking * @param zoom - current zoom * @param i - current i-coordinate * @param j - current j-coordinate * @returns the offset and length of either the next directory or raw data */ async #walk(mapID, dir, zoom, i, j) { const { maxzoom } = this; const path = getPath(zoom, i, j); let offset = 0; let length = 0; // walk the tree if past zoom 0 while (path.length > 0) { // grab position const nodePos = (path.shift() ?? 0) * NODE_SIZE; // set offset = getUint48(dir, nodePos); length = dir.getUint32(nodePos + 6, true); if (length === 0) return; // if we are still walking, grab the new directory if (path.length > 0) { // corner case: if maxzoom matches the zoom and is divisible by 5, the leaf is actually a node if (maxzoom % 5 === 0 && zoom === maxzoom && path.length === 1 && path[0] === 0) { return [offset, length]; } // otherwise fetch the directory const nextDir = await this.#getDir(mapID, offset, length); if (nextDir === undefined) return; dir = nextDir; } } if (length === 0 || length > MAX_SIZE) return; return [offset, length]; } /** * Get a directory * @param mapID - the map id we are are fetching data for * @param offset - the start of the directory * @param length - the length of the directory * @returns the directory */ async #getDir(mapID, offset, length) { if (this.dirCache.has(offset)) return this.dirCache.get(offset); const ab = (await this.getRange(`${this.path}?type=dir`, offset, length, mapID)); if (ab !== undefined) { const dir = new DataView(ab); this.dirCache.set(offset, dir); return dir; } } /** * Get a range request * @param url - the base href to build a range request from * @param offset - the start of the range * @param length - the length of the range * @param mapID - the map id we are going to build render data for * @returns raw tile data or directory data */ async getRange(url, offset, length, mapID) { const { needsToken, type, session } = this; const headers = {}; const bytes = String(offset) + '-' + String(offset + length); if (needsToken) { const Authorization = await session.requestSessionToken(mapID); if (Authorization === 'failed') return; if (Authorization !== undefined) headers.Authorization = Authorization; } if (length === 0 || length > MAX_SIZE) return; const res = await fetch(`${url}&bytes=${bytes}&subtype=${type}`, { headers }); if (res.status !== 200 && res.status !== 206) return; if (res.headers.get('content-type') === 'application/json') return await res.json(); return await res.arrayBuffer(); } } /** * Given a dataview and a position, get a 48-bit integer * @param dataview - the dataview * @param pos - the position to start reading from * @returns a 48-bit integer */ function getUint48(dataview, pos) { return dataview.getUint32(pos + 2, true) * (1 << 16) + dataview.getUint16(pos, true); } /** * Given a starting point, find a list of path offsets to traverse * @param zoom - starting zoom-level * @param x - starting x-coordinate * @param y - starting y-coordinate * @returns a list of path offsets */ function getPath(zoom, x, y) { const { max, pow } = Math; const path = []; // grab 6 bits at a time while (zoom >= 5) { // store at offset path.push([5, x & 31, y & 31]); // adjust x >>= 5; y >>= 5; zoom = max(0, zoom - 5); } // store leftovers path.push([zoom, x, y]); return path.map(([zoom, x, y]) => { let val = 0; // adjust by position at current zoom val += y * (1 << zoom) + x; // adjust by previous zoom tile sizes while (zoom-- > 0) val += pow(1 << zoom, 2); return val; }); }