UNPKG

gis-tools-ts

Version:

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

322 lines 12.2 kB
import { Compression, compressStream } from '../../util/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. It is assumed the map metadata AND the S2Tile format metadata is less than 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 Writer * * ## Description * * An S2 Tile Writer 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. * * Writes data via the [S2Tiles specification](https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md). * * ## Usage * * ### Browser Compatible * ```ts * import { BufferWriter, S2TilesWriter, Compression } from 'gis-tools-ts'; * import type { Metadata } from 'gis-tools-ts'; * * // Setup the writers * const bufWriter = new BufferWriter(); * const writer = new S2TilesWriter(bufWriter, 12, Compression.Gzip); * // example data * const txtEncoder = new TextEncoder(); * const str = 'hello world'; * const uint8 = txtEncoder.encode(str); * const str2 = 'hello world 2'; * const uint8_2 = txtEncoder.encode(str2); * // write data in tile * await writer.writeTileWM(0, 0, 0, uint8); * await writer.writeTileWM(1, 0, 1, uint8); * await writer.writeTileWM(5, 2, 9, uint8_2); * // If S2 tiles: * await writer.writeTileS2(1, 0, 0, 0, uint8); * // finish * await writer.commit({ metadata: true } as unknown as Metadata); * // Get the result Uint8Array * const resultData = bufWriter.commit(); * ``` * * ### Node/Deno/Bun using the filesystem * ```ts * import { S2TilesWriter, Compression } from 'gis-tools-ts'; * import { FileWriter } from 'gis-tools-ts/file'; * * const writer = new S2TilesWriter(new FileWriter('./output.s2tiles'), 10, Compression.Gzip); * // SAME AS ABOVE * ``` * * ## Links * - https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md */ export class S2TilesWriter { writer; maxzoom; compression; offset = ROOT_SIZE; version = 1; encoder = new TextEncoder(); /** * @param writer - the writer to append to * @param maxzoom - the maximum zoom level to write to * @param compression - the compression algorithm to use for internal data like the */ constructor(writer, maxzoom, compression = Compression.Gzip) { this.writer = writer; this.maxzoom = maxzoom; this.compression = compression; this.writer.appendSync(new Uint8Array(ROOT_SIZE)); } /** * Write a tile to the S2Tiles file given its (z, x, y) coordinates. * @param zoom - the zoom level * @param x - the tile X coordinate * @param y - the tile Y coordinate * @param data - the tile data to store */ async writeTileWM(zoom, x, y, data) { await this.putTileIJ(0, zoom, x, y, data); } /** * Write a tile to the S2Tiles file given its (face, zoom, x, y) coordinates. * @param face - the Open S2 projection face * @param zoom - the zoom level * @param x - the tile X coordinate * @param y - the tile Y coordinate * @param data - the tile data to store */ async writeTileS2(face, zoom, x, y, data) { await this.putTileIJ(face, zoom, x, y, data); } /** * Finish writing by building the header with root and leaf directories * @param metadata - the metadata to store * @param tileCompression - the compression algorithm that was used on the tiles [Default: None] */ async commit(metadata, tileCompression) { // set the ID, version, and compression type const data = new DataView(new ArrayBuffer(10)); // Store format metadata data.setUint8(0, 83); // S data.setUint8(1, 50); // 2 data.setUint16(2, this.version, true); data.setUint8(4, this.maxzoom); data.setUint8(5, tileCompression ?? this.compression); // store the metadata's length then actual data let metaBuffer = this.encoder.encode(JSON.stringify(metadata)); metaBuffer = await compress(metaBuffer, this.compression); if (metaBuffer.byteLength > METADATA_SIZE - 10) { throw new Error('Metadata too large for S2Tiles'); } data.setUint32(6, metaBuffer.byteLength, true); // store the format metadata and lengthen the writer to fill METADATA_SIZE. Then store the map metadata await this.writer.write(new Uint8Array(data.buffer, 0, 10), 0); await this.writer.write(metaBuffer, 10); } /** * Write a tile to the S2Tiles file given its (face, zoom, x, y) coordinates. * @param face - the Open S2 projection face * @param zoom - the zoom level * @param x - the tile X coordinate * @param y - the tile Y coordinate * @param data - the tile data to store */ async putTileIJ(face, zoom, x, y, data) { await this.putTile(face, zoom, x, y, data); } /** * Inserts a tile into the S2Tiles store. * @param face - the Open S2 projection face * @param zoom - the zoom level * @param x - the tile X coordinate * @param y - the tile Y coordinate * @param data - the tile data */ async putTile(face, zoom, x, y, data) { const length = data.byteLength; // first create node, setting offset const node = [this.offset, length]; await this.writer.append(await compress(data, this.compression)); this.offset += length; // store node in the correct directory await this.#putNodeInDir(face, zoom, x, y, node); } /** * Work our way towards the correct parent directory. * If parent directory does not exists, we create it. * @param face - the Open S2 projection face * @param zoom - the zoom level * @param x - the tile X coordinate * @param y - the tile Y coordinate * @param node - the node */ async #putNodeInDir(face, zoom, x, y, node) { // use the s2cellID and move the cursor const cursor = await this.#walk(face, zoom, x, y); // finally store await this.#writeNode(cursor, node); } /** * given position and level, explain where to adust the cursor to file * @param face - the Open S2 projection face * @param zoom - the zoom level * @param x - the tile X coordinate * @param y - the tile Y coordinate * @returns - the new cursor position */ async #walk(face, zoom, x, y) { const { maxzoom } = this; let leafNode = new DataView(new ArrayBuffer(NODE_SIZE)); let cursor = METADATA_SIZE + DIR_SIZE * face; let leaf; let depth = 0; const path = getPath(zoom, x, y); while (path.length !== 0) { // grab movement const shift = path.shift() ?? 0; depth++; // update cursor position cursor += shift * NODE_SIZE; if (path.length !== 0) { // if we hit a leaf, adjust nodePos position and move cursor to new directory // if we are at the max zoom, we are already in the correct position (the "leaf" is actually a node instead) if (maxzoom % 5 === 0 && path.length === 1 && zoom === maxzoom && path[0] === 0) return cursor; // grab the leaf from the file leafNode = new DataView((await this.writer.slice(cursor, cursor + NODE_SIZE)).buffer); leaf = _readUInt48LE(leafNode); // if the leaf doesn't exist we create a new directory to host it if (leaf === 0) { cursor = await this.#createLeaf(cursor, depth * 5); } else { cursor = leaf; } // move to where leaf is pointing } } return cursor; } /** * Create a new leaf directory * @param cursor - the cursor * @param depth - the depth * @returns - the offset of the new leaf */ async #createLeaf(cursor, depth) { // build directory size according to maxzoom const dirSize = _buildDirSize(depth, this.maxzoom); // create offset & node const offset = this.offset; const node = [offset, dirSize]; // create a dir of said size and update to new offset await this.writer.write(new Uint8Array(dirSize), offset); this.offset += dirSize; // store our newly created directory as a leaf directory in our current directory await this.#writeNode(cursor, node); // return the offset of the leaf directory return offset; } /** * Writes a node to the file * @param cursor - the cursor * @param node - the node */ async #writeNode(cursor, node) { const [offset, length] = node; // write offset and length to buffer const nodeBuf = new DataView(new ArrayBuffer(NODE_SIZE)); _writeUInt48LE(nodeBuf, offset); nodeBuf.setUint32(6, length, true); // write buffer to file at directory offset await this.writer.write(new Uint8Array(nodeBuf.buffer), cursor); } } /** * write a 32 bit and a 16 bit * @param data - the data to write to * @param num - the number * @param offset - the offset to write at */ function _writeUInt48LE(data, num, offset = 0) { const lower = num & 0xffff; const upper = num / (1 << 16); data.setUint16(offset, lower, true); data.setUint32(offset + 2, upper, true); } /** * 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); } /** * Build a directory size relative to maxzoom * @param depth - the depth * @param maxzoom - the maxzoom * @returns - the directory size */ function _buildDirSize(depth, maxzoom) { const { min, pow } = Math; let dirSize = 0; // grab the remainder let remainder = min(maxzoom - depth, 5); // must be increments of 5, so if level 4 then inc is 0 but if 5, inc is 5 // for each remainder (including 0), we add a quadrant do { dirSize += pow(1 << remainder, 2); } while (remainder-- !== 0); return dirSize * NODE_SIZE; } /** * 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 */ function getPath(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; }); } /** * Compresses a Uint8Array if a compression method is specified * @param input - the input Uint8Array * @param compression - the compression * @returns - the compressed Uint8Array or the original if compression is None */ async function compress(input, compression) { if (compression === Compression.None) return input; else if (compression === Compression.Gzip) return await compressStream(input); else throw new Error(`Unsupported compression: ${compression}`); } //# sourceMappingURL=index.js.map