@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.
Files changed (133) hide show
  1. package/core/client/components/DashboardLayout.vue +1 -1
  2. package/core/client/components/EodashOverlay.vue +27 -4
  3. package/core/client/composables/DefineEodash.js +0 -1
  4. package/core/client/composables/DefineWidgets.js +3 -1
  5. package/core/client/composables/index.js +6 -2
  6. package/core/client/eodashSTAC/EodashCollection.js +79 -45
  7. package/core/client/eodashSTAC/cql.js +64 -0
  8. package/core/client/eodashSTAC/createLayers.js +192 -125
  9. package/core/client/eodashSTAC/helpers.js +423 -97
  10. package/core/client/eodashSTAC/mosaic.js +416 -0
  11. package/core/client/plugins/axios.js +31 -1
  12. package/core/client/store/actions.js +1 -1
  13. package/core/client/store/stac.js +25 -4
  14. package/core/client/store/states.js +20 -2
  15. package/core/client/types.ts +155 -29
  16. package/core/client/utils/bands-editor/exampleSchema.json +50 -50
  17. package/core/client/utils/index.js +4 -0
  18. package/core/client/utils/states.js +10 -0
  19. package/dist/client/{DashboardLayout-CqMb7OXc.js → DashboardLayout-ePbdvUTE.js} +5 -5
  20. package/dist/client/{DynamicWebComponent-DqKqTf7l.js → DynamicWebComponent-g5SdRbjY.js} +1 -1
  21. package/dist/client/{EodashChart-PkUQfH0k.js → EodashChart-ChrkA5oS.js} +7 -6
  22. package/dist/client/{EodashDatePicker-DLJGdTtX.js → EodashDatePicker-CY1TrsDo.js} +3 -3
  23. package/dist/client/{EodashItemFilter-Dk8KjFcX.js → EodashItemFilter-DzshHDlS.js} +2 -1
  24. package/dist/client/{EodashLayerControl-CC1Cogrn.js → EodashLayerControl-DMCeMkSd.js} +70 -22
  25. package/dist/client/{EodashLayoutSwitcher-5WZYd6rr.js → EodashLayoutSwitcher-iJ8mX1rF.js} +3 -3
  26. package/dist/client/EodashMapBtns-C7Cr7q5Y.js +592 -0
  27. package/dist/client/EodashStacInfo-K7-iuXa7.js +169 -0
  28. package/dist/client/{EodashTools-CGZtTh45.js → EodashTools-BEK7e7JM.js} +4 -4
  29. package/dist/client/{ExportState-Ce0ZnrX_.js → ExportState-CqIzfMYQ.js} +6 -168
  30. package/dist/client/{Footer-CnZqEiop.js → Footer-1P0cmBOZ.js} +1 -1
  31. package/dist/client/{Header-Dr5PBIm8.js → Header-CkkBaBNh.js} +2 -2
  32. package/dist/client/MobileLayout-BPf49kDU.js +118 -0
  33. package/dist/client/{PopUp-DjvE_71e.js → PopUp-DPZq5SWI.js} +4 -168
  34. package/dist/client/{ProcessList-kf3olI7W.js → ProcessList-DuI6WpS1.js} +5 -5
  35. package/dist/client/VDivider-Bj9LG_JV.js +168 -0
  36. package/dist/client/{VImg-Bj6uWt6j.js → VImg-Ddv5OyMz.js} +2 -2
  37. package/dist/client/{VMain-Cm0UmKGi.js → VMain-7yqk_qC3.js} +1 -1
  38. package/dist/client/{VTooltip-DbBu-ky3.js → VTooltip-2hAJyEoa.js} +2 -2
  39. package/dist/client/{WidgetsContainer-CfLnd44U.js → WidgetsContainer-2DOrlwCB.js} +1 -1
  40. package/dist/client/{asWebComponent-DfULAbpq.js → asWebComponent-BlFN2lkB.js} +493 -335
  41. package/dist/client/{async-CIbKXEEe.js → async-B2H8ZwUK.js} +21 -10
  42. package/dist/client/create-layers-config-u_vn5hPk.js +182 -0
  43. package/dist/client/dialog-transition-BrMkMiNa.js +170 -0
  44. package/dist/client/eo-dash.js +1 -1
  45. package/dist/client/{forwardRefs-yj3VWOK5.js → forwardRefs-DAlror6d.js} +8 -87
  46. package/dist/client/{handling-s8xt2JwF.js → handling-B6I1V5eG.js} +123 -85
  47. package/dist/client/{helpers-DAVGYl_f.js → helpers-CBqIUSXJ.js} +3034 -1649
  48. package/dist/client/index-BWq1ZKaV.js +3168 -0
  49. package/dist/client/{index-CYz6dT7K.js → index-CJ9UI3ln.js} +2 -2
  50. package/dist/client/index-DXLz4sFR.js +442 -0
  51. package/dist/client/{index-4TrjfJ32.js → index-DhKU6KqM.js} +230 -231
  52. package/dist/client/{index-Bu8BMA9q.js → index-DjQlbWbJ.js} +70 -83
  53. package/dist/client/material-symbols-outlined.woff2 +0 -0
  54. package/dist/client/material-symbols-rounded.woff2 +0 -0
  55. package/dist/client/material-symbols-sharp.woff2 +0 -0
  56. package/dist/client/material-symbols-subset.woff2 +0 -0
  57. package/dist/client/mosaic-BFYLEwyx.js +455 -0
  58. package/dist/client/templates.js +250 -119
  59. package/dist/client/{transition-DaOsiD4u.js → transition-D_W6Wn9v.js} +1 -1
  60. package/dist/node/cli.js +5 -5
  61. package/dist/node/types.d.ts +4 -0
  62. package/dist/types/core/client/composables/DefineEodash.d.ts +1 -1
  63. package/dist/types/core/client/composables/index.d.ts +1 -3
  64. package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +62 -31
  65. package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
  66. package/dist/types/core/client/eodashSTAC/createLayers.d.ts +44 -25
  67. package/dist/types/core/client/eodashSTAC/helpers.d.ts +78 -18
  68. package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
  69. package/dist/types/core/client/store/stac.d.ts +31 -7
  70. package/dist/types/core/client/store/states.d.ts +24 -2
  71. package/dist/types/core/client/types.d.ts +131 -25
  72. package/dist/types/core/client/utils/states.d.ts +12 -0
  73. package/dist/types/templates/baseConfig.d.ts +1 -88
  74. package/dist/types/templates/compare.d.ts +32 -3
  75. package/dist/types/templates/expert.d.ts +18 -0
  76. package/dist/types/templates/explore.d.ts +9 -1
  77. package/dist/types/templates/lite.d.ts +11 -0
  78. package/dist/types/widgets/EodashChart.vue.d.ts +5 -3
  79. package/dist/types/widgets/EodashItemCatalog/index.vue.d.ts +129 -2
  80. package/dist/types/widgets/EodashItemCatalog/methods/filters.d.ts +28 -6
  81. package/dist/types/widgets/EodashItemCatalog/methods/handlers.d.ts +17 -4
  82. package/dist/types/widgets/EodashItemCatalog/methods/map.d.ts +16 -4
  83. package/dist/types/widgets/EodashLayerControl.vue.d.ts +36 -17
  84. package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +23 -10
  85. package/dist/types/widgets/EodashMap/index.vue.d.ts +91 -35
  86. package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -8
  87. package/dist/types/widgets/EodashMap/methods/index.d.ts +5 -1
  88. package/dist/types/widgets/EodashMap/methods/use-map-loading.d.ts +1 -0
  89. package/dist/types/widgets/EodashProcess/index.vue.d.ts +5 -3
  90. package/dist/types/widgets/EodashProcess/methods/composables.d.ts +1 -1
  91. package/dist/types/widgets/EodashProcess/methods/handling.d.ts +13 -8
  92. package/dist/types/widgets/EodashProcess/states.d.ts +1 -1
  93. package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
  94. package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +54 -0
  95. package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +34 -0
  96. package/dist/types/widgets/EodashTimeSlider/types.d.ts +52 -0
  97. package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
  98. package/package.json +46 -46
  99. package/templates/baseConfig.js +36 -6
  100. package/templates/compare.js +62 -7
  101. package/templates/expert.js +23 -0
  102. package/templates/explore.js +11 -5
  103. package/templates/lite.js +14 -0
  104. package/widgets/EodashChart.vue +1 -0
  105. package/widgets/EodashItemCatalog/index.vue +363 -35
  106. package/widgets/EodashItemCatalog/methods/filters.js +134 -70
  107. package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
  108. package/widgets/EodashItemCatalog/methods/map.js +142 -28
  109. package/widgets/EodashLayerControl.vue +64 -14
  110. package/widgets/EodashMap/EodashMapBtns.vue +153 -47
  111. package/widgets/EodashMap/index.vue +101 -105
  112. package/widgets/EodashMap/methods/btns.js +37 -5
  113. package/widgets/EodashMap/methods/create-layers-config.js +18 -16
  114. package/widgets/EodashMap/methods/index.js +89 -8
  115. package/widgets/EodashMap/methods/use-map-loading.js +52 -0
  116. package/widgets/EodashProcess/index.vue +2 -1
  117. package/widgets/EodashProcess/methods/async.js +19 -8
  118. package/widgets/EodashProcess/methods/composables.js +63 -77
  119. package/widgets/EodashProcess/methods/custom-endpoints/chart/sentinelhub-endpoint.js +9 -5
  120. package/widgets/EodashProcess/methods/custom-endpoints/chart/veda-endpoint.js +7 -4
  121. package/widgets/EodashProcess/methods/handling.js +105 -72
  122. package/widgets/EodashProcess/states.js +1 -1
  123. package/widgets/EodashStacInfo.vue +64 -2
  124. package/widgets/EodashTimeSlider/index.vue +208 -0
  125. package/widgets/EodashTimeSlider/methods/index.js +236 -0
  126. package/widgets/EodashTimeSlider/types.ts +54 -0
  127. package/dist/client/EodashMapBtns-BHrSDNoC.js +0 -435
  128. package/dist/client/EodashStacInfo-B90wobPc.js +0 -107
  129. package/dist/client/EodashTimeSlider-5vm7F3ju.js +0 -53
  130. package/dist/client/MobileLayout-DyMjaeif.js +0 -118
  131. package/dist/client/index-CugS__t5.js +0 -570
  132. package/dist/types/widgets/EodashTimeSlider.vue.d.ts +0 -15
  133. package/widgets/EodashTimeSlider.vue +0 -40
