@eodash/eodash 5.6.1 → 5.7.1
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.
- package/core/client/components/DashboardLayout.vue +1 -1
- package/core/client/components/EodashOverlay.vue +27 -4
- package/core/client/composables/DefineEodash.js +0 -1
- package/core/client/composables/DefineWidgets.js +3 -1
- package/core/client/composables/index.js +6 -2
- package/core/client/eodashSTAC/EodashCollection.js +79 -45
- package/core/client/eodashSTAC/cql.js +64 -0
- package/core/client/eodashSTAC/createLayers.js +192 -125
- package/core/client/eodashSTAC/helpers.js +423 -97
- package/core/client/eodashSTAC/mosaic.js +416 -0
- package/core/client/plugins/axios.js +31 -1
- package/core/client/store/actions.js +1 -1
- package/core/client/store/stac.js +25 -4
- package/core/client/store/states.js +20 -2
- package/core/client/types.ts +155 -29
- package/core/client/utils/bands-editor/exampleSchema.json +50 -50
- package/core/client/utils/index.js +4 -0
- package/core/client/utils/states.js +10 -0
- package/dist/client/{DashboardLayout-CqMb7OXc.js → DashboardLayout-ePbdvUTE.js} +5 -5
- package/dist/client/{DynamicWebComponent-DqKqTf7l.js → DynamicWebComponent-g5SdRbjY.js} +1 -1
- package/dist/client/{EodashChart-PkUQfH0k.js → EodashChart-ChrkA5oS.js} +7 -6
- package/dist/client/{EodashDatePicker-DLJGdTtX.js → EodashDatePicker-CY1TrsDo.js} +3 -3
- package/dist/client/{EodashItemFilter-Dk8KjFcX.js → EodashItemFilter-DzshHDlS.js} +2 -1
- package/dist/client/{EodashLayerControl-CC1Cogrn.js → EodashLayerControl-DMCeMkSd.js} +70 -22
- package/dist/client/{EodashLayoutSwitcher-5WZYd6rr.js → EodashLayoutSwitcher-iJ8mX1rF.js} +3 -3
- package/dist/client/EodashMapBtns-C7Cr7q5Y.js +592 -0
- package/dist/client/EodashStacInfo-K7-iuXa7.js +169 -0
- package/dist/client/{EodashTools-CGZtTh45.js → EodashTools-BEK7e7JM.js} +4 -4
- package/dist/client/{ExportState-Ce0ZnrX_.js → ExportState-CqIzfMYQ.js} +6 -168
- package/dist/client/{Footer-CnZqEiop.js → Footer-1P0cmBOZ.js} +1 -1
- package/dist/client/{Header-Dr5PBIm8.js → Header-CkkBaBNh.js} +2 -2
- package/dist/client/MobileLayout-BPf49kDU.js +118 -0
- package/dist/client/{PopUp-DjvE_71e.js → PopUp-DPZq5SWI.js} +4 -168
- package/dist/client/{ProcessList-kf3olI7W.js → ProcessList-DuI6WpS1.js} +5 -5
- package/dist/client/VDivider-Bj9LG_JV.js +168 -0
- package/dist/client/{VImg-Bj6uWt6j.js → VImg-Ddv5OyMz.js} +2 -2
- package/dist/client/{VMain-Cm0UmKGi.js → VMain-7yqk_qC3.js} +1 -1
- package/dist/client/{VTooltip-DbBu-ky3.js → VTooltip-2hAJyEoa.js} +2 -2
- package/dist/client/{WidgetsContainer-CfLnd44U.js → WidgetsContainer-2DOrlwCB.js} +1 -1
- package/dist/client/{asWebComponent-DfULAbpq.js → asWebComponent-BlFN2lkB.js} +493 -335
- package/dist/client/{async-CIbKXEEe.js → async-B2H8ZwUK.js} +21 -10
- package/dist/client/create-layers-config-u_vn5hPk.js +182 -0
- package/dist/client/dialog-transition-BrMkMiNa.js +170 -0
- package/dist/client/eo-dash.js +1 -1
- package/dist/client/{forwardRefs-yj3VWOK5.js → forwardRefs-DAlror6d.js} +8 -87
- package/dist/client/{handling-s8xt2JwF.js → handling-B6I1V5eG.js} +123 -85
- package/dist/client/{helpers-DAVGYl_f.js → helpers-CBqIUSXJ.js} +3034 -1649
- package/dist/client/index-BWq1ZKaV.js +3168 -0
- package/dist/client/{index-CYz6dT7K.js → index-CJ9UI3ln.js} +2 -2
- package/dist/client/index-DXLz4sFR.js +442 -0
- package/dist/client/{index-4TrjfJ32.js → index-DhKU6KqM.js} +230 -231
- package/dist/client/{index-Bu8BMA9q.js → index-DjQlbWbJ.js} +70 -83
- package/dist/client/material-symbols-outlined.woff2 +0 -0
- package/dist/client/material-symbols-rounded.woff2 +0 -0
- package/dist/client/material-symbols-sharp.woff2 +0 -0
- package/dist/client/material-symbols-subset.woff2 +0 -0
- package/dist/client/mosaic-BFYLEwyx.js +455 -0
- package/dist/client/templates.js +250 -119
- package/dist/client/{transition-DaOsiD4u.js → transition-D_W6Wn9v.js} +1 -1
- package/dist/node/cli.js +5 -5
- package/dist/node/types.d.ts +4 -0
- package/dist/types/core/client/composables/DefineEodash.d.ts +1 -1
- package/dist/types/core/client/composables/index.d.ts +1 -3
- package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +62 -31
- package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
- package/dist/types/core/client/eodashSTAC/createLayers.d.ts +44 -25
- package/dist/types/core/client/eodashSTAC/helpers.d.ts +78 -18
- package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
- package/dist/types/core/client/store/stac.d.ts +31 -7
- package/dist/types/core/client/store/states.d.ts +24 -2
- package/dist/types/core/client/types.d.ts +131 -25
- package/dist/types/core/client/utils/states.d.ts +12 -0
- package/dist/types/templates/baseConfig.d.ts +1 -88
- package/dist/types/templates/compare.d.ts +32 -3
- package/dist/types/templates/expert.d.ts +18 -0
- package/dist/types/templates/explore.d.ts +9 -1
- package/dist/types/templates/lite.d.ts +11 -0
- package/dist/types/widgets/EodashChart.vue.d.ts +5 -3
- package/dist/types/widgets/EodashItemCatalog/index.vue.d.ts +129 -2
- package/dist/types/widgets/EodashItemCatalog/methods/filters.d.ts +28 -6
- package/dist/types/widgets/EodashItemCatalog/methods/handlers.d.ts +17 -4
- package/dist/types/widgets/EodashItemCatalog/methods/map.d.ts +16 -4
- package/dist/types/widgets/EodashLayerControl.vue.d.ts +36 -17
- package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +23 -10
- package/dist/types/widgets/EodashMap/index.vue.d.ts +91 -35
- package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -8
- package/dist/types/widgets/EodashMap/methods/index.d.ts +5 -1
- package/dist/types/widgets/EodashMap/methods/use-map-loading.d.ts +1 -0
- package/dist/types/widgets/EodashProcess/index.vue.d.ts +5 -3
- package/dist/types/widgets/EodashProcess/methods/composables.d.ts +1 -1
- package/dist/types/widgets/EodashProcess/methods/handling.d.ts +13 -8
- package/dist/types/widgets/EodashProcess/states.d.ts +1 -1
- package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
- package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +54 -0
- package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +34 -0
- package/dist/types/widgets/EodashTimeSlider/types.d.ts +52 -0
- package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
- package/package.json +46 -46
- package/templates/baseConfig.js +36 -6
- package/templates/compare.js +62 -7
- package/templates/expert.js +23 -0
- package/templates/explore.js +11 -5
- package/templates/lite.js +14 -0
- package/widgets/EodashChart.vue +1 -0
- package/widgets/EodashItemCatalog/index.vue +363 -35
- package/widgets/EodashItemCatalog/methods/filters.js +134 -70
- package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
- package/widgets/EodashItemCatalog/methods/map.js +142 -28
- package/widgets/EodashLayerControl.vue +64 -14
- package/widgets/EodashMap/EodashMapBtns.vue +153 -47
- package/widgets/EodashMap/index.vue +101 -105
- package/widgets/EodashMap/methods/btns.js +37 -5
- package/widgets/EodashMap/methods/create-layers-config.js +18 -16
- package/widgets/EodashMap/methods/index.js +89 -8
- package/widgets/EodashMap/methods/use-map-loading.js +52 -0
- package/widgets/EodashProcess/index.vue +2 -1
- package/widgets/EodashProcess/methods/async.js +19 -8
- package/widgets/EodashProcess/methods/composables.js +63 -77
- package/widgets/EodashProcess/methods/custom-endpoints/chart/sentinelhub-endpoint.js +9 -5
- package/widgets/EodashProcess/methods/custom-endpoints/chart/veda-endpoint.js +7 -4
- package/widgets/EodashProcess/methods/handling.js +105 -72
- package/widgets/EodashProcess/states.js +1 -1
- package/widgets/EodashStacInfo.vue +64 -2
- package/widgets/EodashTimeSlider/index.vue +208 -0
- package/widgets/EodashTimeSlider/methods/index.js +236 -0
- package/widgets/EodashTimeSlider/types.ts +54 -0
- package/dist/client/EodashMapBtns-BHrSDNoC.js +0 -435
- package/dist/client/EodashStacInfo-B90wobPc.js +0 -107
- package/dist/client/EodashTimeSlider-5vm7F3ju.js +0 -53
- package/dist/client/MobileLayout-DyMjaeif.js +0 -118
- package/dist/client/index-CugS__t5.js +0 -570
- package/dist/types/widgets/EodashTimeSlider.vue.d.ts +0 -15
- package/widgets/EodashTimeSlider.vue +0 -40
|
@@ -2,6 +2,7 @@ import { toAbsolute } from "stac-js/src/http.js";
|
|
|
2
2
|
import axios from "@/plugins/axios";
|
|
3
3
|
import log from "loglevel";
|
|
4
4
|
import { getStyleVariablesState } from "./triggers.js";
|
|
5
|
+
import { itemsCache, splitItemsCache } from "@/utils/states.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @param {import("stac-ts").StacLink[]} [links]
|
|
@@ -171,7 +172,7 @@ export const extractRoles = (properties, linkOrAsset) => {
|
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
174
|
* Extracts a single non-link style JSON from a STAC Item optionally for a selected key mapping
|
|
174
|
-
* @param { import("stac-ts").StacItem | import("stac-ts").StacCollection } stacObject
|
|
175
|
+
* @param { import("stac-ts").StacItem | import("stac-ts").StacCollection | null | undefined} stacObject
|
|
175
176
|
* @param {string | undefined} linkKey
|
|
176
177
|
* @param {string | undefined} assetKey
|
|
177
178
|
* @returns
|
|
@@ -181,6 +182,7 @@ export const fetchStyle = async (
|
|
|
181
182
|
linkKey = undefined,
|
|
182
183
|
assetKey = undefined,
|
|
183
184
|
) => {
|
|
185
|
+
if (!stacObject) return undefined;
|
|
184
186
|
let styleLink = null;
|
|
185
187
|
if (linkKey) {
|
|
186
188
|
styleLink = stacObject.links.find(
|
|
@@ -212,6 +214,20 @@ export const fetchStyle = async (
|
|
|
212
214
|
}
|
|
213
215
|
};
|
|
214
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Resolves a style by preferring the item's own `style` link and falling back
|
|
219
|
+
* to the collection's. Takes the same key arguments as `fetchStyle`.
|
|
220
|
+
*
|
|
221
|
+
* @param {import("stac-ts").StacItem | import("stac-ts").StacCollection} item
|
|
222
|
+
* @param {import("stac-ts").StacCollection | null | undefined} collection
|
|
223
|
+
* @param {string} [linkKey]
|
|
224
|
+
* @param {string} [assetKey]
|
|
225
|
+
* @returns {Promise<import("@/types").EodashStyleJson | undefined>}
|
|
226
|
+
*/
|
|
227
|
+
export const resolveStyle = async (item, collection, linkKey, assetKey) =>
|
|
228
|
+
(await fetchStyle(item, linkKey, assetKey)) ??
|
|
229
|
+
(await fetchStyle(collection, linkKey, assetKey));
|
|
230
|
+
|
|
215
231
|
/**
|
|
216
232
|
* Fetches all style JSONs from a STAC Item and returns an array with style objects
|
|
217
233
|
* @param {import("stac-ts").StacItem | import("stac-ts").StacCollection} stacObject
|
|
@@ -251,94 +267,74 @@ export const getProjectionCode = (projection) => {
|
|
|
251
267
|
};
|
|
252
268
|
|
|
253
269
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* @param {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
* Builds layercontrol LayerDatetime + timeControlValues from a list of dates.
|
|
271
|
+
*
|
|
272
|
+
* @param {Date[] | undefined} dates
|
|
273
|
+
* @param {string | null} [currentStep] - target datetime; snapped to the closest available date
|
|
274
|
+
* @returns {{ layerDatetime: Record<string, any> | undefined, timeControlValues: { date: string }[] | undefined }}
|
|
275
|
+
*/
|
|
276
|
+
export const extractLayerTimeValues = (dates, currentStep) => {
|
|
277
|
+
if (!currentStep || !dates?.length || dates.length <= 1) {
|
|
260
278
|
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
261
279
|
}
|
|
262
280
|
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
if (!dateProperty) {
|
|
267
|
-
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
268
|
-
}
|
|
269
|
-
/** @type {{date:string;itemId:string}[]} */
|
|
270
|
-
const timeValues = [];
|
|
271
|
-
try {
|
|
272
|
-
/**
|
|
273
|
-
* @param {typeof timeValues} vals
|
|
274
|
-
* @param {import("stac-ts").StacLink} link
|
|
275
|
-
*/
|
|
276
|
-
const reduceLinks = (vals, link) => {
|
|
277
|
-
if (link[dateProperty] && link.rel === "item") {
|
|
278
|
-
vals.push({
|
|
279
|
-
itemId: /** @type {string} */ (link.id),
|
|
280
|
-
date: new Date(
|
|
281
|
-
/** @type {string} */ (link[dateProperty]),
|
|
282
|
-
).toISOString(),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return vals;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
*
|
|
290
|
-
* @param {typeof timeValues} vals
|
|
291
|
-
* @param {import("stac-ts").StacItem} item
|
|
292
|
-
*/
|
|
293
|
-
const reduceItems = (vals, item) => {
|
|
294
|
-
const date = item.properties?.[dateProperty];
|
|
295
|
-
if (date) {
|
|
296
|
-
vals.push({
|
|
297
|
-
itemId: /** @type {string} */ (item.id),
|
|
298
|
-
date: new Date(/** @type {string} */ (date)).toISOString(),
|
|
299
|
-
...item.properties,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
return vals;
|
|
303
|
-
};
|
|
304
|
-
currentStep = new Date(currentStep).toISOString();
|
|
305
|
-
//@ts-expect-error TODO
|
|
306
|
-
items.reduce(isSTACItem(items[0]) ? reduceItems : reduceLinks, timeValues);
|
|
307
|
-
} catch (e) {
|
|
308
|
-
console.warn("[eodash] not supported datetime format was provided", e);
|
|
309
|
-
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
310
|
-
}
|
|
311
|
-
// not enough timeValues
|
|
312
|
-
if (timeValues.length <= 1) {
|
|
313
|
-
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
314
|
-
}
|
|
281
|
+
const controlValues = dates.map((d) => d.toISOString()).sort();
|
|
282
|
+
const timeControlValues = controlValues.map((date) => ({ date }));
|
|
315
283
|
|
|
316
|
-
|
|
317
|
-
if (!
|
|
318
|
-
const
|
|
319
|
-
currentStep =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
284
|
+
currentStep = new Date(currentStep).toISOString();
|
|
285
|
+
if (!controlValues.includes(currentStep)) {
|
|
286
|
+
const target = new Date(currentStep).getTime();
|
|
287
|
+
currentStep = controlValues.reduce((best, d) =>
|
|
288
|
+
Math.abs(new Date(d).getTime() - target) <
|
|
289
|
+
Math.abs(new Date(best).getTime() - target)
|
|
290
|
+
? d
|
|
291
|
+
: best,
|
|
292
|
+
);
|
|
324
293
|
}
|
|
325
294
|
|
|
326
295
|
const layerDatetime = {
|
|
327
|
-
controlValues
|
|
296
|
+
controlValues,
|
|
328
297
|
currentStep,
|
|
329
298
|
slider: true,
|
|
330
299
|
navigation: true,
|
|
331
300
|
play: false,
|
|
332
301
|
displayFormat: "DD.MM.YYYY HH:mm",
|
|
333
302
|
animateOnClickInterval: false,
|
|
303
|
+
showUTC: true,
|
|
334
304
|
};
|
|
335
305
|
|
|
336
|
-
return {
|
|
337
|
-
layerDatetime,
|
|
338
|
-
timeControlValues: timeValues,
|
|
339
|
-
};
|
|
306
|
+
return { layerDatetime, timeControlValues };
|
|
340
307
|
};
|
|
341
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Fetches the daily pre-aggregation data for a STAC collection if available.
|
|
311
|
+
* Returns the raw AggregationCollection,
|
|
312
|
+
* or null if no daily pre-aggregation link exists, or the fetch fails.
|
|
313
|
+
*
|
|
314
|
+
* @param {import("stac-ts").StacCollection | undefined} stacCollection
|
|
315
|
+
* @param {string} fallbackBaseUrl - base for relative href resolution when no `self` link is present
|
|
316
|
+
* @returns {Promise<any | null>}
|
|
317
|
+
*/
|
|
318
|
+
export async function fetchPreAggregations(stacCollection, fallbackBaseUrl) {
|
|
319
|
+
if (!stacCollection) return null;
|
|
320
|
+
const preAggregationLink = stacCollection.links?.find(
|
|
321
|
+
(l) => l.rel === "pre-aggregation" && l["aggregation:interval"] === "daily",
|
|
322
|
+
);
|
|
323
|
+
if (!preAggregationLink) return null;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const selfLink = stacCollection.links?.find((l) => l.rel === "self")?.href;
|
|
327
|
+
const url = toAbsolute(
|
|
328
|
+
preAggregationLink.href,
|
|
329
|
+
selfLink || fallbackBaseUrl,
|
|
330
|
+
);
|
|
331
|
+
return await axios.get(url).then((resp) => resp.data);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.warn("[eodash] Failed to fetch pre-aggregation", e);
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
342
338
|
/**
|
|
343
339
|
* Recursively find all layers whose ID up to the first ; is same as given layer
|
|
344
340
|
*
|
|
@@ -347,6 +343,9 @@ export const extractLayerTimeValues = (items, currentStep) => {
|
|
|
347
343
|
* @returns {import("@eox/map").EoxLayer[]} Matching layer objects.
|
|
348
344
|
*/
|
|
349
345
|
export const findLayersByLayerPrefix = (layers, referenceLayer) => {
|
|
346
|
+
if (!layers || !referenceLayer) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
350
349
|
const refId = referenceLayer?.properties?.id;
|
|
351
350
|
|
|
352
351
|
if (typeof refId !== "string" || !refId.includes(";:;")) {
|
|
@@ -391,6 +390,39 @@ export const findLayer = (layers, layer) => {
|
|
|
391
390
|
}
|
|
392
391
|
};
|
|
393
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Removes JSON layers by ID from the layer tree
|
|
395
|
+
* @param {import("@eox/map").EoxLayer[]} layers
|
|
396
|
+
* @param {string[]} layerIds
|
|
397
|
+
* @returns {import("@eox/map").EoxLayer[]}
|
|
398
|
+
**/
|
|
399
|
+
export const removeLayers = (layers, layerIds) => {
|
|
400
|
+
const result = [];
|
|
401
|
+
for (const layer of layers) {
|
|
402
|
+
// if the layer is hidden, do not include it without checking if it's a group
|
|
403
|
+
if (layer.properties?.id && layerIds.includes(layer.properties.id)) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (layer.type === "Group" && Array.isArray(layer.layers)) {
|
|
407
|
+
const newGroupLayers = removeLayers(layer.layers, layerIds);
|
|
408
|
+
// if the group is not hidden, add it with the updated layers (if any were removed)
|
|
409
|
+
result.push(
|
|
410
|
+
newGroupLayers !== layer.layers
|
|
411
|
+
? { ...layer, layers: newGroupLayers }
|
|
412
|
+
: layer,
|
|
413
|
+
);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
result.push(layer);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return result.length === layers.length &&
|
|
421
|
+
result.every((l, i) => l === layers[i])
|
|
422
|
+
? layers
|
|
423
|
+
: result;
|
|
424
|
+
};
|
|
425
|
+
|
|
394
426
|
/**
|
|
395
427
|
* Removes one or more layers (by id) from a layer/group structure and inserts
|
|
396
428
|
* new layers in place of the first removed one.
|
|
@@ -701,7 +733,12 @@ export function getDatetimeProperty(linksOrItems) {
|
|
|
701
733
|
}
|
|
702
734
|
|
|
703
735
|
// TODO: consider other properties for datetime ranges
|
|
704
|
-
|
|
736
|
+
|
|
737
|
+
const datetimeProperties = /** @type {const} */ ([
|
|
738
|
+
"datetime",
|
|
739
|
+
"start_datetime",
|
|
740
|
+
"end_datetime",
|
|
741
|
+
]);
|
|
705
742
|
if (checkProperties) {
|
|
706
743
|
for (const prop of datetimeProperties) {
|
|
707
744
|
const propExists = linksOrItems.some(
|
|
@@ -743,6 +780,65 @@ export function isSTACItem(stacObject) {
|
|
|
743
780
|
);
|
|
744
781
|
}
|
|
745
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Fetches items using a split strategy (past/future) around a center date
|
|
785
|
+
* @param {string} itemsUrl
|
|
786
|
+
* @param {string} query
|
|
787
|
+
* @param {string | Date} centerDatetime
|
|
788
|
+
* @param {number} maxNumber
|
|
789
|
+
* @returns {Promise<import("stac-ts").StacItem[]>}
|
|
790
|
+
*/
|
|
791
|
+
export const fetchSplitItems = async (
|
|
792
|
+
itemsUrl,
|
|
793
|
+
query,
|
|
794
|
+
centerDatetime,
|
|
795
|
+
maxNumber,
|
|
796
|
+
) => {
|
|
797
|
+
const center = new Date(centerDatetime).toISOString();
|
|
798
|
+
const limit = Math.ceil(maxNumber / 2);
|
|
799
|
+
|
|
800
|
+
/** @param {"past"|"future"} direction */
|
|
801
|
+
const fetchSide = async (direction) => {
|
|
802
|
+
const isPast = direction === "past";
|
|
803
|
+
const datetimeRange = isPast ? `../${center}` : `${center}/..`;
|
|
804
|
+
const sortOrder = isPast ? "-datetime" : "+datetime";
|
|
805
|
+
const queryParams = new URLSearchParams(query);
|
|
806
|
+
|
|
807
|
+
queryParams.set("limit", limit.toString());
|
|
808
|
+
queryParams.set("datetime", datetimeRange);
|
|
809
|
+
queryParams.set("sortby", sortOrder);
|
|
810
|
+
|
|
811
|
+
const splitUrl = itemsUrl.split("/");
|
|
812
|
+
const collectionId = /** @type {string} */ (splitUrl.at(-2));
|
|
813
|
+
queryParams.set("collection", collectionId);
|
|
814
|
+
|
|
815
|
+
const searchEndpoint = `${splitUrl.slice(0, -3).join("/")}/search`;
|
|
816
|
+
const items = await axios
|
|
817
|
+
.get(searchEndpoint, { params: queryParams })
|
|
818
|
+
.then((res) => res.data.features);
|
|
819
|
+
return items;
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const [pastItems, futureItems] = await Promise.all([
|
|
823
|
+
fetchSide("past"),
|
|
824
|
+
fetchSide("future"),
|
|
825
|
+
]);
|
|
826
|
+
|
|
827
|
+
const allItems = [...pastItems, ...futureItems];
|
|
828
|
+
|
|
829
|
+
// check for duplicates by ids
|
|
830
|
+
const seen = new Set();
|
|
831
|
+
const uniqueItems = [];
|
|
832
|
+
for (const item of allItems) {
|
|
833
|
+
if (!seen.has(item.id)) {
|
|
834
|
+
seen.add(item.id);
|
|
835
|
+
uniqueItems.push(item);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return uniqueItems;
|
|
840
|
+
};
|
|
841
|
+
|
|
746
842
|
/**
|
|
747
843
|
* Fetch all STAC items from a STAC API endpoint.
|
|
748
844
|
* @param {string} itemsUrl
|
|
@@ -750,6 +846,8 @@ export function isSTACItem(stacObject) {
|
|
|
750
846
|
* @param {number} [limit=100] - The maximum number of items to fetch per request.
|
|
751
847
|
* @param {boolean} [returnFirst] - If true, only the first page of results will be returned.
|
|
752
848
|
* @param {number} [maxNumber=1000] - if the matched number of items exceed this, only the first page will be returned.
|
|
849
|
+
* @param {string | Date} [centerDatetime] - Date to center the search around if items exceed maxNumber
|
|
850
|
+
* @returns {Promise<import("stac-ts").StacItem[]>}
|
|
753
851
|
*/
|
|
754
852
|
export async function fetchApiItems(
|
|
755
853
|
itemsUrl,
|
|
@@ -757,23 +855,42 @@ export async function fetchApiItems(
|
|
|
757
855
|
limit = 100,
|
|
758
856
|
returnFirst = false,
|
|
759
857
|
maxNumber = 1000,
|
|
858
|
+
centerDatetime,
|
|
760
859
|
) {
|
|
761
|
-
|
|
762
|
-
|
|
860
|
+
// Exclude centerDatetime from cache key - it's only used for split search fallback
|
|
861
|
+
const cacheKey = JSON.stringify({
|
|
862
|
+
itemsUrl,
|
|
863
|
+
query,
|
|
864
|
+
limit,
|
|
865
|
+
returnFirst,
|
|
866
|
+
maxNumber,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
if (itemsCache.has(cacheKey)) {
|
|
870
|
+
return itemsCache.get(cacheKey) ?? [];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const urlQuery = new URLSearchParams(query);
|
|
874
|
+
urlQuery.set("limit", limit.toString());
|
|
875
|
+
|
|
876
|
+
let finalItemsUrl = itemsUrl.includes("?")
|
|
877
|
+
? itemsUrl.split("?")[0]
|
|
878
|
+
: itemsUrl;
|
|
879
|
+
finalItemsUrl += urlQuery.keys().toArray().length
|
|
880
|
+
? `?${urlQuery.toString()}`
|
|
881
|
+
: "";
|
|
763
882
|
|
|
764
883
|
const itemsFeatureCollection = await axios
|
|
765
|
-
.get(
|
|
884
|
+
.get(finalItemsUrl)
|
|
766
885
|
.then((resp) => resp.data);
|
|
886
|
+
|
|
767
887
|
/** @type {import("stac-ts").StacItem[]} */
|
|
768
888
|
const items = itemsFeatureCollection.features;
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
(link) => link.rel === "next",
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
if (!nextLink || returnFirst) {
|
|
889
|
+
if (returnFirst) {
|
|
890
|
+
itemsCache.set(cacheKey, items);
|
|
775
891
|
return items;
|
|
776
892
|
}
|
|
893
|
+
|
|
777
894
|
/** @type {number} */
|
|
778
895
|
const matchedItems = itemsFeatureCollection.numberMatched;
|
|
779
896
|
// Avoid fetching too many items
|
|
@@ -781,26 +898,70 @@ export async function fetchApiItems(
|
|
|
781
898
|
console.warn(
|
|
782
899
|
`[eodash] The number of items matched (${matchedItems}) exceeds the maximum allowed (${maxNumber})`,
|
|
783
900
|
);
|
|
784
|
-
|
|
785
|
-
|
|
901
|
+
// we try to narrow down the search around the center datetime
|
|
902
|
+
if (centerDatetime) {
|
|
903
|
+
// Check if we have a cached split result that covers this centerDatetime
|
|
904
|
+
const splitCacheKey = JSON.stringify({ itemsUrl, query, maxNumber });
|
|
905
|
+
const cachedSplit = splitItemsCache.get(splitCacheKey);
|
|
906
|
+
const centerTime = new Date(centerDatetime).getTime();
|
|
786
907
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
908
|
+
if (
|
|
909
|
+
cachedSplit &&
|
|
910
|
+
centerTime >= cachedSplit.minTime &&
|
|
911
|
+
centerTime <= cachedSplit.maxTime
|
|
912
|
+
) {
|
|
913
|
+
return cachedSplit.items;
|
|
914
|
+
}
|
|
792
915
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
916
|
+
const narrowedItems = await fetchSplitItems(
|
|
917
|
+
itemsUrl,
|
|
918
|
+
query ?? "",
|
|
919
|
+
centerDatetime,
|
|
920
|
+
maxNumber,
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
if (!narrowedItems.length) {
|
|
924
|
+
return narrowedItems;
|
|
925
|
+
}
|
|
926
|
+
const datetimeProperty = getDatetimeProperty(narrowedItems);
|
|
927
|
+
if (!datetimeProperty) {
|
|
928
|
+
return narrowedItems;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const times = narrowedItems
|
|
932
|
+
.map((i) =>
|
|
933
|
+
i.properties[datetimeProperty]
|
|
934
|
+
? new Date(i.properties[datetimeProperty]).getTime()
|
|
935
|
+
: null,
|
|
936
|
+
)
|
|
937
|
+
.filter((t) => t !== null);
|
|
938
|
+
if (!times.length) {
|
|
939
|
+
return narrowedItems;
|
|
940
|
+
}
|
|
941
|
+
splitItemsCache.set(splitCacheKey, {
|
|
942
|
+
items: narrowedItems,
|
|
943
|
+
minTime: Math.min(...times),
|
|
944
|
+
maxTime: Math.max(...times),
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
return narrowedItems;
|
|
799
948
|
}
|
|
949
|
+
|
|
950
|
+
itemsCache.set(cacheKey, items);
|
|
951
|
+
return items;
|
|
800
952
|
}
|
|
801
953
|
|
|
802
|
-
|
|
803
|
-
|
|
954
|
+
urlQuery.set("limit", maxNumber.toString());
|
|
955
|
+
|
|
956
|
+
const allItems = await axios
|
|
957
|
+
.get(itemsUrl + "?" + urlQuery.toString())
|
|
958
|
+
.then((resp) => resp.data.features)
|
|
959
|
+
.catch((err) => {
|
|
960
|
+
console.error(err);
|
|
961
|
+
return [];
|
|
962
|
+
});
|
|
963
|
+
items.splice(0, items.length, ...allItems);
|
|
964
|
+
itemsCache.set(cacheKey, items);
|
|
804
965
|
return items;
|
|
805
966
|
}
|
|
806
967
|
/**
|
|
@@ -838,3 +999,168 @@ export function extractEoxLegendLink(link) {
|
|
|
838
999
|
}
|
|
839
1000
|
return extraProperties;
|
|
840
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Locate the first sub-schema whose `format` matches by walking `properties`
|
|
1005
|
+
* and the `oneOf` / `allOf` / `anyOf` combinators. Returns the schema path
|
|
1006
|
+
* (array of keys/indices) from the root schema to the matched node, or
|
|
1007
|
+
* undefined if not found. Empty array means the input schema itself matched.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {Record<string, any> | null | undefined} schema
|
|
1010
|
+
* @param {string} [format="bands"]
|
|
1011
|
+
* @returns {(string | number)[] | undefined}
|
|
1012
|
+
*/
|
|
1013
|
+
export function getBandsProperty(schema, format = "bands") {
|
|
1014
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
1015
|
+
if (schema.format === format) return [];
|
|
1016
|
+
|
|
1017
|
+
if (schema.properties) {
|
|
1018
|
+
for (const key of Object.keys(schema.properties)) {
|
|
1019
|
+
const sub = getBandsProperty(schema.properties[key], format);
|
|
1020
|
+
if (sub) return ["properties", key, ...sub];
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for (const combinator of ["oneOf", "allOf", "anyOf"]) {
|
|
1025
|
+
if (!Array.isArray(schema[combinator])) continue;
|
|
1026
|
+
for (let i = 0; i < schema[combinator].length; i++) {
|
|
1027
|
+
const sub = getBandsProperty(schema[combinator][i], format);
|
|
1028
|
+
if (sub) return [combinator, i, ...sub];
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Checks whether a GeoZarr layer's bands changed in the jsonform output and,
|
|
1037
|
+
* if so, rebuilds the source with the new 3 selected bands.
|
|
1038
|
+
* Gamma and rescale are handled automatically by `applyUpdatedStyles` via
|
|
1039
|
+
* `updateStyleVariables` — this function only manages source reconstruction.
|
|
1040
|
+
* Uses the existing source constructor to avoid import-version mismatches.
|
|
1041
|
+
*
|
|
1042
|
+
* @param {import("ol/layer/Layer").default} olLayer - Layer from layerConfig:change event
|
|
1043
|
+
* @param {Record<string, any>} jsonformValue - Current jsonform output
|
|
1044
|
+
* @returns {boolean} true if the source was rebuilt
|
|
1045
|
+
*/
|
|
1046
|
+
export function updateGeoZarrBands(olLayer, jsonformValue) {
|
|
1047
|
+
/** @type {import("@eox/map/src/layers").EOxLayerType<"WebGLTile","GeoZarr">} */
|
|
1048
|
+
const jsonLayer = olLayer.get("_jsonDefinition");
|
|
1049
|
+
const updatedBands = jsonformValue.bands;
|
|
1050
|
+
const isGeoZarr =
|
|
1051
|
+
jsonLayer?.type === "WebGLTile" && jsonLayer?.source?.type === "GeoZarr";
|
|
1052
|
+
if (!jsonLayer || !jsonLayer.source || !isGeoZarr || !updatedBands) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const oldBands = jsonLayer.source?.bands;
|
|
1057
|
+
if (JSON.stringify(updatedBands) === JSON.stringify(oldBands)) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
jsonLayer.source.bands = [...updatedBands];
|
|
1061
|
+
olLayer.setSource(
|
|
1062
|
+
new window.eoxMapAdvancedOlSources.GeoZarr(jsonLayer.source),
|
|
1063
|
+
);
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Applies titiler upscaling to an XYZ tile URL based on the matched endpoint config.
|
|
1069
|
+
* - titiler v1: appends `@2x` to the `{y}` tile coordinate
|
|
1070
|
+
* - titiler v2: adds `tilesize=512` query parameter (v2 removed the `@2x` suffix)
|
|
1071
|
+
* Plain strings in the config default to v1 behavior for backward compatibility.
|
|
1072
|
+
*
|
|
1073
|
+
* @param {string} url - The XYZ tile URL template
|
|
1074
|
+
* @param {Array<string | { url: string; titilerVersion?: 1 | 2 }>} upscalingEndpoints
|
|
1075
|
+
* @returns {{ url: string; tileSize: [number, number] } | null} null if no endpoint matches
|
|
1076
|
+
*/
|
|
1077
|
+
export function applyTitilerUpscaling(url, upscalingEndpoints) {
|
|
1078
|
+
const match = upscalingEndpoints.find((entry) => {
|
|
1079
|
+
const endpointUrl = typeof entry === "string" ? entry : entry.url;
|
|
1080
|
+
return url.includes(endpointUrl);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
if (!match) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const version = typeof match === "string" ? 1 : (match.titilerVersion ?? 1);
|
|
1088
|
+
|
|
1089
|
+
if (version === 2) {
|
|
1090
|
+
const [base, query] = url.split("?");
|
|
1091
|
+
const params = new URLSearchParams(query);
|
|
1092
|
+
params.set("tilesize", "512");
|
|
1093
|
+
return { url: `${base}?${params.toString()}`, tileSize: [512, 512] };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return { url: url.replace("{y}", "{y}@2x"), tileSize: [512, 512] };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Picks the render presets for a collection, preferring the collection's own
|
|
1101
|
+
* STAC `renders` extension and falling back to client-provided config
|
|
1102
|
+
*
|
|
1103
|
+
* @param {import("stac-ts").StacCollection | null | undefined} collection
|
|
1104
|
+
* @param {Record<string, Record<string, import("@/types").Render>> | undefined} [configRenders]
|
|
1105
|
+
* @returns {Record<string, import("@/types").Render> | undefined}
|
|
1106
|
+
*/
|
|
1107
|
+
export function resolveRenders(collection, configRenders) {
|
|
1108
|
+
if (collection?.renders) {
|
|
1109
|
+
return /** @type {Record<string, import("@/types").Render>} */ (
|
|
1110
|
+
collection.renders
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
return collection?.id ? configRenders?.[collection.id] : undefined;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Serializes an object into a TiTiler query string. Arrays join with commas;
|
|
1118
|
+
* nested arrays repeat the key (e.g. `rescale: [[0,1]]` -> `rescale=0,1`);
|
|
1119
|
+
* objects are JSON-encoded. Shared by the render-extension and mosaic paths.
|
|
1120
|
+
* @param {Record<string,any>} obj
|
|
1121
|
+
* @returns {string}
|
|
1122
|
+
*/
|
|
1123
|
+
export function encodeURLObject(obj) {
|
|
1124
|
+
let str = "";
|
|
1125
|
+
for (const key in obj) {
|
|
1126
|
+
const value = obj[key];
|
|
1127
|
+
if (value === null || value === undefined || value === "") {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const valueType = Array.isArray(value) ? "array" : typeof value;
|
|
1132
|
+
|
|
1133
|
+
switch (valueType) {
|
|
1134
|
+
case "array": {
|
|
1135
|
+
// Check if any element in the array is itself an array (multi-dimensional)
|
|
1136
|
+
const hasNestedArrays = value.some((/** @type {any} */ item) =>
|
|
1137
|
+
Array.isArray(item),
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
if (hasNestedArrays) {
|
|
1141
|
+
// For multi-dimensional arrays, repeat the key with different values
|
|
1142
|
+
for (const val of value) {
|
|
1143
|
+
if (Array.isArray(val)) {
|
|
1144
|
+
str += `${key}=${val.join(",")}&`;
|
|
1145
|
+
} else {
|
|
1146
|
+
str += `${key}=${val}&`;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
} else {
|
|
1150
|
+
// For simple arrays, join with commas
|
|
1151
|
+
str += `${key}=${value.join(",")}&`;
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
case "object": {
|
|
1156
|
+
str += `${key}=${encodeURI(JSON.stringify(value))}&`;
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
default: {
|
|
1160
|
+
str += `${key}=${encodeURIComponent(value)}&`;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return str;
|
|
1166
|
+
}
|