@eodash/eodash 5.7.0 → 5.8.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 (119) hide show
  1. package/core/client/components/EodashOverlay.vue +27 -4
  2. package/core/client/composables/DefineEodash.js +0 -1
  3. package/core/client/composables/DefineWidgets.js +3 -1
  4. package/core/client/eodashSTAC/EodashCollection.js +87 -24
  5. package/core/client/eodashSTAC/cql.js +64 -0
  6. package/core/client/eodashSTAC/createLayers.js +353 -187
  7. package/core/client/eodashSTAC/helpers.js +694 -110
  8. package/core/client/eodashSTAC/mosaic.js +422 -0
  9. package/core/client/plugins/axios.js +31 -1
  10. package/core/client/store/actions.js +1 -1
  11. package/core/client/store/stac.js +54 -4
  12. package/core/client/store/states.js +20 -2
  13. package/core/client/types.ts +152 -28
  14. package/core/client/utils/bands-editor/exampleSchema.json +50 -50
  15. package/core/client/utils/states.js +10 -0
  16. package/dist/client/{DashboardLayout-kRsmsNW5.js → DashboardLayout-DEs4f_Vy.js} +2 -2
  17. package/dist/client/{DynamicWebComponent-CnKhYo0K.js → DynamicWebComponent-PvnV88BA.js} +1 -1
  18. package/dist/client/EodashChart-D-3AGKZY.js +260 -0
  19. package/dist/client/{EodashDatePicker-CMPDT-ce.js → EodashDatePicker-Dm_goZzs.js} +3 -3
  20. package/dist/client/{EodashItemFilter-BpdA8QQa.js → EodashItemFilter-CJy3zYzD.js} +2 -1
  21. package/dist/client/{EodashLayerControl-BHpDA4_0.js → EodashLayerControl-rL8UnpkA.js} +55 -33
  22. package/dist/client/{EodashLayoutSwitcher-B1fRUt7G.js → EodashLayoutSwitcher-ZRlESI9O.js} +3 -3
  23. package/dist/client/{EodashMapBtns-Cpned0tc.js → EodashMapBtns-CG7u2Aq8.js} +227 -112
  24. package/dist/client/EodashStacInfo-CJhhxthD.js +169 -0
  25. package/dist/client/{EodashTools-1zHz26gv.js → EodashTools-CWyK8QJT.js} +4 -4
  26. package/dist/client/{ExportState-DbOm8hBI.js → ExportState-BXhy5TOU.js} +20 -174
  27. package/dist/client/{Footer-BKg-VVkj.js → Footer-DZPvnhcT.js} +1 -1
  28. package/dist/client/{Header-DCrwCvmY.js → Header-CiGsDE_a.js} +2 -2
  29. package/dist/client/{MobileLayout-vWSHpluo.js → MobileLayout-BJHW7x5V.js} +2 -2
  30. package/dist/client/{PopUp-9uimfK49.js → PopUp-7_XnhvCc.js} +4 -168
  31. package/dist/client/{ProcessList-CTTIZa06.js → ProcessList-LL_UCbvc.js} +5 -5
  32. package/dist/client/VDivider-DWYM0WUr.js +168 -0
  33. package/dist/client/{VImg-CrmB76ZY.js → VImg-B75dgw2K.js} +2 -2
  34. package/dist/client/{VMain-OKZNpcEG.js → VMain-DvMmOL4f.js} +1 -1
  35. package/dist/client/{VTooltip-CJw2FyWk.js → VTooltip-ClWHUmWL.js} +2 -2
  36. package/dist/client/{WidgetsContainer-BaWOnE5R.js → WidgetsContainer-BqaWs4ch.js} +1 -1
  37. package/dist/client/{asWebComponent-BHWpMuE3.js → asWebComponent-IZanwL-A.js} +677 -379
  38. package/dist/client/{async-DSKj6g1f.js → async-CiNIOBVz.js} +31 -29
  39. package/dist/client/{create-layers-config-CBoYedqB.js → create-layers-config-3S3y_kZg.js} +17 -17
  40. package/dist/client/dialog-transition-c4ZSnzac.js +170 -0
  41. package/dist/client/eo-dash.js +1 -1
  42. package/dist/client/{forwardRefs-DH51zPUr.js → forwardRefs-B-c1_6yo.js} +8 -87
  43. package/dist/client/{handling-BrcDDlgS.js → handling-NvY8Csry.js} +3 -4
  44. package/dist/client/{helpers-DjZVl_WF.js → helpers-BFuh19CP.js} +1094 -194
  45. package/dist/client/{index-1JKE68n3.js → index-1wMAyuNL.js} +219 -67
  46. package/dist/client/{index-CSG_JoOA.js → index-B8jGJSTA.js} +62 -56
  47. package/dist/client/{index-Cu3-13Xg.js → index-BKQQJkhy.js} +167 -61
  48. package/dist/client/index-D0jUyol8.js +3167 -0
  49. package/dist/client/{index-CUjl-ZsI.js → index-KGxz5Egv.js} +2 -2
  50. package/dist/client/mosaic-vHvSuih6.js +459 -0
  51. package/dist/client/templates.js +251 -121
  52. package/dist/client/{transition-OHcz5Qq1.js → transition-BkyUsZh_.js} +1 -1
  53. package/dist/node/cli.js +5 -5
  54. package/dist/node/types.d.ts +4 -0
  55. package/dist/types/core/client/composables/DefineEodash.d.ts +1 -1
  56. package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +57 -25
  57. package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
  58. package/dist/types/core/client/eodashSTAC/createLayers.d.ts +42 -23
  59. package/dist/types/core/client/eodashSTAC/helpers.d.ts +118 -19
  60. package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
  61. package/dist/types/core/client/store/stac.d.ts +38 -8
  62. package/dist/types/core/client/store/states.d.ts +24 -2
  63. package/dist/types/core/client/types.d.ts +128 -24
  64. package/dist/types/core/client/utils/states.d.ts +12 -0
  65. package/dist/types/templates/baseConfig.d.ts +1 -100
  66. package/dist/types/templates/compare.d.ts +32 -3
  67. package/dist/types/templates/expert.d.ts +18 -0
  68. package/dist/types/templates/explore.d.ts +10 -1
  69. package/dist/types/templates/lite.d.ts +11 -0
  70. package/dist/types/widgets/EodashChart.vue.d.ts +5 -3
  71. package/dist/types/widgets/EodashItemCatalog/index.vue.d.ts +129 -2
  72. package/dist/types/widgets/EodashItemCatalog/methods/filters.d.ts +28 -6
  73. package/dist/types/widgets/EodashItemCatalog/methods/handlers.d.ts +17 -4
  74. package/dist/types/widgets/EodashItemCatalog/methods/map.d.ts +16 -4
  75. package/dist/types/widgets/EodashLayerControl.vue.d.ts +35 -16
  76. package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +14 -10
  77. package/dist/types/widgets/EodashMap/index.vue.d.ts +86 -37
  78. package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -1
  79. package/dist/types/widgets/EodashMap/methods/index.d.ts +5 -1
  80. package/dist/types/widgets/EodashMap/methods/use-map-loading.d.ts +1 -0
  81. package/dist/types/widgets/EodashProcess/index.vue.d.ts +5 -3
  82. package/dist/types/widgets/EodashProcess/methods/custom-endpoints/layers/eoxhub-workspaces-endpoint.d.ts +1 -1
  83. package/dist/types/widgets/EodashProcess/states.d.ts +1 -1
  84. package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
  85. package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +20 -0
  86. package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +3 -8
  87. package/dist/types/widgets/EodashTimeSlider/types.d.ts +6 -16
  88. package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
  89. package/package.json +34 -34
  90. package/templates/baseConfig.js +37 -6
  91. package/templates/compare.js +61 -7
  92. package/templates/expert.js +24 -2
  93. package/templates/explore.js +12 -5
  94. package/templates/lite.js +13 -0
  95. package/widgets/EodashChart.vue +162 -21
  96. package/widgets/EodashItemCatalog/index.vue +363 -35
  97. package/widgets/EodashItemCatalog/methods/filters.js +134 -70
  98. package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
  99. package/widgets/EodashItemCatalog/methods/map.js +141 -28
  100. package/widgets/EodashLayerControl.vue +51 -25
  101. package/widgets/EodashMap/EodashMapBtns.vue +109 -48
  102. package/widgets/EodashMap/index.vue +91 -105
  103. package/widgets/EodashMap/methods/btns.js +64 -31
  104. package/widgets/EodashMap/methods/create-layers-config.js +16 -16
  105. package/widgets/EodashMap/methods/index.js +71 -8
  106. package/widgets/EodashMap/methods/use-map-loading.js +58 -0
  107. package/widgets/EodashProcess/index.vue +136 -16
  108. package/widgets/EodashProcess/methods/composables.js +13 -0
  109. package/widgets/EodashProcess/methods/outputs.js +0 -1
  110. package/widgets/EodashProcess/methods/utils.js +29 -27
  111. package/widgets/EodashProcess/states.js +1 -1
  112. package/widgets/EodashStacInfo.vue +64 -2
  113. package/widgets/EodashTimeSlider/index.vue +58 -2
  114. package/widgets/EodashTimeSlider/methods/index.js +5 -48
  115. package/widgets/EodashTimeSlider/types.ts +7 -17
  116. package/widgets/ExportState.vue +13 -4
  117. package/dist/client/EodashChart-CLDqE93C.js +0 -144
  118. package/dist/client/EodashStacInfo-BQspAHKH.js +0 -107
  119. package/dist/client/index-5Jhsa8Xe.js +0 -570