@@ -1,17 +1,29 @@
1
1
  import { useOnLayersUpdate } from "@/composables";
2
- import { mapEl } from "@/store/states";
3
2
  import { onMounted, onUnmounted } from "vue";
3
+ import { createOnSelectHandler } from "./handlers";
4
+ import { tooltipAdapter } from "@/store/states";
4
5
 
5
6
  /**
6
7
  *
7
8
  * @param {import("@/types").GeoJsonFeature[]} features
9
+ * @param {import("vue").Ref<import("@eox/map").EOxMap | null>} mapElement
10
+ * @param {string[] | undefined} hoverProperties
11
+ * @param {object} [stacItemsStyle]
12
+ * @param {object} [stacItemsInteractionStyle]
8
13
  */
9
- export function renderItemsFeatures(features) {
14
+ export function renderItemsFeatures(
15
+ features,
16
+ mapElement,
17
+ hoverProperties,
18
+ stacItemsStyle,
19
+ stacItemsInteractionStyle,
20
+ ) {
21
+ const currentMap = mapElement.value;
10
22
  let analysisLayers =
11
23
  /** @type {import("@eox/map/src/layers").EOxLayerTypeGroup} */ (
12
- mapEl.value?.layers?.find((l) => l.properties?.id === "AnalysisGroup")
24
+ currentMap?.layers?.find((l) => l.properties?.id === "AnalysisGroup")
13
25
  );
14
- if (!mapEl.value || !features) {
26
+ if (!currentMap || !features) {
15
27
  return;
16
28
  }
17
29
  if (!analysisLayers) {
@@ -23,9 +35,9 @@ export function renderItemsFeatures(features) {
23
35
  },
24
36
  layers: [],
25
37
  };
26
- mapEl.value.layers = [...mapEl.value.layers, analysisLayers];
38
+ currentMap.layers = [...currentMap.layers, analysisLayers];
27
39
  }
28
-
40
+ /** @type {import("@eox/map/src/layers").EOxLayerType<"Vector","Vector">} */
29
41
  const stacItemsLayer = {
30
42
  type: "Vector",
31
43
  properties: {
@@ -34,14 +46,16 @@ export function renderItemsFeatures(features) {
34
46
  },
35
47
  source: {
36
48
  type: "Vector",
49
+ //@ts-expect-error todo
37
50
  url:
38
51
  "data:application/geo+json," +
39
52
  encodeURIComponent(
40
53
  JSON.stringify({ type: "FeatureCollection", features }),
41
54
  ),
42
55
  format: "GeoJSON",
56
+ projection: "EPSG:4326",
43
57
  },
44
- style: {
58
+ style: stacItemsStyle || {
45
59
  "fill-color": "transparent",
46
60
  "stroke-color": "#003170",
47
61
  },
@@ -49,35 +63,47 @@ export function renderItemsFeatures(features) {
49
63
  {
50
64
  type: "select",
51
65
  options: {
52
- id: "stac-items",
66
+ id: "stac-item-hover",
53
67
  condition: "pointermove",
54
- style: {
68
+ tooltip: hoverProperties?.length,
69
+ style: stacItemsInteractionStyle || {
55
70
  "stroke-color": "white",
56
71
  "stroke-width": 3,
57
72
  },
58
73
  },
59
74
  },
75
+ {
76
+ type: "select",
77
+ options: {
78
+ id: "stac-items",
79
+ condition: "click",
80
+ tooltip: false,
81
+ style: stacItemsInteractionStyle || {
82
+ "stroke-color": "#003047",
83
+ "stroke-width": 3.2,
84
+ },
85
+ },
86
+ },
60
87
  ],
61
88
  };
62
89
  const exists = analysisLayers.layers.some(
63
- (l) => l.properties?.id === "stac-items",
90
+ (l) => l.properties?.id === stacItemsLayer.properties?.id,
64
91
  );
65
92
  if (exists) {
66
- //@ts-expect-error todo
67
- mapEl.value.addOrUpdateLayer(stacItemsLayer);
93
+ currentMap.addOrUpdateLayer(stacItemsLayer);
68
94
  return;
69
95
  } else {
70
- //@ts-expect-error todo
71
- analysisLayers.layers.push(stacItemsLayer);
72
- mapEl.value.layers = [...mapEl.value.layers];
96
+ analysisLayers.layers.unshift(stacItemsLayer);
97
+ currentMap.layers = [...currentMap.layers];
73
98
  }
74
99
  }
75
100
 
76
101
  /**
77
102
  * @param {import("vue").Ref<any>} itemFilter
78
103
  * @param {boolean} bboxFilter
104
+ * @param {import("vue").Ref<import("@eox/map").EOxMap | null>} mapElement
79
105
  */
80
- export const useSearchOnMapMove = (itemFilter, bboxFilter) => {
106
+ export const useSearchOnMapMove = (itemFilter, bboxFilter, mapElement) => {
81
107
  if (!bboxFilter) {
82
108
  return;
83
109
  }
@@ -86,58 +112,146 @@ export const useSearchOnMapMove = (itemFilter, bboxFilter) => {
86
112
  const handler = () => {
87
113
  clearTimeout(timeout);
88
114
  timeout = setTimeout(() => {
115
+ if (mapElement.value?.projection == "globe") {
116
+ return;
117
+ }
89
118
  itemFilter.value?.search();
90
- }, 800); // 800ms debounce
119
+ }, 500);
91
120
  };
92
121
  onMounted(() => {
93
- mapEl.value?.map.on("moveend", handler);
122
+ mapElement.value?.map.on("moveend", handler);
94
123
  });
95
124
  onUnmounted(() => {
96
- mapEl.value?.map.un("moveend", handler);
125
+ mapElement.value?.map.un("moveend", handler);
97
126
  });
98
127
  };
99
128
  /**
100
129
  *
101
130
  * @param {import("vue").Ref<import("@/types").GeoJsonFeature[]>} currentItems
131
+ * @param {import("vue").Ref<import("@eox/map").EOxMap | null>} mapElement
132
+ * @param {string[] | undefined} hoverProperties
133
+ * @param {object} [stacItemsStyle]
134
+ * @param {object} [stacItemsInteractionStyle]
102
135
  */
103
- export const useRenderItemsFeatures = (currentItems) => {
136
+ export const useRenderItemsFeatures = (
137
+ currentItems,
138
+ mapElement,
139
+ hoverProperties,
140
+ stacItemsStyle,
141
+ stacItemsInteractionStyle,
142
+ ) => {
104
143
  onMounted(() => {
105
- renderItemsFeatures(currentItems.value);
144
+ renderItemsFeatures(
145
+ currentItems.value,
146
+ mapElement,
147
+ hoverProperties,
148
+ stacItemsStyle,
149
+ stacItemsInteractionStyle,
150
+ );
106
151
  });
107
152
 
108
153
  useOnLayersUpdate(() => {
109
154
  // consider cases where this is not needed
110
- renderItemsFeatures(currentItems.value);
155
+ renderItemsFeatures(
156
+ currentItems.value,
157
+ mapElement,
158
+ hoverProperties,
159
+ stacItemsStyle,
160
+ stacItemsInteractionStyle,
161
+ );
111
162
  });
112
163
  };
164
+ /**
165
+ * Registers a tooltip adapter that displays item properties when hovering over map features.
166
+ * @param {string[]} [hoverProperties]
167
+ */
168
+ export function useHoverTooltip(hoverProperties) {
169
+ /**
170
+ * @param {*} val
171
+ */
172
+ const formatValue = (val) => {
173
+ if (typeof val === "number") {
174
+ return val.toFixed(0);
175
+ }
176
+
177
+ if (typeof val === "string") {
178
+ // Attempt to parse as date if string looks like ISO date
179
+ if (val.length > 9 && /\d{4}-\d{2}-\d{2}/.test(val)) {
180
+ const timestamp = Date.parse(val);
181
+ if (!isNaN(timestamp)) {
182
+ return new Date(timestamp).toUTCString();
183
+ }
184
+ }
185
+ }
186
+ return val;
187
+ };
188
+ /**
189
+ * @param {string} key
190
+ */
191
+ const formatKey = (key) => {
192
+ let formattedKey = key;
193
+ if (key.includes(":")) {
194
+ formattedKey = key.split(":")[1];
195
+ }
196
+ return formattedKey
197
+ .replace(/_/g, " ")
198
+ .replace(/\b\w/g, (c) => c.toUpperCase());
199
+ };
200
+ onMounted(() => {
201
+ tooltipAdapter.value = ({ key, value }) => {
202
+ if (hoverProperties && hoverProperties.includes(key)) {
203
+ return { key: formatKey(key), value: formatValue(value) };
204
+ }
205
+ return undefined;
206
+ };
207
+ });
208
+ onUnmounted(() => {
209
+ tooltipAdapter.value = null;
210
+ });
211
+ }
212
+
113
213
  /**
114
214
  *
115
215
  * @param {import("vue").Ref<any>} itemfilterEl
216
+ * @param {ReturnType< typeof import("@/store/stac").useSTAcStore>} store
217
+ * @param {import("vue").Ref<import("@eox/map").EOxMap | null>} mapElement
218
+ * @param {boolean} enableCompare
116
219
  */
117
- export function useHighlightOnFeatureHover(itemfilterEl) {
220
+ export function useRenderOnFeatureClick(
221
+ itemfilterEl,
222
+ store,
223
+ mapElement,
224
+ enableCompare,
225
+ ) {
226
+ const onSelectItem = createOnSelectHandler(store, enableCompare, mapElement);
118
227
  /**
119
228
  *
120
229
  * @param {CustomEvent} evt
121
230
  */
122
231
  const handler = (evt) => {
232
+ if (evt.detail.originalEvent.type !== "click") {
233
+ return;
234
+ }
123
235
  const itemId = evt.detail?.feature?.getId();
124
236
  if (!itemId) {
125
237
  return;
126
238
  }
127
- const item = itemfilterEl.value.items?.find(
239
+ const item = itemfilterEl.value.results?.find(
128
240
  //@ts-expect-error todo
129
- (r) => r.id === itemId,
241
+ (r) => r?.id === itemId,
130
242
  );
243
+
131
244
  if (item) {
132
- itemfilterEl.value.selectedResult = item;
245
+ onSelectItem(new CustomEvent("select", { detail: item }));
133
246
  }
134
247
  };
248
+
135
249
  onMounted(() => {
136
250
  //@ts-expect-error todo
137
- mapEl.value?.addEventListener("select", handler);
251
+ mapElement.value?.addEventListener("select", handler);
138
252
  });
139
253
  onUnmounted(() => {
140
254
  //@ts-expect-error todo
141
- mapEl.value?.removeEventListener("select", handler);
255
+ mapElement.value?.removeEventListener("select", handler);
142
256
  });
143
257
  }
@@ -5,26 +5,31 @@
5
5
  :key="mapElement"
6
6
  v-bind="config"
7
7
  :for="mapElement"
8
+ .showLayerZoomState="true"
8
9
  .customEditorInterfaces="bandsEditorInterface"
9
10
  @datetime:updated="debouncedHandleDateTime"
10
11
  toolsAsList="true"
11
12
  ref="eoxLayercontrol"
12
13
  @layerConfig:change="onLayerConfigChange"
13
14
  >
14
- <slot name="layerstitle">
15
- <div>
16
- <p v-if="title" class="mt-2 mb-2">
17
- <strong>{{ title }}</strong>
18
- </p>
19
- </div>
20
- </slot>
15
+ <span
16
+ slot="layerstitle"
17
+ class="d-flex justify-space-between ma-2 pa-2 flex-shrink-0"
18
+ >
19
+ <h4 v-if="title">{{ title }}</h4>
20
+ <EodashLayoutSwitcher
21
+ v-if="enableLayoutSwitcher"
22
+ :target="layoutTarget"
23
+ :icon="layoutIcon"
24
+ />
25
+ </span>
21
26
  </eox-layercontrol>
22
27
  </span>
23
28
  </template>
24
29
  <script setup>
25
30
  import "color-legend-element";
26
31
  import "@eox/timecontrol";
27
- import { computed, ref } from "vue";
32
+ import { computed, ref, watch } from "vue";
28
33
  import { mapEl, mapCompareEl } from "@/store/states";
29
34
  import { getColFromLayer } from "@/eodashSTAC/helpers";
30
35
  import {
@@ -33,9 +38,13 @@ import {
33
38
  layerControlFormValue,
34
39
  layerControlFormValueCompare,
35
40
  } from "@/utils/states";
41
+ import { updateGeoZarrBands } from "@/eodashSTAC/helpers";
36
42
  import { storeToRefs } from "pinia";
37
43
  import { useSTAcStore } from "@/store/stac";
38
44
  import { bandsEditorInterface } from "@/utils/bands-editor";
45
+ import EodashLayoutSwitcher from "^/EodashLayoutSwitcher.vue";
46
+ import { mdiViewDashboard } from "@mdi/js";
47
+ import { useEmitLayersUpdate } from "@/composables";
39
48
 
40
49
  if (!customElements.get("eox-layercontrol")) {
41
50
  await import("@eox/layercontrol");
@@ -45,22 +54,39 @@ if (!customElements.get("eox-jsonform")) {
45
54
  }
46
55
 
47
56
  const props = defineProps({
57
+ /** Which map instance this control is bound to. Use `"second"` in a compare-mode layout. */
48
58
  map: {
49
59
  /** @type {import("vue").PropType<"first" | "second">} */
50
60
  //@ts-expect-error todo
51
61
  type: String,
52
62
  default: "first",
53
63
  },
64
+ /** Tool tabs shown inside `eox-layercontrol`. Remove entries to hide individual tabs. */
54
65
  tools: {
55
- type: Array,
66
+ type: /** @type {import("vue").PropType<string[]>} */ (Array),
56
67
  default: () => ["datetime", "info", "config", "legend", "opacity"],
57
68
  },
69
+ /** Heading rendered above the layer list. Set to `false` to hide it. */
58
70
  title: {
59
- type: String || Boolean,
71
+ type: /** @type {import("vue").PropType<string | false>} */ ([
72
+ String,
73
+ Boolean,
74
+ ]),
60
75
  default: "Layers",
61
76
  },
77
+ /** CSS custom-property overrides forwarded to the underlying `eox-layercontrol` element via its `style` attribute. */
62
78
  cssVars: {
63
- type: Object,
79
+ type: /** @type {import("vue").PropType<Record<string, string>>} */ (
80
+ Object
81
+ ),
82
+ default: {},
83
+ },
84
+ layoutIcon: {
85
+ type: String,
86
+ default: mdiViewDashboard,
87
+ },
88
+ layoutTarget: {
89
+ type: String,
64
90
  },
65
91
  });
66
92
 
@@ -69,7 +95,12 @@ const config = {
69
95
  style: props.cssVars,
70
96
  };
71
97
 
98
+ const enableLayoutSwitcher = computed(
99
+ () => !!props.layoutTarget && !!props.layoutIcon,
100
+ );
101
+
72
102
  const { selectedCompareStac, selectedStac } = storeToRefs(useSTAcStore());
103
+
73
104
  const showControls = computed(() => {
74
105
  if (props.map === "second") {
75
106
  return mapCompareEl.value !== null && selectedCompareStac.value !== null;
@@ -81,12 +112,23 @@ const eodashCols =
81
112
  props.map === "second" ? eodashCompareCollections : eodashCollections;
82
113
  const mapElement = props.map === "second" ? mapCompareEl : mapEl;
83
114
 
84
- /** @type { import("vue").Ref<HTMLElement & Record<string,any> | null>} */
115
+ /** @type { import("vue").Ref<import("@eox/layercontrol").EOxLayerControl | null>} */
85
116
  const eoxLayercontrol = ref(null);
86
117
 
118
+ // eox-timecontrol re-fires datetime:updated after layer reassignment;
119
+ // dedupe by (collectionId, datetime)
120
+ const processedDatetimes = new Map();
121
+ watch([selectedStac, selectedCompareStac], () => processedDatetimes.clear());
122
+
87
123
  /** @param {CustomEvent<{layer:import('ol/layer').Layer; datetime:string;}>} evt */
88
124
  const handleDatetimeUpdate = async (evt) => {
89
125
  const { layer, datetime } = evt.detail;
126
+ const collectionId = layer.get("id")?.split(";:;")[0] ?? layer.get("id");
127
+ if (processedDatetimes.get(collectionId) === datetime) return;
128
+ // First event per collection is eox-timecontrol's mount echo.
129
+ const isFirstEvent = !processedDatetimes.has(collectionId);
130
+ processedDatetimes.set(collectionId, datetime);
131
+ if (isFirstEvent) return;
90
132
 
91
133
  const ec = await getColFromLayer(eodashCols, layer);
92
134
 
@@ -97,9 +139,10 @@ const handleDatetimeUpdate = async (evt) => {
97
139
  updatedLayers = await ec.updateLayerJson(
98
140
  datetime,
99
141
  layer.get("id"),
100
- props.map,
142
+ mapElement.value?.layers ?? [],
101
143
  );
102
144
  }
145
+ if (!updatedLayers?.length) return;
103
146
  /** @type {Record<String,any>[] | undefined} */
104
147
  const dataLayers = updatedLayers?.find(
105
148
  (l) => l?.properties?.id === "AnalysisGroup",
@@ -111,9 +154,14 @@ const handleDatetimeUpdate = async (evt) => {
111
154
  dl.properties.layerControlExpand = true;
112
155
  dl.properties.layerControlToolsExpand = true;
113
156
  });
114
- // assign layers to the map
115
157
  /** @type {HTMLElement & Record<string,any>} */
116
158
  (mapElement.value).layers = updatedLayers;
159
+ // Emit after layer assignment so listeners resolve against the new layer.
160
+ await useEmitLayersUpdate(
161
+ props.map === "second" ? "compareLayertime:updated" : "layertime:updated",
162
+ mapElement.value,
163
+ updatedLayers,
164
+ );
117
165
  }
118
166
  };
119
167
 
@@ -136,6 +184,8 @@ const debouncedHandleDateTime = (evt) => {
136
184
  * @param {Event & {detail:{layer:import("ol/layer").Layer;jsonformValue:Record<string,any>}}} evt
137
185
  */
138
186
  const onLayerConfigChange = (evt) => {
187
+ updateGeoZarrBands(evt.detail.layer, evt.detail.jsonformValue);
188
+
139
189
  if (props.map === "second") {
140
190
  layerControlFormValueCompare.value = evt.detail.jsonformValue;
141
191
  } else {