s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
330 lines • 12.2 kB
JavaScript
import { bboxST } from '../../geometry/s2/coords';
import { imageDecoder } from '../image';
import { toMetadata } from 's2-tilejson';
import { xyzToBBOX } from '../../geometry/wm/coords';
/**
* @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 `s2-tools/file` instead for local access.
*
* ## Usage
* ```ts
* import { RasterTilesReader, convertTerrariumElevationData } from 's2-tools';
*
* // 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);
*
* // grab all the max zoom tiles:
* for await (const tile of reader) {
* console.log(tile);
* }
* ```
*/
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 getTile(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 hasTile(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);
}
}
/**
* Iterate over all tiles in the archive
* @yields - 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.hasTile(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.getTile(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 - 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, 'WGS84', tileSize);
const lonStep = (east - west) / tileSize;
const latStep = (north - south) / tileSize;
const coordinates = [];
for (let py = 1; py <= tileSize; py++) {
const lat = north - (py - 0.5) * latStep; // Center of the row
for (let px = 1; px <= tileSize; px++) {
const lon = west + (px - 0.5) * lonStep; // Center of the column
const index = ((py - 1) * tileSize + (px - 1)) * 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: 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 - 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 [west, south, east, north] = bboxST(x, y, zoom);
const lonStep = (east - west) / tileSize;
const latStep = (north - south) / tileSize;
const coordinates = [];
for (let py = 1; py <= tileSize; py++) {
const lat = north - (py - 0.5) * latStep; // Center of the row
for (let px = 1; px <= tileSize; px++) {
const lon = west + (px - 0.5) * lonStep; // Center of the column
const index = ((py - 1) * tileSize + (px - 1)) * 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: lon, y: lat, m: m });
}
}
yield {
type: 'S2Feature',
face,
geometry: {
type: 'MultiPoint',
coordinates,
is3D: false,
},
properties: {},
metadata: { face, zoom, x, y },
};
}
}
//# sourceMappingURL=index.js.map