ol 10.2.2-dev.1729024622781 → 10.2.2-dev.1729261410863

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,641 @@
1
+ /**
2
+ * @module ol/source/SentinelHub
3
+ */
4
+
5
+ import DataTileSource from './DataTile.js';
6
+ import {
7
+ equivalent as equivalentProjections,
8
+ get as getProjection,
9
+ } from '../proj.js';
10
+
11
+ const defaultProcessUrl = 'https://services.sentinel-hub.com/api/v1/process';
12
+
13
+ const defaultTokenUrl =
14
+ 'https://services.sentinel-hub.com/auth/realms/main/protocol/openid-connect/token';
15
+
16
+ const defaultEvalscriptVersion = '3';
17
+
18
+ /**
19
+ * @type {import('../size.js').Size}
20
+ */
21
+ const defaultTileSize = [512, 512];
22
+
23
+ const maxRetries = 10;
24
+ const baseDelay = 500;
25
+
26
+ /**
27
+ * @typedef {Object} AuthConfig
28
+ * @property {string} [tokenUrl='https://services.sentinel-hub.com/auth/realms/main/protocol/openid-connect/token'] The URL to get the authentication token.
29
+ * @property {string} clientId The client ID.
30
+ * @property {string} clientSecret The client secret.
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} AccessTokenClaims
35
+ * @property {number} exp The expiration time of the token (in seconds).
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} Evalscript
40
+ * @property {Setup} setup The setup function.
41
+ * @property {EvaluatePixel} evaluatePixel The function to transform input samples into output values.
42
+ * @property {UpdateOutput} [updateOutput] Optional function to adjust the output bands.
43
+ * @property {UpdateOutputMetadata} [updateOutputMetadata] Optional function to update the output metadata.
44
+ * @property {Collections} [preProcessScenes] Optional function called before processing.
45
+ * @property {string} [version='3'] The Evalscript version.
46
+ */
47
+
48
+ /**
49
+ * @typedef {function(): SetupResult} Setup
50
+ */
51
+
52
+ /**
53
+ * @typedef {function(Sample|Array<Sample>, Scenes, InputMetadata, CustomData, OutputMetadata): OutputValues|Array<number>|void} EvaluatePixel
54
+ */
55
+
56
+ /**
57
+ * @typedef {function(Object<string, UpdatedOutputDescription>): void} UpdateOutput
58
+ */
59
+
60
+ /**
61
+ * @typedef {function(Scenes, InputMetadata, OutputMetadata): void} UpdateOutputMetadata
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} SetupResult
66
+ * @property {Array<string>|Array<InputDescription>} input Description of the input data.
67
+ * @property {OutputDescription|Array<OutputDescription>} output Description of the output data.
68
+ * @property {'SIMPLE'|'ORBIT'|'TILE'} [mosaicking='SIMPLE'] Control how samples from input scenes are composed.
69
+ */
70
+
71
+ /**
72
+ * @typedef {Object} InputDescription
73
+ * @property {Array<string>} bands Input band identifiers.
74
+ * @property {string|Array<string>} [units] Input band units.
75
+ * @property {Array<string>} [metadata] Properties to include in the input metadata.
76
+ */
77
+
78
+ /**
79
+ * @typedef {Object} OutputDescription
80
+ * @property {string} [id='default'] Output identifier.
81
+ * @property {number} bands Number of output bands.
82
+ * @property {SampleType} [sampleType='AUTO'] Output sample type.
83
+ * @property {number} [nodataValue] Output nodata value.
84
+ */
85
+
86
+ /**
87
+ * @typedef {Object} UpdatedOutputDescription
88
+ * @property {number} bands Number of output bands.
89
+ */
90
+
91
+ /**
92
+ * @typedef {'INT8'|'UINT8'|'INT16'|'UINT16'|'FLOAT32'|'AUTO'} SampleType
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object<string, number>} Sample
97
+ */
98
+
99
+ /**
100
+ * @typedef {Object} Collections
101
+ * @property {string} [from] For 'ORBIT' mosaicking, this will be the start of the search interval.
102
+ * @property {string} [to] For 'ORBIT' mosaicking, this will be the end of the search interval.
103
+ * @property {Scenes} scenes The scenes in the collection.
104
+ */
105
+
106
+ /**
107
+ * @typedef {Object} Scenes
108
+ * @property {Array<Orbit>} [orbit] Information about scenes included in the tile when 'mosaicking' is 'ORBIT'.
109
+ * @property {Array<Tile>} [tiles] Information about scenes included in the tile when 'mosaicking' is 'TILE'.
110
+ */
111
+
112
+ /**
113
+ * @typedef {Object} Orbit
114
+ * @property {string} dateFrom The earliest date for all scenes included in the tile.
115
+ * @property {string} dateTo The latest date for scenes included in the tile.
116
+ * @property {Array} tiles Metadata for each tile.
117
+ */
118
+
119
+ /**
120
+ * @typedef {Object} Tile
121
+ * @property {string} date The date of scene used in the tile.
122
+ * @property {number} cloudCoverage The estimated percentage of pixels obscured by clouds in the scene.
123
+ * @property {string} dataPath The path to the data in storage.
124
+ * @property {number} shId The internal identifier for the scene.
125
+ */
126
+
127
+ /**
128
+ * @typedef {Object} InputMetadata
129
+ * @property {string} serviceVersion The version of the service used for processing.
130
+ * @property {number} normalizationFactor The factor used to convert digital number (DN) values to reflectance.
131
+ */
132
+
133
+ /**
134
+ * @typedef {Object<string, unknown>} CustomData
135
+ */
136
+
137
+ /**
138
+ * @typedef {Object} OutputMetadata
139
+ * @property {Object} userData Arbitrary user data.
140
+ */
141
+
142
+ /**
143
+ * @typedef {Object<string, Array<number>>} OutputValues
144
+ */
145
+
146
+ /**
147
+ * @typedef {Object} ProcessRequest
148
+ * @property {ProcessRequestInput} input Input data configuration.
149
+ * @property {string} evalscript The Evalscript used for processing.
150
+ * @property {ProcessRequestOutput} [output] The output configuration.
151
+ */
152
+
153
+ /**
154
+ * @typedef {Object} ProcessRequestInput
155
+ * @property {ProcessRequestInputBounds} bounds The bounding box of the input data.
156
+ * @property {Array<ProcessRequestInputDataItem>} data The intput data.
157
+ */
158
+
159
+ /**
160
+ * @typedef {Object} ProcessRequestInputDataItem
161
+ * @property {string} [type] The type of the input data.
162
+ * @property {string} [id] The identifier of the input data.
163
+ * @property {DataFilter} [dataFilter] The filter to apply to the input data.
164
+ * @property {Object<string, unknown>} [processing] The processing to apply to the input data.
165
+ */
166
+
167
+ /**
168
+ * @typedef {Object} DataFilter
169
+ * @property {TimeRange} [timeRange] The data time range.
170
+ * @property {number} [maxCloudCoverage] The maximum cloud coverage (0-100).
171
+ */
172
+
173
+ /**
174
+ * @typedef {Object} TimeRange
175
+ * @property {string} [from] The start time (inclusive).
176
+ * @property {string} [to] The end time (inclusive).
177
+ */
178
+
179
+ /**
180
+ * @typedef {Object} ProcessRequestInputBounds
181
+ * @property {Array<number>} [bbox] The bounding box of the input data.
182
+ * @property {ProcessRequestInputBoundsProperties} [properties] The properties of the bounding box.
183
+ * @property {import("geojson").Geometry} [geometry] The geometry of the bounding box.
184
+ */
185
+
186
+ /**
187
+ * @typedef {Object} ProcessRequestInputBoundsProperties
188
+ * @property {string} crs The coordinate reference system of the bounding box.
189
+ */
190
+
191
+ /**
192
+ * @typedef {Object} ProcessRequestOutput
193
+ * @property {number} [width] Image width in pixels.
194
+ * @property {number} [height] Image height in pixels.
195
+ * @property {number} [resx] Spatial resolution in the x direction.
196
+ * @property {number} [resy] Spatial resolution in the y direction.
197
+ * @property {Array<ProcessRequestOutputResponse>} [responses] Response configuration.
198
+ */
199
+
200
+ /**
201
+ * @typedef {Object} ProcessRequestOutputResponse
202
+ * @property {string} [identifier] Identifier used to connect results to outputs from the setup.
203
+ * @property {ProcessRequestOutputFormat} [format] Response format.
204
+ */
205
+
206
+ /**
207
+ * @typedef {Object} ProcessRequestOutputFormat
208
+ * @property {string} [type] The output format type.
209
+ */
210
+
211
+ /**
212
+ * @param {Evalscript} evalscript The object to serialize.
213
+ * @return {string} The serialized Evalscript.
214
+ */
215
+ function serializeEvalscript(evalscript) {
216
+ const version = evalscript.version || defaultEvalscriptVersion;
217
+ return `//VERSION=${version}
218
+ ${serializeFunction('setup', evalscript.setup)}
219
+ ${serializeFunction('evaluatePixel', evalscript.evaluatePixel)}
220
+ ${serializeFunction('updateOutput', evalscript.updateOutput)}
221
+ `;
222
+ }
223
+
224
+ /**
225
+ * Get a loaded image given a response.
226
+ *
227
+ * @param {Response} response The response.
228
+ * @return {Promise<HTMLImageElement>} The image.
229
+ */
230
+ async function imageFromResponse(response) {
231
+ const blob = await response.blob();
232
+
233
+ return new Promise((resolve, reject) => {
234
+ const image = new Image();
235
+ const blobUrl = URL.createObjectURL(blob);
236
+ image.onload = () => {
237
+ URL.revokeObjectURL(blobUrl);
238
+ resolve(image);
239
+ };
240
+ image.onerror = () => {
241
+ URL.revokeObjectURL(blobUrl);
242
+ reject(new Error('Failed to load image'));
243
+ };
244
+ image.src = blobUrl;
245
+ });
246
+ }
247
+
248
+ /**
249
+ * @param {number} ms Milliseconds.
250
+ * @return {Promise<void>} A promise that resolves after the given time.
251
+ */
252
+ function delay(ms) {
253
+ return new Promise((resolve) => setTimeout(resolve, ms));
254
+ }
255
+
256
+ /**
257
+ * @param {AuthConfig} auth The authentication configuration.
258
+ * @return {Promise<string>} The authentication token.
259
+ */
260
+ async function getToken(auth) {
261
+ const url = auth.tokenUrl || defaultTokenUrl;
262
+ const body = new URLSearchParams();
263
+ body.append('grant_type', 'client_credentials');
264
+ body.append('client_id', auth.clientId);
265
+ body.append('client_secret', auth.clientSecret);
266
+
267
+ /**
268
+ * @type {RequestInit}
269
+ */
270
+ const options = {
271
+ method: 'POST',
272
+ headers: {'Content-Type': 'application/x-www-form-urlencoded'},
273
+ body,
274
+ };
275
+ const response = await fetch(url, options);
276
+ if (!response.ok) {
277
+ if (response.status === 401) {
278
+ throw new Error('Bad client id or secret');
279
+ }
280
+ throw new Error('Failed to get token');
281
+ }
282
+ const data = await response.json();
283
+ return data.access_token;
284
+ }
285
+
286
+ /**
287
+ * @param {string} token The access token to parse.
288
+ * @return {AccessTokenClaims} The parsed token claims.
289
+ */
290
+ export function parseTokenClaims(token) {
291
+ const base64EncodedClaims = token
292
+ .split('.')[1]
293
+ .replace(/-/g, '+')
294
+ .replace(/_/g, '/');
295
+
296
+ const chars = atob(base64EncodedClaims).split('');
297
+ const count = chars.length;
298
+ const uriEncodedChars = new Array(count);
299
+ for (let i = 0; i < count; ++i) {
300
+ const c = chars[i];
301
+ uriEncodedChars[i] = '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
302
+ }
303
+
304
+ return JSON.parse(decodeURIComponent(uriEncodedChars.join('')));
305
+ }
306
+
307
+ /**
308
+ * Gets a CRS identifier accepted by Sentinel Hub.
309
+ * See https://docs.sentinel-hub.com/api/latest/api/process/crs/.
310
+ *
311
+ * @param {import("../proj/Projection.js").default} projection The projection.
312
+ * @return {string} The projection identifier accepted by Sentinel Hub.
313
+ */
314
+ export function getProjectionIdentifier(projection) {
315
+ const ogcId = 'http://www.opengis.net/def/crs/';
316
+ const code = projection.getCode();
317
+ if (code.startsWith(ogcId)) {
318
+ return code;
319
+ }
320
+ if (code.startsWith('EPSG:')) {
321
+ return `${ogcId}EPSG/0/${code.slice(5)}`;
322
+ }
323
+ if (equivalentProjections(projection, getProjection('EPSG:4326'))) {
324
+ return `${ogcId}EPSG/0/4326`;
325
+ }
326
+
327
+ // hope for the best
328
+ return code;
329
+ }
330
+
331
+ /**
332
+ * This is intended to work with named functions, anonymous functions, arrow functions, and object methods.
333
+ * Due to how the Evalscript is executed, these are serialized as function expressions using `var`.
334
+ *
335
+ * @param {string} name The name of the function.
336
+ * @param {Function|undefined} func The function to serialize.
337
+ * @return {string} The serialized function.
338
+ */
339
+ export function serializeFunction(name, func) {
340
+ if (!func) {
341
+ return '';
342
+ }
343
+ let expression = func.toString();
344
+ if (
345
+ func.name &&
346
+ func.name !== 'function' &&
347
+ expression.match(new RegExp('^' + func.name.replace('$', '\\$') + '\\b'))
348
+ ) {
349
+ // assume function came from an object property using method syntax
350
+ expression = 'function ' + expression;
351
+ }
352
+ return `var ${name} = ${expression};`;
353
+ }
354
+
355
+ /**
356
+ * @typedef {Object} Options
357
+ * @property {AuthConfig|string} [auth] The authentication configuration with `clientId` and `clientSecret` or an access token.
358
+ * See [Sentinel Hub authentication](https://docs.sentinel-hub.com/api/latest/api/overview/authentication/)
359
+ * for details. If not provided in the constructor, the source will not be rendered until {@link module:ol/source/SentinelHub~SentinelHub#setAuth}
360
+ * is called.
361
+ * @property {Array<ProcessRequestInputDataItem>} [data] The input data configuration. If not provided in the constructor,
362
+ * the source will not be rendered until {@link module:ol/source/SentinelHub~SentinelHub#setData} is called.
363
+ * @property {Evalscript|string} [evalscript] The process applied to the input data. If not provided in the constructor,
364
+ * the source will not be rendered until {@link module:ol/source/SentinelHub~SentinelHub#setEvalscript} is called. See the
365
+ * `setEvalscript` documentation for details on the restrictions when passing process functions.
366
+ * @property {number|import("../size.js").Size} [tileSize=[512, 512]] The pixel width and height of the source tiles.
367
+ * @property {string} [url='https://services.sentinel-hub.com/api/v1/process'] The Sentinel Hub Processing API URL.
368
+ * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
369
+ * @property {boolean} [attributionsCollapsible=true] Allow the attributions to be collapsed.
370
+ * @property {boolean} [interpolate=true] Use interpolated values when resampling. By default,
371
+ * linear interpolation is used when resampling. Set to false to use the nearest neighbor instead.
372
+ * @property {boolean} [wrapX=true] Wrap the world horizontally.
373
+ * @property {number} [transition] Duration of the opacity transition for rendering.
374
+ * To disable the opacity transition, pass `transition: 0`.
375
+ */
376
+
377
+ /**
378
+ * @classdesc
379
+ * A tile source that generates tiles using the Sentinel Hub [Processing API](https://docs.sentinel-hub.com/api/latest/api/process/).
380
+ * All of the constructor options are optional, however the source will not be ready for rendering until the `auth`, `data`,
381
+ * and `evalscript` properties are provided. These can be set after construction with the {@link module:ol/source/SentinelHub~SentinelHub#setAuth},
382
+ * {@link module:ol/source/SentinelHub~SentinelHub#setData}, and {@link module:ol/source/SentinelHub~SentinelHub#setEvalscript}
383
+ * methods.
384
+ *
385
+ * If there are errors while configuring the source or fetching an access token, the `change` event will be fired and the
386
+ * source state will be set to `error`. See the {@link module:ol/source/SentinelHub~SentinelHub#getError} method for
387
+ * details on handling these errors.
388
+ * @api
389
+ */
390
+ class SentinelHub extends DataTileSource {
391
+ /**
392
+ * @param {Options} [options] Sentinel Hub options.
393
+ */
394
+ constructor(options) {
395
+ /**
396
+ * @type {Options}
397
+ */
398
+ const config = options || {};
399
+
400
+ super({
401
+ state: 'loading',
402
+ projection: config.projection,
403
+ attributionsCollapsible: config.attributionsCollapsible,
404
+ interpolate: config.interpolate,
405
+ tileSize: config.tileSize || defaultTileSize,
406
+ wrapX: config.wrapX !== undefined ? config.wrapX : true,
407
+ transition: config.transition,
408
+ });
409
+
410
+ this.setLoader((x, y, z) => this.loadTile_(x, y, z, 1));
411
+
412
+ /**
413
+ * @type {Error|null}
414
+ */
415
+ this.error_ = null;
416
+
417
+ /**
418
+ * @type {string}
419
+ * @private
420
+ */
421
+ this.evalscript_ = '';
422
+
423
+ /**
424
+ * @type {Array<ProcessRequestInputDataItem>|null}
425
+ * @private
426
+ */
427
+ this.inputData_ = null;
428
+
429
+ /**
430
+ * @type {string}
431
+ * @private
432
+ */
433
+ this.processUrl_ = config.url || defaultProcessUrl;
434
+
435
+ /**
436
+ * @type {string}
437
+ * @private
438
+ */
439
+ this.token_ = '';
440
+
441
+ /**
442
+ * @type {ReturnType<typeof setTimeout>}
443
+ * @private
444
+ */
445
+ this.tokenRenewalId_;
446
+
447
+ if (config.auth) {
448
+ this.setAuth(config.auth);
449
+ }
450
+
451
+ if (config.data) {
452
+ this.setData(config.data);
453
+ }
454
+
455
+ if (config.evalscript) {
456
+ this.setEvalscript(config.evalscript);
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Set the authentication configuration for the source (if not provided in the constructor).
462
+ * If an object with `clientId` and `clientSecret` is provided, an access token will be fetched
463
+ * and used with processing requests. Alternatively, an access token can be supplied directly.
464
+ *
465
+ * @param {AuthConfig|string} auth The auth config or access token.
466
+ * @api
467
+ */
468
+ async setAuth(auth) {
469
+ clearTimeout(this.tokenRenewalId_);
470
+
471
+ if (typeof auth === 'string') {
472
+ this.token_ = auth;
473
+ this.fireWhenReady_();
474
+ return;
475
+ }
476
+
477
+ /**
478
+ * @type {string}
479
+ */
480
+ let token;
481
+
482
+ /**
483
+ * @type {AccessTokenClaims}
484
+ */
485
+ let claims;
486
+
487
+ try {
488
+ token = await getToken(auth);
489
+ claims = parseTokenClaims(token);
490
+ } catch (error) {
491
+ this.error_ = error;
492
+ this.setState('error');
493
+ return;
494
+ }
495
+ this.token_ = token;
496
+
497
+ const expiry = claims.exp * 1000;
498
+ const timeout = Math.max(expiry - Date.now() - 60 * 1000, 1);
499
+ this.tokenRenewalId_ = setTimeout(() => this.setAuth(auth), timeout);
500
+ this.fireWhenReady_();
501
+ }
502
+
503
+ /**
504
+ * Set or update the input data used.
505
+ *
506
+ * @param {Array<ProcessRequestInputDataItem>} data The input data configuration.
507
+ * @api
508
+ */
509
+ setData(data) {
510
+ this.inputData_ = data;
511
+ this.fireWhenReady_();
512
+ }
513
+
514
+ /**
515
+ * Set or update the Evalscript used to process the data. Either a process object or a string
516
+ * Evalscript can be provided. If a process object is provided, it will be serialized to produce the
517
+ * Evalscript string. Because these functions will be serialized and executed by the Processing API,
518
+ * they cannot refer to other variables or functions that are not provided by the Processing API
519
+ * context.
520
+ *
521
+ * @param {Evalscript|string} evalscript The process to apply to the input data.
522
+ * @api
523
+ */
524
+ setEvalscript(evalscript) {
525
+ let script;
526
+ if (typeof evalscript === 'string') {
527
+ script = evalscript;
528
+ } else {
529
+ try {
530
+ script = serializeEvalscript(evalscript);
531
+ } catch (error) {
532
+ this.error_ = error;
533
+ this.setState('error');
534
+ return;
535
+ }
536
+ }
537
+ this.evalscript_ = script;
538
+ this.fireWhenReady_();
539
+ }
540
+
541
+ fireWhenReady_() {
542
+ if (!this.token_ || !this.evalscript_ || !this.inputData_) {
543
+ return;
544
+ }
545
+ const state = this.getState();
546
+ if (state === 'ready') {
547
+ this.changed();
548
+ return;
549
+ }
550
+ this.setState('ready');
551
+ }
552
+
553
+ /**
554
+ * @param {number} z The z tile index.
555
+ * @param {number} x The x tile index.
556
+ * @param {number} y The y tile index.
557
+ * @param {number} attempt The attempt number (starting with 1). Incremented with retries.
558
+ * @return {Promise<import('../DataTile.js').Data>} The composed tile data.
559
+ * @private
560
+ */
561
+ async loadTile_(z, x, y, attempt) {
562
+ const tileGrid = this.getTileGrid();
563
+ const extent = tileGrid.getTileCoordExtent([z, x, y]);
564
+ const tileSize = this.getTileSize(z);
565
+ const projection = this.getProjection();
566
+
567
+ /**
568
+ * @type {ProcessRequest}
569
+ */
570
+ const body = {
571
+ input: {
572
+ bounds: {
573
+ bbox: extent,
574
+ properties: {crs: getProjectionIdentifier(projection)},
575
+ },
576
+ data: this.inputData_,
577
+ },
578
+ output: {
579
+ width: tileSize[0],
580
+ height: tileSize[1],
581
+ },
582
+ evalscript: this.evalscript_,
583
+ };
584
+
585
+ /**
586
+ * @type {RequestInit}
587
+ */
588
+ const options = {
589
+ method: 'POST',
590
+ headers: {
591
+ 'Content-Type': 'application/json',
592
+ Authorization: `Bearer ${this.token_}`,
593
+ 'Access-Control-Request-Headers': 'Retry-After',
594
+ },
595
+ body: JSON.stringify(body),
596
+ credentials: 'include',
597
+ };
598
+
599
+ const response = await fetch(this.processUrl_, options);
600
+ if (!response.ok) {
601
+ if (response.status === 429 && attempt < maxRetries - 1) {
602
+ // The Retry-After header includes unreasonable wait times, instead use exponential backoff.
603
+ const retryAfter = baseDelay * 2 ** attempt;
604
+ await delay(retryAfter);
605
+ return this.loadTile_(x, y, z, attempt + 1);
606
+ }
607
+ throw new Error(`Failed to get tile: ${response.statusText}`);
608
+ }
609
+
610
+ return imageFromResponse(response);
611
+ }
612
+
613
+ /**
614
+ * When the source state is `error`, use this function to get more information about the error.
615
+ * To debug a faulty configuration, you may want to use a listener like this:
616
+ * ```js
617
+ * source.on('change', () => {
618
+ * if (source.getState() === 'error') {
619
+ * console.error(source.getError());
620
+ * }
621
+ * });
622
+ * ```
623
+ *
624
+ * @return {Error|null} A source loading error.
625
+ * @api
626
+ */
627
+ getError() {
628
+ return this.error_;
629
+ }
630
+
631
+ /**
632
+ * Clean up.
633
+ * @override
634
+ */
635
+ disposeInternal() {
636
+ clearTimeout(this.tokenRenewalId_);
637
+ super.disposeInternal();
638
+ }
639
+ }
640
+
641
+ export default SentinelHub;
package/util.js CHANGED
@@ -33,4 +33,4 @@ export function getUid(obj) {
33
33
  * OpenLayers version.
34
34
  * @type {string}
35
35
  */
36
- export const VERSION = '10.2.2-dev.1729024622781';
36
+ export const VERSION = '10.2.2-dev.1729261410863';