gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
415 lines • 15.9 kB
JavaScript
import { toMetadata } from 's2-tilejson';
import { bboxST, imageDecoder, llToPX, lonLatToXYZ, mercToLL, pointToST, pxToTile, tileXYFromSTZoom, xyzToBBOX, } from '../../index.js';
/**
* @param r - red
* @param g - green
* @param b - blue
* @returns - elevation
*/
export function convertTerrariumElevationData(r, g, b) {
return r * 256.0 + g + b / 256.0 - 32768.0;
}
/**
* @param r - red
* @param g - green
* @param b - blue
* @returns - elevation
*/
export function convertMapboxElevationData(r, g, b) {
return -10000 + (r * 256 * 256 + g * 256 + b) * 0.1;
}
/**
* # Raster Tiles 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.
*
* NOTE: Consider using the `RasterTilesFileReader` from `gis-tools-ts/file` instead for local access.
*
* ## Usage
* ```ts
* import { RasterTilesReader, convertTerrariumElevationData } from 'gis-tools-ts';
*
* // creates a reader for a tile set treating the max zoom as 3 instead of the metadata's max zoom
* const reader = new RasterTilesReader('https://example.com/satellite-data', 3);
* // example of reading in an elevation dataset
* const reader2 = new RasterTilesReader('https://example.com/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 RasterTilesReader {
input;
threshold;
converter;
metadata;
/**
* @param input - the URL 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 meta = await fetch(`${this.input}/metadata.json`).then(async (res) => (await res.json()));
this.metadata = toMetadata(meta);
}
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 fetch(`${this.input}/${zoom}/${x}/${y}.${extension}`).then(async (res) => await res.arrayBuffer())
: 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 fetch(`${this.input}/${face}/${zoom}/${x}/${y}.${extension}`).then(async (res) => await res.arrayBuffer())
: 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 response = await fetch(`${this.input}/${zoom}/${x}/${y}.${extension}`, {
method: 'HEAD',
});
return response.ok;
}
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 response = await fetch(`${this.input}/${face}/${zoom}/${x}/${y}.${extension}`, {
method: 'HEAD',
});
return response.ok;
}
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, Properties> | VectorFeature<TileMetadata, T, Properties>}
* 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, minzoom, maxzoom } = await this.getMetadata();
const threshold = 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 stack = [[0, 0, 0]];
while (stack.length > 0) {
const [zoom, x, y] = stack.pop();
// if zoom not reached yet, push children and continue
const hasTile = isS2
? await this.hasTileS2(face, zoom, x, y)
: await this.hasTileWM(zoom, x, y);
if (zoom < minzoom || (zoom !== threshold && hasTile)) {
stack.push([zoom + 1, x * 2, y * 2], [zoom + 1, x * 2 + 1, y * 2], [zoom + 1, x * 2, y * 2 + 1], [zoom + 1, x * 2 + 1, y * 2 + 1]);
continue;
}
else if (zoom === threshold) {
const tile = isS2
? await this.getTileS2(face, zoom, x, y)
: await this.getTileWM(zoom, x, y);
if (tile === undefined)
continue;
yield* tile;
}
}
}
}
}
/**
* Raster Tile Reader
*/
export class RasterTileReader {
zoom;
x;
y;
image;
tmsStyle;
converter;
/**
* @param zoom - the zoom level of the tile
* @param x - the x coordinate of the tile
* @param y - the y coordinate of the tile
* @param image - the raw RGB(A) image data
* @param tmsStyle - if true, the y is inverted
* @param converter - the elevation converter (if provided its not an RGBA image but rather elevation data)
*/
constructor(zoom, x, y, image, tmsStyle = false, converter) {
this.zoom = zoom;
this.x = x;
this.y = y;
this.image = image;
this.tmsStyle = tmsStyle;
this.converter = converter;
}
/**
* Iterate over all tiles in the archive
* @yields {VectorFeature<TileMetadata, T, P>} the each of the tile's pixel RGBA data as lon-lat
* coordinates with the RGBA as m-values
*/
async *[Symbol.asyncIterator]() {
const { zoom, x, y, image, tmsStyle } = this;
const { width: tileSize, data } = image;
const channels = data.length / (tileSize * tileSize);
// Get the bounding box of the tile in lon-lat
const [west, south, east, north] = xyzToBBOX(x, y, zoom, tmsStyle, '900913', tileSize);
const xStep = (east - west) / tileSize;
const yStep = (north - south) / tileSize;
const coordinates = [];
for (let py = 0; py < tileSize; py++) {
const yPos = north - (py + 0.5) * yStep; // Center of the row
for (let px = 0; px < tileSize; px++) {
const xPos = west + (px + 0.5) * xStep; // Center of the column
const index = (py * tileSize + px) * channels;
const { x: lon, y: lat } = mercToLL({ x: xPos, y: yPos });
const m = this.converter !== undefined
? { elev: this.converter(data[index], data[index + 1], data[index + 2]) }
: {
r: data[index],
g: data[index + 1],
b: data[index + 2],
a: channels === 4 ? data[index + 3] : 255,
};
coordinates.push({ x: lon, y: lat, m: m });
}
}
yield {
type: 'VectorFeature',
geometry: {
type: 'MultiPoint',
coordinates,
is3D: false,
},
properties: {},
metadata: { zoom, x, y },
};
}
}
/**
* S2 Raster Tile Reader
*/
export class RasterS2TileReader {
face;
zoom;
x;
y;
image;
converter;
/**
* @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
* @param image - the raw image RGB(A) data
* @param converter - the elevation converter (if provided its not an RGBA image but rather elevation data)
*/
constructor(face, zoom, x, y, image, converter) {
this.face = face;
this.zoom = zoom;
this.x = x;
this.y = y;
this.image = image;
this.converter = converter;
}
/**
* Iterate over all tiles in the archive
* @yields {S2Feature<S2TileMetadata, T, P>} The each of the tile's pixel RGBA data as S2 s-t
* coordinates with the RGBA as m-values
*/
async *[Symbol.asyncIterator]() {
const { face, zoom, x, y, image } = this;
const { width: tileSize, data } = image;
const channels = data.length / (tileSize * tileSize);
// Get the bounding box of the tile in s-t space
const [minS, minT, maxS, maxT] = bboxST(x, y, zoom);
const sStep = (maxS - minS) / tileSize;
const tStep = (maxT - minT) / tileSize;
const coordinates = [];
for (let py = 0; py < tileSize; py++) {
const y = minS + (py + 0.5) * tStep; // Center of the row
for (let px = 0; px < tileSize; px++) {
const x = minT + (px + 0.5) * sStep; // Center of the column
const index = (py * tileSize + px) * channels;
const m = this.converter !== undefined
? { elev: this.converter(data[index], data[index + 1], data[index + 2]) }
: {
r: data[index],
g: data[index + 1],
b: data[index + 2],
a: channels === 4 ? data[index + 3] : 255,
};
coordinates.push({ x, y, m: m });
}
}
yield {
type: 'S2Feature',
face,
geometry: {
type: 'MultiPoint',
coordinates,
is3D: false,
},
properties: {},
metadata: { face, zoom, x, y },
};
}
}
//# sourceMappingURL=index.js.map