ol-elevation-parser
Version:
Sample geometries and retrieve parsed elevation data from Open Layers sources
799 lines (781 loc) • 28.2 kB
JavaScript
/*!
* ol-elevation-parser - v1.3.19
* https://github.com/GastonZalba/ol-elevation-parser#readme
* Built: Mon Jul 21 2025 13:58:32 GMT-0300 (hora estándar de Argentina)
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('ol/geom/LineString.js'), require('ol/geom/Point.js'), require('ol/geom/Polygon.js'), require('ol/control/Control.js'), require('ol/source/TileWMS.js'), require('@turf/bbox'), require('@turf/area'), require('@turf/intersect'), require('@turf/helpers'), require('@turf/square-grid'), require('ol/format/GeoJSON.js'), require('ol/tilegrid.js'), require('ol/tilegrid/TileGrid.js'), require('ol/source/XYZ.js'), require('ol/DataTile.js'), require('ol/ImageTile.js')) :
typeof define === 'function' && define.amd ? define(['ol/geom/LineString.js', 'ol/geom/Point.js', 'ol/geom/Polygon.js', 'ol/control/Control.js', 'ol/source/TileWMS.js', '@turf/bbox', '@turf/area', '@turf/intersect', '@turf/helpers', '@turf/square-grid', 'ol/format/GeoJSON.js', 'ol/tilegrid.js', 'ol/tilegrid/TileGrid.js', 'ol/source/XYZ.js', 'ol/DataTile.js', 'ol/ImageTile.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ElevationParser = factory(global.ol.geom.LineString, global.ol.geom.Point, global.ol.geom.Polygon, global.ol.control.Control, global.ol.source.TileWMS, global["@turf/bbox"], global["@turf/area"], global["@turf/intersect"], global["@turf/helpers"], global["@turf/square-grid"], global.ol.format.GeoJSON, global.ol.tilegrid, global.ol.tilegrid.TileGrid, global.ol.source.XYZ, global.ol.DataTile, global.ol.ImageTile));
})(this, (function (LineString, Point, Polygon, Control, TileWMS, bbox, area, intersect, helpers, squareGrid, GeoJSON, tilegrid_js, TileGrid, XYZ, DataTile, ImageTile) { 'use strict';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function smooth(arr, windowSize, getter = (value) => value, setter) {
const get = getter;
const result = [];
for (let i = 0; i < arr.length; i += 1) {
const leftOffeset = i - windowSize;
const from = leftOffeset >= 0 ? leftOffeset : 0;
const to = i + windowSize + 1;
let count = 0;
let sum = 0;
for (let j = from; j < to && j < arr.length; j += 1) {
sum += get(arr[j]);
count += 1;
}
result[i] = setter ? setter(arr[i], sum / count) : sum / count;
}
return result
}
var lib = smooth;
var smooth$1 = /*@__PURE__*/getDefaultExportFromCjs(lib);
// @turf
const geojson = new GeoJSON();
/**
*
* @param target
* @param sources
* @returns
*/
const deepObjectAssign = (target, ...sources) => {
sources.forEach((source) => {
Object.keys(source).forEach((key) => {
const s_val = source[key];
const t_val = target[key];
target[key] =
t_val &&
s_val &&
typeof t_val === 'object' &&
typeof s_val === 'object' &&
!Array.isArray(t_val) // Don't merge arrays
? deepObjectAssign(t_val, s_val)
: s_val;
});
});
return target;
};
const getLineSamples = (geom, nSamples) => {
const totalLength = geom.getLength();
if (typeof nSamples === 'function') {
nSamples = nSamples(totalLength);
}
const stepPercentage = 100 / nSamples;
const metersSample = totalLength * (stepPercentage / 100);
const sampledCoords = [];
let segmentCount = 0;
// Get samples every percentage step while conserving all the vertex
geom.forEachSegment((start, end) => {
// Only get the first start segment
if (!segmentCount) {
sampledCoords.push([start[0], start[1]]);
}
segmentCount++;
const segmentGeom = new LineString([
[start[0], start[1]],
[end[0], end[1]]
]);
const segmentLength = segmentGeom.getLength();
/**
* segmentLength -> 100
* metersSample -> x
*/
const newPercentage = (100 * metersSample) / segmentLength;
// skip 0 and 100
let segmentStepPercent = newPercentage;
while (segmentStepPercent < 100) {
const coordAt = segmentGeom.getCoordinateAt(segmentStepPercent / 100);
sampledCoords.push(coordAt);
segmentStepPercent = segmentStepPercent + newPercentage;
}
sampledCoords.push([end[0], end[1]]);
});
return sampledCoords;
};
/**
* @param polygonFeature
* @param nSamples
* @returns
*/
const getPolygonSamples = (polygonFeature, projection, nSamples) => {
const polygon = geojson.writeFeatureObject(polygonFeature, {
dataProjection: 'EPSG:4326',
featureProjection: projection
});
const areaPol = area(polygon.geometry);
let sampleMeters;
if (nSamples !== 'auto') {
if (typeof nSamples === 'number') {
sampleMeters = nSamples;
}
else if (typeof nSamples === 'function') {
sampleMeters = nSamples(areaPol);
}
}
else {
if (areaPol <= 1000)
sampleMeters = 0.5;
else if (areaPol < 10000)
sampleMeters = 1;
else if (areaPol < 100000)
sampleMeters = 10;
else if (areaPol < 1000000)
sampleMeters = 50;
else
sampleMeters = 100;
}
const polygonBbox = bbox(polygon);
const grid = squareGrid(polygonBbox, sampleMeters / 1000, {
units: 'kilometers',
mask: polygon.geometry
});
let clippedGrid = grid.features.map((feature) => intersect(feature.geometry, polygon));
// Remove some random null values
clippedGrid = clippedGrid.filter((feature) => feature);
const clippedGridF = helpers.featureCollection(clippedGrid);
return geojson.readFeatures(clippedGridF, {
dataProjection: 'EPSG:4326',
featureProjection: projection
});
};
/**
*
* @param coordsWithZ
* @param smoothValue
* @returns
*/
const getSmoothedCoords = (coordsWithZ, smoothValue = 0) => {
coordsWithZ = [...coordsWithZ];
const zCoords = coordsWithZ.map((coord) => coord[2]);
const zSmooth = smooth$1(zCoords, smoothValue);
return coordsWithZ.map((coord, i) => {
coord[2] = zSmooth[i];
return coord;
});
};
let loggerIsEnabled = false;
const setLoggerActive = (bool) => {
loggerIsEnabled = bool;
};
function logger(...args) {
if (loggerIsEnabled)
console.log(...args);
}
const options = {
source: null,
calculateZMethod: 'getFeatureInfo',
tilesResolution: 'current',
bands: 4,
samples: 50,
smooth: 0,
sampleSizeArea: 'auto',
noDataValue: -10000,
timeout: 5000,
verbose: loggerIsEnabled
};
const mapboxExtractElevation = (r, g, b) => {
return (r * 256 * 256 + g * 256 + b) * 0.1 - 10000;
};
const terrariumExtractElevation = (r, g, b) => {
return r * 256 + g + b / 256 - 32768;
};
class ReadFromImage {
constructor(source, calculateZMethod, bands, map) {
this._projection =
source.getProjection() || map.getView().getProjection();
this._view = map.getView();
this._source = source;
this._bands = bands;
this._calculateZMethod = calculateZMethod;
this._canvas = document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
}
async read(coordinate, resolution) {
// clear canvas
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
const tileGrid = this._getTileGrid();
const tileCoord = tileGrid.getTileCoordForCoordAndResolution(coordinate, resolution);
const zoom = tileCoord[0];
const tileSize = tileGrid.getTileSize(zoom);
const tile = this._source.getTile(tileCoord[0], tileCoord[1], tileCoord[2], 1, this._projection);
if (tile.getState() !== 2) {
await new Promise((resolve) => {
const changeListener = () => {
if (tile.getState() === 2) {
// loaded
tile.removeEventListener('change', changeListener);
resolve(null);
}
else if (tile.getState() === 3) {
// error
resolve(null);
}
};
tile.addEventListener('change', changeListener);
tile.load();
});
}
let tileData;
if (tile instanceof DataTile) {
tileData = tile.getData();
}
else if (tile instanceof ImageTile) {
tileData = tile.getImage();
}
if (!tileData)
return;
//@ts-ignore
// sometimes tilesize is wrong, so use tileData if exists
const width = tileData.width || tileSize[0] || tileSize;
//@ts-ignore
const height = tileData.height || tileSize[1] || tileSize;
this._canvas.width = width;
this._canvas.height = height;
//@ts-expect-error
this._ctx.mozImageSmoothingEnabled = false;
//@ts-expect-error
this._ctx.oImageSmoothingEnabled = false;
//@ts-expect-error
this._ctx.webkitImageSmoothingEnabled = false;
//@ts-expect-error
this._ctx.msImageSmoothingEnabled = false;
this._ctx.imageSmoothingEnabled = false;
let imageData;
if (tileData instanceof HTMLImageElement) {
// Add image to a canvas
this._ctx.drawImage(tileData, 0, 0);
imageData = this._ctx.getImageData(0, 0, width, height);
}
else {
// GeoTIFF
imageData = this._ctx.createImageData(width, height);
//@ts-expect-error
imageData.data.set(tileData);
}
const origin = tileGrid.getOrigin(zoom);
const res = tileGrid.getResolution(zoom);
const w = Math.floor(((coordinate[0] - origin[0]) / res) % width);
const h = Math.floor(((origin[1] - coordinate[1]) / res) % height);
const imgData = imageData.data;
const index = (w + h * width) * this._bands;
const pixel = [
imgData[index + 0],
imgData[index + 1],
imgData[index + 2],
imgData[index + 3]
];
return this._extractValuesFromPixelDEM(pixel);
}
/**
* Get the Max Resolution of the source
* @returns
*/
getMaxResolution() {
const zoom = this._getTileGrid().getMaxZoom();
if (zoom)
return this._getTileGrid().getResolution(zoom);
return null;
}
/**
* Check if this is now necesary
* @returns
*/
_getTileGrid() {
let tilegrid = this._source.getTileGrid();
// If not tileGrid is provided, set a default for XYZ sources
if (!tilegrid) {
if (this._source instanceof XYZ) {
const defaultTileGrid = tilegrid_js.createXYZ();
tilegrid = new TileGrid({
origin: defaultTileGrid.getOrigin(0),
resolutions: defaultTileGrid.getResolutions()
});
}
else {
tilegrid = tilegrid_js.getForProjection(this._projection);
}
}
return tilegrid;
}
/**
* @param pixel
* @returns
*/
_extractValuesFromPixelDEM(pixel) {
if (this._calculateZMethod &&
typeof this._calculateZMethod === 'function') {
return this._calculateZMethod(pixel[0], pixel[1], pixel[2]);
}
else if (this._calculateZMethod === 'Mapbox') {
return mapboxExtractElevation(pixel[0], pixel[1], pixel[2]);
}
else if (this._calculateZMethod === 'Terrarium') {
return terrariumExtractElevation(pixel[0], pixel[1], pixel[2]);
}
}
}
/**
* @extends {ol/control/Control~Control}
* @fires change:samples
* @fires change:sampleSizeArea
* @fires change:source
* @fires change:calculateZMethod
* @fires change:noDataValue
* @fires change:smooth
* @fires change:tilesResolution
* @fires change:bands
* @param options
*/
class ElevationParser extends Control {
constructor(options$1) {
super({
element: document.createElement('div')
});
this._countConnections = 0;
this._rasterSourceIsLoaded = false;
this._initialized = false;
/**
*
* @param coords
* @param optOptions To overwrite the general ones
* @returns
* @private
*/
this._getZFromSampledCoords = async (coords, optOptions = null) => {
const RESOLUTION_NUMBER_FALLBACK = 0.01;
this._countConnections++;
const countConnections = this._countConnections;
let errorCount = 0;
const coordsWithZ = [];
const source = this.get('source');
// Flexible error trigger if multiples coords must be requested.
// If only one coord is needed, the error is strict and raised inmediatly
// This is useful if multipels coords are needed, and maybe one or two return error
const countErrorsLimit = coords.length >= 5 ? 1 : 5;
let resolutionNumber;
const _resolution = (optOptions === null || optOptions === void 0 ? void 0 : optOptions.tilesResolution) || this.getTilesResolution();
if (_resolution === 'current') {
resolutionNumber = this.getMap().getView().getResolution();
// if the view of a GeoTIFF is used in the map
if (!resolutionNumber) {
console.warn('Cannot calculate current view resolution');
}
}
else if (_resolution === 'max') {
const maxRes = this.getMaxTilesResolution();
if (maxRes)
resolutionNumber = maxRes;
else
console.warn("Cannot calculate source's max resolution");
}
else {
// resolution is a explicit number provided in the config
resolutionNumber = _resolution;
}
if (!resolutionNumber) {
resolutionNumber =
this.getMap().getView().getMinResolution() ||
RESOLUTION_NUMBER_FALLBACK;
console.warn('Using fallback resolution:', resolutionNumber);
}
for (const coord of coords) {
try {
// If there is a new connection (onChange event), abort this
if (this._countConnections !== countConnections) {
logger('New geometry detected, previous requests aborted');
return null;
}
let zValue;
if (source instanceof TileWMS &&
this.get('calculateZMethod') === 'getFeatureInfo') {
zValue = await this._getZValuesFromWMS(coord, source, this.getMap().getView());
}
else {
zValue = await this._getZValuesFromImage(coord, resolutionNumber);
}
if (this.get('noDataValue') !== false) {
zValue =
zValue === this.get('noDataValue') ? undefined : zValue;
}
// If null or undefined value is returned, transform to 0
const zValueRound = typeof zValue !== 'undefined'
? Number(zValue.toFixed(3))
: undefined;
coordsWithZ.push([...coord, zValueRound]);
}
catch (err) {
errorCount++;
console.error(err);
if (errorCount >= countErrorsLimit) {
throw err;
}
}
}
return coordsWithZ;
};
this._options = deepObjectAssign({}, options, options$1);
// Change the default 'getFeatureInfo' method if the source is not TileWMS
if (!(this._options.source instanceof TileWMS) &&
this._options.calculateZMethod === 'getFeatureInfo') {
this._options.calculateZMethod = 'Mapbox';
}
setLoggerActive(this._options.verbose);
}
/**
* Get Feature's elevation values.
* Use custom options to overwrite the general ones for specific cases
*
* @param feature
* @param customOptions
* @returns
* @public
*/
async getElevationValues(feature, customOptions = null) {
try {
const waitUntilRasterSourceIsLoaded = () => new Promise((resolve, reject) => {
const isSourceReady = (retryNum = 0, maxRetries = 5, waitMilliseconds = 500) => {
if (source.getState() !== 'ready') {
retryNum++;
if (retryNum > maxRetries) {
reject();
}
else {
setTimeout(() => isSourceReady(retryNum++), waitMilliseconds);
}
}
else {
resolve(null);
}
};
isSourceReady();
});
const { sampledCoords, gridPolygons } = this._sampleFeatureCoords(feature, customOptions);
let contourCoords, mainCoords;
const source = this.get('source');
if (typeof source === 'function') {
// Use a custom function. Useful for using apis to retrieve the zvalues
({ mainCoords, contourCoords } = await source(feature, sampledCoords));
}
else {
if (!this._rasterSourceIsLoaded) {
await waitUntilRasterSourceIsLoaded();
this._rasterSourceIsLoaded = true;
}
mainCoords = await this._getZFromSampledCoords(sampledCoords.mainCoords, customOptions);
// Only Polygons
if (mainCoords && sampledCoords.contourCoords) {
contourCoords = await this._getZFromSampledCoords(sampledCoords.contourCoords, customOptions);
}
}
if (mainCoords === null) {
return null;
}
const smooth = (customOptions === null || customOptions === void 0 ? void 0 : customOptions.smooth) || this.get('smooth');
if (smooth) {
mainCoords = getSmoothedCoords(mainCoords, smooth);
}
return Object.assign(Object.assign({ mainCoords }, (contourCoords && {
contourCoords
})), (gridPolygons && {
gridPolygons
}));
}
catch (err) {
this.dispatchEvent('error');
return err;
}
}
/**
* @public
* @returns
*/
getSource() {
return this.get('source');
}
/**
* @public
* @param source
*/
setSource(source, silent = false) {
this.set('source', source, silent);
}
/**
* @public
* @returns
*/
getSamples() {
return this.get('samples');
}
/**
* @public
* @param samples
*/
setSamples(samples, silent = false) {
this.set('samples', samples, silent);
}
/**
* @public
* @returns
*/
getSampleSizeArea() {
return this.get('sampleSizeArea');
}
/**
* @public
* @param sampleSizeArea
*/
setSampleSizeArea(sampleSizeArea, silent) {
this.set('sampleSizeArea', sampleSizeArea, silent);
}
/**
* @public
* @returns
*/
getCalculateZMethod() {
return this.get('calculateZMethod');
}
/**
* @public
* @param calculateZMethod
*/
setCalculateZMethod(calculateZMethod, silent = false) {
this.set('calculateZMethod', calculateZMethod, silent);
}
/**
* @public
* @returns
*/
getSmooth() {
return this.get('smooth');
}
/**
* @public
* @param smooth
*/
setSmooth(smooth, silent = false) {
this.set('smooth', smooth, silent);
}
/**
* @public
* @returns
*/
getNoDataValue() {
return this.get('noDataValue');
}
/**
* @public
* @param noDataValue
*/
setNoDataValue(noDataValue, silent = false) {
this.set('noDataValue', noDataValue, silent);
}
/**
* @public
* @returns
*/
getTilesResolution() {
return this.get('tilesResolution');
}
/**
* @public
* @param tilesResolution
*/
setTilesResolution(tilesResolution, silent = false) {
this.set('tilesResolution', tilesResolution, silent);
}
/**
* @public
* @returns
*/
getBands() {
return this.get('bands');
}
/**
* @public
* @param bands
*/
setBands(bands, silent = false) {
this.set('bands', bands, silent);
}
/**
* @public
* @returns
*/
getTimeout() {
return this.get('timeout');
}
/**
* @public
* @param timeout
*/
setTimeout(timeout, silent = false) {
this.set('timeout', timeout, silent);
}
/**
* Maximum tile resolution of the image source
* Only if the source is a raster
*
* @public
* @returns
*/
getMaxTilesResolution() {
if (this._readFromImage)
return this._readFromImage.getMaxResolution();
return null;
}
/**
* Current view resolution
* Unsupported if the view of a GeoTIFF is used in the map
*
* @public
* @returns
*/
getCurrentViewResolution() {
return this.getMap().getView().getResolution();
}
/**
* @public
* @param map
* @TODO remove events if map is null
*/
setMap(map) {
super.setMap(map);
if (map) {
// Run once
if (!this._initialized)
this._init();
}
}
/**
* This is trigged once
* @private
*/
_init() {
this.setSamples(this._options.samples, /* silent = */ true);
this.setSampleSizeArea(this._options.sampleSizeArea,
/* silent = */ true);
this.setCalculateZMethod(this._options.calculateZMethod,
/* silent = */ true);
this.setNoDataValue(this._options.noDataValue, /* silent = */ true);
this.setSmooth(this._options.smooth, /* silent = */ true);
this.setTilesResolution(this._options.tilesResolution,
/* silent = */ true);
this.setBands(this._options.bands, /* silent = */ true);
this.setTimeout(this._options.timeout, /* silent = */ true);
// Need to be the latest, fires the change event
this.setSource(this._options.source, /* silent = */ true);
this._addPropertyEvents();
this._onInitModifySource();
this._initialized = true;
this.dispatchEvent(GeneralEventTypes.LOAD);
}
/**
* @private
*/
_addPropertyEvents() {
this.on(['change:source', 'change:bands', 'change:calculateZMethod'], () => {
this._onInitModifySource();
});
}
/**
* Run on init or every time the source is modified
* @private
*/
_onInitModifySource() {
const source = this.getSource();
if (!(source instanceof Function) &&
this.get('calculateZMethod') !== 'getFeatureInfo') {
this._readFromImage = new ReadFromImage(source, this.get('calculateZMethod'), this.get('bands'), this.getMap());
}
else {
this._readFromImage = null;
}
}
/**
* Get some sample coords from the geometry while preserving the vertices.
*
* @param feature
* @param params
* @returns
* @private
*/
_sampleFeatureCoords(feature, params) {
const geom = feature.getGeometry();
let gridPolygons, contourCoords, mainCoords; // For polygons
const mergedParams = {
samples: (params === null || params === void 0 ? void 0 : params.samples) || this.getSamples(),
sampleSizeArea: (params === null || params === void 0 ? void 0 : params.sampleSizeArea) || this.getSampleSizeArea()
};
if (geom instanceof Point) {
mainCoords = [[geom.getCoordinates()[0], geom.getCoordinates()[1]]];
}
else if (geom instanceof Polygon) {
const polygonFeature = feature;
const sub_coords = polygonFeature.getGeometry().getCoordinates()[0];
const contourGeom = new LineString([sub_coords[0], sub_coords[1]]);
contourCoords = getLineSamples(contourGeom, mergedParams.samples);
gridPolygons = getPolygonSamples(polygonFeature, this.getMap().getView().getProjection().getCode(), mergedParams.sampleSizeArea);
mainCoords = gridPolygons.map((g) => {
const coords = g
.getGeometry()
.getInteriorPoint()
.getCoordinates();
return [coords[0], coords[1]];
});
}
else if (geom instanceof LineString) {
mainCoords = getLineSamples(geom, mergedParams.samples);
}
return {
sampledCoords: {
mainCoords,
contourCoords
},
gridPolygons
};
}
/**
*
* @param coordinate
* @param tilesResolution
* @returns
* @private
*/
async _getZValuesFromImage(coordinate, tilesResolution) {
return await this._readFromImage.read(coordinate, tilesResolution);
}
/**
*
* @param coordinate
* @param source
* @param view
* @returns
* @private
*/
async _getZValuesFromWMS(coordinate, source, view) {
var _a, _b;
const url = source.getFeatureInfoUrl(coordinate, view.getResolution(), view.getProjection(), {
INFO_FORMAT: 'application/json',
BUFFER: 0,
FEATURE_COUNT: 1
});
const response = await fetch(url, {
signal: AbortSignal.timeout(this.getTimeout())
});
const data = await response.json();
return (_b = (_a = data.features[0]) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.GRAY_INDEX;
}
}
var GeneralEventTypes;
(function (GeneralEventTypes) {
GeneralEventTypes["LOAD"] = "load";
})(GeneralEventTypes || (GeneralEventTypes = {}));
var utils = /*#__PURE__*/Object.freeze({
__proto__: null,
get GeneralEventTypes () { return GeneralEventTypes; },
default: ElevationParser
});
Object.assign(ElevationParser, utils);
return ElevationParser;
}));
//# sourceMappingURL=ol-elevation-parser.js.map