gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
250 lines • 10.1 kB
JavaScript
import { toMetadata } from 's2-tilejson';
import { RasterS2TileReader, RasterTileReader, imageDecoder, llToPX, lonLatToXYZ, pointToST, pxToTile, tileXYFromSTZoom, } from '../../index.js';
import { readFile, readdir, stat } from 'fs/promises';
// TODO: Get encoding from the metadata and decode the data if necessary
/**
* # Raster Tiles File Reader
*
* ## Description
* Read an entire archive of raster tiles, where the max zoom data is iterated upon
*
* Supports reading either RGB(A) data and/or RGB(A) encoded elevation data.
*
* ## Usage
* ```ts
* import { convertTerrariumElevationData } from 'gis-tools-ts';
* import { RasterTilesFileReader } from 'gis-tools-ts/file';
*
* // creates a reader for a tile set treating the max zoom as 3 instead of the metadata's max zoom
* const reader = new RasterTilesFileReader('./raster-tiles-top-level-folder', 3);
* // example of reading in an elevation dataset
* const reader2 = new RasterTilesFileReader('./terrariumData', -1, convertTerrariumElevationData);
*
* // grab the metadata
* const metadata = await reader.getMetadata();
*
* // grab a WM tile
* const tile1 = await reader.getTile(0, 0, 0);
* // or if it's an S2 tile spec
* const tile2 = await reader.getTileS2(0, 0, 0, 0);
*
* // get a specfic WM value given a longitude and latitude
* const value = await reader.getLonLatValuesWM(0, 0, 0);
* // get a specfic S2 value given a longitude and latitude
* const value2 = await reader.getLonLatValuesS2(0, 0, 0);
*
* // grab all the max zoom tiles:
* for await (const tile of reader) {
* console.log(tile);
* }
* ```
*
* ## Links
* - https://satakagi.github.io/mapsForWebWS2020-docs/QuadTreeCompositeTilingAndVectorTileStandard.html
* - https://cesium.com/blog/2015/04/07/quadtree-cheatseet/
*/
export class RasterTilesFileReader {
input;
threshold;
converter;
metadata;
/**
* @param input - the file path or S2PMTilesReader to read from
* @param threshold - if non-zero its the max zoom to read all tiles in the FeatureIterator
* @param converter - the elevation converter
*/
constructor(input, threshold = -1, converter) {
this.input = input;
this.threshold = threshold;
this.converter = converter;
}
/**
* Get the metadata of the archive
* @returns - the metadata
*/
async getMetadata() {
if (this.metadata !== undefined)
return this.metadata;
if (typeof this.input === 'string') {
const file = await readFile(`${this.input}/metadata.json`, { encoding: 'utf-8' });
this.metadata = toMetadata(JSON.parse(file));
}
else {
this.metadata = await this.input.getMetadata();
}
return this.metadata;
}
/**
* Grab 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 tile
*/
async getTileWM(zoom, x, y) {
const { extension, scheme } = await this.getMetadata();
const isTMS = scheme === 'tms';
const data = typeof this.input === 'string'
? await readFile(`${this.input}/${zoom}/${x}/${y}.${extension}`)
: await this.input.getTile(zoom, x, y);
if (data === undefined)
return undefined;
const imageData = await imageDecoder(data, { modulo: 256 });
return new RasterTileReader(zoom, x, y, imageData, isTMS, this.converter);
}
/**
* Grab 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 tile
*/
async getTileS2(face, zoom, x, y) {
const { extension } = await this.getMetadata();
const data = typeof this.input === 'string'
? await readFile(`${this.input}/${face}/${zoom}/${x}/${y}.${extension}`)
: await this.input.getTileS2(face, zoom, x, y);
if (data === undefined)
return undefined;
const imageData = await imageDecoder(data, { modulo: 256 });
return new RasterS2TileReader(face, zoom, x, y, imageData, this.converter);
}
/**
* Return true if the tile exists
* @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
*/
async hasTileWM(zoom, x, y) {
const { extension } = await this.getMetadata();
if (typeof this.input === 'string') {
const stats = await stat(`${this.input}/${zoom}/${x}/${y}.${extension}`);
return stats.isFile();
}
else {
return await this.input.hasTile(zoom, x, y);
}
}
/**
* Return true if the tile exists
* @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
*/
async hasTileS2(face, zoom, x, y) {
const { extension } = await this.getMetadata();
if (typeof this.input === 'string') {
const stats = await stat(`${this.input}/${face}/${zoom}/${x}/${y}.${extension}`);
return stats.isFile();
}
else {
return await this.input.hasTileS2(face, zoom, x, y);
}
}
/**
* Get the value of the given longitude and latitude
* @param zoom - the zoom level
* @param lon - the longitude
* @param lat - the latitude
* @param tileSize - in pixels
* @returns - the value at the given longitude and latitude
*/
async getLonLatValuesWM(zoom, lon, lat, tileSize = 512) {
const { floor } = Math;
const mod = (n, m) => ((n % m) + m) % m;
// get the tile coordinates
const { x, y } = llToPX({ x: lon, y: lat }, zoom, false, tileSize);
const { x: tileX, y: tileY } = pxToTile({ x, y }, tileSize);
// get the tile
const tile = await this.getTileWM(zoom, tileX, tileY);
if (tile === undefined)
return undefined;
// get the pixel
const localX = mod(x, tileSize);
const localY = mod(y, tileSize);
const pixelX = floor(localX);
// If TMS style, invert the y position
const pixelY = tile.tmsStyle ? floor(tileSize - 1 - localY) : floor(localY);
const channels = tile.image.data.length / (tileSize * tileSize);
const position = (pixelY * tileSize + pixelX) * channels;
const r = tile.image.data[position];
const g = tile.image.data[position + 1];
const b = tile.image.data[position + 2];
const a = channels >= 4 ? tile.image.data[position + 3] : 255;
// set to the elevation or RGBA
if (this.converter !== undefined) {
return { elev: this.converter(r, g, b, a) };
}
else {
return { r, g, b, a };
}
}
/**
* Get the value of the given longitude and latitude
* @param zoom - the zoom level
* @param lon - the longitude
* @param lat - the latitude
* @param tileSize - in pixels
* @returns - the value at the given longitude and latitude
*/
async getLonLatValuesS2(zoom, lon, lat, tileSize = 512) {
const { floor } = Math;
const mod = (n, m) => ((n % m) + m) % m;
// get the tile coordinates
const xyz = lonLatToXYZ({ x: lon, y: lat });
const [face, s, t] = pointToST(xyz);
const [tileX, tileY] = tileXYFromSTZoom(s, t, zoom);
// get the tile
const tile = await this.getTileS2(face, zoom, tileX, tileY);
if (tile === undefined)
return undefined;
// get the pixel
const zoomSize = tileSize * (1 << zoom);
const pixelX = floor(mod(zoomSize * s, tileSize));
const pixelY = floor(mod(zoomSize * t, tileSize));
const channels = tile.image.data.length / (tileSize * tileSize);
const position = (pixelY * tileSize + pixelX) * channels;
const r = tile.image.data[position];
const g = tile.image.data[position + 1];
const b = tile.image.data[position + 2];
const a = channels >= 4 ? tile.image.data[position + 3] : 255;
if (this.converter !== undefined) {
return { elev: this.converter(r, g, b, a) };
}
else {
return { r, g, b, a };
}
}
/**
* Iterate over all tiles in the archive
* @yields {S2Feature<S2TileMetadata, T, P> | VectorFeature<TileMetadata, T, P>} the each of the
* tile's pixel RGBA data as lon-lat or S2 s-t coordinates with the RGBA as m-values
*/
async *[Symbol.asyncIterator]() {
// iterate down from min zoom. Upon reaching maxzoom store all pixels
const { scheme, maxzoom } = await this.getMetadata();
const zoom = this.threshold >= 0 ? this.threshold : maxzoom;
const isS2 = scheme === 'fzxy' || scheme === 'tfzxy';
for (const face of (isS2 ? [0, 1, 2, 3, 4, 5] : [0])) {
const xPath = isS2 ? `${this.input}/${face}/${zoom}` : `${this.input}/${zoom}`;
for (const x of await readdir(xPath)) {
const yPath = `${xPath}/${x}`;
const xNumber = Number(x);
for (const y of await readdir(yPath)) {
const yNumber = Number(y.split('.')[0]);
const tile = isS2
? await this.getTileS2(face, zoom, xNumber, yNumber)
: await this.getTileWM(zoom, xNumber, yNumber);
if (tile === undefined)
continue;
yield* tile;
}
}
}
}
}
//# sourceMappingURL=file.js.map