@eodash/eodash 5.6.1 → 5.7.0

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 (81) hide show
  1. package/core/client/components/DashboardLayout.vue +1 -1
  2. package/core/client/composables/index.js +6 -2
  3. package/core/client/eodashSTAC/EodashCollection.js +5 -25
  4. package/core/client/eodashSTAC/createLayers.js +23 -9
  5. package/core/client/eodashSTAC/helpers.js +33 -0
  6. package/core/client/types.ts +8 -1
  7. package/core/client/utils/index.js +4 -0
  8. package/dist/client/{DashboardLayout-CqMb7OXc.js → DashboardLayout-kRsmsNW5.js} +5 -5
  9. package/dist/client/{DynamicWebComponent-DqKqTf7l.js → DynamicWebComponent-CnKhYo0K.js} +1 -1
  10. package/dist/client/{EodashChart-PkUQfH0k.js → EodashChart-CLDqE93C.js} +4 -4
  11. package/dist/client/{EodashDatePicker-DLJGdTtX.js → EodashDatePicker-CMPDT-ce.js} +3 -3
  12. package/dist/client/{EodashItemFilter-Dk8KjFcX.js → EodashItemFilter-BpdA8QQa.js} +1 -1
  13. package/dist/client/{EodashLayerControl-CC1Cogrn.js → EodashLayerControl-BHpDA4_0.js} +25 -7
  14. package/dist/client/{EodashLayoutSwitcher-5WZYd6rr.js → EodashLayoutSwitcher-B1fRUt7G.js} +3 -3
  15. package/dist/client/EodashMapBtns-Cpned0tc.js +478 -0
  16. package/dist/client/{EodashStacInfo-B90wobPc.js → EodashStacInfo-BQspAHKH.js} +1 -1
  17. package/dist/client/{EodashTools-CGZtTh45.js → EodashTools-1zHz26gv.js} +4 -4
  18. package/dist/client/{ExportState-Ce0ZnrX_.js → ExportState-DbOm8hBI.js} +4 -4
  19. package/dist/client/{Footer-CnZqEiop.js → Footer-BKg-VVkj.js} +1 -1
  20. package/dist/client/{Header-Dr5PBIm8.js → Header-DCrwCvmY.js} +2 -2
  21. package/dist/client/MobileLayout-vWSHpluo.js +118 -0
  22. package/dist/client/{PopUp-DjvE_71e.js → PopUp-9uimfK49.js} +2 -2
  23. package/dist/client/{ProcessList-kf3olI7W.js → ProcessList-CTTIZa06.js} +4 -4
  24. package/dist/client/{VImg-Bj6uWt6j.js → VImg-CrmB76ZY.js} +2 -2
  25. package/dist/client/{VMain-Cm0UmKGi.js → VMain-OKZNpcEG.js} +1 -1
  26. package/dist/client/{VTooltip-DbBu-ky3.js → VTooltip-CJw2FyWk.js} +2 -2
  27. package/dist/client/{WidgetsContainer-CfLnd44U.js → WidgetsContainer-BaWOnE5R.js} +1 -1
  28. package/dist/client/{asWebComponent-DfULAbpq.js → asWebComponent-BHWpMuE3.js} +47 -39
  29. package/dist/client/{async-CIbKXEEe.js → async-DSKj6g1f.js} +21 -10
  30. package/dist/client/create-layers-config-CBoYedqB.js +182 -0
  31. package/dist/client/eo-dash.js +1 -1
  32. package/dist/client/{forwardRefs-yj3VWOK5.js → forwardRefs-DH51zPUr.js} +2 -2
  33. package/dist/client/{handling-s8xt2JwF.js → handling-BrcDDlgS.js} +123 -85
  34. package/dist/client/{helpers-DAVGYl_f.js → helpers-DjZVl_WF.js} +2273 -1500
  35. package/dist/client/{index-4TrjfJ32.js → index-1JKE68n3.js} +49 -194
  36. package/dist/client/{index-CugS__t5.js → index-5Jhsa8Xe.js} +3 -3
  37. package/dist/client/index-CSG_JoOA.js +436 -0
  38. package/dist/client/{index-CYz6dT7K.js → index-CUjl-ZsI.js} +2 -2
  39. package/dist/client/{index-Bu8BMA9q.js → index-Cu3-13Xg.js} +69 -83
  40. package/dist/client/material-symbols-outlined.woff2 +0 -0
  41. package/dist/client/material-symbols-rounded.woff2 +0 -0
  42. package/dist/client/material-symbols-sharp.woff2 +0 -0
  43. package/dist/client/material-symbols-subset.woff2 +0 -0
  44. package/dist/client/templates.js +1 -1
  45. package/dist/client/{transition-DaOsiD4u.js → transition-OHcz5Qq1.js} +1 -1
  46. package/dist/node/cli.js +1 -1
  47. package/dist/types/core/client/composables/index.d.ts +1 -3
  48. package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +6 -7
  49. package/dist/types/core/client/eodashSTAC/createLayers.d.ts +3 -3
  50. package/dist/types/core/client/eodashSTAC/helpers.d.ts +1 -0
  51. package/dist/types/core/client/types.d.ts +8 -1
  52. package/dist/types/templates/baseConfig.d.ts +12 -0
  53. package/dist/types/widgets/EodashLayerControl.vue.d.ts +1 -1
  54. package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +9 -0
  55. package/dist/types/widgets/EodashMap/index.vue.d.ts +7 -0
  56. package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -8
  57. package/dist/types/widgets/EodashProcess/methods/composables.d.ts +1 -1
  58. package/dist/types/widgets/EodashProcess/methods/handling.d.ts +13 -8
  59. package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +34 -0
  60. package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +39 -0
  61. package/dist/types/widgets/EodashTimeSlider/types.d.ts +62 -0
  62. package/package.json +32 -32
  63. package/widgets/EodashLayerControl.vue +22 -3
  64. package/widgets/EodashMap/EodashMapBtns.vue +46 -1
  65. package/widgets/EodashMap/index.vue +17 -5
  66. package/widgets/EodashMap/methods/create-layers-config.js +2 -0
  67. package/widgets/EodashMap/methods/index.js +23 -5
  68. package/widgets/EodashProcess/index.vue +1 -1
  69. package/widgets/EodashProcess/methods/async.js +19 -8
  70. package/widgets/EodashProcess/methods/composables.js +63 -77
  71. package/widgets/EodashProcess/methods/custom-endpoints/chart/sentinelhub-endpoint.js +9 -5
  72. package/widgets/EodashProcess/methods/custom-endpoints/chart/veda-endpoint.js +7 -4
  73. package/widgets/EodashProcess/methods/handling.js +105 -72
  74. package/widgets/EodashTimeSlider/index.vue +152 -0
  75. package/widgets/EodashTimeSlider/methods/index.js +279 -0
  76. package/widgets/EodashTimeSlider/types.ts +64 -0
  77. package/dist/client/EodashMapBtns-BHrSDNoC.js +0 -435
  78. package/dist/client/EodashTimeSlider-5vm7F3ju.js +0 -53
  79. package/dist/client/MobileLayout-DyMjaeif.js +0 -118
  80. package/dist/types/widgets/EodashTimeSlider.vue.d.ts +0 -15
  81. package/widgets/EodashTimeSlider.vue +0 -40
