@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
@@ -0,0 +1,416 @@
1
+ import axios from "@/plugins/axios";
2
+ import {
3
+ datetime,
4
+ indicator,
5
+ isGlobe,
6
+ mapEl,
7
+ mapPosition,
8
+ } from "@/store/states";
9
+ import { getLayers } from "@/store/actions";
10
+ import {
11
+ computed,
12
+ nextTick,
13
+ onMounted,
14
+ onUnmounted,
15
+ ref,
16
+ shallowRef,
17
+ watch,
18
+ } from "vue";
19
+ import {
20
+ createSharedComposable,
21
+ useDebounceFn,
22
+ useEventBus,
23
+ } from "@vueuse/core";
24
+ import { useEodash, useOnLayersUpdate } from "@/composables";
25
+ import {
26
+ encodeURLObject,
27
+ extractLayerTimeValues,
28
+ resolveRenders,
29
+ } from "./helpers";
30
+ import { useSTAcStore } from "@/store/stac";
31
+ import { eodashCollections } from "@/utils/states";
32
+ import log from "loglevel";
33
+ import { buildCqlFilter } from "./cql";
34
+
35
+ const mosaicReturnToOverviewKey = Symbol("mosaic:return-to-overview");
36
+
37
+ /**
38
+ * Shared mosaic state across all components. Stores the latest mosaic layer,
39
+ * query parameters, visibility threshold, item-view flag, and an event bus
40
+ * for the "Back to overview" user action .
41
+ */
42
+ export const useMosaicState = createSharedComposable(() => {
43
+ /** @type {import("vue").ShallowRef<Record<string, any> | null>} */
44
+ const latestLayer = shallowRef(null);
45
+ /** @type {import("vue").Ref<Record<string, string> | null>} */
46
+ const query = ref(null);
47
+ /** @type {import("vue").Ref<number>} */
48
+ const visibilityThreshold = ref(8);
49
+ /** @type {import("vue").Ref<boolean>} */
50
+ const isItemView = ref(false);
51
+ /** @type {import("vue").Ref<boolean>} - filtered mosaic has assets in the current view */
52
+ const hasDataInView = ref(true);
53
+ const returnToOverview = useEventBus(mosaicReturnToOverviewKey);
54
+
55
+ const store = useSTAcStore();
56
+ /** @type {import("vue").ComputedRef<string | null>} */
57
+ const mosaicEndpoint = computed(() => {
58
+ if (!store.rasterEndpoint || !store.selectedStac?.id) return null;
59
+ return `${store.rasterEndpoint}/collections/${store.selectedStac.id}/WebMercatorQuad/tilejson.json`;
60
+ });
61
+
62
+ return {
63
+ latestLayer,
64
+ query,
65
+ visibilityThreshold,
66
+ isItemView,
67
+ hasDataInView,
68
+ returnToOverview,
69
+ mosaicEndpoint,
70
+ };
71
+ });
72
+
73
+ /**
74
+ * Modifies the layers collection to display the mosaic layer.
75
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
76
+ * @param {Record<string, string> | null} [params] - Query params forwarded to the tilejson URL.
77
+ */
78
+ export async function renderMosaic(mosaicEndpoint, params) {
79
+ const mosaicLayers = await createMosaicLayers(mosaicEndpoint, params);
80
+
81
+ if (!mosaicLayers.length) {
82
+ return;
83
+ }
84
+
85
+ const { selectedStac } = useSTAcStore();
86
+ if (selectedStac) {
87
+ const eodashCol = eodashCollections.find(
88
+ (ec) => ec.collectionStac?.id === selectedStac.id,
89
+ );
90
+ const dates = (await eodashCol?.getDates()) ?? [];
91
+ const { timeControlValues } = extractLayerTimeValues(dates, datetime.value);
92
+ if (timeControlValues) {
93
+ mosaicLayers[0].properties.timeControlValues = timeControlValues;
94
+ mosaicLayers[0].properties.timeControlProperty = "TIME";
95
+ }
96
+ }
97
+
98
+ assignAnalysisLayer(mosaicLayers);
99
+ const { latestLayer } = useMosaicState();
100
+ latestLayer.value = mosaicLayers[0];
101
+ log.debug("[eodash] Mosaic layer rendered.", mosaicLayers[0].source.url);
102
+ }
103
+
104
+ /**
105
+ * Updates the mosaic layer based on the current filters.
106
+ * @param {string | undefined | null} mosaicEndpoint - The TileJSON URL for the mosaic.
107
+ * @param {{ timeRange?: [string, string]; filters?: import("@/types").ItemFilterFilters }} [queries]
108
+ */
109
+ export async function updateMosaicLayer(
110
+ mosaicEndpoint,
111
+ { timeRange, filters } = {},
112
+ ) {
113
+ if (!mosaicEndpoint) return;
114
+
115
+ /** @type {Record<string, string>} */
116
+ const params = {};
117
+
118
+ if (timeRange && Array.isArray(timeRange) && timeRange.length === 2) {
119
+ const start = timeToDate(timeRange[0]);
120
+ const end = timeToDate(timeRange[1]);
121
+ if (start && end) {
122
+ params.datetime = start === end ? start : `${start}/${end}`;
123
+ }
124
+ }
125
+
126
+ const cqlFilter = buildCqlFilter(filters);
127
+ if (cqlFilter) {
128
+ params.filter = cqlFilter;
129
+ params["filter-lang"] = "cql2-text";
130
+ }
131
+
132
+ const queryParams = Object.keys(params).length > 0 ? params : null;
133
+ const { query } = useMosaicState();
134
+ query.value = queryParams;
135
+ await renderMosaic(mosaicEndpoint, queryParams);
136
+ }
137
+
138
+ /**
139
+ * Renders the latest mosaic layer stored in the shared mosaic state.
140
+ */
141
+ export function renderLatestMosaic() {
142
+ const { latestLayer } = useMosaicState();
143
+ if (!latestLayer.value) return;
144
+ assignAnalysisLayer([latestLayer.value]);
145
+ }
146
+
147
+ /**
148
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
149
+ * @param {[string, string] | undefined} timeRange
150
+ */
151
+ export async function initMosaic(mosaicEndpoint, timeRange) {
152
+ await updateMosaicLayer(mosaicEndpoint, { timeRange });
153
+ await nextTick();
154
+ const zoom =
155
+ mapPosition.value[2] ?? mapEl.value?.map?.getView()?.getZoom() ?? 0;
156
+ toggleMosaicVisibility(zoom);
157
+ }
158
+
159
+ /**
160
+ * Vue composable that initializes the mosaic layer and keeps it in sync with
161
+ * map zoom and layer updates.
162
+ * @param {string | null | undefined} mosaicEndpoint - Pass null/undefined to disable.
163
+ * @param {import("vue").Ref<[string, string]> | undefined} timeRange
164
+ * @param {string[]} [indicators] - If provided, mosaic only activates for these collection IDs.
165
+ */
166
+ export function useInitMosaic(mosaicEndpoint, timeRange, indicators) {
167
+ if (!mosaicEndpoint) return;
168
+
169
+ const store = useSTAcStore();
170
+ const { isItemView, latestLayer, query, hasDataInView } = useMosaicState();
171
+
172
+ function shouldInitiate() {
173
+ if (isItemView.value) return false;
174
+ if (!store.selectedStac?.id) return false;
175
+ if (indicators !== undefined && !indicators.includes(store.selectedStac.id))
176
+ return false;
177
+ return true;
178
+ }
179
+
180
+ const stopWatcher = watch(mapPosition, (updatedPos, oldPos) => {
181
+ // Any pan or zoom changes the viewport bbox; re-check data presence.
182
+ scheduleMosaicDataCheck();
183
+ const [_oldX, _oldY, oldZ] = oldPos;
184
+ const [_x, _y, z] = updatedPos;
185
+ if (!z || z === oldZ) return;
186
+ toggleMosaicVisibility(z);
187
+ });
188
+
189
+ // Re-check when filters/time change, and re-apply visibility once it lands.
190
+ const stopQueryWatch = watch(query, () => scheduleMosaicDataCheck());
191
+
192
+ onMounted(async () => {
193
+ if (!shouldInitiate()) return;
194
+ initMosaic(mosaicEndpoint, timeRange?.value);
195
+ });
196
+
197
+ useOnLayersUpdate((evt) => {
198
+ if (evt !== "layers:updated") return;
199
+ if (!shouldInitiate()) return;
200
+ initMosaic(mosaicEndpoint, timeRange?.value);
201
+ });
202
+
203
+ onUnmounted(() => {
204
+ latestLayer.value = null;
205
+ query.value = null;
206
+ hasDataInView.value = true;
207
+ stopWatcher();
208
+ stopQueryWatch();
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Shared debounced scheduler for mosaic updates. All callers share a single
214
+ * timer so concurrent updates (e.g. datetime + filter changes from different
215
+ * widgets within the debounce window) coalesce into one final update.
216
+ *
217
+ * @returns {(mosaicEndpoint: string | undefined | null, timeRange: [string,string] | undefined, filters?: import("@/types").ItemFilterFilters) => void}
218
+ */
219
+ export const useScheduleMosaicUpdate = createSharedComposable(() =>
220
+ useDebounceFn(
221
+ /**
222
+ * @param {string | undefined | null} mosaicEndpoint
223
+ * @param {[string, string] | undefined} timeRange
224
+ * @param {import("@/types").ItemFilterFilters} [filters]
225
+ */
226
+ (mosaicEndpoint, timeRange, filters) => {
227
+ updateMosaicLayer(mosaicEndpoint, { timeRange, filters });
228
+ },
229
+ 300,
230
+ ),
231
+ );
232
+
233
+ /**
234
+ * Creates a layer configuration for the mosaic.
235
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
236
+ * @param {Record<string, string> | null} [params] - Query params (datetime, filter, etc.).
237
+ * @returns {Promise<Record<string, any>[]>}
238
+ */
239
+ async function createMosaicLayers(mosaicEndpoint, params) {
240
+ const { selectedStac } = useSTAcStore();
241
+ const renders = resolveRenders(selectedStac, useEodash()?.options?.renders);
242
+ const preset = renders ? Object.values(renders)[0] : undefined;
243
+ if (!preset) {
244
+ console.warn(
245
+ "[eodash] No render preset (collection `renders` nor `options.renders`) for the mosaic layer.",
246
+ );
247
+ return [];
248
+ }
249
+
250
+ const renderParamsStr = encodeURLObject({
251
+ assets: preset.assets,
252
+ expression: preset.expression,
253
+ nodata: preset.nodata,
254
+ resampling: preset.resampling,
255
+ color_formula: preset.color_formula,
256
+ colormap: preset.colormap,
257
+ colormap_name: preset.colormap_name,
258
+ rescale: preset.rescale,
259
+ });
260
+
261
+ const tileParams = new URLSearchParams({ tilesize: "512", ...params });
262
+ const tileJsonUrl = `${mosaicEndpoint}?${renderParamsStr}${tileParams.toString()}`;
263
+
264
+ const tileJSON = await axios
265
+ .get(tileJsonUrl)
266
+ .then((res) => res.data)
267
+ .catch((err) => {
268
+ console.error("Failed to fetch mosaic TileJSON", err);
269
+ return null;
270
+ });
271
+ if (!tileJSON?.tiles?.[0]) {
272
+ console.warn("[eodash] No tile URL found in mosaic TileJSON response.");
273
+ return [];
274
+ }
275
+
276
+ return [
277
+ {
278
+ type: "Tile",
279
+ minZoom: useMosaicState().visibilityThreshold.value,
280
+ properties: {
281
+ id: `${indicator.value};:;mosaic`,
282
+ title: "Mosaic Layer",
283
+ },
284
+ source: {
285
+ type: "XYZ",
286
+ url: tileJSON.tiles[0],
287
+ },
288
+ },
289
+ ];
290
+ }
291
+
292
+ /**
293
+ * @param {string | Date} time
294
+ * @returns {string | null}
295
+ */
296
+ function timeToDate(time) {
297
+ if (!time) return null;
298
+ const date = new Date(time);
299
+ if (isNaN(date.getTime())) return null;
300
+ return date.toISOString().split("T")[0];
301
+ }
302
+
303
+ /**
304
+ * Returns the OL-equivalent zoom from `mapPosition[2]`. In 2D the raw value
305
+ * is already the OL zoom. In globe mode the raw value is camera altitude in
306
+ * meters; convert it via eox-map's internal formula so callers can compare
307
+ * against a single (2D-style) threshold.
308
+ *
309
+ * @param {number} rawZ
310
+ * @returns {number}
311
+ */
312
+ export function normalizeGlobeZoom(rawZ) {
313
+ if (!isGlobe.value) return rawZ;
314
+ const zoomFactor = mapEl.value?.globeConfig?.useHighLOD ? 1 : 2;
315
+ return Math.log2(21_050_000 / rawZ) + zoomFactor;
316
+ }
317
+
318
+ /**
319
+ * check whether the filtered dataset has any assets in the current 2D view
320
+ */
321
+ export async function checkMosaicDataInView() {
322
+ const { hasDataInView, query, mosaicEndpoint, visibilityThreshold } =
323
+ useMosaicState();
324
+ const extent = mapEl.value?.lonLatExtent;
325
+ if (
326
+ !extent ||
327
+ isGlobe.value ||
328
+ !mosaicEndpoint.value ||
329
+ normalizeGlobeZoom(mapPosition.value[2] ?? 0) < visibilityThreshold.value
330
+ ) {
331
+ hasDataInView.value = true;
332
+ return;
333
+ }
334
+
335
+ if (extent.some(Number.isNaN)) return;
336
+
337
+ const base = mosaicEndpoint.value.replace(
338
+ "/WebMercatorQuad/tilejson.json",
339
+ "",
340
+ );
341
+ const [minx, miny, maxx, maxy] = extent;
342
+ const params = new URLSearchParams({ limit: "1", ...(query.value ?? {}) });
343
+ try {
344
+ const { data } = await axios.get(
345
+ `${base}/bbox/${minx},${miny},${maxx},${maxy}/assets?${params}`,
346
+ );
347
+ const count = Array.isArray(data)
348
+ ? data.length
349
+ : Object.keys(data ?? {}).length;
350
+ hasDataInView.value = count > 0;
351
+ } catch {
352
+ hasDataInView.value = true;
353
+ }
354
+ }
355
+
356
+ /** Shared debounced data-presence check; coalesces rapid pan/zoom/filter changes. */
357
+ const scheduleMosaicDataCheck = useDebounceFn(checkMosaicDataInView, 300);
358
+
359
+ /**
360
+ * @param {number} zoomLevel
361
+ * @param {number} [threshold] - defaults to the shared visibility threshold
362
+ */
363
+ function toggleMosaicVisibility(zoomLevel, threshold) {
364
+ // Only globe needs manual toggling.
365
+ if (!isGlobe.value) return;
366
+ const { latestLayer, visibilityThreshold } = useMosaicState();
367
+ if (!latestLayer.value) return;
368
+
369
+ const layerId = /** @type {string} */ (latestLayer.value.properties.id);
370
+ const layer = mapEl.value?.getLayerById(layerId);
371
+ if (!layer) return;
372
+
373
+ const eqZoom = normalizeGlobeZoom(zoomLevel);
374
+ layer.setVisible(eqZoom >= (threshold ?? visibilityThreshold.value));
375
+ }
376
+
377
+ /**
378
+ * Writes layers into the AnalysisGroup and triggers a map re-render.
379
+ * @param {Record<string, any>[]} layersToAssign
380
+ */
381
+ function assignAnalysisLayer(layersToAssign) {
382
+ const mapLayers = getLayers();
383
+ const { analysisGroup, layers } = ensureAnalysisGroup(mapLayers);
384
+ analysisGroup.layers = layersToAssign;
385
+ if (mapEl.value) {
386
+ // Reassign to trigger map re-render after in-place layer mutation
387
+ mapEl.value.layers = /** @type {import("@eox/map").EoxLayer[]} */ ([
388
+ ...layers,
389
+ ]);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Ensure the AnalysisGroup exists in the layers collection.
395
+ * @param {Record<string, any>[]} layersCollection
396
+ * @returns {{ layers: Record<string, any>[], analysisGroup: Record<string, any> }}
397
+ */
398
+ function ensureAnalysisGroup(layersCollection) {
399
+ let analysisGroup = layersCollection.find(
400
+ (l) => l?.properties?.id === "AnalysisGroup",
401
+ );
402
+
403
+ if (!analysisGroup) {
404
+ analysisGroup = {
405
+ type: "Group",
406
+ properties: {
407
+ id: "AnalysisGroup",
408
+ title: "Data Layers",
409
+ },
410
+ layers: [],
411
+ };
412
+ layersCollection.push(analysisGroup);
413
+ }
414
+
415
+ return { layers: layersCollection, analysisGroup };
416
+ }
@@ -1,8 +1,38 @@
1
1
  import Axios from "axios";
2
2
  import { setupCache } from "axios-cache-interceptor";
3
3
 
4
- const instance = Axios.create();
4
+ import { loading } from "@/store/states";
5
5
 
6
+ const instance = Axios.create();
6
7
  export const axios = setupCache(instance, { cacheTakeover: false });
7
8
 
9
+ function installLoadingInterceptors() {
10
+ const reqId = axios.interceptors.request.use((config) => {
11
+ loading.activeLoads++;
12
+ return config;
13
+ });
14
+
15
+ const resId = axios.interceptors.response.use(
16
+ (response) => {
17
+ loading.activeLoads = Math.max(0, loading.activeLoads - 1);
18
+ return response;
19
+ },
20
+ (error) => {
21
+ loading.activeLoads = Math.max(0, loading.activeLoads - 1);
22
+ return Promise.reject(error);
23
+ },
24
+ );
25
+
26
+ return () => {
27
+ axios.interceptors.request.eject(reqId);
28
+ axios.interceptors.response.eject(resId);
29
+ };
30
+ }
31
+
32
+ const dispose = installLoadingInterceptors();
33
+
34
+ if (import.meta.hot) {
35
+ import.meta.hot.dispose(() => dispose());
36
+ }
37
+
8
38
  export default axios;
@@ -20,7 +20,7 @@ export const getLayers = () => mapEl.value?.layers ?? [];
20
20
 
21
21
  /**
22
22
  * Returns the current layers of {@link mapCompareEl}
23
- * * @returns {import("@eox/map").EoxLayer[]}
23
+ * @returns {import("@eox/map").EoxLayer[]}
24
24
  */
25
25
  export const getCompareLayers = () => mapCompareEl.value?.layers ?? [];
26
26
 
@@ -31,7 +31,7 @@ export const useSTAcStore = defineStore("stac", () => {
31
31
 
32
32
  /**
33
33
  * List of supported endpoints for upscaling
34
- * @type {import("vue").Ref<string[]>}
34
+ * @type {import("vue").Ref<Array<string | { url: string; titilerVersion?: 1 | 2 }>>}
35
35
  */
36
36
  const supportedUpscalingEndpoints = ref([]);
37
37
 
@@ -61,11 +61,20 @@ export const useSTAcStore = defineStore("stac", () => {
61
61
  * >}
62
62
  */
63
63
  const selectedCompareStac = ref(null);
64
+ /**
65
+ * Currently selected item
66
+ * @type {import("vue").Ref<import("stac-ts").StacLink | import("stac-ts").StacItem | null>}
67
+ */
64
68
  /**
65
69
  * Currently selected item
66
70
  * @type {import("vue").Ref<import("stac-ts").StacLink | import("stac-ts").StacItem | null>}
67
71
  */
68
72
  const selectedItem = ref(null);
73
+ /**
74
+ * Currently selected compare item
75
+ * @type {import("vue").Ref<import("stac-ts").StacLink | import("stac-ts").StacItem | null>}
76
+ */
77
+ const selectedCompareItem = ref(null);
69
78
 
70
79
  /**
71
80
  * Initializes the store by assigning the STAC endpoint.
@@ -151,8 +160,8 @@ export const useSTAcStore = defineStore("stac", () => {
151
160
  }
152
161
  //@ts-expect-error "this" type is not exported by pinia
153
162
  const patch = this?.$patch;
154
- if (stacItem && patch) {
155
- patch({ selectedItem: stacItem });
163
+ if (patch) {
164
+ patch({ selectedItem: stacItem ?? null });
156
165
  }
157
166
 
158
167
  await axios
@@ -182,10 +191,15 @@ export const useSTAcStore = defineStore("stac", () => {
182
191
  *
183
192
  * @param {string} relativePath - Stac link href
184
193
  * @param {boolean} [isPOI=false] - If true, the STAC is loaded for a point of interest
194
+ * @param {Object} [stacItem] - The STAC item to load
185
195
  * @returns {Promise<void>}
186
196
  * @see {@link selectedCompareStac}
187
197
  */
188
- async function loadSelectedCompareSTAC(relativePath = "", isPOI = false) {
198
+ async function loadSelectedCompareSTAC(
199
+ relativePath = "",
200
+ isPOI = false,
201
+ stacItem,
202
+ ) {
189
203
  if (!stacEndpoint.value) {
190
204
  return Promise.reject(
191
205
  new Error("STAC endpoint is not defined in eodash configuration"),
@@ -196,6 +210,11 @@ export const useSTAcStore = defineStore("stac", () => {
196
210
  // construct absolute URL of a poi
197
211
  absoluteUrl.value = constructPoiUrl(relativePath, compareIndicator.value);
198
212
  }
213
+ //@ts-expect-error "this" type is not exported by pinia
214
+ const patch = this?.$patch;
215
+ if (stacItem && patch) {
216
+ patch({ selectedCompareItem: stacItem });
217
+ }
199
218
  await axios
200
219
  .get(absoluteUrl.value)
201
220
  .then(async (resp) => {
@@ -247,6 +266,7 @@ export const useSTAcStore = defineStore("stac", () => {
247
266
 
248
267
  return {
249
268
  stacEndpoint,
269
+ rasterEndpoint,
250
270
  isApi,
251
271
  stac,
252
272
  init,
@@ -257,6 +277,7 @@ export const useSTAcStore = defineStore("stac", () => {
257
277
  selectedStac,
258
278
  selectedCompareStac,
259
279
  selectedItem,
280
+ selectedCompareItem,
260
281
  supportedUpscalingEndpoints,
261
282
  };
262
283
  });
@@ -2,7 +2,7 @@
2
2
  import log from "loglevel";
3
3
  log.setLevel(log.levels.WARN, true);
4
4
 
5
- import { ref, shallowRef } from "vue";
5
+ import { reactive, ref, shallowRef } from "vue";
6
6
 
7
7
  /** Currently selected STAC endpoint */
8
8
  export const currentUrl = ref("");
@@ -30,7 +30,7 @@ export const registeredProjections = ["EPSG:4326", "EPSG:3857"];
30
30
  /** available projection to be rendered by `EodashMap` */
31
31
  export const availableMapProjection = ref("EPSG:3857");
32
32
 
33
- /** @type {import("vue").Ref<import("@eox/map").EOxMap | null>} */
33
+ /** @type {import("vue").Ref<import("@eox/map").EOxMap & { mapUpdateId?: number } | null>} */
34
34
  export const mapEl = shallowRef(null);
35
35
 
36
36
  /** @type {import("vue").Ref<import("@eox/map").EOxMap | null>} */
@@ -73,3 +73,21 @@ export const chartSpec = ref(null);
73
73
  * @type {import("vue").Ref<import("vega-embed").VisualizationSpec | null>}
74
74
  */
75
75
  export const compareChartSpec = ref(null);
76
+
77
+ /**
78
+ * Global loading state.
79
+ * - `loading.activeLoads` — increment/decrement to track concurrent loads
80
+ * - `loading.value` — derived boolean, true when `activeLoads > 0`
81
+ */
82
+ export const loading = reactive({
83
+ activeLoads: 0,
84
+ get value() {
85
+ return this.activeLoads > 0;
86
+ },
87
+ });
88
+
89
+ /**
90
+ * Adapter allows external widgets to hook into tooltip property transformation
91
+ * @type {import("vue").Ref<((param: {key: string, value: any}, map?: string) => {key: string, value: any} | undefined) | null>}
92
+ */
93
+ export const tooltipAdapter = shallowRef(null);