@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.
Files changed (113) hide show
  1. package/core/client/components/EodashOverlay.vue +27 -4
  2. package/core/client/composables/DefineEodash.js +0 -1
  3. package/core/client/composables/DefineWidgets.js +3 -1
  4. package/core/client/eodashSTAC/EodashCollection.js +74 -20
  5. package/core/client/eodashSTAC/cql.js +64 -0
  6. package/core/client/eodashSTAC/createLayers.js +169 -116
  7. package/core/client/eodashSTAC/helpers.js +391 -98
  8. package/core/client/eodashSTAC/mosaic.js +416 -0
  9. package/core/client/plugins/axios.js +31 -1
  10. package/core/client/store/actions.js +1 -1
  11. package/core/client/store/stac.js +25 -4
  12. package/core/client/store/states.js +20 -2
  13. package/core/client/types.ts +147 -28
  14. package/core/client/utils/bands-editor/exampleSchema.json +50 -50
  15. package/core/client/utils/states.js +10 -0
  16. package/dist/client/{DashboardLayout-kRsmsNW5.js → DashboardLayout-ePbdvUTE.js} +2 -2
  17. package/dist/client/{DynamicWebComponent-CnKhYo0K.js → DynamicWebComponent-g5SdRbjY.js} +1 -1
  18. package/dist/client/{EodashChart-CLDqE93C.js → EodashChart-ChrkA5oS.js} +7 -6
  19. package/dist/client/{EodashDatePicker-CMPDT-ce.js → EodashDatePicker-CY1TrsDo.js} +3 -3
  20. package/dist/client/{EodashItemFilter-BpdA8QQa.js → EodashItemFilter-DzshHDlS.js} +2 -1
  21. package/dist/client/{EodashLayerControl-BHpDA4_0.js → EodashLayerControl-DMCeMkSd.js} +50 -20
  22. package/dist/client/{EodashLayoutSwitcher-B1fRUt7G.js → EodashLayoutSwitcher-iJ8mX1rF.js} +3 -3
  23. package/dist/client/{EodashMapBtns-Cpned0tc.js → EodashMapBtns-C7Cr7q5Y.js} +200 -86
  24. package/dist/client/EodashStacInfo-K7-iuXa7.js +169 -0
  25. package/dist/client/{EodashTools-1zHz26gv.js → EodashTools-BEK7e7JM.js} +4 -4
  26. package/dist/client/{ExportState-DbOm8hBI.js → ExportState-CqIzfMYQ.js} +6 -168
  27. package/dist/client/{Footer-BKg-VVkj.js → Footer-1P0cmBOZ.js} +1 -1
  28. package/dist/client/{Header-DCrwCvmY.js → Header-CkkBaBNh.js} +2 -2
  29. package/dist/client/{MobileLayout-vWSHpluo.js → MobileLayout-BPf49kDU.js} +2 -2
  30. package/dist/client/{PopUp-9uimfK49.js → PopUp-DPZq5SWI.js} +4 -168
  31. package/dist/client/{ProcessList-CTTIZa06.js → ProcessList-DuI6WpS1.js} +5 -5
  32. package/dist/client/VDivider-Bj9LG_JV.js +168 -0
  33. package/dist/client/{VImg-CrmB76ZY.js → VImg-Ddv5OyMz.js} +2 -2
  34. package/dist/client/{VMain-OKZNpcEG.js → VMain-7yqk_qC3.js} +1 -1
  35. package/dist/client/{VTooltip-CJw2FyWk.js → VTooltip-2hAJyEoa.js} +2 -2
  36. package/dist/client/{WidgetsContainer-BaWOnE5R.js → WidgetsContainer-2DOrlwCB.js} +1 -1
  37. package/dist/client/{asWebComponent-BHWpMuE3.js → asWebComponent-BlFN2lkB.js} +456 -306
  38. package/dist/client/{async-DSKj6g1f.js → async-B2H8ZwUK.js} +2 -2
  39. package/dist/client/{create-layers-config-CBoYedqB.js → create-layers-config-u_vn5hPk.js} +17 -17
  40. package/dist/client/dialog-transition-BrMkMiNa.js +170 -0
  41. package/dist/client/eo-dash.js +1 -1
  42. package/dist/client/{forwardRefs-DH51zPUr.js → forwardRefs-DAlror6d.js} +8 -87
  43. package/dist/client/{handling-BrcDDlgS.js → handling-B6I1V5eG.js} +3 -3
  44. package/dist/client/{helpers-DjZVl_WF.js → helpers-CBqIUSXJ.js} +794 -182
  45. package/dist/client/index-BWq1ZKaV.js +3168 -0
  46. package/dist/client/{index-CUjl-ZsI.js → index-CJ9UI3ln.js} +2 -2
  47. package/dist/client/{index-CSG_JoOA.js → index-DXLz4sFR.js} +62 -56
  48. package/dist/client/{index-1JKE68n3.js → index-DhKU6KqM.js} +211 -67
  49. package/dist/client/{index-Cu3-13Xg.js → index-DjQlbWbJ.js} +7 -6
  50. package/dist/client/mosaic-BFYLEwyx.js +455 -0
  51. package/dist/client/templates.js +250 -119
  52. package/dist/client/{transition-OHcz5Qq1.js → transition-D_W6Wn9v.js} +1 -1
  53. package/dist/node/cli.js +5 -5
  54. package/dist/node/types.d.ts +4 -0
  55. package/dist/types/core/client/composables/DefineEodash.d.ts +1 -1
  56. package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +57 -25
  57. package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
  58. package/dist/types/core/client/eodashSTAC/createLayers.d.ts +42 -23
  59. package/dist/types/core/client/eodashSTAC/helpers.d.ts +77 -18
  60. package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
  61. package/dist/types/core/client/store/stac.d.ts +31 -7
  62. package/dist/types/core/client/store/states.d.ts +24 -2
  63. package/dist/types/core/client/types.d.ts +123 -24
  64. package/dist/types/core/client/utils/states.d.ts +12 -0
  65. package/dist/types/templates/baseConfig.d.ts +1 -100
  66. package/dist/types/templates/compare.d.ts +32 -3
  67. package/dist/types/templates/expert.d.ts +18 -0
  68. package/dist/types/templates/explore.d.ts +9 -1
  69. package/dist/types/templates/lite.d.ts +11 -0
  70. package/dist/types/widgets/EodashChart.vue.d.ts +5 -3
  71. package/dist/types/widgets/EodashItemCatalog/index.vue.d.ts +129 -2
  72. package/dist/types/widgets/EodashItemCatalog/methods/filters.d.ts +28 -6
  73. package/dist/types/widgets/EodashItemCatalog/methods/handlers.d.ts +17 -4
  74. package/dist/types/widgets/EodashItemCatalog/methods/map.d.ts +16 -4
  75. package/dist/types/widgets/EodashLayerControl.vue.d.ts +35 -16
  76. package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +14 -10
  77. package/dist/types/widgets/EodashMap/index.vue.d.ts +86 -37
  78. package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -1
  79. package/dist/types/widgets/EodashMap/methods/index.d.ts +5 -1
  80. package/dist/types/widgets/EodashMap/methods/use-map-loading.d.ts +1 -0
  81. package/dist/types/widgets/EodashProcess/index.vue.d.ts +5 -3
  82. package/dist/types/widgets/EodashProcess/states.d.ts +1 -1
  83. package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
  84. package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +20 -0
  85. package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +3 -8
  86. package/dist/types/widgets/EodashTimeSlider/types.d.ts +6 -16
  87. package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
  88. package/package.json +33 -33
  89. package/templates/baseConfig.js +36 -6
  90. package/templates/compare.js +62 -7
  91. package/templates/expert.js +23 -0
  92. package/templates/explore.js +11 -5
  93. package/templates/lite.js +14 -0
  94. package/widgets/EodashChart.vue +1 -0
  95. package/widgets/EodashItemCatalog/index.vue +363 -35
  96. package/widgets/EodashItemCatalog/methods/filters.js +134 -70
  97. package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
  98. package/widgets/EodashItemCatalog/methods/map.js +142 -28
  99. package/widgets/EodashLayerControl.vue +42 -11
  100. package/widgets/EodashMap/EodashMapBtns.vue +109 -48
  101. package/widgets/EodashMap/index.vue +89 -105
  102. package/widgets/EodashMap/methods/btns.js +37 -5
  103. package/widgets/EodashMap/methods/create-layers-config.js +16 -16
  104. package/widgets/EodashMap/methods/index.js +71 -8
  105. package/widgets/EodashMap/methods/use-map-loading.js +52 -0
  106. package/widgets/EodashProcess/index.vue +1 -0
  107. package/widgets/EodashProcess/states.js +1 -1
  108. package/widgets/EodashStacInfo.vue +64 -2
  109. package/widgets/EodashTimeSlider/index.vue +58 -2
  110. package/widgets/EodashTimeSlider/methods/index.js +5 -48
  111. package/widgets/EodashTimeSlider/types.ts +7 -17
  112. package/dist/client/EodashStacInfo-BQspAHKH.js +0 -107
  113. 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
