@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,15 +1,14 @@
1
- import { onMounted, watch, nextTick, ref, useTemplateRef, computed, openBlock, createElementBlock, createVNode, unref, createCommentVNode, createBlock, createElementVNode, withCtx, createTextVNode } from 'vue';
1
+ import { onMounted, watch, nextTick, onUnmounted, ref, useTemplateRef, computed, openBlock, createElementBlock, createVNode, unref, createCommentVNode, createBlock, createElementVNode, withCtx, createTextVNode } from 'vue';
2
2
  import '@eox/chart';
3
3
  import '@eox/drawtools';
4
4
  import '@eox/jsonform';
5
- import { U as eoxLayersKey, N as useOnLayersUpdate, _ as _export_sfc, H as useSTAcStore, J as VBtn } from './asWebComponent-DfULAbpq.js';
5
+ import { L as useOnLayersUpdate, _ as _export_sfc, H as useSTAcStore, J as VBtn } from './asWebComponent-BlFN2lkB.js';
6
6
  import { storeToRefs } from 'pinia';
7
- import { P as ProcessList, c as compareJobs, j as jobs } from './ProcessList-kf3olI7W.js';
8
- import EodashChart from './EodashChart-PkUQfH0k.js';
9
- import { i as initProcess, u as updateJsonformIdentifier, h as handleProcesses } from './handling-s8xt2JwF.js';
10
- import { useEventBus } from '@vueuse/core';
11
- import { u as updateJobsStatus, d as download, g as getDrawToolsProperties } from './async-CIbKXEEe.js';
12
- import { I as compareIndicator, F as indicator, S as mapCompareEl, i as mapEl, M as areChartsSeparateLayout, O as compareChartSpec, N as chartSpec } from './helpers-DAVGYl_f.js';
7
+ import { P as ProcessList, c as compareJobs, j as jobs } from './ProcessList-DuI6WpS1.js';
8
+ import EodashChart from './EodashChart-ChrkA5oS.js';
9
+ import { i as initProcess, u as updateJsonformIdentifier, h as handleProcesses } from './handling-B6I1V5eG.js';
10
+ import { ab as getCompareLayers, B as getLayers, Q as compareIndicator, M as indicator, a1 as mapCompareEl, n as mapEl, V as areChartsSeparateLayout, X as compareChartSpec, W as chartSpec } from './helpers-CBqIUSXJ.js';
11
+ import { u as updateJobsStatus, d as download, g as getDrawToolsProperties } from './async-B2H8ZwUK.js';
13
12
  import { mdiCogPlayOutline, mdiDownloadCircleOutline } from '@mdi/js';
14
13
 
15
14
  /**
@@ -24,7 +23,7 @@ import { mdiCogPlayOutline, mdiDownloadCircleOutline } from '@mdi/js';
24
23
  * @param {import("vue").Ref<boolean>} params.isProcessed
25
24
  * @param {import("vue").Ref<boolean>} params.loading
26
25
  * @param {import("vue").Ref<boolean>} params.isPolling
27
- * @param {import("@eox/map").EOxMap | null} params.mapElement
26
+ * @param {import("vue").Ref<import("@eox/map").EOxMap | null>} params.mapElement
28
27
  */
