terrain/GeoTiffTerrain.tstypescript
import { ProceduralTerrain } from "./ProceduralTerrain";
import type { IProceduralTerrainCommon, ProceduralHeights } from "./ProceduralTerrain";
import { TERRAIN_GRID_SIZE_BY_ZOOM } from "./terrainUtils";
import { GeoTIFFReader } from "../layer/geotiff/GeoTIFFReader";
import { MultiGeoTIFFReader } from "../layer/geotiff/MultiGeoTIFFReader";
import type { IGeoTIFFSourceCommon, IGeoTIFFSourceItem } from "../layer/geotiff/types";
import type { Extent } from "../Extent";
import type { LonLat } from "../LonLat";
import type { Segment } from "../segment/Segment";
export interface IGeoTiffTerrainParams extends IProceduralTerrainCommon, IGeoTIFFSourceCommon {
sources?: IGeoTIFFSourceItem[];
sampleSize?: number;
band?: number;
}
/**
* Terrain with the heights of a GeoTIFF or COG elevation band.
* @class
* @extends {ProceduralTerrain}
* @param {string} [name="GeoTiffTerrain"] - Terrain provider name.
* @param {IGeoTiffTerrainParams} options - Provider options:
* @param {string | Blob | File | ArrayBuffer} [options.src] - GeoTIFF url or binary.
* @param {Array.<IGeoTIFFSourceItem>} [options.sources] - GeoTIFF sources of one mosaic instead of src. Where they
* overlap the heights of the first one are taken, so the more detailed ones go first.
* @param {number} [options.sampleSize=256] - Pixels by the segment side read from the GeoTIFF, power of two,
* not less than plainGridSize. The GeoTIFF overview is taken by them.
* @param {number} [options.band=1] - Elevation band.
* @param {string} [options.attribution=""] - Elevation source attribution shown in the attribution area.
* @param {number} [options.plainGridSize=128] - Elevation grid size.
* @param {number} [options.maxZoom=21] - Last zoom of the segment heights, the last zoom of the default grid sizes.
* @param {number} [options.maxNativeZoom] - Last zoom of the GeoTIFF reads, by default, the first zoom which
* sampleSize pixels are not larger than the GeoTIFF ones, of the most detailed source in a mosaic. The tile size is
* the one of the planet quad tree strategy. Deeper segments are its subdivision levels.
* @param {string} [options.geoidSrc="https://openglobus.org/geoid/egm84-30.pgm"] - Geoid of the heights.
* @param {Array.<number>} [options.noDataValues=[-10000]] - Heights of no data, GeoTIFF nodata pixels
* become the first one.
* @param {number | null} [options.nodata] - GeoTIFF nodata value, from the GeoTIFF by default.
* @param {number | string} [options.crs] - GeoTIFF CRS code, when the GeoTIFF has another one or none.
*/
class GeoTiffTerrain extends ProceduralTerrain {
protected _reader: GeoTIFFReader | MultiGeoTIFFReader;
protected _sampleSize: number;
protected _samples: number[];
protected _sourceReady: boolean;
protected _readyPromise: Promise<void>;
// The native zoom is found from the sources, and the root tile width it was found for
protected _autoNativeZoom: boolean;
protected _nativeZoomRootTileWidth: number;
constructor(name: string | null, options: IGeoTiffTerrainParams) {
super(name || "GeoTiffTerrain", {
plainGridSize: 128,
maxZoom: TERRAIN_GRID_SIZE_BY_ZOOM.length - 1,
geoidSrc: "https://openglobus.org/geoid/egm84-30.pgm",
noDataValues: [-10000],
...options,
getHeights: (segment) => this._readHeights(segment)
});
this._sampleSize = options.sampleSize || 256;
this._samples = [(options.band || 1) - 1];
this._sourceReady = false;
this._autoNativeZoom = options.maxNativeZoom == undefined;
this._nativeZoomRootTileWidth = NaN;
let init: Promise<unknown>;
if (options.sources && options.sources.length) {
const reader = new MultiGeoTIFFReader({ ...options, mosaic: true });
this._reader = reader;
init = reader.init(options.sources);
} else {
const reader = new GeoTIFFReader(options);
this._reader = reader;
init = reader.init(options.src!);
}
this._readyPromise = init
.then(() => this._updateNativeZoom())
.catch((err) => console.error(err))
.then(() => {
this._sourceReady = true;
});
}
public get reader(): GeoTIFFReader | MultiGeoTIFFReader {
return this._reader;
}
public whenReady(): Promise<void> {
return this._readyPromise;
}
public override getExtent(): Extent {
return this._reader.extentWgs84;
}
/**
* Flies over the highest point of the GeoTIFF: the heights of the extent center may be not loaded or no data.
* @public
*/
public override flyExtent() {
this._readyPromise.then(() => {
if (this._reader.isReady) {
this._planet?.flyExtent(this.getExtent(), this._reader.metadata?.bands[this._samples[0] + 1]?.max);
}
});
}
// The planet comes after the sources, so the native zoom is found again for the tile size of its strategy
protected _updateNativeZoom() {
const width = this._planet ? this._planet.quadTreeStrategy.rootTileWidth : 360;
if (this._autoNativeZoom && this._reader.isReady && width !== this._nativeZoomRootTileWidth) {
this._nativeZoomRootTileWidth = width;
this.maxNativeZoom = Math.min(this._reader.getNativeZoom(width, this._sampleSize), this.maxZoom);
}
}
public override loadTerrain(segment: Segment) {
if (!this._sourceReady) {
segment.terrainIsLoading = false;
return;
}
this._updateNativeZoom();
super.loadTerrain(segment);
}
public override getHeightAsync(lonLat: LonLat, callback: (h: number) => void, zoom?: number): boolean {
if (this._sourceReady) {
this._updateNativeZoom();
return super.getHeightAsync(lonLat, callback, zoom);
}
this._readyPromise.then(() => super.getHeightAsync(lonLat, callback, zoom));
return false;
}
protected _readHeights(segment: Segment): ProceduralHeights | Promise<ProceduralHeights> {
let extent = segment.getExtentLonLat();
if (!this._reader.extentWgs84.overlaps(extent)) {
return null;
}
return this._reader
.readTileRasters(segment, this._sampleSize, this._samples)
.then((data) => data && data.rasters[0]);
}
}
export { GeoTiffTerrain };