@eodash/eodash 5.7.0 → 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/EodashOverlay.vue +27 -4
- package/core/client/composables/DefineEodash.js +0 -1
- package/core/client/composables/DefineWidgets.js +3 -1
- package/core/client/eodashSTAC/EodashCollection.js +74 -20
- package/core/client/eodashSTAC/cql.js +64 -0
- package/core/client/eodashSTAC/createLayers.js +169 -116
- package/core/client/eodashSTAC/helpers.js +391 -98
- 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 +147 -28
- package/core/client/utils/bands-editor/exampleSchema.json +50 -50
- package/core/client/utils/states.js +10 -0
- package/dist/client/{DashboardLayout-kRsmsNW5.js → DashboardLayout-ePbdvUTE.js} +2 -2
- package/dist/client/{DynamicWebComponent-CnKhYo0K.js → DynamicWebComponent-g5SdRbjY.js} +1 -1
- package/dist/client/{EodashChart-CLDqE93C.js → EodashChart-ChrkA5oS.js} +7 -6
- package/dist/client/{EodashDatePicker-CMPDT-ce.js → EodashDatePicker-CY1TrsDo.js} +3 -3
- package/dist/client/{EodashItemFilter-BpdA8QQa.js → EodashItemFilter-DzshHDlS.js} +2 -1
- package/dist/client/{EodashLayerControl-BHpDA4_0.js → EodashLayerControl-DMCeMkSd.js} +50 -20
- package/dist/client/{EodashLayoutSwitcher-B1fRUt7G.js → EodashLayoutSwitcher-iJ8mX1rF.js} +3 -3
- package/dist/client/{EodashMapBtns-Cpned0tc.js → EodashMapBtns-C7Cr7q5Y.js} +200 -86
- package/dist/client/EodashStacInfo-K7-iuXa7.js +169 -0
- package/dist/client/{EodashTools-1zHz26gv.js → EodashTools-BEK7e7JM.js} +4 -4
- package/dist/client/{ExportState-DbOm8hBI.js → ExportState-CqIzfMYQ.js} +6 -168
- package/dist/client/{Footer-BKg-VVkj.js → Footer-1P0cmBOZ.js} +1 -1
- package/dist/client/{Header-DCrwCvmY.js → Header-CkkBaBNh.js} +2 -2
- package/dist/client/{MobileLayout-vWSHpluo.js → MobileLayout-BPf49kDU.js} +2 -2
- package/dist/client/{PopUp-9uimfK49.js → PopUp-DPZq5SWI.js} +4 -168
- package/dist/client/{ProcessList-CTTIZa06.js → ProcessList-DuI6WpS1.js} +5 -5
- package/dist/client/VDivider-Bj9LG_JV.js +168 -0
- package/dist/client/{VImg-CrmB76ZY.js → VImg-Ddv5OyMz.js} +2 -2
- package/dist/client/{VMain-OKZNpcEG.js → VMain-7yqk_qC3.js} +1 -1
- package/dist/client/{VTooltip-CJw2FyWk.js → VTooltip-2hAJyEoa.js} +2 -2
- package/dist/client/{WidgetsContainer-BaWOnE5R.js → WidgetsContainer-2DOrlwCB.js} +1 -1
- package/dist/client/{asWebComponent-BHWpMuE3.js → asWebComponent-BlFN2lkB.js} +456 -306
- package/dist/client/{async-DSKj6g1f.js → async-B2H8ZwUK.js} +2 -2
- package/dist/client/{create-layers-config-CBoYedqB.js → create-layers-config-u_vn5hPk.js} +17 -17
- package/dist/client/dialog-transition-BrMkMiNa.js +170 -0
- package/dist/client/eo-dash.js +1 -1
- package/dist/client/{forwardRefs-DH51zPUr.js → forwardRefs-DAlror6d.js} +8 -87
- package/dist/client/{handling-BrcDDlgS.js → handling-B6I1V5eG.js} +3 -3
- package/dist/client/{helpers-DjZVl_WF.js → helpers-CBqIUSXJ.js} +794 -182
- package/dist/client/index-BWq1ZKaV.js +3168 -0
- package/dist/client/{index-CUjl-ZsI.js → index-CJ9UI3ln.js} +2 -2
- package/dist/client/{index-CSG_JoOA.js → index-DXLz4sFR.js} +62 -56
- package/dist/client/{index-1JKE68n3.js → index-DhKU6KqM.js} +211 -67
- package/dist/client/{index-Cu3-13Xg.js → index-DjQlbWbJ.js} +7 -6
- package/dist/client/mosaic-BFYLEwyx.js +455 -0
- package/dist/client/templates.js +250 -119
- package/dist/client/{transition-OHcz5Qq1.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/eodashSTAC/EodashCollection.d.ts +57 -25
- package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
- package/dist/types/core/client/eodashSTAC/createLayers.d.ts +42 -23
- package/dist/types/core/client/eodashSTAC/helpers.d.ts +77 -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 +123 -24
- package/dist/types/core/client/utils/states.d.ts +12 -0
- package/dist/types/templates/baseConfig.d.ts +1 -100
- 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 +35 -16
- package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +14 -10
- package/dist/types/widgets/EodashMap/index.vue.d.ts +86 -37
- package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -1
- 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/states.d.ts +1 -1
- package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
- package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +20 -0
- package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +3 -8
- package/dist/types/widgets/EodashTimeSlider/types.d.ts +6 -16
- package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
- package/package.json +33 -33
- 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 +42 -11
- package/widgets/EodashMap/EodashMapBtns.vue +109 -48
- package/widgets/EodashMap/index.vue +89 -105
- package/widgets/EodashMap/methods/btns.js +37 -5
- package/widgets/EodashMap/methods/create-layers-config.js +16 -16
- package/widgets/EodashMap/methods/index.js +71 -8
- package/widgets/EodashMap/methods/use-map-loading.js +52 -0
- package/widgets/EodashProcess/index.vue +1 -0
- package/widgets/EodashProcess/states.js +1 -1
- package/widgets/EodashStacInfo.vue +64 -2
- package/widgets/EodashTimeSlider/index.vue +58 -2
- package/widgets/EodashTimeSlider/methods/index.js +5 -48
- package/widgets/EodashTimeSlider/types.ts +7 -17
- package/dist/client/EodashStacInfo-BQspAHKH.js +0 -107
- package/dist/client/index-5Jhsa8Xe.js +0 -570
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// check if items has a datetime value
|
|
264
|
-
const dateProperty = getDatetimeProperty(items);
|
|
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) {
|
|
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) {
|
|
313
278
|
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
314
279
|
}
|
|
315
280
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
281
|
+
const controlValues = dates.map((d) => d.toISOString()).sort();
|
|
282
|
+
const timeControlValues = controlValues.map((date) => ({ date }));
|
|
283
|
+
|
|
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(";:;")) {
|
|
@@ -734,7 +733,12 @@ export function getDatetimeProperty(linksOrItems) {
|
|
|
734
733
|
}
|
|
735
734
|
|
|
736
735
|
// TODO: consider other properties for datetime ranges
|
|
737
|
-
|
|
736
|
+
|
|
737
|
+
const datetimeProperties = /** @type {const} */ ([
|
|
738
|
+
"datetime",
|
|
739
|
+
"start_datetime",
|
|
740
|
+
"end_datetime",
|
|
741
|
+
]);
|
|
738
742
|
if (checkProperties) {
|
|
739
743
|
for (const prop of datetimeProperties) {
|
|
740
744
|
const propExists = linksOrItems.some(
|
|
@@ -776,6 +780,65 @@ export function isSTACItem(stacObject) {
|
|
|
776
780
|
);
|
|
777
781
|
}
|
|
778
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
|
+
|
|
779
842
|
/**
|
|
780
843
|
* Fetch all STAC items from a STAC API endpoint.
|
|
781
844
|
* @param {string} itemsUrl
|
|
@@ -783,6 +846,8 @@ export function isSTACItem(stacObject) {
|
|
|
783
846
|
* @param {number} [limit=100] - The maximum number of items to fetch per request.
|
|
784
847
|
* @param {boolean} [returnFirst] - If true, only the first page of results will be returned.
|
|
785
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[]>}
|
|
786
851
|
*/
|
|
787
852
|
export async function fetchApiItems(
|
|
788
853
|
itemsUrl,
|
|
@@ -790,23 +855,42 @@ export async function fetchApiItems(
|
|
|
790
855
|
limit = 100,
|
|
791
856
|
returnFirst = false,
|
|
792
857
|
maxNumber = 1000,
|
|
858
|
+
centerDatetime,
|
|
793
859
|
) {
|
|
794
|
-
|
|
795
|
-
|
|
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
|
+
: "";
|
|
796
882
|
|
|
797
883
|
const itemsFeatureCollection = await axios
|
|
798
|
-
.get(
|
|
884
|
+
.get(finalItemsUrl)
|
|
799
885
|
.then((resp) => resp.data);
|
|
886
|
+
|
|
800
887
|
/** @type {import("stac-ts").StacItem[]} */
|
|
801
888
|
const items = itemsFeatureCollection.features;
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
(link) => link.rel === "next",
|
|
805
|
-
);
|
|
806
|
-
|
|
807
|
-
if (!nextLink || returnFirst) {
|
|
889
|
+
if (returnFirst) {
|
|
890
|
+
itemsCache.set(cacheKey, items);
|
|
808
891
|
return items;
|
|
809
892
|
}
|
|
893
|
+
|
|
810
894
|
/** @type {number} */
|
|
811
895
|
const matchedItems = itemsFeatureCollection.numberMatched;
|
|
812
896
|
// Avoid fetching too many items
|
|
@@ -814,26 +898,70 @@ export async function fetchApiItems(
|
|
|
814
898
|
console.warn(
|
|
815
899
|
`[eodash] The number of items matched (${matchedItems}) exceeds the maximum allowed (${maxNumber})`,
|
|
816
900
|
);
|
|
817
|
-
|
|
818
|
-
|
|
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();
|
|
907
|
+
|
|
908
|
+
if (
|
|
909
|
+
cachedSplit &&
|
|
910
|
+
centerTime >= cachedSplit.minTime &&
|
|
911
|
+
centerTime <= cachedSplit.maxTime
|
|
912
|
+
) {
|
|
913
|
+
return cachedSplit.items;
|
|
914
|
+
}
|
|
819
915
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
916
|
+
const narrowedItems = await fetchSplitItems(
|
|
917
|
+
itemsUrl,
|
|
918
|
+
query ?? "",
|
|
919
|
+
centerDatetime,
|
|
920
|
+
maxNumber,
|
|
921
|
+
);
|
|
825
922
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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;
|
|
832
948
|
}
|
|
949
|
+
|
|
950
|
+
itemsCache.set(cacheKey, items);
|
|
951
|
+
return items;
|
|
833
952
|
}
|
|
834
953
|
|
|
835
|
-
|
|
836
|
-
|
|
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);
|
|
837
965
|
return items;
|
|
838
966
|
}
|
|
839
967
|
/**
|
|
@@ -871,3 +999,168 @@ export function extractEoxLegendLink(link) {
|
|
|
871
999
|
}
|
|
872
1000
|
return extraProperties;
|
|
873
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
|
+
}
|