@@ -0,0 +1,422 @@
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
+ normalizeNodata,
29
+ normalizeRescale,
30
+ resolveRenders,
31
+ } from "./helpers";
32
+ import { useSTAcStore } from "@/store/stac";
33
+ import { eodashCollections } from "@/utils/states";
34
+ import log from "loglevel";
35
+ import { buildCqlFilter } from "./cql";
36
+
37
+ const mosaicReturnToOverviewKey = Symbol("mosaic:return-to-overview");
38
+
39
+ /**
40
+ * Shared mosaic state across all components. Stores the latest mosaic layer,
41
+ * query parameters, visibility threshold, item-view flag, and an event bus
42
+ * for the "Back to overview" user action .
43
+ */
44
+ export const useMosaicState = createSharedComposable(() => {
45
+ /** @type {import("vue").ShallowRef<Record<string, any> | null>} */
46
+ const latestLayer = shallowRef(null);
47
+ /** @type {import("vue").Ref<Record<string, string> | null>} */
48
+ const query = ref(null);
49
+ /** @type {import("vue").Ref<number>} */
50
+ const visibilityThreshold = ref(8);
51
+ /** @type {import("vue").Ref<boolean>} */
52
+ const isItemView = ref(false);
53
+ /** @type {import("vue").Ref<boolean>} - filtered mosaic has assets in the current view */
54
+ const hasDataInView = ref(true);
55
+ const returnToOverview = useEventBus(mosaicReturnToOverviewKey);
56
+
57
+ const store = useSTAcStore();
58
+ /** @type {import("vue").ComputedRef<string | null>} */
59
+ const mosaicEndpoint = computed(() => {
60
+ if (!store.rasterEndpoint || !store.selectedStac?.id) return null;
61
+ return `${store.rasterEndpoint}/collections/${store.selectedStac.id}/WebMercatorQuad/tilejson.json`;
62
+ });
63
+
64
+ return {
65
+ latestLayer,
66
+ query,
67
+ visibilityThreshold,
68
+ isItemView,
69
+ hasDataInView,
70
+ returnToOverview,
71
+ mosaicEndpoint,
72
+ };
73
+ });
74
+
75
+ /**
76
+ * Modifies the layers collection to display the mosaic layer.
77
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
78
+ * @param {Record<string, string> | null} [params] - Query params forwarded to the tilejson URL.
79
+ */
80
+ export async function renderMosaic(mosaicEndpoint, params) {
81
+ const mosaicLayers = await createMosaicLayers(mosaicEndpoint, params);
82
+
83
+ if (!mosaicLayers.length) {
84
+ return;
85
+ }
86
+
87
+ const { selectedStac } = useSTAcStore();
88
+ if (selectedStac) {
89
+ const eodashCol = eodashCollections.find(
90
+ (ec) => ec.collectionStac?.id === selectedStac.id,
91
+ );
92
+ const dates = (await eodashCol?.getDates()) ?? [];
93
+ const { timeControlValues } = extractLayerTimeValues(dates, datetime.value);
94
+ if (timeControlValues) {
95
+ mosaicLayers[0].properties.timeControlValues = timeControlValues;
96
+ mosaicLayers[0].properties.timeControlProperty = "TIME";
97
+ }
98
+ }
99
+
100
+ assignAnalysisLayer(mosaicLayers);
101
+ const { latestLayer } = useMosaicState();
102
+ latestLayer.value = mosaicLayers[0];
103
+ log.debug("[eodash] Mosaic layer rendered.", mosaicLayers[0].source.url);
104
+ }
105
+
106
+ /**
107
+ * Updates the mosaic layer based on the current filters.
108
+ * @param {string | undefined | null} mosaicEndpoint - The TileJSON URL for the mosaic.
109
+ * @param {{ timeRange?: [string, string]; filters?: import("@/types").ItemFilterFilters }} [queries]
110
+ */
111
+ export async function updateMosaicLayer(
112
+ mosaicEndpoint,
113
+ { timeRange, filters } = {},
114
+ ) {
115
+ if (!mosaicEndpoint) return;
116
+
117
+ /** @type {Record<string, string>} */
118
+ const params = {};
119
+
120
+ if (timeRange && Array.isArray(timeRange) && timeRange.length === 2) {
121
+ const start = timeToDate(timeRange[0]);
122
+ const end = timeToDate(timeRange[1]);
123
+ if (start && end) {
124
+ params.datetime = start === end ? start : `${start}/${end}`;
125
+ }
126
+ }
127
+
128
+ const cqlFilter = buildCqlFilter(filters);
129
+ if (cqlFilter) {
130
+ params.filter = cqlFilter;
131
+ params["filter-lang"] = "cql2-text";
132
+ }
133
+
134
+ const queryParams = Object.keys(params).length > 0 ? params : null;
135
+ const { query } = useMosaicState();
136
+ query.value = queryParams;
137
+ await renderMosaic(mosaicEndpoint, queryParams);
138
+ }
139
+
140
+ /**
141
+ * Renders the latest mosaic layer stored in the shared mosaic state.
142
+ */
143
+ export function renderLatestMosaic() {
144
+ const { latestLayer } = useMosaicState();
145
+ if (!latestLayer.value) return;
146
+ assignAnalysisLayer([latestLayer.value]);
147
+ }
148
+
149
+ /**
150
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
151
+ * @param {[string, string] | undefined} timeRange
152
+ */
153
+ export async function initMosaic(mosaicEndpoint, timeRange) {
154
+ await updateMosaicLayer(mosaicEndpoint, { timeRange });
155
+ await nextTick();
156
+ const zoom =
157
+ mapPosition.value[2] ?? mapEl.value?.map?.getView()?.getZoom() ?? 0;
158
+ toggleMosaicVisibility(zoom);
159
+ }
160
+
161
+ /**
162
+ * Vue composable that initializes the mosaic layer and keeps it in sync with
163
+ * map zoom and layer updates.
164
+ * @param {string | null | undefined} mosaicEndpoint - Pass null/undefined to disable.
165
+ * @param {import("vue").Ref<[string, string]> | undefined} timeRange
166
+ * @param {string[]} [indicators] - If provided, mosaic only activates for these collection IDs.
167
+ */
168
+ export function useInitMosaic(mosaicEndpoint, timeRange, indicators) {
169
+ if (!mosaicEndpoint) return;
170
+
171
+ const store = useSTAcStore();
172
+ const { isItemView, latestLayer, query, hasDataInView } = useMosaicState();
173
+
174
+ function shouldInitiate() {
175
+ if (isItemView.value) return false;
176
+ if (!store.selectedStac?.id) return false;
177
+ if (indicators !== undefined && !indicators.includes(store.selectedStac.id))
178
+ return false;
179
+ return true;
180
+ }
181
+
182
+ const stopWatcher = watch(mapPosition, (updatedPos, oldPos) => {
183
+ // Any pan or zoom changes the viewport bbox; re-check data presence.
184
+ scheduleMosaicDataCheck();
185
+ const [_oldX, _oldY, oldZ] = oldPos;
186
+ const [_x, _y, z] = updatedPos;
187
+ if (!z || z === oldZ) return;
188
+ toggleMosaicVisibility(z);
189
+ });
190
+
191
+ // Re-check when filters/time change, and re-apply visibility once it lands.
192
+ const stopQueryWatch = watch(query, () => scheduleMosaicDataCheck());
193
+
194
+ onMounted(async () => {
195
+ if (!shouldInitiate()) return;
196
+ initMosaic(mosaicEndpoint, timeRange?.value);
197
+ });
198
+
199
+ useOnLayersUpdate((evt) => {
200
+ if (evt !== "layers:updated") return;
201
+ if (!shouldInitiate()) return;
202
+ initMosaic(mosaicEndpoint, timeRange?.value);
203
+ });
204
+
205
+ onUnmounted(() => {
206
+ latestLayer.value = null;
207
+ query.value = null;
208
+ hasDataInView.value = true;
209
+ stopWatcher();
210
+ stopQueryWatch();
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Shared debounced scheduler for mosaic updates. All callers share a single
216
+ * timer so concurrent updates (e.g. datetime + filter changes from different
217
+ * widgets within the debounce window) coalesce into one final update.
218
+ *
219
+ * @returns {(mosaicEndpoint: string | undefined | null, timeRange: [string,string] | undefined, filters?: import("@/types").ItemFilterFilters) => void}
220
+ */
221
+ export const useScheduleMosaicUpdate = createSharedComposable(() =>
222
+ useDebounceFn(
223
+ /**
224
+ * @param {string | undefined | null} mosaicEndpoint
225
+ * @param {[string, string] | undefined} timeRange
226
+ * @param {import("@/types").ItemFilterFilters} [filters]
227
+ */
228
+ (mosaicEndpoint, timeRange, filters) => {
229
+ updateMosaicLayer(mosaicEndpoint, { timeRange, filters });
230
+ },
231
+ 300,
232
+ ),
233
+ );
234
+
235
+ /**
236
+ * Creates a layer configuration for the mosaic.
237
+ * @param {string} mosaicEndpoint - The TileJSON URL for the mosaic.
238
+ * @param {Record<string, string> | null} [params] - Query params (datetime, filter, etc.).
239
+ * @returns {Promise<Record<string, any>[]>}
240
+ */
241
+ async function createMosaicLayers(mosaicEndpoint, params) {
242
+ const { selectedStac } = useSTAcStore();
243
+ const renders = resolveRenders(selectedStac, useEodash()?.options?.renders);
244
+ const preset = renders ? Object.values(renders)[0] : undefined;
245
+ if (!preset) {
246
+ console.warn(
247
+ "[eodash] No render preset (collection `renders` nor `options.renders`) for the mosaic layer.",
248
+ );
249
+ return [];
250
+ }
251
+
252
+ const renderParamsStr = encodeURLObject({
253
+ // TiTiler treats assets and expression as mutually exclusive band selection
254
+ assets: preset.expression ? undefined : preset.assets,
255
+ expression: preset.expression,
256
+ nodata: normalizeNodata(preset.nodata),
257
+ resampling: preset.resampling,
258
+ color_formula: preset.color_formula,
259
+ colormap: preset.colormap,
260
+ colormap_name: preset.colormap_name,
261
+ rescale: normalizeRescale(preset.rescale),
262
+ });
263
+
264
+ const tileParams = new URLSearchParams({
265
+ tilesize: `${preset.tilesize ?? "512"}`,
266
+ ...params,
267
+ });
268
+ const tileJsonUrl = `${mosaicEndpoint}?${renderParamsStr}${tileParams.toString()}`;
269
+
270
+ const tileJSON = await axios
271
+ .get(tileJsonUrl)
272
+ .then((res) => res.data)
273
+ .catch((err) => {
274
+ console.error("Failed to fetch mosaic TileJSON", err);
275
+ return null;
276
+ });
277
+ if (!tileJSON?.tiles?.[0]) {
278
+ console.warn("[eodash] No tile URL found in mosaic TileJSON response.");
279
+ return [];
280
+ }
281
+
282
+ return [
283
+ {
284
+ type: "Tile",
285
+ minZoom: useMosaicState().visibilityThreshold.value,
286
+ properties: {
287
+ id: `${indicator.value};:;mosaic`,
288
+ title: "Mosaic Layer",
289
+ },
290
+ source: {
291
+ type: "XYZ",
292
+ url: tileJSON.tiles[0],
293
+ },
294
+ },
295
+ ];
296
+ }
297
+
298
+ /**
299
+ * @param {string | Date} time
300
+ * @returns {string | null}
301
+ */
302
+ function timeToDate(time) {
303
+ if (!time) return null;
304
+ const date = new Date(time);
305
+ if (isNaN(date.getTime())) return null;
306
+ return date.toISOString().split("T")[0];
307
+ }
308
+
309
+ /**
310
+ * Returns the OL-equivalent zoom from `mapPosition[2]`. In 2D the raw value
311
+ * is already the OL zoom. In globe mode the raw value is camera altitude in
312
+ * meters; convert it via eox-map's internal formula so callers can compare
313
+ * against a single (2D-style) threshold.
314
+ *
315
+ * @param {number} rawZ
316
+ * @returns {number}
317
+ */
318
+ export function normalizeGlobeZoom(rawZ) {
319
+ if (!isGlobe.value) return rawZ;
320
+ const zoomFactor = mapEl.value?.globeConfig?.useHighLOD ? 1 : 2;
321
+ return Math.log2(21_050_000 / rawZ) + zoomFactor;
322
+ }
323
+
324
+ /**
325
+ * check whether the filtered dataset has any assets in the current 2D view
326
+ */
327
+ export async function checkMosaicDataInView() {
328
+ const { hasDataInView, query, mosaicEndpoint, visibilityThreshold } =
329
+ useMosaicState();
330
+ const extent = mapEl.value?.lonLatExtent;
331
+ if (
332
+ !extent ||
333
+ isGlobe.value ||
334
+ !mosaicEndpoint.value ||
335
+ normalizeGlobeZoom(mapPosition.value[2] ?? 0) < visibilityThreshold.value
336
+ ) {
337
+ hasDataInView.value = true;
338
+ return;
339
+ }
340
+
341
+ if (extent.some(Number.isNaN)) return;
342
+
343
+ const base = mosaicEndpoint.value.replace(
344
+ "/WebMercatorQuad/tilejson.json",
345
+ "",
346
+ );
347
+ const [minx, miny, maxx, maxy] = extent;
348
+ const params = new URLSearchParams({ limit: "1", ...(query.value ?? {}) });
349
+ try {
350
+ const { data } = await axios.get(
351
+ `${base}/bbox/${minx},${miny},${maxx},${maxy}/assets?${params}`,
352
+ );
353
+ const count = Array.isArray(data)
354
+ ? data.length
355
+ : Object.keys(data ?? {}).length;
356
+ hasDataInView.value = count > 0;
357
+ } catch {
358
+ hasDataInView.value = true;
359
+ }
360
+ }
361
+
362
+ /** Shared debounced data-presence check; coalesces rapid pan/zoom/filter changes. */
363
+ const scheduleMosaicDataCheck = useDebounceFn(checkMosaicDataInView, 300);
364
+
365
+ /**
366
+ * @param {number} zoomLevel
367
+ * @param {number} [threshold] - defaults to the shared visibility threshold
368
+ */
369
+ function toggleMosaicVisibility(zoomLevel, threshold) {
370
+ // Only globe needs manual toggling.
371
+ if (!isGlobe.value) return;
372
+ const { latestLayer, visibilityThreshold } = useMosaicState();
373
+ if (!latestLayer.value) return;
374
+
375
+ const layerId = /** @type {string} */ (latestLayer.value.properties.id);
376
+ const layer = mapEl.value?.getLayerById(layerId);
377
+ if (!layer) return;
378
+
379
+ const eqZoom = normalizeGlobeZoom(zoomLevel);
380
+ layer.setVisible(eqZoom >= (threshold ?? visibilityThreshold.value));
381
+ }
382
+
383
+ /**
384
+ * Writes layers into the AnalysisGroup and triggers a map re-render.
385
+ * @param {Record<string, any>[]} layersToAssign
386
+ */
387
+ function assignAnalysisLayer(layersToAssign) {
388
+ const mapLayers = getLayers();
389
+ const { analysisGroup, layers } = ensureAnalysisGroup(mapLayers);
390
+ analysisGroup.layers = layersToAssign;
391
+ if (mapEl.value) {
392
+ // Reassign to trigger map re-render after in-place layer mutation
393
+ mapEl.value.layers = /** @type {import("@eox/map").EoxLayer[]} */ ([
394
+ ...layers,
395
+ ]);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Ensure the AnalysisGroup exists in the layers collection.
401
+ * @param {Record<string, any>[]} layersCollection
402
+ * @returns {{ layers: Record<string, any>[], analysisGroup: Record<string, any> }}
403
+ */
404
+ function ensureAnalysisGroup(layersCollection) {
405
+ let analysisGroup = layersCollection.find(
406
+ (l) => l?.properties?.id === "AnalysisGroup",
407
+ );
408
+
409
+ if (!analysisGroup) {
410
+ analysisGroup = {
411
+ type: "Group",
412
+ properties: {
413
+ id: "AnalysisGroup",
414
+ title: "Data Layers",
415
+ },
416
+ layers: [],
417
+ };
418
+ layersCollection.push(analysisGroup);
419
+ }
420
+
421
+ return { layers: layersCollection, analysisGroup };
422
+ }
@@ -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,10 +31,16 @@ 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
 
38
+ /**
39
+ * Registry of colormap ranges
40
+ * @type {import("vue").Ref<Record<string, string[]> | null>}
41
+ */
42
+ const colormapRegistry = ref(null);
43
+
38
44
  /**
39
45
  * Links of the root STAC catalog
40
46
  *
@@ -61,11 +67,20 @@ export const useSTAcStore = defineStore("stac", () => {
61
67
  * >}
62
68
  */
63
69
  const selectedCompareStac = ref(null);
70
+ /**
71
+ * Currently selected item
72
+ * @type {import("vue").Ref<import("stac-ts").StacLink | import("stac-ts").StacItem | null>}
73
+ */
64
74
  /**
65
75
  * Currently selected item
66
76
  * @type {import("vue").Ref<import("stac-ts").StacLink | import("stac-ts").StacItem | null>}
67
77
  */
68
78
  const selectedItem = ref(null);
79
+ /**
80
+ * Currently selected compare item
81
+ * @type {import("vue").Ref<import("stac-ts").StacLink | import("stac-ts").StacItem | null>}
82
+ */
83
+ const selectedCompareItem = ref(null);
69
84
 
70
85
  /**
71
86
  * Initializes the store by assigning the STAC endpoint.
@@ -85,6 +100,26 @@ export const useSTAcStore = defineStore("stac", () => {
85
100
  rasterEndpoint.value = endpoint.rasterEndpoint ?? null;
86
101
  supportedUpscalingEndpoints.value =
87
102
  endpoint.supportedUpscalingEndpoints ?? [];
103
+ if (endpoint.colormapRegistry) {
104
+ loadColormapRegistry(endpoint.colormapRegistry);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Loads the colormap registry from a URL or object
110
+ * @param {string | Record<string, string[]>} registry
111
+ */
112
+ async function loadColormapRegistry(registry) {
113
+ if (typeof registry === "object") {
114
+ colormapRegistry.value = registry;
115
+ return;
116
+ }
117
+ try {
118
+ const resp = await axios.get(registry);
119
+ colormapRegistry.value = resp.data;
120
+ } catch (err) {
121
+ log.error("Error loading colormap registry", err);
122
+ }
88
123
  }
89
124
 
90
125
  /**
@@ -151,13 +186,14 @@ export const useSTAcStore = defineStore("stac", () => {
151
186
  }
152
187
  //@ts-expect-error "this" type is not exported by pinia
153
188
  const patch = this?.$patch;
154
- if (stacItem && patch) {
155
- patch({ selectedItem: stacItem });
189
+ if (patch) {
190
+ patch({ selectedItem: stacItem ?? null });
156
191
  }
157
192
 
158
193
  await axios
159
194
  .get(absoluteUrl.value)
160
195
  .then(async (resp) => {
196
+ resp.data["eodash:layerExclusive"] = true;
161
197
  await updateEodashCollections(
162
198
  eodashCollections,
163
199
  resp.data,
@@ -182,10 +218,15 @@ export const useSTAcStore = defineStore("stac", () => {
182
218
  *
183
219
  * @param {string} relativePath - Stac link href
184
220
  * @param {boolean} [isPOI=false] - If true, the STAC is loaded for a point of interest
221
+ * @param {Object} [stacItem] - The STAC item to load
185
222
  * @returns {Promise<void>}
186
223
  * @see {@link selectedCompareStac}
187
224
  */
188
- async function loadSelectedCompareSTAC(relativePath = "", isPOI = false) {
225
+ async function loadSelectedCompareSTAC(
226
+ relativePath = "",
227
+ isPOI = false,
228
+ stacItem,
229
+ ) {
189
230
  if (!stacEndpoint.value) {
190
231
  return Promise.reject(
191
232
  new Error("STAC endpoint is not defined in eodash configuration"),
@@ -196,6 +237,11 @@ export const useSTAcStore = defineStore("stac", () => {
196
237
  // construct absolute URL of a poi
197
238
  absoluteUrl.value = constructPoiUrl(relativePath, compareIndicator.value);
198
239
  }
240
+ //@ts-expect-error "this" type is not exported by pinia
241
+ const patch = this?.$patch;
242
+ if (stacItem && patch) {
243
+ patch({ selectedCompareItem: stacItem });
244
+ }
199
245
  await axios
200
246
  .get(absoluteUrl.value)
201
247
  .then(async (resp) => {
@@ -247,6 +293,7 @@ export const useSTAcStore = defineStore("stac", () => {
247
293
 
248
294
  return {
249
295
  stacEndpoint,
296
+ rasterEndpoint,
250
297
  isApi,
251
298
  stac,
252
299
  init,
@@ -257,6 +304,9 @@ export const useSTAcStore = defineStore("stac", () => {
257
304
  selectedStac,
258
305
  selectedCompareStac,
259
306
  selectedItem,
307
+ selectedCompareItem,
260
308
  supportedUpscalingEndpoints,
309
+ colormapRegistry,
310
+ loadColormapRegistry,
261
311
  };
262
312
  });
@@ -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);