29
28
  const useInitProcess = ({
30
29
  selectedStac,
@@ -35,60 +34,45 @@ const useInitProcess = ({
35
34
  isPolling,
36
35
  mapElement,
37
36
  }) => {
38
- const layersEvents = useEventBus(eoxLayersKey);
39
-
40
37
  onMounted(async () => {
41
- // wait for the layers to be rendered
42
- if ((mapElement?.layers.length ?? 0) > 1) {
43
- await initProcess({
44
- enableCompare: mapElement?.id === "compare",
45
- selectedStac,
46
- jsonformSchema,
47
- isProcessed,
48
- processResults,
49
- loading,
50
- isPolling,
51
- });
52
- } else {
53
- layersEvents.once(async () => {
54
- await initProcess({
55
- enableCompare: mapElement?.id === "compare",
56
- selectedStac,
57
- jsonformSchema,
58
- isProcessed,
59
- loading,
60
- processResults,
61
- isPolling,
62
- });
63
- });
64
- }
38
+ await initProcess({
39
+ enableCompare: mapElement.value?.id === "compare",
40
+ selectedStac,
41
+ jsonformSchema,
42
+ isProcessed,
43
+ processResults,
44
+ loading,
45
+ isPolling,
46
+ mapElement: mapElement.value,
47
+ });
65
48
  });
66
- const enableCompare = mapElement?.id === "compare";
67
- const evtKey = enableCompare ? "compareLayers:updated" : "layers:updated";
68
- useOnLayersUpdate(async (evt, _payload) => {
69
- if (
70
- evt == "layertime:updated" ||
71
- evt == "compareLayertime:updated" ||
72
- evt == "time:updated" ||
73
- evt == "compareTime:updated"
74
- ) {
75
- const shouldMainJsonFormUpdate =
76
- ["layertime:updated", "time:updated"].includes(evt) && !enableCompare;
77
- const shouldCompareJsonFormUpdate =
78
- ["compareLayertime:updated", "compareTime:updated"].includes(evt) &&
79
- enableCompare;
80
- // we need to update jsonform on time change in cases when the feature selection layer was time-based, so that it attaches to a correct new layer
81
- if (shouldMainJsonFormUpdate || shouldCompareJsonFormUpdate) {
82
- const newJsonForm = await updateJsonformIdentifier({
83
- jsonformSchema: jsonformSchema.value,
84
- // @ts-expect-error TODO payload coming from time update events is not an object with layers property
85
- newLayers: _payload,
86
- enableCompare,
87
- });
49
+
50
+ useOnLayersUpdate(async (evt) => {
51
+ const enableCompare = mapElement.value?.id === "compare";
52
+ const layerUpdatedKey = enableCompare
53
+ ? "compareLayers:updated"
54
+ : "layers:updated";
55
+ const timeUpdatedKeys = enableCompare
56
+ ? ["compareLayertime:updated", "compareTime:updated"]
57
+ : ["layertime:updated", "time:updated"];
58
+
59
+ if (timeUpdatedKeys.includes(evt)) {
60
+ // we need to update jsonform on time change in cases
61
+ // when the feature selection layer was time-based,
62
+ // so that it attaches to a correct new layer
63
+ const newJsonForm = await updateJsonformIdentifier({
64
+ jsonformSchema: jsonformSchema.value,
65
+ newLayers: enableCompare ? getCompareLayers() : getLayers(),
66
+ enableCompare,
67
+ mapElement: mapElement.value,
68
+ });
69
+
70
+ if (newJsonForm) {
88
71
  jsonformSchema.value = newJsonForm;
89
72
  }
90
73
  }
91
- if (evt !== evtKey) {
74
+
75
+ if (evt !== layerUpdatedKey) {
92
76
  return;
93
77
  }
94
78
  await initProcess({
@@ -99,6 +83,7 @@ const useInitProcess = ({
99
83
  processResults,
100
84
  loading,
101
85
  isPolling,
86
+ mapElement: mapElement.value,
102
87
  });
103
88
  });
104
89
  };
@@ -118,39 +103,40 @@ function useAutoExec(
118
103
  startProcess,
119
104
  ) {
120
105
  /**
121
- * @param {CustomEvent} _e
106
+ * @param {any} _e
122
107
  **/
123
108
  const onJsonFormChange = async (_e) => {
124
109
  await startProcess();
125
110
  };
126
111
 
127
- const addEventListener = async () => {
128
- await nextTick(() => {
129
- //@ts-expect-error TODO
130
- jsonformEl.value?.addEventListener("change", onJsonFormChange);
131
- });
132
- };
133
- const removeEventListener = () => {
134
- //@ts-expect-error TODO
135
- jsonformEl.value?.removeEventListener("change", onJsonFormChange);
136
- };
137
-
138
112
  watch(jsonformSchema, (updatedSchema) => {
139
113
  autoExec.value = updatedSchema?.options?.["execute"] || false;
140
114
  });
141
115
 
142
- onMounted(() => {
143
- watch(
144
- autoExec,
145
- async (exec) => {
146
- if (exec) {
147
- await addEventListener();
148
- } else {
149
- removeEventListener();
150
- }
151
- },
152
- { immediate: true },
153
- );
116
+ // Re-attach listener whenever the element or autoExec changes
117
+ const stop = watch(
118
+ [autoExec, jsonformEl],
119
+ async ([exec, el], [_prevExec, prevEl]) => {
120
+ // Cleanup if previous element existed
121
+ if (prevEl) {
122
+ prevEl.removeEventListener("change", onJsonFormChange);
123
+ }
124
+ // Add if enabled and we have an element
125
+ if (exec && el) {
126
+ // remove listener before adding to avoid duplicates in case of element change
127
+ el.removeEventListener("change", onJsonFormChange);
128
+ await nextTick();
129
+ el.addEventListener("change", onJsonFormChange);
130
+ }
131
+ },
132
+ { immediate: true },
133
+ );
134
+
135
+ onUnmounted(() => {
136
+ if (jsonformEl.value) {
137
+ jsonformEl.value.removeEventListener("change", onJsonFormChange);
138
+ }
139
+ stop();
154
140
  });
155
141
  }
156
142
 
@@ -172,6 +158,7 @@ const _sfc_main = {
172
158
  default: false,
173
159
  },
174
160
  vegaEmbedOptions: {
161
+ /** @type {import("vue").PropType<import("vega-embed").EmbedOptions>} */
175
162
  type: Object,
176
163
  default() {
177
164
  return { actions: true };
@@ -224,7 +211,7 @@ const jsonformKey = computed(
224
211
 
225
212
  useInitProcess({
226
213
  selectedStac: currentSelectedStac,
227
- mapElement: mapElement.value,
214
+ mapElement,
228
215
  jsonformSchema,
229
216
  isProcessed,
230
217
  processResults,
@@ -0,0 +1,455 @@
1
+ import { K as mapPosition, a6 as isGlobe, n as mapEl, S as eodashCollections, q as extractLayerTimeValues, H as datetime, f as axios, ag as resolveRenders, l as encodeURLObject, M as indicator, B as getLayers } from './helpers-CBqIUSXJ.js';
2
+ import { shallowRef, ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
3
+ import { createSharedComposable, useEventBus, useDebounceFn } from '@vueuse/core';
4
+ import { H as useSTAcStore, L as useOnLayersUpdate, ab as useEodash } from './asWebComponent-BlFN2lkB.js';
5
+ import log from 'loglevel';
6
+
7
+ /**
8
+ * Keys that are catalog based and have dedicated parameters
9
+ * sent to a remote API (e.g. TiTiler, eoAPI STAC search).
10
+ * @type {string[]}
11
+ */
12
+ const SKIP_KEYS = ["collection", "datetime", "bbox"];
13
+
14
+ /**
15
+ * Strips the "properties." prefix that eox-itemfilter catalog configs add to
16
+ * item property keys (e.g. "properties.eo:cloud_cover" -> "eo:cloud_cover").
17
+ * Keys without the prefix are returned unchanged.
18
+ * @param {string} key
19
+ * @returns {string}
20
+ */
21
+ function normalizeFilterKey(key) {
22
+ return key.startsWith("properties.") ? key.slice("properties.".length) : key;
23
+ }
24
+
25
+ /**
26
+ * Builds a CQL2-text filter string from an eox-itemfilter filters record.
27
+ *
28
+ * Handles range, multiselect, and select filter types.
29
+ * - Strips "properties." prefix from keys (catalog eox-itemfilter convention).
30
+ * - Double-quotes property names that contain non-word characters (e.g. "eo:cloud_cover").
31
+ * - Skips catalog-only keys (e.g. "collection").
32
+ *
33
+ * Compatible with both TiTiler's `filter` param and the STAC API `filter` param
34
+ * (both accept CQL2-text).
35
+ *
36
+ * @param {import("@/types").ItemFilterFilters | null | undefined} filters
37
+ * @returns {string}
38
+ */
39
+ function buildCqlFilter(filters) {
40
+ if (!filters) return "";
41
+
42
+ /** @type {string[]} */
43
+ const parts = [];
44
+
45
+ for (const filter of Object.values(filters)) {
46
+ if (!filter?.key || SKIP_KEYS.includes(filter.key)) continue;
47
+
48
+ const key = normalizeFilterKey(filter.key);
49
+ const prop = /\W/.test(key) ? `"${key}"` : key;
50
+
51
+ if (filter.type === "range" && filter.state) {
52
+ const { min, max } = filter.state;
53
+ if (min != null && min > (filter.min ?? -Infinity)) {
54
+ parts.push(`${prop} >= ${min}`);
55
+ }
56
+ if (max != null && max < (filter.max ?? Infinity)) {
57
+ parts.push(`${prop} <= ${max}`);
58
+ }
59
+ } else if (
60
+ filter.type === "multiselect" &&
61
+ filter.stringifiedState?.length
62
+ ) {
63
+ parts.push(`${prop} IN (${filter.stringifiedState})`);
64
+ } else if (filter.type === "select" && filter.stringifiedState) {
65
+ parts.push(`${prop} = '${filter.stringifiedState}'`);
66
+ }
67
+ }
68
+
69
+ return parts.join(" AND ");
70
+ }
71
+
72
+ const mosaicReturnToOverviewKey = Symbol("mosaic:return-to-overview");
73
+
74
+ /**
75
+ * Shared mosaic state across all components. Stores the latest mosaic layer,
76
+ * query parameters, visibility threshold, item-view flag, and an event bus
77
+ * for the "Back to overview" user action .
78
+ */
79
+ const useMosaicState = createSharedComposable(() => {
80
+ /** @type {import("vue").ShallowRef<Record<string, any> | null>} */
81
+ const latestLayer = shallowRef(null);
82
+ /** @type {import("vue").Ref<Record<string, string> | null>} */
83
+ const query = ref(null);
84
+ /** @type {import("vue").Ref<number>} */
85
+ const visibilityThreshold = ref(8);
86
+ /** @type {import("vue").Ref<boolean>} */
87
+ const isItemView = ref(false);
88
+ /** @type {import("vue").Ref<boolean>} - filtered mosaic has assets in the current view */
89
+ const hasDataInView = ref(true);
90
+ const returnToOverview = useEventBus(mosaicReturnToOverviewKey);
91
+
92
+ const store = useSTAcStore();
93
+ /** @type {import("vue").ComputedRef<string | null>} */
94
+ const mosaicEndpoint = computed(() => {
95
+ if (!store.rasterEndpoint || !store.selectedStac?.id) return null;
96
+ return `${store.rasterEndpoint}/collections/${store.selectedStac.id}/WebMercatorQuad/tilejson.json`;
97
+ });
98
+
99
+ return {
100
+ latestLayer,
101
+ query,
102
+ visibilityThreshold,
103
+ isItemView,
104
+ hasDataInView,
105
+ returnToOverview,
106
+ mosaicEndpoint,
107
+ };
108
+ });
109
+
110
+ /**
111
+ * Modifies the layers collection to display the mosaic layer.
112
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
113
+ * @param {Record<string, string> | null} [params] - Query params forwarded to the tilejson URL.
114
+ */
115
+ async function renderMosaic(mosaicEndpoint, params) {
116
+ const mosaicLayers = await createMosaicLayers(mosaicEndpoint, params);
117
+
118
+ if (!mosaicLayers.length) {
119
+ return;
120
+ }
121
+
122
+ const { selectedStac } = useSTAcStore();
123
+ if (selectedStac) {
124
+ const eodashCol = eodashCollections.find(
125
+ (ec) => ec.collectionStac?.id === selectedStac.id,
126
+ );
127
+ const dates = (await eodashCol?.getDates()) ?? [];
128
+ const { timeControlValues } = extractLayerTimeValues(dates, datetime.value);
129
+ if (timeControlValues) {
130
+ mosaicLayers[0].properties.timeControlValues = timeControlValues;
131
+ mosaicLayers[0].properties.timeControlProperty = "TIME";
132
+ }
133
+ }
134
+
135
+ assignAnalysisLayer(mosaicLayers);
136
+ const { latestLayer } = useMosaicState();
137
+ latestLayer.value = mosaicLayers[0];
138
+ log.debug("[eodash] Mosaic layer rendered.", mosaicLayers[0].source.url);
139
+ }
140
+
141
+ /**
142
+ * Updates the mosaic layer based on the current filters.
143
+ * @param {string | undefined | null} mosaicEndpoint - The TileJSON URL for the mosaic.
144
+ * @param {{ timeRange?: [string, string]; filters?: import("@/types").ItemFilterFilters }} [queries]
145
+ */
146
+ async function updateMosaicLayer(
147
+ mosaicEndpoint,
148
+ { timeRange, filters } = {},
149
+ ) {
150
+ if (!mosaicEndpoint) return;
151
+
152
+ /** @type {Record<string, string>} */
153
+ const params = {};
154
+
155
+ if (timeRange && Array.isArray(timeRange) && timeRange.length === 2) {
156
+ const start = timeToDate(timeRange[0]);
157
+ const end = timeToDate(timeRange[1]);
158
+ if (start && end) {
159
+ params.datetime = start === end ? start : `${start}/${end}`;
160
+ }
161
+ }
162
+
163
+ const cqlFilter = buildCqlFilter(filters);
164
+ if (cqlFilter) {
165
+ params.filter = cqlFilter;
166
+ params["filter-lang"] = "cql2-text";
167
+ }
168
+
169
+ const queryParams = Object.keys(params).length > 0 ? params : null;
170
+ const { query } = useMosaicState();
171
+ query.value = queryParams;
172
+ await renderMosaic(mosaicEndpoint, queryParams);
173
+ }
174
+
175
+ /**
176
+ * Renders the latest mosaic layer stored in the shared mosaic state.
177
+ */
178
+ function renderLatestMosaic() {
179
+ const { latestLayer } = useMosaicState();
180
+ if (!latestLayer.value) return;
181
+ assignAnalysisLayer([latestLayer.value]);
182
+ }
183
+
184
+ /**
185
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
186
+ * @param {[string, string] | undefined} timeRange
187
+ */
188
+ async function initMosaic(mosaicEndpoint, timeRange) {
189
+ await updateMosaicLayer(mosaicEndpoint, { timeRange });
190
+ await nextTick();
191
+ const zoom =
192
+ mapPosition.value[2] ?? mapEl.value?.map?.getView()?.getZoom() ?? 0;
193
+ toggleMosaicVisibility(zoom);
194
+ }
195
+
196
+ /**
197
+ * Vue composable that initializes the mosaic layer and keeps it in sync with
198
+ * map zoom and layer updates.
199
+ * @param {string | null | undefined} mosaicEndpoint - Pass null/undefined to disable.
200
+ * @param {import("vue").Ref<[string, string]> | undefined} timeRange
201
+ * @param {string[]} [indicators] - If provided, mosaic only activates for these collection IDs.
202
+ */
203
+ function useInitMosaic(mosaicEndpoint, timeRange, indicators) {
204
+ if (!mosaicEndpoint) return;
205
+
206
+ const store = useSTAcStore();
207
+ const { isItemView, latestLayer, query, hasDataInView } = useMosaicState();
208
+
209
+ function shouldInitiate() {
210
+ if (isItemView.value) return false;
211
+ if (!store.selectedStac?.id) return false;
212
+ if (indicators !== undefined && !indicators.includes(store.selectedStac.id))
213
+ return false;
214
+ return true;
215
+ }
216
+
217
+ const stopWatcher = watch(mapPosition, (updatedPos, oldPos) => {
218
+ // Any pan or zoom changes the viewport bbox; re-check data presence.
219
+ scheduleMosaicDataCheck();
220
+ const [_oldX, _oldY, oldZ] = oldPos;
221
+ const [_x, _y, z] = updatedPos;
222
+ if (!z || z === oldZ) return;
223
+ toggleMosaicVisibility(z);
224
+ });
225
+
226
+ // Re-check when filters/time change, and re-apply visibility once it lands.
227
+ const stopQueryWatch = watch(query, () => scheduleMosaicDataCheck());
228
+
229
+ onMounted(async () => {
230
+ if (!shouldInitiate()) return;
231
+ initMosaic(mosaicEndpoint, timeRange?.value);
232
+ });
233
+
234
+ useOnLayersUpdate((evt) => {
235
+ if (evt !== "layers:updated") return;
236
+ if (!shouldInitiate()) return;
237
+ initMosaic(mosaicEndpoint, timeRange?.value);
238
+ });
239
+
240
+ onUnmounted(() => {
241
+ latestLayer.value = null;
242
+ query.value = null;
243
+ hasDataInView.value = true;
244
+ stopWatcher();
245
+ stopQueryWatch();
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Shared debounced scheduler for mosaic updates. All callers share a single
251
+ * timer so concurrent updates (e.g. datetime + filter changes from different
252
+ * widgets within the debounce window) coalesce into one final update.
253
+ *
254
+ * @returns {(mosaicEndpoint: string | undefined | null, timeRange: [string,string] | undefined, filters?: import("@/types").ItemFilterFilters) => void}
255
+ */
256
+ const useScheduleMosaicUpdate = createSharedComposable(() =>
257
+ useDebounceFn(
258
+ /**
259
+ * @param {string | undefined | null} mosaicEndpoint
260
+ * @param {[string, string] | undefined} timeRange
261
+ * @param {import("@/types").ItemFilterFilters} [filters]
262
+ */
263
+ (mosaicEndpoint, timeRange, filters) => {
264
+ updateMosaicLayer(mosaicEndpoint, { timeRange, filters });
265
+ },
266
+ 300,
267
+ ),
268
+ );
269
+
270
+ /**
271
+ * Creates a layer configuration for the mosaic.
272
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
273
+ * @param {Record<string, string> | null} [params] - Query params (datetime, filter, etc.).
274
+ * @returns {Promise<Record<string, any>[]>}
275
+ */
276
+ async function createMosaicLayers(mosaicEndpoint, params) {
277
+ const { selectedStac } = useSTAcStore();
278
+ const renders = resolveRenders(selectedStac, useEodash()?.options?.renders);
279
+ const preset = renders ? Object.values(renders)[0] : undefined;
280
+ if (!preset) {
281
+ console.warn(
282
+ "[eodash] No render preset (collection `renders` nor `options.renders`) for the mosaic layer.",
283
+ );
284
+ return [];
285
+ }
286
+
287
+ const renderParamsStr = encodeURLObject({
288
+ assets: preset.assets,
289
+ expression: preset.expression,
290
+ nodata: preset.nodata,
291
+ resampling: preset.resampling,
292
+ color_formula: preset.color_formula,
293
+ colormap: preset.colormap,
294
+ colormap_name: preset.colormap_name,
295
+ rescale: preset.rescale,
296
+ });
297
+
298
+ const tileParams = new URLSearchParams({ tilesize: "512", ...params });
299
+ const tileJsonUrl = `${mosaicEndpoint}?${renderParamsStr}${tileParams.toString()}`;
300
+
301
+ const tileJSON = await axios
302
+ .get(tileJsonUrl)
303
+ .then((res) => res.data)
304
+ .catch((err) => {
305
+ console.error("Failed to fetch mosaic TileJSON", err);
306
+ return null;
307
+ });
308
+ if (!tileJSON?.tiles?.[0]) {
309
+ console.warn("[eodash] No tile URL found in mosaic TileJSON response.");
310
+ return [];
311
+ }
312
+
313
+ return [
314
+ {
315
+ type: "Tile",
316
+ minZoom: useMosaicState().visibilityThreshold.value,
317
+ properties: {
318
+ id: `${indicator.value};:;mosaic`,
319
+ title: "Mosaic Layer",
320
+ },
321
+ source: {
322
+ type: "XYZ",
323
+ url: tileJSON.tiles[0],
324
+ },
325
+ },
326
+ ];
327
+ }
328
+
329
+ /**
330
+ * @param {string | Date} time
331
+ * @returns {string | null}
332
+ */
333
+ function timeToDate(time) {
334
+ if (!time) return null;
335
+ const date = new Date(time);
336
+ if (isNaN(date.getTime())) return null;
337
+ return date.toISOString().split("T")[0];
338
+ }
339
+
340
+ /**
341
+ * Returns the OL-equivalent zoom from `mapPosition[2]`. In 2D the raw value
342
+ * is already the OL zoom. In globe mode the raw value is camera altitude in
343
+ * meters; convert it via eox-map's internal formula so callers can compare
344
+ * against a single (2D-style) threshold.
345
+ *
346
+ * @param {number} rawZ
347
+ * @returns {number}
348
+ */
349
+ function normalizeGlobeZoom(rawZ) {
350
+ if (!isGlobe.value) return rawZ;
351
+ const zoomFactor = mapEl.value?.globeConfig?.useHighLOD ? 1 : 2;
352
+ return Math.log2(21_050_000 / rawZ) + zoomFactor;
353
+ }
354
+
355
+ /**
356
+ * check whether the filtered dataset has any assets in the current 2D view
357
+ */
358
+ async function checkMosaicDataInView() {
359
+ const { hasDataInView, query, mosaicEndpoint, visibilityThreshold } =
360
+ useMosaicState();
361
+ const extent = mapEl.value?.lonLatExtent;
362
+ if (
363
+ !extent ||
364
+ isGlobe.value ||
365
+ !mosaicEndpoint.value ||
366
+ normalizeGlobeZoom(mapPosition.value[2] ?? 0) < visibilityThreshold.value
367
+ ) {
368
+ hasDataInView.value = true;
369
+ return;
370
+ }
371
+
372
+ if (extent.some(Number.isNaN)) return;
373
+
374
+ const base = mosaicEndpoint.value.replace(
375
+ "/WebMercatorQuad/tilejson.json",
376
+ "",
377
+ );
378
+ const [minx, miny, maxx, maxy] = extent;
379
+ const params = new URLSearchParams({ limit: "1", ...(query.value ?? {}) });
380
+ try {
381
+ const { data } = await axios.get(
382
+ `${base}/bbox/${minx},${miny},${maxx},${maxy}/assets?${params}`,
383
+ );
384
+ const count = Array.isArray(data)
385
+ ? data.length
386
+ : Object.keys(data ?? {}).length;
387
+ hasDataInView.value = count > 0;
388
+ } catch {
389
+ hasDataInView.value = true;
390
+ }
391
+ }
392
+
393
+ /** Shared debounced data-presence check; coalesces rapid pan/zoom/filter changes. */
394
+ const scheduleMosaicDataCheck = useDebounceFn(checkMosaicDataInView, 300);
395
+
396
+ /**
397
+ * @param {number} zoomLevel
398
+ * @param {number} [threshold] - defaults to the shared visibility threshold
399
+ */
400
+ function toggleMosaicVisibility(zoomLevel, threshold) {
401
+ // Only globe needs manual toggling.
402
+ if (!isGlobe.value) return;
403
+ const { latestLayer, visibilityThreshold } = useMosaicState();
404
+ if (!latestLayer.value) return;
405
+
406
+ const layerId = /** @type {string} */ (latestLayer.value.properties.id);
407
+ const layer = mapEl.value?.getLayerById(layerId);
408
+ if (!layer) return;
409
+
410
+ const eqZoom = normalizeGlobeZoom(zoomLevel);
411
+ layer.setVisible(eqZoom >= (visibilityThreshold.value));
412
+ }
413
+
414
+ /**
415
+ * Writes layers into the AnalysisGroup and triggers a map re-render.
416
+ * @param {Record<string, any>[]} layersToAssign
417
+ */
418
+ function assignAnalysisLayer(layersToAssign) {
419
+ const mapLayers = getLayers();
420
+ const { analysisGroup, layers } = ensureAnalysisGroup(mapLayers);
421
+ analysisGroup.layers = layersToAssign;
422
+ if (mapEl.value) {
423
+ // Reassign to trigger map re-render after in-place layer mutation
424
+ mapEl.value.layers = /** @type {import("@eox/map").EoxLayer[]} */ ([
425
+ ...layers,
426
+ ]);
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Ensure the AnalysisGroup exists in the layers collection.
432
+ * @param {Record<string, any>[]} layersCollection
433
+ * @returns {{ layers: Record<string, any>[], analysisGroup: Record<string, any> }}
434
+ */
435
+ function ensureAnalysisGroup(layersCollection) {
436
+ let analysisGroup = layersCollection.find(
437
+ (l) => l?.properties?.id === "AnalysisGroup",
438
+ );
439
+
440
+ if (!analysisGroup) {
441
+ analysisGroup = {
442
+ type: "Group",
443
+ properties: {
444
+ id: "AnalysisGroup",
445
+ title: "Data Layers",
446
+ },
447
+ layers: [],
448
+ };
449
+ layersCollection.push(analysisGroup);
450
+ }
451
+
452
+ return { layers: layersCollection, analysisGroup };
453
+ }
454
+
455
+ export { useScheduleMosaicUpdate as a, buildCqlFilter as b, useInitMosaic as c, normalizeGlobeZoom as n, renderLatestMosaic as r, useMosaicState as u };