File: /var/www/html/laravel/node_modules/ol/source/SentinelHub.js
/**
* @module ol/source/SentinelHub
*/
import {
equivalent as equivalentProjections,
get as getProjection,
} from '../proj.js';
import DataTileSource from './DataTile.js';
const defaultProcessUrl = 'https://services.sentinel-hub.com/api/v1/process';
const defaultTokenUrl =
'https://services.sentinel-hub.com/auth/realms/main/protocol/openid-connect/token';
const defaultEvalscriptVersion = '3';
/**
* @type {import('../size.js').Size}
*/
const defaultTileSize = [512, 512];
const maxRetries = 10;
const baseDelay = 500;
/**
* @typedef {Object} AuthConfig
* @property {string} [tokenUrl='https://services.sentinel-hub.com/auth/realms/main/protocol/openid-connect/token'] The URL to get the authentication token.
* @property {string} clientId The client ID.
* @property {string} clientSecret The client secret.
*/
/**
* @typedef {Object} AccessTokenClaims
* @property {number} exp The expiration time of the token (in seconds).
*/
/**
* @typedef {Object} Evalscript
* @property {Setup} setup The setup function.
* @property {EvaluatePixel} evaluatePixel The function to transform input samples into output values.
* @property {UpdateOutput} [updateOutput] Optional function to adjust the output bands.
* @property {UpdateOutputMetadata} [updateOutputMetadata] Optional function to update the output metadata.
* @property {Collections} [preProcessScenes] Optional function called before processing.
* @property {string} [version='3'] The Evalscript version.
*/
/**
* @typedef {function(): SetupResult} Setup
*/
/**
* @typedef {function(Sample|Array<Sample>, Scenes, InputMetadata, CustomData, OutputMetadata): OutputValues|Array<number>|void} EvaluatePixel
*/
/**
* @typedef {function(Object<string, UpdatedOutputDescription>): void} UpdateOutput
*/
/**
* @typedef {function(Scenes, InputMetadata, OutputMetadata): void} UpdateOutputMetadata
*/
/**
* @typedef {Object} SetupResult
* @property {Array<string>|Array<InputDescription>} input Description of the input data.
* @property {OutputDescription|Array<OutputDescription>} output Description of the output data.
* @property {'SIMPLE'|'ORBIT'|'TILE'} [mosaicking='SIMPLE'] Control how samples from input scenes are composed.
*/
/**
* @typedef {Object} InputDescription
* @property {Array<string>} bands Input band identifiers.
* @property {string|Array<string>} [units] Input band units.
* @property {Array<string>} [metadata] Properties to include in the input metadata.
*/
/**
* @typedef {Object} OutputDescription
* @property {string} [id='default'] Output identifier.
* @property {number} bands Number of output bands.
* @property {SampleType} [sampleType='AUTO'] Output sample type.
* @property {number} [nodataValue] Output nodata value.
*/
/**
* @typedef {Object} UpdatedOutputDescription
* @property {number} bands Number of output bands.
*/
/**
* @typedef {'INT8'|'UINT8'|'INT16'|'UINT16'|'FLOAT32'|'AUTO'} SampleType
*/
/**
* @typedef {Object<string, number>} Sample
*/
/**
* @typedef {Object} Collections
* @property {string} [from] For 'ORBIT' mosaicking, this will be the start of the search interval.
* @property {string} [to] For 'ORBIT' mosaicking, this will be the end of the search interval.
* @property {Scenes} scenes The scenes in the collection.
*/
/**
* @typedef {Object} Scenes
* @property {Array<Orbit>} [orbit] Information about scenes included in the tile when 'mosaicking' is 'ORBIT'.
* @property {Array<Tile>} [tiles] Information about scenes included in the tile when 'mosaicking' is 'TILE'.
*/
/**
* @typedef {Object} Orbit
* @property {string} dateFrom The earliest date for all scenes included in the tile.
* @property {string} dateTo The latest date for scenes included in the tile.
* @property {Array} tiles Metadata for each tile.
*/
/**
* @typedef {Object} Tile
* @property {string} date The date of scene used in the tile.
* @property {number} cloudCoverage The estimated percentage of pixels obscured by clouds in the scene.
* @property {string} dataPath The path to the data in storage.
* @property {number} shId The internal identifier for the scene.
*/
/**
* @typedef {Object} InputMetadata
* @property {string} serviceVersion The version of the service used for processing.
* @property {number} normalizationFactor The factor used to convert digital number (DN) values to reflectance.
*/
/**
* @typedef {Object<string, unknown>} CustomData
*/
/**
* @typedef {Object} OutputMetadata
* @property {Object} userData Arbitrary user data.
*/
/**
* @typedef {Object<string, Array<number>>} OutputValues
*/
/**
* @typedef {Object} ProcessRequest
* @property {ProcessRequestInput} input Input data configuration.
* @property {string} evalscript The Evalscript used for processing.
* @property {ProcessRequestOutput} [output] The output configuration.
*/
/**
* @typedef {Object} ProcessRequestInput
* @property {ProcessRequestInputBounds} bounds The bounding box of the input data.
* @property {Array<ProcessRequestInputDataItem>} data The intput data.
*/
/**
* @typedef {Object} ProcessRequestInputDataItem
* @property {string} [type] The type of the input data.
* @property {string} [id] The identifier of the input data.
* @property {DataFilter} [dataFilter] The filter to apply to the input data.
* @property {Object<string, unknown>} [processing] The processing to apply to the input data.
*/
/**
* @typedef {Object} DataFilter
* @property {TimeRange} [timeRange] The data time range.
* @property {number} [maxCloudCoverage] The maximum cloud coverage (0-100).
*/
/**
* @typedef {Object} TimeRange
* @property {string} [from] The start time (inclusive).
* @property {string} [to] The end time (inclusive).
*/
/**
* @typedef {Object} ProcessRequestInputBounds
* @property {Array<number>} [bbox] The bounding box of the input data.
* @property {ProcessRequestInputBoundsProperties} [properties] The properties of the bounding box.
* @property {import("geojson").Geometry} [geometry] The geometry of the bounding box.
*/
/**
* @typedef {Object} ProcessRequestInputBoundsProperties
* @property {string} crs The coordinate reference system of the bounding box.
*/
/**
* @typedef {Object} ProcessRequestOutput
* @property {number} [width] Image width in pixels.
* @property {number} [height] Image height in pixels.
* @property {number} [resx] Spatial resolution in the x direction.
* @property {number} [resy] Spatial resolution in the y direction.
* @property {Array<ProcessRequestOutputResponse>} [responses] Response configuration.
*/
/**
* @typedef {Object} ProcessRequestOutputResponse
* @property {string} [identifier] Identifier used to connect results to outputs from the setup.
* @property {ProcessRequestOutputFormat} [format] Response format.
*/
/**
* @typedef {Object} ProcessRequestOutputFormat
* @property {string} [type] The output format type.
*/
/**
* @param {Evalscript} evalscript The object to serialize.
* @return {string} The serialized Evalscript.
*/
function serializeEvalscript(evalscript) {
const version = evalscript.version || defaultEvalscriptVersion;
return `//VERSION=${version}
${serializeFunction('setup', evalscript.setup)}
${serializeFunction('evaluatePixel', evalscript.evaluatePixel)}
${serializeFunction('updateOutput', evalscript.updateOutput)}
`;
}
/**
* Get a loaded image given a response.
*
* @param {Response} response The response.
* @return {Promise<HTMLImageElement>} The image.
*/
async function imageFromResponse(response) {
const blob = await response.blob();
return new Promise((resolve, reject) => {
const image = new Image();
const blobUrl = URL.createObjectURL(blob);
image.onload = () => {
URL.revokeObjectURL(blobUrl);
resolve(image);
};
image.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error('Failed to load image'));
};
image.src = blobUrl;
});
}
/**
* @param {number} ms Milliseconds.
* @return {Promise<void>} A promise that resolves after the given time.
*/
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* @param {AuthConfig} auth The authentication configuration.
* @return {Promise<string>} The authentication token.
*/
async function getToken(auth) {
const url = auth.tokenUrl || defaultTokenUrl;
const body = new URLSearchParams();
body.append('grant_type', 'client_credentials');
body.append('client_id', auth.clientId);
body.append('client_secret', auth.clientSecret);
/**
* @type {RequestInit}
*/
const options = {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body,
};
const response = await fetch(url, options);
if (!response.ok) {
if (response.status === 401) {
throw new Error('Bad client id or secret');
}
throw new Error('Failed to get token');
}
const data = await response.json();
return data.access_token;
}
/**
* @param {string} token The access token to parse.
* @return {AccessTokenClaims} The parsed token claims.
*/
export function parseTokenClaims(token) {
const base64EncodedClaims = token
.split('.')[1]
.replace(/-/g, '+')
.replace(/_/g, '/');
const chars = atob(base64EncodedClaims).split('');
const count = chars.length;
const uriEncodedChars = new Array(count);
for (let i = 0; i < count; ++i) {
const c = chars[i];
uriEncodedChars[i] = '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}
return JSON.parse(decodeURIComponent(uriEncodedChars.join('')));
}
/**
* Gets a CRS identifier accepted by Sentinel Hub.
* See https://docs.sentinel-hub.com/api/latest/api/process/crs/.
*
* @param {import("../proj/Projection.js").default} projection The projection.
* @return {string} The projection identifier accepted by Sentinel Hub.
*/
export function getProjectionIdentifier(projection) {
const ogcId = 'http://www.opengis.net/def/crs/';
const code = projection.getCode();
if (code.startsWith(ogcId)) {
return code;
}
if (code.startsWith('EPSG:')) {
return `${ogcId}EPSG/0/${code.slice(5)}`;
}
if (equivalentProjections(projection, getProjection('EPSG:4326'))) {
return `${ogcId}EPSG/0/4326`;
}
// hope for the best
return code;
}
/**
* This is intended to work with named functions, anonymous functions, arrow functions, and object methods.
* Due to how the Evalscript is executed, these are serialized as function expressions using `var`.
*
* @param {string} name The name of the function.
* @param {Function|undefined} func The function to serialize.
* @return {string} The serialized function.
*/
export function serializeFunction(name, func) {
if (!func) {
return '';
}
let expression = func.toString();
if (
func.name &&
func.name !== 'function' &&
expression.match(new RegExp('^' + func.name.replace('$', '\\$') + '\\b'))
) {
// assume function came from an object property using method syntax
expression = 'function ' + expression;
}
return `var ${name} = ${expression};`;
}
/**
* @typedef {Object} Options
* @property {AuthConfig|string} [auth] The authentication configuration with `clientId` and `clientSecret` or an access token.
* See [Sentinel Hub authentication](https://docs.sentinel-hub.com/api/latest/api/overview/authentication/)
* for details. If not provided in the constructor, the source will not be rendered until {@link module:ol/source/SentinelHub~SentinelHub#setAuth}
* is called.
* @property {Array<ProcessRequestInputDataItem>} [data] The input data configuration. If not provided in the constructor,
* the source will not be rendered until {@link module:ol/source/SentinelHub~SentinelHub#setData} is called.
* @property {Evalscript|string} [evalscript] The process applied to the input data. If not provided in the constructor,
* the source will not be rendered until {@link module:ol/source/SentinelHub~SentinelHub#setEvalscript} is called. See the
* `setEvalscript` documentation for details on the restrictions when passing process functions.
* @property {number|import("../size.js").Size} [tileSize=[512, 512]] The pixel width and height of the source tiles.
* @property {string} [url='https://services.sentinel-hub.com/api/v1/process'] The Sentinel Hub Processing API URL.
* @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
* @property {boolean} [attributionsCollapsible=true] Allow the attributions to be collapsed.
* @property {boolean} [interpolate=true] Use interpolated values when resampling. By default,
* linear interpolation is used when resampling. Set to false to use the nearest neighbor instead.
* @property {boolean} [wrapX=true] Wrap the world horizontally.
* @property {number} [transition] Duration of the opacity transition for rendering.
* To disable the opacity transition, pass `transition: 0`.
*/
/**
* @classdesc
* A tile source that generates tiles using the Sentinel Hub [Processing API](https://docs.sentinel-hub.com/api/latest/api/process/).
* All of the constructor options are optional, however the source will not be ready for rendering until the `auth`, `data`,
* and `evalscript` properties are provided. These can be set after construction with the {@link module:ol/source/SentinelHub~SentinelHub#setAuth},
* {@link module:ol/source/SentinelHub~SentinelHub#setData}, and {@link module:ol/source/SentinelHub~SentinelHub#setEvalscript}
* methods.
*
* If there are errors while configuring the source or fetching an access token, the `change` event will be fired and the
* source state will be set to `error`. See the {@link module:ol/source/SentinelHub~SentinelHub#getError} method for
* details on handling these errors.
* @api
*/
class SentinelHub extends DataTileSource {
/**
* @param {Options} [options] Sentinel Hub options.
*/
constructor(options) {
/**
* @type {Options}
*/
const config = options || {};
super({
state: 'loading',
projection: config.projection,
attributionsCollapsible: config.attributionsCollapsible,
interpolate: config.interpolate,
tileSize: config.tileSize || defaultTileSize,
wrapX: config.wrapX !== undefined ? config.wrapX : true,
transition: config.transition,
});
this.setLoader((x, y, z) => this.loadTile_(x, y, z, 1));
/**
* @type {Error|null}
*/
this.error_ = null;
/**
* @type {string}
* @private
*/
this.evalscript_ = '';
/**
* @type {Array<ProcessRequestInputDataItem>|null}
* @private
*/
this.inputData_ = null;
/**
* @type {string}
* @private
*/
this.processUrl_ = config.url || defaultProcessUrl;
/**
* @type {string}
* @private
*/
this.token_ = '';
/**
* @type {ReturnType<typeof setTimeout>}
* @private
*/
this.tokenRenewalId_;
if (config.auth) {
this.setAuth(config.auth);
}
if (config.data) {
this.setData(config.data);
}
if (config.evalscript) {
this.setEvalscript(config.evalscript);
}
}
/**
* Set the authentication configuration for the source (if not provided in the constructor).
* If an object with `clientId` and `clientSecret` is provided, an access token will be fetched
* and used with processing requests. Alternatively, an access token can be supplied directly.
*
* @param {AuthConfig|string} auth The auth config or access token.
* @api
*/
async setAuth(auth) {
clearTimeout(this.tokenRenewalId_);
if (typeof auth === 'string') {
this.token_ = auth;
this.fireWhenReady_();
return;
}
/**
* @type {string}
*/
let token;
/**
* @type {AccessTokenClaims}
*/
let claims;
try {
token = await getToken(auth);
claims = parseTokenClaims(token);
} catch (error) {
this.error_ = error;
this.setState('error');
return;
}
this.token_ = token;
const expiry = claims.exp * 1000;
const timeout = Math.max(expiry - Date.now() - 60 * 1000, 1);
this.tokenRenewalId_ = setTimeout(() => this.setAuth(auth), timeout);
this.fireWhenReady_();
}
/**
* Set or update the input data used.
*
* @param {Array<ProcessRequestInputDataItem>} data The input data configuration.
* @api
*/
setData(data) {
this.inputData_ = data;
this.fireWhenReady_();
}
/**
* Set or update the Evalscript used to process the data. Either a process object or a string
* Evalscript can be provided. If a process object is provided, it will be serialized to produce the
* Evalscript string. Because these functions will be serialized and executed by the Processing API,
* they cannot refer to other variables or functions that are not provided by the Processing API
* context.
*
* @param {Evalscript|string} evalscript The process to apply to the input data.
* @api
*/
setEvalscript(evalscript) {
let script;
if (typeof evalscript === 'string') {
script = evalscript;
} else {
try {
script = serializeEvalscript(evalscript);
} catch (error) {
this.error_ = error;
this.setState('error');
return;
}
}
this.evalscript_ = script;
this.fireWhenReady_();
}
fireWhenReady_() {
if (!this.token_ || !this.evalscript_ || !this.inputData_) {
return;
}
const state = this.getState();
if (state === 'ready') {
this.changed();
return;
}
this.setState('ready');
}
/**
* @param {number} z The z tile index.
* @param {number} x The x tile index.
* @param {number} y The y tile index.
* @param {number} attempt The attempt number (starting with 1). Incremented with retries.
* @return {Promise<import('../DataTile.js').Data>} The composed tile data.
* @private
*/
async loadTile_(z, x, y, attempt) {
const tileGrid = this.getTileGrid();
const extent = tileGrid.getTileCoordExtent([z, x, y]);
const tileSize = this.getTileSize(z);
const projection = this.getProjection();
/**
* @type {ProcessRequest}
*/
const body = {
input: {
bounds: {
bbox: extent,
properties: {crs: getProjectionIdentifier(projection)},
},
data: this.inputData_,
},
output: {
width: tileSize[0],
height: tileSize[1],
},
evalscript: this.evalscript_,
};
/**
* @type {RequestInit}
*/
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.token_}`,
'Access-Control-Request-Headers': 'Retry-After',
},
body: JSON.stringify(body),
credentials: 'include',
};
const response = await fetch(this.processUrl_, options);
if (!response.ok) {
if (response.status === 429 && attempt < maxRetries - 1) {
// The Retry-After header includes unreasonable wait times, instead use exponential backoff.
const retryAfter = baseDelay * 2 ** attempt;
await delay(retryAfter);
return this.loadTile_(x, y, z, attempt + 1);
}
throw new Error(`Failed to get tile: ${response.statusText}`);
}
return imageFromResponse(response);
}
/**
* When the source state is `error`, use this function to get more information about the error.
* To debug a faulty configuration, you may want to use a listener like this:
* ```js
* source.on('change', () => {
* if (source.getState() === 'error') {
* console.error(source.getError());
* }
* });
* ```
*
* @return {Error|null} A source loading error.
* @api
*/
getError() {
return this.error_;
}
/**
* Clean up.
* @override
*/
disposeInternal() {
clearTimeout(this.tokenRenewalId_);
super.disposeInternal();
}
}
export default SentinelHub;