gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
280 lines • 10.5 kB
JavaScript
import { Compression, compressionToFormat, decompressStream } from '../../util/index.js';
import { Cache as DirCache, FetchReader, toReader } from '../../index.js';
const NODE_SIZE = 10; // [offset, length] => [6 bytes, 4 bytes]
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 => (1365)+1365 => 2_730
const METADATA_SIZE = 131_072; // 131,072 bytes is 128kB
const ROOT_DIR_SIZE = DIR_SIZE * 6; // 27_300 * 6 = 163_800
const ROOT_SIZE = METADATA_SIZE + ROOT_DIR_SIZE;
// assuming all tiles exist for every face from 0->30 the max leafs to reach depth of 30 is 5
// root: 6sides * 27_300bytes/dir = (163_800 bytes)
// all leafs at 6: 1024 * 6sides * 27_300bytes/dir (0.167731 GB)
// al leafs at 12: 524_288 * 6sides * 27_300bytes/dir (85.8783744 GB) - obviously most of this is water
/**
* # S2 Tiles Reader
*
* ## Description
*
* An S2 Tile Reader to store tile and metadata in a cloud optimized format. Similar to PMTiles
* but simplified to have as few features as possible.
*
* Reads either a Web Mercator tile or an S2 tile to the folder location given its (zoom, x, y) or (face, zoom, x, y) coordinates.
*
* Reads data via the [S2Tiles specification](https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md).
*
* ## Usage
*
* ```ts
* import { S2TilesReader } from 'gis-tools-ts';
* import { FileReader } from 'gis-tools-ts/file';
* // or use the MMapReader if using Bun / FetchReader for web:
* // import { MMapReader } from 'gis-tools-ts/mmap';
* // import { FetchReader } from 'gis-tools-ts';
*
* const reader = new S2TilesReader(new FileReader('./data.s2tiles'));
*
* // get the metadata
* const metadata = await reader.getMetadata();
*
* // S2 specific functions
* const hasTile = await reader.hasTileS2(0, 0, 0, 0);
* const tile = await reader.getTileS2(0, 0, 0, 0);
*
* // WM functions
* const hasTile = await reader.hasTileWM(0, 0, 0);
* const tile = await reader.getTileWM(0, 0, 0);
* ```
*
* ## Links
* - https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md
*/
export class S2TilesReader {
path;
#isSetup = false;
version = 1;
maxzoom = 0;
#reader;
compression = Compression.Gzip;
metadata;
#rootDir = {};
#dirCache;
decoder = new TextDecoder();
/**
* @param path - the location of the S2Tiles 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 of the archive
* @returns - the metadata of the archive
*/
async getMetadata() {
if (this.metadata !== undefined)
return this.metadata;
await this.setup();
return this.metadata;
}
/** Setup the reader */
async setup() {
if (this.#isSetup)
return;
this.#isSetup = true;
// fetch the metadata
const data = await this.#reader.getRange(0, ROOT_SIZE);
// prep a data view, store in header, build metadata
const dv = new DataView(data.buffer, 0, ROOT_SIZE);
if (dv.getUint16(0, true) !== 12883) {
// the first two bytes are S and 2, we validate
throw new Error(`Bad metadata from ${this.path}`);
}
// parse the version, maxzoom, and compression
this.version = dv.getUint16(2, true);
this.maxzoom = dv.getUint8(4);
this.compression = dv.getUint8(5);
// parse the JSON metadata length and offset
const mL = dv.getUint32(6, true);
if (mL === 0) {
// if the metadata is empty, we failed
throw new Error(`Failed to extrapolate ${this.path} metadata`);
}
const meta_data = await decompress(data.slice(10, 10 + mL), this.compression);
this.metadata = JSON.parse(this.decoder.decode(meta_data));
// create root directories
for (const face of [0, 1, 2, 3, 4, 5])
this.#rootDir[face] = new DataView(data.buffer, METADATA_SIZE + face * DIR_SIZE, DIR_SIZE);
}
/**
* 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 hasTileWM(zoom, x, y) {
return await this.hasTileS2(0, zoom, x, y);
}
/**
* 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) {
await this.setup();
// 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(dir, zoom, x, y); // [offset, length]
if (node === undefined)
return false;
const [offset, length] = node;
return offset !== 0 && length !== 0;
}
/**
* 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 getTileWM(zoom, x, y) {
await this.setup();
return await this.getTileS2(0, zoom, x, y);
}
/**
* 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) {
await this.setup();
const { compression } = this;
// 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(dir, zoom, x, y); // [offset, length]
if (node === undefined) {
return;
}
const [offset, length] = node;
// we found the vector file, let's send the details off to the tile worker
const data = await this.#reader.getRange(offset, length);
return await decompress(data, compression);
}
/**
* given position and level, find the tile offset and length
* @param dir - the directory to walk
* @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 offset and length of the tile if it exists
*/
async #walk(dir, zoom, x, y) {
const { maxzoom } = this;
const path = getS2TilePath(zoom, x, y);
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 = _readUInt48LE(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(offset, length);
if (nextDir === undefined)
return;
dir = nextDir;
}
}
if (length === 0)
return;
return [offset, length];
}
/**
* get a directory given an offset and length
* @param offset - the offset
* @param length - the length
* @returns - the directory
*/
async #getDir(offset, length) {
if (this.#dirCache.has(offset))
return this.#dirCache.get(offset);
const data = await this.#reader.getRange(offset, length);
const dir = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.#dirCache.set(offset, dir);
return dir;
}
}
/**
* read a 48 bit number
* @param buffer - the buffer
* @param offset - the offset
* @returns - the number
*/
function _readUInt48LE(buffer, offset = 0) {
return buffer.getUint32(offset + 2, true) * (1 << 16) + buffer.getUint16(offset, true);
}
/**
* Get the path to a tile
* @param zoom - the zoom
* @param x - the x
* @param y - the y
* @returns - The path as a collection of offsets pointing to the tile Node in the directory
*/
export function getS2TilePath(zoom, x, y) {
const { max, pow } = Math;
const path = [];
while (zoom >= 5) {
path.push([5, x & 31, y & 31]);
x >>= 5;
y >>= 5;
zoom = max(zoom - 5, 0);
}
path.push([zoom, x, y]);
return path.map(([zoom, x, y]) => {
let val = 0;
val += y * (1 << zoom) + x;
while (zoom-- !== 0)
val += pow(1 << zoom, 2);
return val;
});
}
/**
* Decompress the data
* @param data - the data to decompress
* @param compression - the compression type
* @returns - the decompressed data
*/
async function decompress(data, compression) {
const format = compressionToFormat(compression);
if (format === 'none')
return data;
return await decompressStream(data, format);
}
//# sourceMappingURL=index.js.map