@@ -35,6 +35,7 @@ import { getLayers, getCompareLayers } from "@/store/actions";
35
35
  * @param {import("vue").Ref<boolean>} params.isProcessed
36
36
  * @param {import("vue").Ref<boolean>} params.loading
37
37
  * @param {import("vue").Ref<boolean>} params.isPolling
38
+ * @param {import("@eox/map").EOxMap | null} params.mapElement
38
39
  * @param {boolean} params.enableCompare
39
40
  */
40
41
  export async function initProcess({
@@ -45,6 +46,7 @@ export async function initProcess({
45
46
  loading,
46
47
  isPolling,
47
48
  enableCompare,
49
+ mapElement,
48
50
  }) {
49
51
  const isPoiAlive = enableCompare ? !!comparePoi.value : !!poi.value;
50
52
  let updatedJsonform = null;
@@ -59,6 +61,27 @@ export async function initProcess({
59
61
  jsonformSchema.value = null;
60
62
  return;
61
63
  }
64
+
65
+ if (!updatedJsonform) {
66
+ resetProcess({
67
+ loading,
68
+ isProcessed,
69
+ jsonformSchema,
70
+ isPolling,
71
+ processResults,
72
+ enableCompare,
73
+ });
74
+ return;
75
+ }
76
+
77
+ const newJsonForm = await updateJsonformIdentifier({
78
+ jsonformSchema: updatedJsonform,
79
+ newLayers: enableCompare ? getCompareLayers() : getLayers(),
80
+ enableCompare,
81
+ mapElement,
82
+ });
83
+ if (!newJsonForm) return;
84
+
62
85
  resetProcess({
63
86
  loading,
64
87
  isProcessed,
@@ -67,99 +90,109 @@ export async function initProcess({
67
90
  processResults,
68
91
  enableCompare,
69
92
  });
93
+ jsonformSchema.value = newJsonForm;
94
+ }
70
95
 
71
- if (updatedJsonform) {
72
- // make sure correct target layer id is used in jsonform
73
- const newJsonForm = await updateJsonformIdentifier({
74
- jsonformSchema: updatedJsonform,
75
- newLayers: enableCompare ? getCompareLayers() : getLayers(),
76
- enableCompare,
77
- });
78
- // trigger jsonform update in next tick
79
- jsonformSchema.value = null;
80
- await new Promise((resolve) => setTimeout(resolve, 0));
81
- jsonformSchema.value = newJsonForm;
96
+ /**
97
+ * Recursively search grouped layer tree for a layer whose id starts with the given prefix
98
+ *
99
+ * @param {Record<string, any>[]} layers
100
+ * @param {string} prefix
101
+ * @returns {string | null}
102
+ */
103
+ function findLayerIdByPrefix(layers, prefix) {
104
+ for (const layer of layers) {
105
+ if (layer.type === "Group" && Array.isArray(layer.layers)) {
106
+ const found = findLayerIdByPrefix(layer.layers, prefix);
107
+ if (found) return found;
108
+ } else if (layer.properties?.id?.startsWith(prefix)) {
109
+ return layer.properties.id;
110
+ }
82
111
  }
112
+ return null;
83
113
  }
84
114
 
85
115
  /**
86
- * Update the jsonform schema to have the correct layer id from the map
116
+ * Wait until a layer's features are loaded for the drawtools to attach correctly
117
+ * Resolves immediately if already loaded.
118
+ *
119
+ * @param {import("@eox/map").AnyLayerWithSource | null | undefined} layer
120
+ * @returns {Promise<void>}
121
+ */
122
+ function waitForLayerRender(layer) {
123
+ return new Promise((resolve, reject) => {
124
+ if (!layer || layer.get("type") !== "Vector" || !("getSource" in layer))
125
+ return resolve();
126
+ const source = /** @type {import("ol/source/Vector").default} */ (
127
+ layer.getSource()
128
+ );
129
+ if (!source || source.getFeatures().length > 0) return resolve();
130
+ source.once("featuresloadend", () => resolve());
131
+ source.once("featuresloaderror", () =>
132
+ reject(new Error(`Layer ${layer.get("id")} failed to load features`)),
133
+ );
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Update the jsonform schema to have the correct layer id from the map.
139
+ * Returns null if any required layerId cannot be resolved (layer not yet in map).
140
+ * Callers should treat null as "retry when layers:updated fires".
87
141
  *
88
142
  * @export
89
- * @async
90
143
  * @param {Object} params
91
- * @param {Record<string,any> | null} params.jsonformSchema params.jsonformSchema
92
- * @param {Record<string, any>[] | undefined} params.newLayers params.newLayers
93
- * @param { boolean } params.enableCompare params.enableCompare
94
- * @returns {Promise<Record<string,any> | null>} updated jsonform schema
144
+ * @param {Record<string,any> | null} params.jsonformSchema
145
+ * @param {Record<string, any>[] | undefined} params.newLayers
146
+ * @param {boolean} params.enableCompare
147
+ * @param {import("@eox/map").EOxMap | null | undefined} params.mapElement
148
+ * @returns {Promise<Record<string,any> | null>} updated schema, or null if a layerId could not be resolved
95
149
  */
96
150
  export async function updateJsonformIdentifier({
97
151
  jsonformSchema,
98
152
  newLayers,
99
153
  enableCompare,
154
+ mapElement,
100
155
  }) {
101
- const form = jsonformSchema;
102
- if (!form) {
103
- return null;
104
- }
156
+ if (!jsonformSchema || !newLayers) return null;
157
+
158
+ const form = JSON.parse(JSON.stringify(jsonformSchema));
105
159
  const drawToolsProperties = getDrawToolsProperties(form);
106
- drawToolsProperties.forEach((drawToolsProperty) => {
107
- if (
108
- drawToolsProperty &&
109
- form?.properties[drawToolsProperty]?.options?.drawtools?.for &&
110
- enableCompare
111
- ) {
112
- form.properties[drawToolsProperty].options.drawtools.for =
113
- "eox-map#compare";
160
+ /** @type {Promise<void>[]} */
161
+ const renderPromises = [];
162
+
163
+ for (const drawToolsProperty of drawToolsProperties) {
164
+ if (!drawToolsProperty) continue;
165
+
166
+ const drawtoolsOptions =
167
+ form?.properties[drawToolsProperty]?.options?.drawtools;
168
+ if (!drawtoolsOptions) continue;
169
+
170
+ if (drawtoolsOptions.for && enableCompare) {
171
+ drawtoolsOptions.for = "eox-map#compare";
114
172
  }
115
- if (
116
- drawToolsProperty &&
117
- newLayers &&
118
- form?.properties[drawToolsProperty]?.options?.drawtools?.layerId
119
- ) {
120
- // get partial or full id and try to match with correct eoxmap layer
121
- // check if newLayers is an array or an object with layers property
122
- let layers = newLayers;
123
- // @ts-expect-error TODO payload coming from time update sometimes is not an object with layers property
124
- if (newLayers.layers && Array.isArray(newLayers.layers)) {
125
- // @ts-expect-error TODO payload coming from time update sometimes is not an object with layers property
126
- layers = newLayers.layers;
127
- }
128
173
 
129
- const layerId =
130
- form.properties[drawToolsProperty].options.drawtools.layerId.split(
131
- ";:;",
132
- )[0];
133
- let matchedLayerId = null;
134
- // layers are not flat can be grouped, we need to recursively search
135
- const traverseLayers = (
136
- /** @type {Record<string, any>[] | undefined} */ layersArray,
137
- ) => {
138
- if (!layersArray) {
139
- return;
140
- }
141
- for (const layer of layersArray) {
142
- if (layer.type === "Group" && Array.isArray(layer.layers)) {
143
- traverseLayers(layer.layers);
144
- } else {
145
- if (layer.properties?.id?.startsWith(layerId)) {
146
- matchedLayerId = layer.properties.id;
147
- break;
148
- }
149
- }
174
+ if (drawtoolsOptions.layerId) {
175
+ let layer = mapElement?.getLayerById(drawtoolsOptions.layerId);
176
+ if (!layer) {
177
+ const prefix = drawtoolsOptions.layerId.split(";:;")[0];
178
+ const resolvedId = findLayerIdByPrefix(newLayers, prefix);
179
+ if (!resolvedId) {
180
+ console.warn(
181
+ `Could not find matching layer for processing form with id: ${prefix}`,
182
+ );
183
+ return null;
150
184
  }
151
- };
152
- traverseLayers(layers);
153
- if (matchedLayerId) {
154
- form.properties[drawToolsProperty].options.drawtools.layerId =
155
- matchedLayerId;
156
- } else {
157
- console.warn(
158
- `Could not find matching layer for processing form with id: ${layerId}`,
159
- );
185
+ drawtoolsOptions.layerId = resolvedId;
186
+ layer = mapElement?.getLayerById(resolvedId);
160
187
  }
188
+ renderPromises.push(waitForLayerRender(layer));
161
189
  }
190
+ }
191
+
192
+ await Promise.all(renderPromises).catch((err) => {
193
+ console.warn("[eodash] layer load failed before jsonform attach:", err);
162
194
  });
195
+
163
196
  return form;
164
197
  }
165
198
 
@@ -0,0 +1,152 @@
1
+ <template>
2
+ <eox-timecontrol
3
+ v-if="hasMultipleItems"
4
+ :key="unref(mapEl)"
5
+ ref="eoxTimecontrol"
6
+ .for="mapEl"
7
+ @select="onSelect"
8
+ titleKey="title"
9
+ .externalMapRendering="true"
10
+ .animate="animate"
11
+ .initDate="initDate"
12
+ >
13
+ <div class="d-flex g-10 align-center">
14
+ <eox-timecontrol-date class="flex-grow-1"></eox-timecontrol-date>
15
+ <eox-timecontrol-picker
16
+ .range="false"
17
+ .showDots="true"
18
+ .showItems="true"
19
+ .popup="true"
20
+ ></eox-timecontrol-picker>
21
+
22
+ <eox-itemfilter
23
+ v-if="filters.length"
24
+ .inlineMode="true"
25
+ :showResults="false"
26
+ .filterProperties="filters"
27
+ ></eox-itemfilter>
28
+ <eox-timecontrol-timelapse @export="onExport"></eox-timecontrol-timelapse>
29
+ </div>
30
+
31
+ <eox-timecontrol-timeline class="mt-2"></eox-timecontrol-timeline>
32
+ </eox-timecontrol>
33
+ </template>
34
+ <script setup>
35
+ import { datetime, mapEl } from "@/store/states";
36
+ import { eodashCollections } from "@/utils/states";
37
+ import "@eox/timecontrol";
38
+ import "@eox/itemfilter";
39
+
40
+ import { computed, onMounted, ref, unref, useTemplateRef } from "vue";
41
+ import { storeToRefs } from "pinia";
42
+ import { useSTAcStore } from "@/store/stac";
43
+ import { createAnimationLayers } from "./methods";
44
+
45
+ const { animate } = defineProps({
46
+ filters: {
47
+ /** @type {import("vue").PropType<import("@eox/itemfilter").EOxItemFilter["filterProperties"]>} */
48
+ type: Array,
49
+ default: () => [],
50
+ },
51
+ animate: {
52
+ type: Boolean,
53
+ default: true,
54
+ },
55
+ });
56
+
57
+ /** @type {import("vue").ShallowRef<HTMLElement>} */
58
+ const timesliderEl = useTemplateRef("eoxTimecontrol");
59
+
60
+ const startDate = new Date(datetime.value);
61
+ const endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
62
+
63
+ const selectedRange = /** @type {import("vue").Ref<[string, string]>} */ (
64
+ ref([startDate, endDate].map((d) => d.toISOString()))
65
+ );
66
+ const initDate = [startDate.toISOString().split("T")[0]];
67
+
68
+ const hasMultipleItems = computed(() => {
69
+ return eodashCollections.some((ec) => {
70
+ const itemLinks = ec.collectionStac?.links.filter((l) => l.rel === "item");
71
+ const itemsLink = ec.collectionStac?.links.some((l) => l.rel === "items");
72
+ return (itemLinks && itemLinks.length > 1) || itemsLink;
73
+ });
74
+ });
75
+
76
+ /**
77
+ * Handles the selection event from the time control component.
78
+ * It finds the closest item to the "from" selected date
79
+ * and updates the global datetime state with that item's original date.
80
+ *
81
+ * @param {CustomEvent<import("./types").TimelineSelectionEventDetail>} e
82
+ */
83
+ const onSelect = (e) => {
84
+ const { selectedItems, date } = e.detail;
85
+ // Update the selected range with the new dates
86
+ selectedRange.value = date;
87
+
88
+ const allItems = Object.keys(selectedItems ?? {}).flatMap(
89
+ (id) => selectedItems[id],
90
+ );
91
+ if (!allItems.length) {
92
+ return;
93
+ }
94
+ const [from, _to] = date;
95
+ const fromDate = new Date(from).getTime();
96
+
97
+ const closestItem = allItems.reduce((prev, curr) => {
98
+ const prevDiff = Math.abs(new Date(prev.originalDate).getTime() - fromDate);
99
+ const currDiff = Math.abs(new Date(curr.originalDate).getTime() - fromDate);
100
+ return currDiff < prevDiff ? curr : prev;
101
+ });
102
+
103
+ if (closestItem) {
104
+ datetime.value = closestItem.originalDate;
105
+ }
106
+ };
107
+
108
+ const { selectedStac, stacEndpoint } = storeToRefs(useSTAcStore());
109
+
110
+ /**
111
+ *
112
+ * @param {CustomEvent<import("./types").TimelineExportEventDetail>} evt
113
+ */
114
+ const onExport = async (evt) => {
115
+ const { generate, selectedRangeItems, filters } = evt.detail;
116
+
117
+ if (!stacEndpoint.value) {
118
+ return;
119
+ }
120
+
121
+ const mapLayers = await createAnimationLayers(
122
+ stacEndpoint.value,
123
+ selectedRange.value,
124
+ selectedRangeItems,
125
+ selectedStac,
126
+ filters,
127
+ );
128
+
129
+ if (!mapLayers?.length) {
130
+ console.warn("[eodash] No map layers generated for the animation.");
131
+ return;
132
+ }
133
+
134
+ generate({
135
+ mapLayers,
136
+ });
137
+ };
138
+
139
+ onMounted(() => {
140
+ const parentDiv = timesliderEl.value?.parentElement;
141
+ const layoutItem = parentDiv?.parentElement;
142
+ if (parentDiv && layoutItem && animate) {
143
+ parentDiv.style.overflow = "visible";
144
+ layoutItem.style.overflow = "visible";
145
+ }
146
+ });
147
+ </script>
148
+ <style scoped>
149
+ eox-itemfilter {
150
+ --inline-container-height: 40px;
151
+ }
152
+ </style>
@@ -0,0 +1,279 @@
1
+ import { createLayersConfig } from "^/EodashMap/methods/create-layers-config";
2
+ import { eodashCollections } from "@/utils/states";
3
+ import axios from "@/plugins/axios";
4
+ import { mapEl } from "@/store/states";
5
+ import { removeLayers, sanitizeBbox } from "@/eodashSTAC/helpers";
6
+ import { getLayers } from "@/store/actions";
7
+
8
+ /**
9
+ * @param {string} stacEndpoint
10
+ * @param {[string, string]} selectedRange
11
+ * @param {import("../types").TimelineExportEventDetail["selectedRangeItems"]} selectedRangeItems
12
+ * @param {import("vue").Ref<import("stac-ts").StacCollection|null>} selectedStac
13
+ * @param {Record<string, import("../types").Filter>} filters
14
+ */
15
+ export async function createAnimationLayers(
16
+ stacEndpoint,
17
+ selectedRange,
18
+ selectedRangeItems,
19
+ selectedStac,
20
+ filters,
21
+ ) {
22
+ if (eodashCollections[0].isAPI) {
23
+ return await createAPILayers(
24
+ stacEndpoint,
25
+ { min: selectedRange[0], max: selectedRange[1] },
26
+ mapEl.value?.lonLatExtent,
27
+ selectedStac,
28
+ filters,
29
+ );
30
+ }
31
+ const { collections: hiddenCollections, layers: hiddenLayers } =
32
+ getHiddenLayers([...getLayers()]);
33
+
34
+ return await Promise.all(
35
+ Object.values(selectedRangeItems).flatMap(async (itemSet) => {
36
+ /** @type {Array<{ layers: Record<string, any>[]; date: string }>} */
37
+ const mapLayersArr = [];
38
+ for (const dateItem of itemSet) {
39
+ await createLayersConfig(
40
+ selectedStac.value,
41
+ eodashCollections.filter(
42
+ (collection) =>
43
+ !hiddenCollections.includes(collection.collectionStac?.id ?? ""),
44
+ ),
45
+ dateItem.originalDate,
46
+ ).then((layers) => {
47
+ //@ts-expect-error createLayersConfig is not typed strictly
48
+ layers = removeLayers(layers, hiddenLayers);
49
+ //@ts-expect-error createLayersConfig is not typed strictly
50
+ layers = restoreLayersVisibility(layers);
51
+ layers = anonimizeLayersCORS(layers);
52
+ mapLayersArr.push({
53
+ layers,
54
+ date: dateItem.originalDate,
55
+ });
56
+ });
57
+ }
58
+ return mapLayersArr;
59
+ }),
60
+ ).then((results) => results.flat());
61
+ }
62
+ /**
63
+ *
64
+ * @param {string} stacEndpoint
65
+ * @param {{min: string, max: string}} date
66
+ * @param {number[] | undefined} bbox
67
+ * @param {import("vue").Ref<import("stac-ts").StacCollection|null>} selectedStac
68
+ * @param {Record<string, import("../types").Filter>} filters
69
+ * @return {Promise<Array<{ layers: Record<string, any>[]; date: string }>>}
70
+ */
71
+ async function createAPILayers(
72
+ stacEndpoint,
73
+ date = { min: "", max: "" },
74
+ bbox,
75
+ selectedStac,
76
+ filters,
77
+ ) {
78
+ if (!bbox) {
79
+ return [];
80
+ }
81
+ const url = new URL(stacEndpoint + "/search");
82
+ url.searchParams.set("limit", "100");
83
+ url.searchParams.set("collections", selectedStac.value?.id ?? "");
84
+ url.searchParams.set(
85
+ "datetime",
86
+ `${new Date(date.min).toISOString()}/${new Date(date.max).toISOString()}`,
87
+ );
88
+ url.searchParams.set("bbox", sanitizeBbox(bbox).join(","));
89
+
90
+ const stacFilter = buildStacFilters(filters);
91
+ if (stacFilter) {
92
+ url.searchParams.set("filter", stacFilter);
93
+ }
94
+
95
+ /** @type {import("stac-ts").StacItem[]} */
96
+ const items = await axios
97
+ .get(url.href)
98
+ .then((res) => res.data.features)
99
+ .catch((err) => {
100
+ console.error("[eodash] Error fetching items for animation:", err);
101
+ return [];
102
+ });
103
+ if (!items || !items.length) {
104
+ console.warn("[eodash] No items found for animation.");
105
+ return [];
106
+ }
107
+ const { collections: hiddenCollections, layers: hiddenLayers } =
108
+ getHiddenLayers([...getLayers()]);
109
+ return await Promise.all(
110
+ items.map(async (item) => {
111
+ /** @type {Array<{ layers: Record<string, any>[]; date: string }>} */
112
+ const mapLayersArr = [];
113
+ await createLayersConfig(
114
+ selectedStac.value,
115
+ eodashCollections.filter(
116
+ (collection) =>
117
+ !hiddenCollections.includes(collection.collectionStac?.id ?? ""),
118
+ ),
119
+ item,
120
+ ).then((layers) => {
121
+ //@ts-expect-error createLayersConfig is not typed strictly
122
+ layers = removeLayers(layers, hiddenLayers);
123
+ //@ts-expect-error createLayersConfig is not typed strictly
124
+ layers = restoreLayersVisibility(layers);
125
+ layers = anonimizeLayersCORS(layers);
126
+ mapLayersArr.push({
127
+ layers,
128
+ date: /** @type {string} */ (item.properties.datetime),
129
+ });
130
+ return mapLayersArr;
131
+ });
132
+ return mapLayersArr;
133
+ }),
134
+ ).then((results) => results.flat());
135
+ }
136
+ /**
137
+ *
138
+ * @param {Record<string, any>[]} layers
139
+ * @returns {Record<string, any>[]}
140
+ */
141
+ export function anonimizeLayersCORS(layers) {
142
+ return layers.map((layer) => {
143
+ if (layer.type === "Group") {
144
+ layer.layers = anonimizeLayersCORS(layer.layers);
145
+ return layer;
146
+ }
147
+ if (layer.source) {
148
+ layer.source.crossOrigin = "anonymous";
149
+ }
150
+ return layer;
151
+ });
152
+ }
153
+ /**
154
+ * returns the list of layers that has visibility hidden
155
+ * @param {import("@eox/map").EoxLayer[]} layers
156
+ * @returns {{ collections: string[]; layers: string[] }}
157
+ */
158
+ export function getHiddenLayers(layers) {
159
+ /** @type {{ collections: string[]; layers: string[] }} */
160
+ const result = { collections: [], layers: [] };
161
+
162
+ for (const layer of layers) {
163
+ // check inner layers if it's a group layer first
164
+ if (layer.type === "Group" && Array.isArray(layer.layers)) {
165
+ const childResult = getHiddenLayers(layer.layers);
166
+ for (const col of childResult.collections) {
167
+ if (!result.collections.includes(col)) {
168
+ result.collections.push(col);
169
+ }
170
+ }
171
+ for (const lyr of childResult.layers) {
172
+ if (!result.layers.includes(lyr)) {
173
+ result.layers.push(lyr);
174
+ }
175
+ }
176
+ }
177
+
178
+ if (!layer.properties?.id) {
179
+ continue;
180
+ }
181
+
182
+ const olLayer = mapEl.value?.getLayerById(layer.properties?.id);
183
+ if (!olLayer) {
184
+ continue;
185
+ }
186
+
187
+ if (olLayer.getVisible() === false) {
188
+ const refId = layer.properties.id;
189
+ if (refId) {
190
+ if (refId.includes(";:;")) {
191
+ // Check if this looks like a typical eodash collection ID with separator
192
+ const parts = refId.split(";:;");
193
+ if (parts.length > 2) {
194
+ const prefix = parts[0];
195
+ if (!result.collections.includes(prefix)) {
196
+ result.collections.push(prefix);
197
+ }
198
+ } else {
199
+ // It has a separator but might just be a base layer like `layerId;:;EPSG`
200
+ if (!result.layers.includes(refId)) {
201
+ result.layers.push(refId);
202
+ }
203
+ }
204
+ } else {
205
+ if (!result.layers.includes(refId)) {
206
+ result.layers.push(refId);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * Iterates through a list of layers and updates layer.properties.visible to true
218
+ * if it is set to false
219
+ * @param {import("@eox/map").EoxLayer[]} layers
220
+ * @returns {import("@eox/map").EoxLayer[]}
221
+ */
222
+ export function restoreLayersVisibility(layers) {
223
+ for (const layer of layers) {
224
+ if (layer.properties && layer.properties.visible === false) {
225
+ layer.properties.visible = true;
226
+ }
227
+
228
+ if (layer.type === "Group" && Array.isArray(layer.layers)) {
229
+ restoreLayersVisibility(layer.layers);
230
+ }
231
+ }
232
+ return layers;
233
+ }
234
+
235
+ /**
236
+ * Build STAC API filter string from TimeSlider filters
237
+ * @param {Record<string, import("../types").Filter>} filters
238
+ * @returns {string}
239
+ */
240
+ export function buildStacFilters(filters) {
241
+ if (!filters) return "";
242
+
243
+ /** @type {string[]} */
244
+ const stacFilters = [];
245
+
246
+ Object.values(filters).forEach((filter) => {
247
+ if (!filter || !filter.key) return;
248
+
249
+ // strip 'properties.' from key if present
250
+ const propName = filter.key.startsWith("properties.")
251
+ ? filter.key.replace("properties.", "")
252
+ : filter.key;
253
+
254
+ if (filter.type === "range" && filter.state) {
255
+ if (
256
+ filter.state.min !== undefined &&
257
+ filter.state.min > (filter.min ?? -Infinity)
258
+ ) {
259
+ stacFilters.push(`${propName}>=${filter.state.min}`);
260
+ }
261
+ if (
262
+ filter.state.max !== undefined &&
263
+ filter.state.max < (filter.max ?? Infinity)
264
+ ) {
265
+ stacFilters.push(`${propName}<=${filter.state.max}`);
266
+ }
267
+ } else if (filter.type === "multiselect" && filter.stringifiedState) {
268
+ if (filter.stringifiedState.length > 0) {
269
+ stacFilters.push(`${propName} IN (${filter.stringifiedState})`);
270
+ }
271
+ } else if (filter.type === "select" && filter.stringifiedState) {
272
+ if (filter.stringifiedState) {
273
+ stacFilters.push(`${propName}='${filter.stringifiedState}'`);
274
+ }
275
+ }
276
+ });
277
+
278
+ return stacFilters.join(" AND ");
279
+ }