@eodash/eodash 5.6.1 → 5.7.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/DashboardLayout.vue +1 -1
- package/core/client/composables/index.js +6 -2
- package/core/client/eodashSTAC/EodashCollection.js +5 -25
- package/core/client/eodashSTAC/createLayers.js +23 -9
- package/core/client/eodashSTAC/helpers.js +33 -0
- package/core/client/types.ts +8 -1
- package/core/client/utils/index.js +4 -0
- package/dist/client/{DashboardLayout-CqMb7OXc.js → DashboardLayout-kRsmsNW5.js} +5 -5
- package/dist/client/{DynamicWebComponent-DqKqTf7l.js → DynamicWebComponent-CnKhYo0K.js} +1 -1
- package/dist/client/{EodashChart-PkUQfH0k.js → EodashChart-CLDqE93C.js} +4 -4
- package/dist/client/{EodashDatePicker-DLJGdTtX.js → EodashDatePicker-CMPDT-ce.js} +3 -3
- package/dist/client/{EodashItemFilter-Dk8KjFcX.js → EodashItemFilter-BpdA8QQa.js} +1 -1
- package/dist/client/{EodashLayerControl-CC1Cogrn.js → EodashLayerControl-BHpDA4_0.js} +25 -7
- package/dist/client/{EodashLayoutSwitcher-5WZYd6rr.js → EodashLayoutSwitcher-B1fRUt7G.js} +3 -3
- package/dist/client/EodashMapBtns-Cpned0tc.js +478 -0
- package/dist/client/{EodashStacInfo-B90wobPc.js → EodashStacInfo-BQspAHKH.js} +1 -1
- package/dist/client/{EodashTools-CGZtTh45.js → EodashTools-1zHz26gv.js} +4 -4
- package/dist/client/{ExportState-Ce0ZnrX_.js → ExportState-DbOm8hBI.js} +4 -4
- package/dist/client/{Footer-CnZqEiop.js → Footer-BKg-VVkj.js} +1 -1
- package/dist/client/{Header-Dr5PBIm8.js → Header-DCrwCvmY.js} +2 -2
- package/dist/client/MobileLayout-vWSHpluo.js +118 -0
- package/dist/client/{PopUp-DjvE_71e.js → PopUp-9uimfK49.js} +2 -2
- package/dist/client/{ProcessList-kf3olI7W.js → ProcessList-CTTIZa06.js} +4 -4
- package/dist/client/{VImg-Bj6uWt6j.js → VImg-CrmB76ZY.js} +2 -2
- package/dist/client/{VMain-Cm0UmKGi.js → VMain-OKZNpcEG.js} +1 -1
- package/dist/client/{VTooltip-DbBu-ky3.js → VTooltip-CJw2FyWk.js} +2 -2
- package/dist/client/{WidgetsContainer-CfLnd44U.js → WidgetsContainer-BaWOnE5R.js} +1 -1
- package/dist/client/{asWebComponent-DfULAbpq.js → asWebComponent-BHWpMuE3.js} +47 -39
- package/dist/client/{async-CIbKXEEe.js → async-DSKj6g1f.js} +21 -10
- package/dist/client/create-layers-config-CBoYedqB.js +182 -0
- package/dist/client/eo-dash.js +1 -1
- package/dist/client/{forwardRefs-yj3VWOK5.js → forwardRefs-DH51zPUr.js} +2 -2
- package/dist/client/{handling-s8xt2JwF.js → handling-BrcDDlgS.js} +123 -85
- package/dist/client/{helpers-DAVGYl_f.js → helpers-DjZVl_WF.js} +2273 -1500
- package/dist/client/{index-4TrjfJ32.js → index-1JKE68n3.js} +49 -194
- package/dist/client/{index-CugS__t5.js → index-5Jhsa8Xe.js} +3 -3
- package/dist/client/index-CSG_JoOA.js +436 -0
- package/dist/client/{index-CYz6dT7K.js → index-CUjl-ZsI.js} +2 -2
- package/dist/client/{index-Bu8BMA9q.js → index-Cu3-13Xg.js} +69 -83
- package/dist/client/material-symbols-outlined.woff2 +0 -0
- package/dist/client/material-symbols-rounded.woff2 +0 -0
- package/dist/client/material-symbols-sharp.woff2 +0 -0
- package/dist/client/material-symbols-subset.woff2 +0 -0
- package/dist/client/templates.js +1 -1
- package/dist/client/{transition-DaOsiD4u.js → transition-OHcz5Qq1.js} +1 -1
- package/dist/node/cli.js +1 -1
- package/dist/types/core/client/composables/index.d.ts +1 -3
- package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +6 -7
- package/dist/types/core/client/eodashSTAC/createLayers.d.ts +3 -3
- package/dist/types/core/client/eodashSTAC/helpers.d.ts +1 -0
- package/dist/types/core/client/types.d.ts +8 -1
- package/dist/types/templates/baseConfig.d.ts +12 -0
- package/dist/types/widgets/EodashLayerControl.vue.d.ts +1 -1
- package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +9 -0
- package/dist/types/widgets/EodashMap/index.vue.d.ts +7 -0
- package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -8
- package/dist/types/widgets/EodashProcess/methods/composables.d.ts +1 -1
- package/dist/types/widgets/EodashProcess/methods/handling.d.ts +13 -8
- package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +34 -0
- package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +39 -0
- package/dist/types/widgets/EodashTimeSlider/types.d.ts +62 -0
- package/package.json +32 -32
- package/widgets/EodashLayerControl.vue +22 -3
- package/widgets/EodashMap/EodashMapBtns.vue +46 -1
- package/widgets/EodashMap/index.vue +17 -5
- package/widgets/EodashMap/methods/create-layers-config.js +2 -0
- package/widgets/EodashMap/methods/index.js +23 -5
- package/widgets/EodashProcess/index.vue +1 -1
- package/widgets/EodashProcess/methods/async.js +19 -8
- package/widgets/EodashProcess/methods/composables.js +63 -77
- package/widgets/EodashProcess/methods/custom-endpoints/chart/sentinelhub-endpoint.js +9 -5
- package/widgets/EodashProcess/methods/custom-endpoints/chart/veda-endpoint.js +7 -4
- package/widgets/EodashProcess/methods/handling.js +105 -72
- package/widgets/EodashTimeSlider/index.vue +152 -0
- package/widgets/EodashTimeSlider/methods/index.js +279 -0
- package/widgets/EodashTimeSlider/types.ts +64 -0
- package/dist/client/EodashMapBtns-BHrSDNoC.js +0 -435
- package/dist/client/EodashTimeSlider-5vm7F3ju.js +0 -53
- package/dist/client/MobileLayout-DyMjaeif.js +0 -118
- package/dist/types/widgets/EodashTimeSlider.vue.d.ts +0 -15
- package/widgets/EodashTimeSlider.vue +0 -40
|
@@ -35,6 +35,7 @@ import { getLayers, getCompareLayers } from "@/store/actions";
|
|
|
35
35
|
* @param {import("vue").Ref<boolean>} params.isProcessed
|
|
36
36
|
* @param {import("vue").Ref<boolean>} params.loading
|
|
37
37
|
* @param {import("vue").Ref<boolean>} params.isPolling
|
|
38
|
+
* @param {import("@eox/map").EOxMap | null} params.mapElement
|
|
38
39
|
* @param {boolean} params.enableCompare
|
|
39
40
|
*/
|
|
40
41
|
export async function initProcess({
|
|
@@ -45,6 +46,7 @@ export async function initProcess({
|
|
|
45
46
|
loading,
|
|
46
47
|
isPolling,
|
|
47
48
|
enableCompare,
|
|
49
|
+
mapElement,
|
|
48
50
|
}) {
|
|
49
51
|
const isPoiAlive = enableCompare ? !!comparePoi.value : !!poi.value;
|
|
50
52
|
let updatedJsonform = null;
|
|
@@ -59,6 +61,27 @@ export async function initProcess({
|
|
|
59
61
|
jsonformSchema.value = null;
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
64
|
+
|
|
65
|
+
if (!updatedJsonform) {
|
|
66
|
+
resetProcess({
|
|
67
|
+
loading,
|
|
68
|
+
isProcessed,
|
|
69
|
+
jsonformSchema,
|
|
70
|
+
isPolling,
|
|
71
|
+
processResults,
|
|
72
|
+
enableCompare,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const newJsonForm = await updateJsonformIdentifier({
|
|
78
|
+
jsonformSchema: updatedJsonform,
|
|
79
|
+
newLayers: enableCompare ? getCompareLayers() : getLayers(),
|
|
80
|
+
enableCompare,
|
|
81
|
+
mapElement,
|
|
82
|
+
});
|
|
83
|
+
if (!newJsonForm) return;
|
|
84
|
+
|
|
62
85
|
resetProcess({
|
|
63
86
|
loading,
|
|
64
87
|
isProcessed,
|
|
@@ -67,99 +90,109 @@ export async function initProcess({
|
|
|
67
90
|
processResults,
|
|
68
91
|
enableCompare,
|
|
69
92
|
});
|
|
93
|
+
jsonformSchema.value = newJsonForm;
|
|
94
|
+
}
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Recursively search grouped layer tree for a layer whose id starts with the given prefix
|
|
98
|
+
*
|
|
99
|
+
* @param {Record<string, any>[]} layers
|
|
100
|
+
* @param {string} prefix
|
|
101
|
+
* @returns {string | null}
|
|
102
|
+
*/
|
|
103
|
+
function findLayerIdByPrefix(layers, prefix) {
|
|
104
|
+
for (const layer of layers) {
|
|
105
|
+
if (layer.type === "Group" && Array.isArray(layer.layers)) {
|
|
106
|
+
const found = findLayerIdByPrefix(layer.layers, prefix);
|
|
107
|
+
if (found) return found;
|
|
108
|
+
} else if (layer.properties?.id?.startsWith(prefix)) {
|
|
109
|
+
return layer.properties.id;
|
|
110
|
+
}
|
|
82
111
|
}
|
|
112
|
+
return null;
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
/**
|
|
86
|
-
*
|
|
116
|
+
* Wait until a layer's features are loaded for the drawtools to attach correctly
|
|
117
|
+
* Resolves immediately if already loaded.
|
|
118
|
+
*
|
|
119
|
+
* @param {import("@eox/map").AnyLayerWithSource | null | undefined} layer
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
function waitForLayerRender(layer) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
if (!layer || layer.get("type") !== "Vector" || !("getSource" in layer))
|
|
125
|
+
return resolve();
|
|
126
|
+
const source = /** @type {import("ol/source/Vector").default} */ (
|
|
127
|
+
layer.getSource()
|
|
128
|
+
);
|
|
129
|
+
if (!source || source.getFeatures().length > 0) return resolve();
|
|
130
|
+
source.once("featuresloadend", () => resolve());
|
|
131
|
+
source.once("featuresloaderror", () =>
|
|
132
|
+
reject(new Error(`Layer ${layer.get("id")} failed to load features`)),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Update the jsonform schema to have the correct layer id from the map.
|
|
139
|
+
* Returns null if any required layerId cannot be resolved (layer not yet in map).
|
|
140
|
+
* Callers should treat null as "retry when layers:updated fires".
|
|
87
141
|
*
|
|
88
142
|
* @export
|
|
89
|
-
* @async
|
|
90
143
|
* @param {Object} params
|
|
91
|
-
* @param {Record<string,any> | null} params.jsonformSchema
|
|
92
|
-
* @param {Record<string, any>[] | undefined} params.newLayers
|
|
93
|
-
* @param {
|
|
94
|
-
* @
|
|
144
|
+
* @param {Record<string,any> | null} params.jsonformSchema
|
|
145
|
+
* @param {Record<string, any>[] | undefined} params.newLayers
|
|
146
|
+
* @param {boolean} params.enableCompare
|
|
147
|
+
* @param {import("@eox/map").EOxMap | null | undefined} params.mapElement
|
|
148
|
+
* @returns {Promise<Record<string,any> | null>} updated schema, or null if a layerId could not be resolved
|
|
95
149
|
*/
|
|
96
150
|
export async function updateJsonformIdentifier({
|
|
97
151
|
jsonformSchema,
|
|
98
152
|
newLayers,
|
|
99
153
|
enableCompare,
|
|
154
|
+
mapElement,
|
|
100
155
|
}) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
156
|
+
if (!jsonformSchema || !newLayers) return null;
|
|
157
|
+
|
|
158
|
+
const form = JSON.parse(JSON.stringify(jsonformSchema));
|
|
105
159
|
const drawToolsProperties = getDrawToolsProperties(form);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
/** @type {Promise<void>[]} */
|
|
161
|
+
const renderPromises = [];
|
|
162
|
+
|
|
163
|
+
for (const drawToolsProperty of drawToolsProperties) {
|
|
164
|
+
if (!drawToolsProperty) continue;
|
|
165
|
+
|
|
166
|
+
const drawtoolsOptions =
|
|
167
|
+
form?.properties[drawToolsProperty]?.options?.drawtools;
|
|
168
|
+
if (!drawtoolsOptions) continue;
|
|
169
|
+
|
|
170
|
+
if (drawtoolsOptions.for && enableCompare) {
|
|
171
|
+
drawtoolsOptions.for = "eox-map#compare";
|
|
114
172
|
}
|
|
115
|
-
if (
|
|
116
|
-
drawToolsProperty &&
|
|
117
|
-
newLayers &&
|
|
118
|
-
form?.properties[drawToolsProperty]?.options?.drawtools?.layerId
|
|
119
|
-
) {
|
|
120
|
-
// get partial or full id and try to match with correct eoxmap layer
|
|
121
|
-
// check if newLayers is an array or an object with layers property
|
|
122
|
-
let layers = newLayers;
|
|
123
|
-
// @ts-expect-error TODO payload coming from time update sometimes is not an object with layers property
|
|
124
|
-
if (newLayers.layers && Array.isArray(newLayers.layers)) {
|
|
125
|
-
// @ts-expect-error TODO payload coming from time update sometimes is not an object with layers property
|
|
126
|
-
layers = newLayers.layers;
|
|
127
|
-
}
|
|
128
173
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)[0];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
for (const layer of layersArray) {
|
|
142
|
-
if (layer.type === "Group" && Array.isArray(layer.layers)) {
|
|
143
|
-
traverseLayers(layer.layers);
|
|
144
|
-
} else {
|
|
145
|
-
if (layer.properties?.id?.startsWith(layerId)) {
|
|
146
|
-
matchedLayerId = layer.properties.id;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
174
|
+
if (drawtoolsOptions.layerId) {
|
|
175
|
+
let layer = mapElement?.getLayerById(drawtoolsOptions.layerId);
|
|
176
|
+
if (!layer) {
|
|
177
|
+
const prefix = drawtoolsOptions.layerId.split(";:;")[0];
|
|
178
|
+
const resolvedId = findLayerIdByPrefix(newLayers, prefix);
|
|
179
|
+
if (!resolvedId) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`Could not find matching layer for processing form with id: ${prefix}`,
|
|
182
|
+
);
|
|
183
|
+
return null;
|
|
150
184
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (matchedLayerId) {
|
|
154
|
-
form.properties[drawToolsProperty].options.drawtools.layerId =
|
|
155
|
-
matchedLayerId;
|
|
156
|
-
} else {
|
|
157
|
-
console.warn(
|
|
158
|
-
`Could not find matching layer for processing form with id: ${layerId}`,
|
|
159
|
-
);
|
|
185
|
+
drawtoolsOptions.layerId = resolvedId;
|
|
186
|
+
layer = mapElement?.getLayerById(resolvedId);
|
|
160
187
|
}
|
|
188
|
+
renderPromises.push(waitForLayerRender(layer));
|
|
161
189
|
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await Promise.all(renderPromises).catch((err) => {
|
|
193
|
+
console.warn("[eodash] layer load failed before jsonform attach:", err);
|
|
162
194
|
});
|
|
195
|
+
|
|
163
196
|
return form;
|
|
164
197
|
}
|
|
165
198
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<eox-timecontrol
|
|
3
|
+
v-if="hasMultipleItems"
|
|
4
|
+
:key="unref(mapEl)"
|
|
5
|
+
ref="eoxTimecontrol"
|
|
6
|
+
.for="mapEl"
|
|
7
|
+
@select="onSelect"
|
|
8
|
+
titleKey="title"
|
|
9
|
+
.externalMapRendering="true"
|
|
10
|
+
.animate="animate"
|
|
11
|
+
.initDate="initDate"
|
|
12
|
+
>
|
|
13
|
+
<div class="d-flex g-10 align-center">
|
|
14
|
+
<eox-timecontrol-date class="flex-grow-1"></eox-timecontrol-date>
|
|
15
|
+
<eox-timecontrol-picker
|
|
16
|
+
.range="false"
|
|
17
|
+
.showDots="true"
|
|
18
|
+
.showItems="true"
|
|
19
|
+
.popup="true"
|
|
20
|
+
></eox-timecontrol-picker>
|
|
21
|
+
|
|
22
|
+
<eox-itemfilter
|
|
23
|
+
v-if="filters.length"
|
|
24
|
+
.inlineMode="true"
|
|
25
|
+
:showResults="false"
|
|
26
|
+
.filterProperties="filters"
|
|
27
|
+
></eox-itemfilter>
|
|
28
|
+
<eox-timecontrol-timelapse @export="onExport"></eox-timecontrol-timelapse>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<eox-timecontrol-timeline class="mt-2"></eox-timecontrol-timeline>
|
|
32
|
+
</eox-timecontrol>
|
|
33
|
+
</template>
|
|
34
|
+
<script setup>
|
|
35
|
+
import { datetime, mapEl } from "@/store/states";
|
|
36
|
+
import { eodashCollections } from "@/utils/states";
|
|
37
|
+
import "@eox/timecontrol";
|
|
38
|
+
import "@eox/itemfilter";
|
|
39
|
+
|
|
40
|
+
import { computed, onMounted, ref, unref, useTemplateRef } from "vue";
|
|
41
|
+
import { storeToRefs } from "pinia";
|
|
42
|
+
import { useSTAcStore } from "@/store/stac";
|
|
43
|
+
import { createAnimationLayers } from "./methods";
|
|
44
|
+
|
|
45
|
+
const { animate } = defineProps({
|
|
46
|
+
filters: {
|
|
47
|
+
/** @type {import("vue").PropType<import("@eox/itemfilter").EOxItemFilter["filterProperties"]>} */
|
|
48
|
+
type: Array,
|
|
49
|
+
default: () => [],
|
|
50
|
+
},
|
|
51
|
+
animate: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: true,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** @type {import("vue").ShallowRef<HTMLElement>} */
|
|
58
|
+
const timesliderEl = useTemplateRef("eoxTimecontrol");
|
|
59
|
+
|
|
60
|
+
const startDate = new Date(datetime.value);
|
|
61
|
+
const endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
|
|
62
|
+
|
|
63
|
+
const selectedRange = /** @type {import("vue").Ref<[string, string]>} */ (
|
|
64
|
+
ref([startDate, endDate].map((d) => d.toISOString()))
|
|
65
|
+
);
|
|
66
|
+
const initDate = [startDate.toISOString().split("T")[0]];
|
|
67
|
+
|
|
68
|
+
const hasMultipleItems = computed(() => {
|
|
69
|
+
return eodashCollections.some((ec) => {
|
|
70
|
+
const itemLinks = ec.collectionStac?.links.filter((l) => l.rel === "item");
|
|
71
|
+
const itemsLink = ec.collectionStac?.links.some((l) => l.rel === "items");
|
|
72
|
+
return (itemLinks && itemLinks.length > 1) || itemsLink;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handles the selection event from the time control component.
|
|
78
|
+
* It finds the closest item to the "from" selected date
|
|
79
|
+
* and updates the global datetime state with that item's original date.
|
|
80
|
+
*
|
|
81
|
+
* @param {CustomEvent<import("./types").TimelineSelectionEventDetail>} e
|
|
82
|
+
*/
|
|
83
|
+
const onSelect = (e) => {
|
|
84
|
+
const { selectedItems, date } = e.detail;
|
|
85
|
+
// Update the selected range with the new dates
|
|
86
|
+
selectedRange.value = date;
|
|
87
|
+
|
|
88
|
+
const allItems = Object.keys(selectedItems ?? {}).flatMap(
|
|
89
|
+
(id) => selectedItems[id],
|
|
90
|
+
);
|
|
91
|
+
if (!allItems.length) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const [from, _to] = date;
|
|
95
|
+
const fromDate = new Date(from).getTime();
|
|
96
|
+
|
|
97
|
+
const closestItem = allItems.reduce((prev, curr) => {
|
|
98
|
+
const prevDiff = Math.abs(new Date(prev.originalDate).getTime() - fromDate);
|
|
99
|
+
const currDiff = Math.abs(new Date(curr.originalDate).getTime() - fromDate);
|
|
100
|
+
return currDiff < prevDiff ? curr : prev;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (closestItem) {
|
|
104
|
+
datetime.value = closestItem.originalDate;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const { selectedStac, stacEndpoint } = storeToRefs(useSTAcStore());
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
*
|
|
112
|
+
* @param {CustomEvent<import("./types").TimelineExportEventDetail>} evt
|
|
113
|
+
*/
|
|
114
|
+
const onExport = async (evt) => {
|
|
115
|
+
const { generate, selectedRangeItems, filters } = evt.detail;
|
|
116
|
+
|
|
117
|
+
if (!stacEndpoint.value) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mapLayers = await createAnimationLayers(
|
|
122
|
+
stacEndpoint.value,
|
|
123
|
+
selectedRange.value,
|
|
124
|
+
selectedRangeItems,
|
|
125
|
+
selectedStac,
|
|
126
|
+
filters,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!mapLayers?.length) {
|
|
130
|
+
console.warn("[eodash] No map layers generated for the animation.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
generate({
|
|
135
|
+
mapLayers,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
onMounted(() => {
|
|
140
|
+
const parentDiv = timesliderEl.value?.parentElement;
|
|
141
|
+
const layoutItem = parentDiv?.parentElement;
|
|
142
|
+
if (parentDiv && layoutItem && animate) {
|
|
143
|
+
parentDiv.style.overflow = "visible";
|
|
144
|
+
layoutItem.style.overflow = "visible";
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
</script>
|
|
148
|
+
<style scoped>
|
|
149
|
+
eox-itemfilter {
|
|
150
|
+
--inline-container-height: 40px;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { createLayersConfig } from "^/EodashMap/methods/create-layers-config";
|
|
2
|
+
import { eodashCollections } from "@/utils/states";
|
|
3
|
+
import axios from "@/plugins/axios";
|
|
4
|
+
import { mapEl } from "@/store/states";
|
|
5
|
+
import { removeLayers, sanitizeBbox } from "@/eodashSTAC/helpers";
|
|
6
|
+
import { getLayers } from "@/store/actions";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} stacEndpoint
|
|
10
|
+
* @param {[string, string]} selectedRange
|
|
11
|
+
* @param {import("../types").TimelineExportEventDetail["selectedRangeItems"]} selectedRangeItems
|
|
12
|
+
* @param {import("vue").Ref<import("stac-ts").StacCollection|null>} selectedStac
|
|
13
|
+
* @param {Record<string, import("../types").Filter>} filters
|
|
14
|
+
*/
|
|
15
|
+
export async function createAnimationLayers(
|
|
16
|
+
stacEndpoint,
|
|
17
|
+
selectedRange,
|
|
18
|
+
selectedRangeItems,
|
|
19
|
+
selectedStac,
|
|
20
|
+
filters,
|
|
21
|
+
) {
|
|
22
|
+
if (eodashCollections[0].isAPI) {
|
|
23
|
+
return await createAPILayers(
|
|
24
|
+
stacEndpoint,
|
|
25
|
+
{ min: selectedRange[0], max: selectedRange[1] },
|
|
26
|
+
mapEl.value?.lonLatExtent,
|
|
27
|
+
selectedStac,
|
|
28
|
+
filters,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const { collections: hiddenCollections, layers: hiddenLayers } =
|
|
32
|
+
getHiddenLayers([...getLayers()]);
|
|
33
|
+
|
|
34
|
+
return await Promise.all(
|
|
35
|
+
Object.values(selectedRangeItems).flatMap(async (itemSet) => {
|
|
36
|
+
/** @type {Array<{ layers: Record<string, any>[]; date: string }>} */
|
|
37
|
+
const mapLayersArr = [];
|
|
38
|
+
for (const dateItem of itemSet) {
|
|
39
|
+
await createLayersConfig(
|
|
40
|
+
selectedStac.value,
|
|
41
|
+
eodashCollections.filter(
|
|
42
|
+
(collection) =>
|
|
43
|
+
!hiddenCollections.includes(collection.collectionStac?.id ?? ""),
|
|
44
|
+
),
|
|
45
|
+
dateItem.originalDate,
|
|
46
|
+
).then((layers) => {
|
|
47
|
+
//@ts-expect-error createLayersConfig is not typed strictly
|
|
48
|
+
layers = removeLayers(layers, hiddenLayers);
|
|
49
|
+
//@ts-expect-error createLayersConfig is not typed strictly
|
|
50
|
+
layers = restoreLayersVisibility(layers);
|
|
51
|
+
layers = anonimizeLayersCORS(layers);
|
|
52
|
+
mapLayersArr.push({
|
|
53
|
+
layers,
|
|
54
|
+
date: dateItem.originalDate,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return mapLayersArr;
|
|
59
|
+
}),
|
|
60
|
+
).then((results) => results.flat());
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
*
|
|
64
|
+
* @param {string} stacEndpoint
|
|
65
|
+
* @param {{min: string, max: string}} date
|
|
66
|
+
* @param {number[] | undefined} bbox
|
|
67
|
+
* @param {import("vue").Ref<import("stac-ts").StacCollection|null>} selectedStac
|
|
68
|
+
* @param {Record<string, import("../types").Filter>} filters
|
|
69
|
+
* @return {Promise<Array<{ layers: Record<string, any>[]; date: string }>>}
|
|
70
|
+
*/
|
|
71
|
+
async function createAPILayers(
|
|
72
|
+
stacEndpoint,
|
|
73
|
+
date = { min: "", max: "" },
|
|
74
|
+
bbox,
|
|
75
|
+
selectedStac,
|
|
76
|
+
filters,
|
|
77
|
+
) {
|
|
78
|
+
if (!bbox) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const url = new URL(stacEndpoint + "/search");
|
|
82
|
+
url.searchParams.set("limit", "100");
|
|
83
|
+
url.searchParams.set("collections", selectedStac.value?.id ?? "");
|
|
84
|
+
url.searchParams.set(
|
|
85
|
+
"datetime",
|
|
86
|
+
`${new Date(date.min).toISOString()}/${new Date(date.max).toISOString()}`,
|
|
87
|
+
);
|
|
88
|
+
url.searchParams.set("bbox", sanitizeBbox(bbox).join(","));
|
|
89
|
+
|
|
90
|
+
const stacFilter = buildStacFilters(filters);
|
|
91
|
+
if (stacFilter) {
|
|
92
|
+
url.searchParams.set("filter", stacFilter);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @type {import("stac-ts").StacItem[]} */
|
|
96
|
+
const items = await axios
|
|
97
|
+
.get(url.href)
|
|
98
|
+
.then((res) => res.data.features)
|
|
99
|
+
.catch((err) => {
|
|
100
|
+
console.error("[eodash] Error fetching items for animation:", err);
|
|
101
|
+
return [];
|
|
102
|
+
});
|
|
103
|
+
if (!items || !items.length) {
|
|
104
|
+
console.warn("[eodash] No items found for animation.");
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const { collections: hiddenCollections, layers: hiddenLayers } =
|
|
108
|
+
getHiddenLayers([...getLayers()]);
|
|
109
|
+
return await Promise.all(
|
|
110
|
+
items.map(async (item) => {
|
|
111
|
+
/** @type {Array<{ layers: Record<string, any>[]; date: string }>} */
|
|
112
|
+
const mapLayersArr = [];
|
|
113
|
+
await createLayersConfig(
|
|
114
|
+
selectedStac.value,
|
|
115
|
+
eodashCollections.filter(
|
|
116
|
+
(collection) =>
|
|
117
|
+
!hiddenCollections.includes(collection.collectionStac?.id ?? ""),
|
|
118
|
+
),
|
|
119
|
+
item,
|
|
120
|
+
).then((layers) => {
|
|
121
|
+
//@ts-expect-error createLayersConfig is not typed strictly
|
|
122
|
+
layers = removeLayers(layers, hiddenLayers);
|
|
123
|
+
//@ts-expect-error createLayersConfig is not typed strictly
|
|
124
|
+
layers = restoreLayersVisibility(layers);
|
|
125
|
+
layers = anonimizeLayersCORS(layers);
|
|
126
|
+
mapLayersArr.push({
|
|
127
|
+
layers,
|
|
128
|
+
date: /** @type {string} */ (item.properties.datetime),
|
|
129
|
+
});
|
|
130
|
+
return mapLayersArr;
|
|
131
|
+
});
|
|
132
|
+
return mapLayersArr;
|
|
133
|
+
}),
|
|
134
|
+
).then((results) => results.flat());
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
*
|
|
138
|
+
* @param {Record<string, any>[]} layers
|
|
139
|
+
* @returns {Record<string, any>[]}
|
|
140
|
+
*/
|
|
141
|
+
export function anonimizeLayersCORS(layers) {
|
|
142
|
+
return layers.map((layer) => {
|
|
143
|
+
if (layer.type === "Group") {
|
|
144
|
+
layer.layers = anonimizeLayersCORS(layer.layers);
|
|
145
|
+
return layer;
|
|
146
|
+
}
|
|
147
|
+
if (layer.source) {
|
|
148
|
+
layer.source.crossOrigin = "anonymous";
|
|
149
|
+
}
|
|
150
|
+
return layer;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* returns the list of layers that has visibility hidden
|
|
155
|
+
* @param {import("@eox/map").EoxLayer[]} layers
|
|
156
|
+
* @returns {{ collections: string[]; layers: string[] }}
|
|
157
|
+
*/
|
|
158
|
+
export function getHiddenLayers(layers) {
|
|
159
|
+
/** @type {{ collections: string[]; layers: string[] }} */
|
|
160
|
+
const result = { collections: [], layers: [] };
|
|
161
|
+
|
|
162
|
+
for (const layer of layers) {
|
|
163
|
+
// check inner layers if it's a group layer first
|
|
164
|
+
if (layer.type === "Group" && Array.isArray(layer.layers)) {
|
|
165
|
+
const childResult = getHiddenLayers(layer.layers);
|
|
166
|
+
for (const col of childResult.collections) {
|
|
167
|
+
if (!result.collections.includes(col)) {
|
|
168
|
+
result.collections.push(col);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const lyr of childResult.layers) {
|
|
172
|
+
if (!result.layers.includes(lyr)) {
|
|
173
|
+
result.layers.push(lyr);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!layer.properties?.id) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const olLayer = mapEl.value?.getLayerById(layer.properties?.id);
|
|
183
|
+
if (!olLayer) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (olLayer.getVisible() === false) {
|
|
188
|
+
const refId = layer.properties.id;
|
|
189
|
+
if (refId) {
|
|
190
|
+
if (refId.includes(";:;")) {
|
|
191
|
+
// Check if this looks like a typical eodash collection ID with separator
|
|
192
|
+
const parts = refId.split(";:;");
|
|
193
|
+
if (parts.length > 2) {
|
|
194
|
+
const prefix = parts[0];
|
|
195
|
+
if (!result.collections.includes(prefix)) {
|
|
196
|
+
result.collections.push(prefix);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// It has a separator but might just be a base layer like `layerId;:;EPSG`
|
|
200
|
+
if (!result.layers.includes(refId)) {
|
|
201
|
+
result.layers.push(refId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (!result.layers.includes(refId)) {
|
|
206
|
+
result.layers.push(refId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Iterates through a list of layers and updates layer.properties.visible to true
|
|
218
|
+
* if it is set to false
|
|
219
|
+
* @param {import("@eox/map").EoxLayer[]} layers
|
|
220
|
+
* @returns {import("@eox/map").EoxLayer[]}
|
|
221
|
+
*/
|
|
222
|
+
export function restoreLayersVisibility(layers) {
|
|
223
|
+
for (const layer of layers) {
|
|
224
|
+
if (layer.properties && layer.properties.visible === false) {
|
|
225
|
+
layer.properties.visible = true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (layer.type === "Group" && Array.isArray(layer.layers)) {
|
|
229
|
+
restoreLayersVisibility(layer.layers);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return layers;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build STAC API filter string from TimeSlider filters
|
|
237
|
+
* @param {Record<string, import("../types").Filter>} filters
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
export function buildStacFilters(filters) {
|
|
241
|
+
if (!filters) return "";
|
|
242
|
+
|
|
243
|
+
/** @type {string[]} */
|
|
244
|
+
const stacFilters = [];
|
|
245
|
+
|
|
246
|
+
Object.values(filters).forEach((filter) => {
|
|
247
|
+
if (!filter || !filter.key) return;
|
|
248
|
+
|
|
249
|
+
// strip 'properties.' from key if present
|
|
250
|
+
const propName = filter.key.startsWith("properties.")
|
|
251
|
+
? filter.key.replace("properties.", "")
|
|
252
|
+
: filter.key;
|
|
253
|
+
|
|
254
|
+
if (filter.type === "range" && filter.state) {
|
|
255
|
+
if (
|
|
256
|
+
filter.state.min !== undefined &&
|
|
257
|
+
filter.state.min > (filter.min ?? -Infinity)
|
|
258
|
+
) {
|
|
259
|
+
stacFilters.push(`${propName}>=${filter.state.min}`);
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
filter.state.max !== undefined &&
|
|
263
|
+
filter.state.max < (filter.max ?? Infinity)
|
|
264
|
+
) {
|
|
265
|
+
stacFilters.push(`${propName}<=${filter.state.max}`);
|
|
266
|
+
}
|
|
267
|
+
} else if (filter.type === "multiselect" && filter.stringifiedState) {
|
|
268
|
+
if (filter.stringifiedState.length > 0) {
|
|
269
|
+
stacFilters.push(`${propName} IN (${filter.stringifiedState})`);
|
|
270
|
+
}
|
|
271
|
+
} else if (filter.type === "select" && filter.stringifiedState) {
|
|
272
|
+
if (filter.stringifiedState) {
|
|
273
|
+
stacFilters.push(`${propName}='${filter.stringifiedState}'`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return stacFilters.join(" AND ");
|
|
279
|
+
}
|