UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

229 lines 9.62 kB
import { Cache as DirCache } from '../../dataStructures/cache'; import { decompressStream } from '../../util'; import { Compression, bytesToHeader, deserializeDir, findTile, zxyToTileID } from './pmtiles'; import { FetchReader, toReader } from '..'; import { S2_HEADER_SIZE_BYTES, S2_ROOT_SIZE, s2BytesToHeader } from './s2pmtiles'; /** The File reader is to be used by bun/node/deno on the local filesystem. */ export class S2PMTilesReader { path; #header; #reader; // root directory will exist if header does #rootDir = []; #rootDirS2 = { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [] }; #metadata; #dirCache; #decoder = new TextDecoder('utf-8'); /** * Given an input path, read in the header and root directory * @param path - the location of the PMTiles data * @param rangeRequests - FetchReader specific; enable range requests or use urlParam "bytes" * @param maxSize - the max size of the cache before dumping old data. Defaults to 20. */ constructor(path, rangeRequests = false, maxSize = 20) { this.path = path; if (typeof path === 'string') { this.#reader = new FetchReader(path, rangeRequests); } else { this.#reader = toReader(path); } this.#dirCache = new DirCache(maxSize); } /** * Get the metadata for the archive * @returns - the header of the archive along with the root directory, * including information such as tile type, min/max zoom, bounds, and summary statistics. */ async #getMetadata() { if (this.#header !== undefined) return this.#header; const data = await this.#reader.getRange(0, S2_ROOT_SIZE); const headerData = data.slice(0, S2_HEADER_SIZE_BYTES); // check if s2 const isS2 = headerData[0] === 83 && headerData[1] === 50; // header const headerFunction = isS2 ? s2BytesToHeader : bytesToHeader; const header = (this.#header = headerFunction(headerData)); // json metadata const jsonMetadata = data.slice(header.jsonMetadataOffset, header.jsonMetadataOffset + header.jsonMetadataLength); this.#metadata = JSON.parse(this.#decoder.decode(await decompress(jsonMetadata, header.internalCompression))); // root directory data const rootDirData = data.slice(header.rootDirectoryOffset, header.rootDirectoryOffset + header.rootDirectoryLength); this.#rootDir = deserializeDir(await decompress(rootDirData, header.internalCompression)); if (isS2) await this.#getS2Metadata(data, header); return header; } /** * If S2 Projection, pull in the rest of the data * @param data - the root data * @param header - the S2 header with pointers to the rest of the data */ async #getS2Metadata(data, header) { // move the root directory to the s2 root this.#rootDirS2[0] = this.#rootDir; // add the 4 other faces for (const face of [1, 2, 3, 4, 5]) { const rootOffset = `rootDirectoryOffset${face}`; const rootLenght = `rootDirectoryLength${face}`; const faceDirData = data.slice(header[rootOffset], header[rootOffset] + header[rootLenght]); this.#rootDirS2[face] = deserializeDir(await decompress(faceDirData, header.internalCompression)); } } /** * Get the header of the archive * @returns - the header of the archive */ async getHeader() { return await this.#getMetadata(); } /** * Get the metadata of the archive * @returns - the metadata of the archive */ async getMetadata() { await this.#getMetadata(); // ensure loaded first return this.#metadata; } /** * Check if an S2 tile exists in the archive * @param face - the Open S2 projection face * @param zoom - the zoom level of the tile * @param x - the x coordinate of the tile * @param y - the y coordinate of the tile * @returns - true if the tile exists in the archive */ async hasTileS2(face, zoom, x, y) { return (await this.#getTileEntry(face, zoom, x, y)) !== undefined; } /** * Get the bytes of the tile at the given (face, zoom, x, y) coordinates * @param face - the Open S2 projection face * @param zoom - the zoom level of the tile * @param x - the x coordinate of the tile * @param y - the y coordinate of the tile * @returns - the bytes of the tile at the given (face, zoom, x, y) coordinates, or undefined if the tile does not exist in the archive. */ async getTileS2(face, zoom, x, y) { return await this.#getTile(face, zoom, x, y); } /** * Check if a tile exists in the archive * @param zoom - the zoom level of the tile * @param x - the x coordinate of the tile * @param y - the y coordinate of the tile * @returns - true if the tile exists in the archive */ async hasTile(zoom, x, y) { return (await this.#getTileEntry(-1, zoom, x, y)) !== undefined; } /** * Get the bytes of the tile at the given (zoom, x, y) coordinates * @param zoom - the zoom level of the tile * @param x - the x coordinate of the tile * @param y - the y coordinate of the tile * @returns - the bytes of the tile at the given (z, x, y) coordinates, or undefined if the tile does not exist in the archive. */ async getTile(zoom, x, y) { return await this.#getTile(-1, zoom, x, y); } /** * Get the bytes of the tile at the given (zoom, x, y) coordinates * @param face - the Open S2 projection face * @param zoom - the zoom level of the tile * @param x - the x coordinate of the tile * @param y - the y coordinate of the tile * @returns - the bytes of the tile at the given (z, x, y) coordinates, or undefined if the tile does not exist in the archive. */ async #getTile(face, zoom, x, y) { const { tileCompression } = await this.#getMetadata(); const entry = await this.#getTileEntry(face, zoom, x, y); if (entry === undefined) return undefined; const { offset, length } = entry; const entryData = await this.#reader.getRange(offset, length); return await decompress(entryData, tileCompression); } /** * Find the tile entry relative to the root directory * @param face - the Open S2 projection face * @param zoom - the zoom level of the tile * @param x - the x coordinate of the tile * @param y - the y coordinate of the tile * @returns - the position and length of bytes for the tile. Undefined if it does not exist */ async #getTileEntry(face, zoom, x, y) { const header = await this.#getMetadata(); const tileID = zxyToTileID(zoom, x, y); const { minZoom, maxZoom, rootDirectoryOffset, rootDirectoryLength, tileDataOffset } = header; if (zoom < minZoom || zoom > maxZoom) return undefined; let dO = rootDirectoryOffset; let dL = rootDirectoryLength; for (let depth = 0; depth <= 3; depth++) { const directory = await this.#getDirectory(dO, dL, face); if (directory === undefined) return undefined; const entry = findTile(directory, tileID); if (entry !== null) { if (entry.runLength > 0) { return { offset: tileDataOffset + entry.offset, length: entry.length }; } dO = header.leafDirectoryOffset + entry.offset; dL = entry.length; } else return undefined; } throw Error('Maximum directory depth exceeded'); } /** * Get the directory at the given offset * @param offset - the offset of the directory * @param length - the length of the directory * @param face - -1 for WM root, 0-5 for S2 * @returns - the entries in the directory if it exists */ async #getDirectory(offset, length, face) { const dir = face === -1 ? this.#rootDir : this.#rootDirS2[face]; const header = await this.#getMetadata(); const { internalCompression, rootDirectoryOffset } = header; // if rootDirectoryOffset, return roon if (offset === rootDirectoryOffset) return dir; // check cache const cache = this.#dirCache.get(offset); if (cache !== undefined) return cache; // get from archive const resp = await this.#reader.getRange(offset, length); const data = await decompress(resp, internalCompression); const directory = deserializeDir(data); if (directory.length === 0) throw new Error('Empty directory is invalid'); // save in cache this.#dirCache.set(offset, directory); return directory; } } /** * Decompress the data * @param data - the data to decompress * @param compression - the compression type * @returns - the decompressed data */ async function decompress(data, compression) { switch (compression) { case Compression.Gzip: return await decompressStream(data); case Compression.Brotli: throw new Error('Brotli decompression not implemented'); case Compression.Zstd: throw new Error('Zstd decompression not implemented'); case Compression.None: default: return data; } } //# sourceMappingURL=reader.js.map