@eodash/eodash 5.7.0 → 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 (113) 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 +74 -20
  5. package/core/client/eodashSTAC/cql.js +64 -0
  6. package/core/client/eodashSTAC/createLayers.js +169 -116
  7. package/core/client/eodashSTAC/helpers.js +391 -98
  8. package/core/client/eodashSTAC/mosaic.js +416 -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 +25 -4
  12. package/core/client/store/states.js +20 -2
  13. package/core/client/types.ts +147 -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-ePbdvUTE.js} +2 -2
  17. package/dist/client/{DynamicWebComponent-CnKhYo0K.js → DynamicWebComponent-g5SdRbjY.js} +1 -1
  18. package/dist/client/{EodashChart-CLDqE93C.js → EodashChart-ChrkA5oS.js} +7 -6
  19. package/dist/client/{EodashDatePicker-CMPDT-ce.js → EodashDatePicker-CY1TrsDo.js} +3 -3
  20. package/dist/client/{EodashItemFilter-BpdA8QQa.js → EodashItemFilter-DzshHDlS.js} +2 -1
  21. package/dist/client/{EodashLayerControl-BHpDA4_0.js → EodashLayerControl-DMCeMkSd.js} +50 -20
  22. package/dist/client/{EodashLayoutSwitcher-B1fRUt7G.js → EodashLayoutSwitcher-iJ8mX1rF.js} +3 -3
  23. package/dist/client/{EodashMapBtns-Cpned0tc.js → EodashMapBtns-C7Cr7q5Y.js} +200 -86
  24. package/dist/client/EodashStacInfo-K7-iuXa7.js +169 -0
  25. package/dist/client/{EodashTools-1zHz26gv.js → EodashTools-BEK7e7JM.js} +4 -4
  26. package/dist/client/{ExportState-DbOm8hBI.js → ExportState-CqIzfMYQ.js} +6 -168
  27. package/dist/client/{Footer-BKg-VVkj.js → Footer-1P0cmBOZ.js} +1 -1
  28. package/dist/client/{Header-DCrwCvmY.js → Header-CkkBaBNh.js} +2 -2
  29. package/dist/client/{MobileLayout-vWSHpluo.js → MobileLayout-BPf49kDU.js} +2 -2
  30. package/dist/client/{PopUp-9uimfK49.js → PopUp-DPZq5SWI.js} +4 -168
  31. package/dist/client/{ProcessList-CTTIZa06.js → ProcessList-DuI6WpS1.js} +5 -5
  32. package/dist/client/VDivider-Bj9LG_JV.js +168 -0
  33. package/dist/client/{VImg-CrmB76ZY.js → VImg-Ddv5OyMz.js} +2 -2
  34. package/dist/client/{VMain-OKZNpcEG.js → VMain-7yqk_qC3.js} +1 -1
  35. package/dist/client/{VTooltip-CJw2FyWk.js → VTooltip-2hAJyEoa.js} +2 -2
  36. package/dist/client/{WidgetsContainer-BaWOnE5R.js → WidgetsContainer-2DOrlwCB.js} +1 -1
  37. package/dist/client/{asWebComponent-BHWpMuE3.js → asWebComponent-BlFN2lkB.js} +456 -306
  38. package/dist/client/{async-DSKj6g1f.js → async-B2H8ZwUK.js} +2 -2
  39. package/dist/client/{create-layers-config-CBoYedqB.js → create-layers-config-u_vn5hPk.js} +17 -17
  40. package/dist/client/dialog-transition-BrMkMiNa.js +170 -0
  41. package/dist/client/eo-dash.js +1 -1
  42. package/dist/client/{forwardRefs-DH51zPUr.js → forwardRefs-DAlror6d.js} +8 -87
  43. package/dist/client/{handling-BrcDDlgS.js → handling-B6I1V5eG.js} +3 -3
  44. package/dist/client/{helpers-DjZVl_WF.js → helpers-CBqIUSXJ.js} +794 -182
  45. package/dist/client/index-BWq1ZKaV.js +3168 -0
  46. package/dist/client/{index-CUjl-ZsI.js → index-CJ9UI3ln.js} +2 -2
  47. package/dist/client/{index-CSG_JoOA.js → index-DXLz4sFR.js} +62 -56
  48. package/dist/client/{index-1JKE68n3.js → index-DhKU6KqM.js} +211 -67
  49. package/dist/client/{index-Cu3-13Xg.js → index-DjQlbWbJ.js} +7 -6
  50. package/dist/client/mosaic-BFYLEwyx.js +455 -0
  51. package/dist/client/templates.js +250 -119
  52. package/dist/client/{transition-OHcz5Qq1.js → transition-D_W6Wn9v.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 +77 -18
  60. package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
  61. package/dist/types/core/client/store/stac.d.ts +31 -7
  62. package/dist/types/core/client/store/states.d.ts +24 -2
  63. package/dist/types/core/client/types.d.ts +123 -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 +9 -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/states.d.ts +1 -1
  83. package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
  84. package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +20 -0
  85. package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +3 -8
  86. package/dist/types/widgets/EodashTimeSlider/types.d.ts +6 -16
  87. package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
  88. package/package.json +33 -33
  89. package/templates/baseConfig.js +36 -6
  90. package/templates/compare.js +62 -7
  91. package/templates/expert.js +23 -0
  92. package/templates/explore.js +11 -5
  93. package/templates/lite.js +14 -0
  94. package/widgets/EodashChart.vue +1 -0
  95. package/widgets/EodashItemCatalog/index.vue +363 -35
  96. package/widgets/EodashItemCatalog/methods/filters.js +134 -70
  97. package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
  98. package/widgets/EodashItemCatalog/methods/map.js +142 -28
  99. package/widgets/EodashLayerControl.vue +42 -11
  100. package/widgets/EodashMap/EodashMapBtns.vue +109 -48
  101. package/widgets/EodashMap/index.vue +89 -105
  102. package/widgets/EodashMap/methods/btns.js +37 -5
  103. package/widgets/EodashMap/methods/create-layers-config.js +16 -16
  104. package/widgets/EodashMap/methods/index.js +71 -8
  105. package/widgets/EodashMap/methods/use-map-loading.js +52 -0
  106. package/widgets/EodashProcess/index.vue +1 -0
  107. package/widgets/EodashProcess/states.js +1 -1
  108. package/widgets/EodashStacInfo.vue +64 -2
  109. package/widgets/EodashTimeSlider/index.vue +58 -2
  110. package/widgets/EodashTimeSlider/methods/index.js +5 -48
  111. package/widgets/EodashTimeSlider/types.ts +7 -17
  112. package/dist/client/EodashStacInfo-BQspAHKH.js +0 -107
  113. package/dist/client/index-5Jhsa8Xe.js +0 -570
@@ -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 };