s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
229 lines • 9.62 kB
JavaScript
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