- * Extracts layercontrol LayerDatetime property from STAC Links
255
- * @param {import("stac-ts").StacLink[] | import("stac-ts").StacItem[] | undefined} [items]
256
- * @param {string|null} [currentStep]
257
- **/
258
- export const extractLayerTimeValues = (items, currentStep) => {
259
- if (!currentStep || !items?.length) {
260
- return { layerDatetime: undefined, timeControlValues: undefined };
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
- // item datetime is not included in the item links datetime
317
- if (!timeValues.some((val) => val.date === currentStep)) {
318
- const currentStepTime = new Date(currentStep).getTime();
319
- currentStep = timeValues.reduce((time, step) => {
320
- const aDiff = Math.abs(new Date(time).getTime() - currentStepTime);
321
- const bDiff = Math.abs(new Date(step.date).getTime() - currentStepTime);
322
- return bDiff < aDiff ? step.date : time;
323
- }, timeValues[0].date);
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: timeValues.map((d) => d.date).sort(),
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
- const datetimeProperties = ["datetime", "start_datetime", "end_datetime"];
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
- itemsUrl = itemsUrl.includes("?") ? itemsUrl.split("?")[0] : itemsUrl;
795
- itemsUrl += query ? `?limit=${limit}&${query}` : `?limit=${limit}`;
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(itemsUrl)
884
+ .get(finalItemsUrl)
799
885
  .then((resp) => resp.data);
886
+
800
887
  /** @type {import("stac-ts").StacItem[]} */
801
888
  const items = itemsFeatureCollection.features;
802
- const nextLink = itemsFeatureCollection.links?.find(
803
- //@ts-expect-error TODO: itemsFeatureCollection is not typed
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
- return items;
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
- let [nextLinkURL, nextLinkQuery] = nextLink.href.split("?");
821
- nextLinkQuery = nextLinkQuery.replace(/limit=\d+/, "");
822
- if (query) {
823
- const queryParams = new URLSearchParams(query);
824
- const nextLinkParams = new URLSearchParams(nextLinkQuery);
916
+ const narrowedItems = await fetchSplitItems(
917
+ itemsUrl,
918
+ query ?? "",
919
+ centerDatetime,
920
+ maxNumber,
921
+ );
825
922
 
826
- for (const key of nextLinkParams.keys()) {
827
- queryParams.delete(key);
828
- }
829
- const remainingQuery = queryParams.toString();
830
- if (remainingQuery) {
831
- nextLinkQuery += `&${remainingQuery}`;
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
- const nextPage = await fetchApiItems(nextLinkURL, nextLinkQuery);
836
- items.push(...nextPage);
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
+ }