@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.
- package/core/client/components/EodashOverlay.vue +27 -4
- package/core/client/composables/DefineEodash.js +0 -1
- package/core/client/composables/DefineWidgets.js +3 -1
- package/core/client/eodashSTAC/EodashCollection.js +87 -24
- package/core/client/eodashSTAC/cql.js +64 -0
- package/core/client/eodashSTAC/createLayers.js +353 -187
- package/core/client/eodashSTAC/helpers.js +694 -110
- package/core/client/eodashSTAC/mosaic.js +422 -0
- package/core/client/plugins/axios.js +31 -1
- package/core/client/store/actions.js +1 -1
- package/core/client/store/stac.js +54 -4
- package/core/client/store/states.js +20 -2
- package/core/client/types.ts +152 -28
- package/core/client/utils/bands-editor/exampleSchema.json +50 -50
- package/core/client/utils/states.js +10 -0
- package/dist/client/{DashboardLayout-kRsmsNW5.js → DashboardLayout-DEs4f_Vy.js} +2 -2
- package/dist/client/{DynamicWebComponent-CnKhYo0K.js → DynamicWebComponent-PvnV88BA.js} +1 -1
- package/dist/client/EodashChart-D-3AGKZY.js +260 -0
- package/dist/client/{EodashDatePicker-CMPDT-ce.js → EodashDatePicker-Dm_goZzs.js} +3 -3
- package/dist/client/{EodashItemFilter-BpdA8QQa.js → EodashItemFilter-CJy3zYzD.js} +2 -1
- package/dist/client/{EodashLayerControl-BHpDA4_0.js → EodashLayerControl-rL8UnpkA.js} +55 -33
- package/dist/client/{EodashLayoutSwitcher-B1fRUt7G.js → EodashLayoutSwitcher-ZRlESI9O.js} +3 -3
- package/dist/client/{EodashMapBtns-Cpned0tc.js → EodashMapBtns-CG7u2Aq8.js} +227 -112
- package/dist/client/EodashStacInfo-CJhhxthD.js +169 -0
- package/dist/client/{EodashTools-1zHz26gv.js → EodashTools-CWyK8QJT.js} +4 -4
- package/dist/client/{ExportState-DbOm8hBI.js → ExportState-BXhy5TOU.js} +20 -174
- package/dist/client/{Footer-BKg-VVkj.js → Footer-DZPvnhcT.js} +1 -1
- package/dist/client/{Header-DCrwCvmY.js → Header-CiGsDE_a.js} +2 -2
- package/dist/client/{MobileLayout-vWSHpluo.js → MobileLayout-BJHW7x5V.js} +2 -2
- package/dist/client/{PopUp-9uimfK49.js → PopUp-7_XnhvCc.js} +4 -168
- package/dist/client/{ProcessList-CTTIZa06.js → ProcessList-LL_UCbvc.js} +5 -5
- package/dist/client/VDivider-DWYM0WUr.js +168 -0
- package/dist/client/{VImg-CrmB76ZY.js → VImg-B75dgw2K.js} +2 -2
- package/dist/client/{VMain-OKZNpcEG.js → VMain-DvMmOL4f.js} +1 -1
- package/dist/client/{VTooltip-CJw2FyWk.js → VTooltip-ClWHUmWL.js} +2 -2
- package/dist/client/{WidgetsContainer-BaWOnE5R.js → WidgetsContainer-BqaWs4ch.js} +1 -1
- package/dist/client/{asWebComponent-BHWpMuE3.js → asWebComponent-IZanwL-A.js} +677 -379
- package/dist/client/{async-DSKj6g1f.js → async-CiNIOBVz.js} +31 -29
- package/dist/client/{create-layers-config-CBoYedqB.js → create-layers-config-3S3y_kZg.js} +17 -17
- package/dist/client/dialog-transition-c4ZSnzac.js +170 -0
- package/dist/client/eo-dash.js +1 -1
- package/dist/client/{forwardRefs-DH51zPUr.js → forwardRefs-B-c1_6yo.js} +8 -87
- package/dist/client/{handling-BrcDDlgS.js → handling-NvY8Csry.js} +3 -4
- package/dist/client/{helpers-DjZVl_WF.js → helpers-BFuh19CP.js} +1094 -194
- package/dist/client/{index-1JKE68n3.js → index-1wMAyuNL.js} +219 -67
- package/dist/client/{index-CSG_JoOA.js → index-B8jGJSTA.js} +62 -56
- package/dist/client/{index-Cu3-13Xg.js → index-BKQQJkhy.js} +167 -61
- package/dist/client/index-D0jUyol8.js +3167 -0
- package/dist/client/{index-CUjl-ZsI.js → index-KGxz5Egv.js} +2 -2
- package/dist/client/mosaic-vHvSuih6.js +459 -0
- package/dist/client/templates.js +251 -121
- package/dist/client/{transition-OHcz5Qq1.js → transition-BkyUsZh_.js} +1 -1
- package/dist/node/cli.js +5 -5
- package/dist/node/types.d.ts +4 -0
- package/dist/types/core/client/composables/DefineEodash.d.ts +1 -1
- package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +57 -25
- package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
- package/dist/types/core/client/eodashSTAC/createLayers.d.ts +42 -23
- package/dist/types/core/client/eodashSTAC/helpers.d.ts +118 -19
- package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
- package/dist/types/core/client/store/stac.d.ts +38 -8
- package/dist/types/core/client/store/states.d.ts +24 -2
- package/dist/types/core/client/types.d.ts +128 -24
- package/dist/types/core/client/utils/states.d.ts +12 -0
- package/dist/types/templates/baseConfig.d.ts +1 -100
- package/dist/types/templates/compare.d.ts +32 -3
- package/dist/types/templates/expert.d.ts +18 -0
- package/dist/types/templates/explore.d.ts +10 -1
- package/dist/types/templates/lite.d.ts +11 -0
- package/dist/types/widgets/EodashChart.vue.d.ts +5 -3
- package/dist/types/widgets/EodashItemCatalog/index.vue.d.ts +129 -2
- package/dist/types/widgets/EodashItemCatalog/methods/filters.d.ts +28 -6
- package/dist/types/widgets/EodashItemCatalog/methods/handlers.d.ts +17 -4
- package/dist/types/widgets/EodashItemCatalog/methods/map.d.ts +16 -4
- package/dist/types/widgets/EodashLayerControl.vue.d.ts +35 -16
- package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +14 -10
- package/dist/types/widgets/EodashMap/index.vue.d.ts +86 -37
- package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -1
- package/dist/types/widgets/EodashMap/methods/index.d.ts +5 -1
- package/dist/types/widgets/EodashMap/methods/use-map-loading.d.ts +1 -0
- package/dist/types/widgets/EodashProcess/index.vue.d.ts +5 -3
- package/dist/types/widgets/EodashProcess/methods/custom-endpoints/layers/eoxhub-workspaces-endpoint.d.ts +1 -1
- package/dist/types/widgets/EodashProcess/states.d.ts +1 -1
- package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
- package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +20 -0
- package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +3 -8
- package/dist/types/widgets/EodashTimeSlider/types.d.ts +6 -16
- package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
- package/package.json +34 -34
- package/templates/baseConfig.js +37 -6
- package/templates/compare.js +61 -7
- package/templates/expert.js +24 -2
- package/templates/explore.js +12 -5
- package/templates/lite.js +13 -0
- package/widgets/EodashChart.vue +162 -21
- package/widgets/EodashItemCatalog/index.vue +363 -35
- package/widgets/EodashItemCatalog/methods/filters.js +134 -70
- package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
- package/widgets/EodashItemCatalog/methods/map.js +141 -28
- package/widgets/EodashLayerControl.vue +51 -25
- package/widgets/EodashMap/EodashMapBtns.vue +109 -48
- package/widgets/EodashMap/index.vue +91 -105
- package/widgets/EodashMap/methods/btns.js +64 -31
- package/widgets/EodashMap/methods/create-layers-config.js +16 -16
- package/widgets/EodashMap/methods/index.js +71 -8
- package/widgets/EodashMap/methods/use-map-loading.js +58 -0
- package/widgets/EodashProcess/index.vue +136 -16
- package/widgets/EodashProcess/methods/composables.js +13 -0
- package/widgets/EodashProcess/methods/outputs.js +0 -1
- package/widgets/EodashProcess/methods/utils.js +29 -27
- package/widgets/EodashProcess/states.js +1 -1
- package/widgets/EodashStacInfo.vue +64 -2
- package/widgets/EodashTimeSlider/index.vue +58 -2
- package/widgets/EodashTimeSlider/methods/index.js +5 -48
- package/widgets/EodashTimeSlider/types.ts +7 -17
- package/widgets/ExportState.vue +13 -4
- package/dist/client/EodashChart-CLDqE93C.js +0 -144
- package/dist/client/EodashStacInfo-BQspAHKH.js +0 -107
- package/dist/client/index-5Jhsa8Xe.js +0 -570
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveComponent, render, h, mergeProps } from 'vue';
|
|
2
|
-
import {
|
|
3
|
-
import { V as VTooltip } from './VTooltip-
|
|
2
|
+
import { O as consoleError, ag as isObject } from './asWebComponent-IZanwL-A.js';
|
|
3
|
+
import { V as VTooltip } from './VTooltip-ClWHUmWL.js';
|
|
4
4
|
|
|
5
5
|
// Utilities
|
|
6
6
|
function useDirectiveComponent(component, props) {
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { O as mapPosition, ab as isGlobe, q as mapEl, W as eodashCollections, u as extractLayerTimeValues, L as datetime, g as axios, l as resolveRenders, p as encodeURLObject, n as normalizeRescale, o as normalizeNodata, Q as indicator, F as getLayers } from './helpers-BFuh19CP.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-IZanwL-A.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
|
+
// TiTiler treats assets and expression as mutually exclusive band selection
|
|
289
|
+
assets: preset.expression ? undefined : preset.assets,
|
|
290
|
+
expression: preset.expression,
|
|
291
|
+
nodata: normalizeNodata(preset.nodata),
|
|
292
|
+
resampling: preset.resampling,
|
|
293
|
+
color_formula: preset.color_formula,
|
|
294
|
+
colormap: preset.colormap,
|
|
295
|
+
colormap_name: preset.colormap_name,
|
|
296
|
+
rescale: normalizeRescale(preset.rescale),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const tileParams = new URLSearchParams({
|
|
300
|
+
tilesize: `${preset.tilesize ?? "512"}`,
|
|
301
|
+
...params,
|
|
302
|
+
});
|
|
303
|
+
const tileJsonUrl = `${mosaicEndpoint}?${renderParamsStr}${tileParams.toString()}`;
|
|
304
|
+
|
|
305
|
+
const tileJSON = await axios
|
|
306
|
+
.get(tileJsonUrl)
|
|
307
|
+
.then((res) => res.data)
|
|
308
|
+
.catch((err) => {
|
|
309
|
+
console.error("Failed to fetch mosaic TileJSON", err);
|
|
310
|
+
return null;
|
|
311
|
+
});
|
|
312
|
+
if (!tileJSON?.tiles?.[0]) {
|
|
313
|
+
console.warn("[eodash] No tile URL found in mosaic TileJSON response.");
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
type: "Tile",
|
|
320
|
+
minZoom: useMosaicState().visibilityThreshold.value,
|
|
321
|
+
properties: {
|
|
322
|
+
id: `${indicator.value};:;mosaic`,
|
|
323
|
+
title: "Mosaic Layer",
|
|
324
|
+
},
|
|
325
|
+
source: {
|
|
326
|
+
type: "XYZ",
|
|
327
|
+
url: tileJSON.tiles[0],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @param {string | Date} time
|
|
335
|
+
* @returns {string | null}
|
|
336
|
+
*/
|
|
337
|
+
function timeToDate(time) {
|
|
338
|
+
if (!time) return null;
|
|
339
|
+
const date = new Date(time);
|
|
340
|
+
if (isNaN(date.getTime())) return null;
|
|
341
|
+
return date.toISOString().split("T")[0];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Returns the OL-equivalent zoom from `mapPosition[2]`. In 2D the raw value
|
|
346
|
+
* is already the OL zoom. In globe mode the raw value is camera altitude in
|
|
347
|
+
* meters; convert it via eox-map's internal formula so callers can compare
|
|
348
|
+
* against a single (2D-style) threshold.
|
|
349
|
+
*
|
|
350
|
+
* @param {number} rawZ
|
|
351
|
+
* @returns {number}
|
|
352
|
+
*/
|
|
353
|
+
function normalizeGlobeZoom(rawZ) {
|
|
354
|
+
if (!isGlobe.value) return rawZ;
|
|
355
|
+
const zoomFactor = mapEl.value?.globeConfig?.useHighLOD ? 1 : 2;
|
|
356
|
+
return Math.log2(21_050_000 / rawZ) + zoomFactor;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* check whether the filtered dataset has any assets in the current 2D view
|
|
361
|
+
*/
|
|
362
|
+
async function checkMosaicDataInView() {
|
|
363
|
+
const { hasDataInView, query, mosaicEndpoint, visibilityThreshold } =
|
|
364
|
+
useMosaicState();
|
|
365
|
+
const extent = mapEl.value?.lonLatExtent;
|
|
366
|
+
if (
|
|
367
|
+
!extent ||
|
|
368
|
+
isGlobe.value ||
|
|
369
|
+
!mosaicEndpoint.value ||
|
|
370
|
+
normalizeGlobeZoom(mapPosition.value[2] ?? 0) < visibilityThreshold.value
|
|
371
|
+
) {
|
|
372
|
+
hasDataInView.value = true;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (extent.some(Number.isNaN)) return;
|
|
377
|
+
|
|
378
|
+
const base = mosaicEndpoint.value.replace(
|
|
379
|
+
"/WebMercatorQuad/tilejson.json",
|
|
380
|
+
"",
|
|
381
|
+
);
|
|
382
|
+
const [minx, miny, maxx, maxy] = extent;
|
|
383
|
+
const params = new URLSearchParams({ limit: "1", ...(query.value ?? {}) });
|
|
384
|
+
try {
|
|
385
|
+
const { data } = await axios.get(
|
|
386
|
+
`${base}/bbox/${minx},${miny},${maxx},${maxy}/assets?${params}`,
|
|
387
|
+
);
|
|
388
|
+
const count = Array.isArray(data)
|
|
389
|
+
? data.length
|
|
390
|
+
: Object.keys(data ?? {}).length;
|
|
391
|
+
hasDataInView.value = count > 0;
|
|
392
|
+
} catch {
|
|
393
|
+
hasDataInView.value = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Shared debounced data-presence check; coalesces rapid pan/zoom/filter changes. */
|
|
398
|
+
const scheduleMosaicDataCheck = useDebounceFn(checkMosaicDataInView, 300);
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @param {number} zoomLevel
|
|
402
|
+
* @param {number} [threshold] - defaults to the shared visibility threshold
|
|
403
|
+
*/
|
|
404
|
+
function toggleMosaicVisibility(zoomLevel, threshold) {
|
|
405
|
+
// Only globe needs manual toggling.
|
|
406
|
+
if (!isGlobe.value) return;
|
|
407
|
+
const { latestLayer, visibilityThreshold } = useMosaicState();
|
|
408
|
+
if (!latestLayer.value) return;
|
|
409
|
+
|
|
410
|
+
const layerId = /** @type {string} */ (latestLayer.value.properties.id);
|
|
411
|
+
const layer = mapEl.value?.getLayerById(layerId);
|
|
412
|
+
if (!layer) return;
|
|
413
|
+
|
|
414
|
+
const eqZoom = normalizeGlobeZoom(zoomLevel);
|
|
415
|
+
layer.setVisible(eqZoom >= (visibilityThreshold.value));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Writes layers into the AnalysisGroup and triggers a map re-render.
|
|
420
|
+
* @param {Record<string, any>[]} layersToAssign
|
|
421
|
+
*/
|
|
422
|
+
function assignAnalysisLayer(layersToAssign) {
|
|
423
|
+
const mapLayers = getLayers();
|
|
424
|
+
const { analysisGroup, layers } = ensureAnalysisGroup(mapLayers);
|
|
425
|
+
analysisGroup.layers = layersToAssign;
|
|
426
|
+
if (mapEl.value) {
|
|
427
|
+
// Reassign to trigger map re-render after in-place layer mutation
|
|
428
|
+
mapEl.value.layers = /** @type {import("@eox/map").EoxLayer[]} */ ([
|
|
429
|
+
...layers,
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Ensure the AnalysisGroup exists in the layers collection.
|
|
436
|
+
* @param {Record<string, any>[]} layersCollection
|
|
437
|
+
* @returns {{ layers: Record<string, any>[], analysisGroup: Record<string, any> }}
|
|
438
|
+
*/
|
|
439
|
+
function ensureAnalysisGroup(layersCollection) {
|
|
440
|
+
let analysisGroup = layersCollection.find(
|
|
441
|
+
(l) => l?.properties?.id === "AnalysisGroup",
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (!analysisGroup) {
|
|
445
|
+
analysisGroup = {
|
|
446
|
+
type: "Group",
|
|
447
|
+
properties: {
|
|
448
|
+
id: "AnalysisGroup",
|
|
449
|
+
title: "Data Layers",
|
|
450
|
+
},
|
|
451
|
+
layers: [],
|
|
452
|
+
};
|
|
453
|
+
layersCollection.push(analysisGroup);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { layers: layersCollection, analysisGroup };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export { useScheduleMosaicUpdate as a, buildCqlFilter as b, useInitMosaic as c, normalizeGlobeZoom as n, renderLatestMosaic as r, useMosaicState as u };
|