@eodash/eodash 5.6.0 → 5.6.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 (51) hide show
  1. package/core/client/eodashSTAC/EodashCollection.js +5 -1
  2. package/core/client/eodashSTAC/helpers.js +4 -4
  3. package/dist/client/{DashboardLayout-DkKDhfgH.js → DashboardLayout-CqMb7OXc.js} +2 -2
  4. package/dist/client/{DynamicWebComponent-BrpkA-e3.js → DynamicWebComponent-DqKqTf7l.js} +1 -1
  5. package/dist/client/{EodashChart-p7_1FRlF.js → EodashChart-PkUQfH0k.js} +4 -4
  6. package/dist/client/{EodashDatePicker-HK0Jl7wQ.js → EodashDatePicker-DLJGdTtX.js} +3 -3
  7. package/dist/client/{EodashItemFilter-BYQDA78B.js → EodashItemFilter-Dk8KjFcX.js} +1 -1
  8. package/dist/client/{EodashLayerControl-NcrNncGn.js → EodashLayerControl-CC1Cogrn.js} +2 -2
  9. package/dist/client/{EodashLayoutSwitcher-CC2Znd0I.js → EodashLayoutSwitcher-5WZYd6rr.js} +3 -3
  10. package/dist/client/{EodashMapBtns-BJSh7qT-.js → EodashMapBtns-BHrSDNoC.js} +6 -6
  11. package/dist/client/{EodashStacInfo-BA-0UjvT.js → EodashStacInfo-B90wobPc.js} +31 -31
  12. package/dist/client/{EodashTimeSlider-GDv1hbw7.js → EodashTimeSlider-5vm7F3ju.js} +2 -2
  13. package/dist/client/{EodashTools-CWmoH0g_.js → EodashTools-CGZtTh45.js} +4 -4
  14. package/dist/client/{ExportState-CfAfX4RN.js → ExportState-Ce0ZnrX_.js} +4 -4
  15. package/dist/client/{Footer-DLGxQTZt.js → Footer-CnZqEiop.js} +1 -1
  16. package/dist/client/{Header-eyEyyzD6.js → Header-Dr5PBIm8.js} +2 -2
  17. package/dist/client/{MobileLayout-RzLOyXwL.js → MobileLayout-DyMjaeif.js} +2 -2
  18. package/dist/client/{PopUp-WNs6Io2N.js → PopUp-DjvE_71e.js} +2 -2
  19. package/dist/client/{ProcessList-RzVqbd5E.js → ProcessList-kf3olI7W.js} +4 -4
  20. package/dist/client/{VImg-GKUTFoV-.js → VImg-Bj6uWt6j.js} +2 -2
  21. package/dist/client/{VMain-D7_72mHr.js → VMain-Cm0UmKGi.js} +1 -1
  22. package/dist/client/{VTooltip-BbmvtvYw.js → VTooltip-DbBu-ky3.js} +2 -2
  23. package/dist/client/{WidgetsContainer-Dw5d7SOW.js → WidgetsContainer-CfLnd44U.js} +1 -1
  24. package/dist/client/{asWebComponent-DDt7V3Bl.js → asWebComponent-DfULAbpq.js} +13 -9
  25. package/dist/client/{async-CT3KEV52.js → async-CIbKXEEe.js} +10 -20
  26. package/dist/client/eo-dash.js +1 -1
  27. package/dist/client/{forwardRefs-CVzxyoSq.js → forwardRefs-yj3VWOK5.js} +2 -2
  28. package/dist/client/{handling-C285F_Mq.js → handling-s8xt2JwF.js} +390 -90
  29. package/dist/client/{helpers-CZFH0EVN.js → helpers-DAVGYl_f.js} +4 -4
  30. package/dist/client/{index-B-m7V3Kb.js → index-4TrjfJ32.js} +3 -3
  31. package/dist/client/{index-B_6H-N14.js → index-Bu8BMA9q.js} +39 -31
  32. package/dist/client/{index-DgOXD7wd.js → index-CYz6dT7K.js} +2 -2
  33. package/dist/client/{index-CF3ZMzTz.js → index-CugS__t5.js} +3 -3
  34. package/dist/client/templates.js +1 -1
  35. package/dist/client/{transition-C2gnrWg0.js → transition-DaOsiD4u.js} +1 -1
  36. package/dist/types/widgets/EodashProcess/methods/composables.d.ts +1 -2
  37. package/dist/types/widgets/EodashProcess/methods/handling.d.ts +6 -6
  38. package/dist/types/widgets/EodashProcess/methods/outputs.d.ts +3 -1
  39. package/dist/types/widgets/EodashProcess/methods/template-helpers.d.ts +99 -0
  40. package/dist/types/widgets/EodashProcess/methods/utils.d.ts +1 -6
  41. package/dist/types/widgets/EodashProcess/types.d.ts +1 -0
  42. package/dist/types/widgets/EodashStacInfo.vue.d.ts +10 -10
  43. package/package.json +1 -1
  44. package/widgets/EodashProcess/index.vue +16 -12
  45. package/widgets/EodashProcess/methods/composables.js +18 -15
  46. package/widgets/EodashProcess/methods/handling.js +78 -62
  47. package/widgets/EodashProcess/methods/outputs.js +74 -29
  48. package/widgets/EodashProcess/methods/template-helpers.js +246 -0
  49. package/widgets/EodashProcess/methods/utils.js +7 -17
  50. package/widgets/EodashProcess/types.ts +1 -0
  51. package/widgets/EodashStacInfo.vue +30 -30
@@ -3,8 +3,7 @@ import {
3
3
  applyProcessLayersToMap,
4
4
  extractGeometries,
5
5
  getBboxProperty,
6
- getDrawToolsProperty,
7
- updateJsonformSchemaTarget,
6
+ getDrawToolsProperties,
8
7
  } from "./utils";
9
8
  import {
10
9
  compareIndicator,
@@ -17,13 +16,12 @@ import {
17
16
  compareChartData,
18
17
  compareChartSpec,
19
18
  } from "@/store/states";
20
- import axios from "@/plugins/axios";
21
19
  import { processCharts, processLayers, processSTAC } from "./outputs";
22
20
  import { handleLayersCustomEndpoints } from "./custom-endpoints/layers";
23
21
  import { handleChartCustomEndpoints } from "./custom-endpoints/chart";
24
22
  import { useSTAcStore } from "@/store/stac";
25
23
  import { useGetSubCodeId } from "@/composables";
26
- import { getLayers } from "@/store/actions";
24
+ import { getLayers, getCompareLayers } from "@/store/actions";
27
25
 
28
26
  /**
29
27
  * Fetch and set the jsonform schema to initialize the process
@@ -32,7 +30,6 @@ import { getLayers } from "@/store/actions";
32
30
  * @async
33
31
  * @param {Object} params
34
32
  * @param {import("vue").Ref<import("stac-ts").StacCollection | null>} params.selectedStac
35
- * @param {import("vue").Ref<import("@eox/jsonform").EOxJSONForm | null>} params.jsonformEl
36
33
  * @param {import("vue").Ref<Record<string,any> | null>} params.jsonformSchema
37
34
  * @param {import("vue").Ref<any[]>} params.processResults
38
35
  * @param {import("vue").Ref<boolean>} params.isProcessed
@@ -42,7 +39,6 @@ import { getLayers } from "@/store/actions";
42
39
  */
43
40
  export async function initProcess({
44
41
  selectedStac,
45
- jsonformEl,
46
42
  jsonformSchema,
47
43
  isProcessed,
48
44
  processResults,
@@ -53,10 +49,10 @@ export async function initProcess({
53
49
  const isPoiAlive = enableCompare ? !!comparePoi.value : !!poi.value;
54
50
  let updatedJsonform = null;
55
51
  if (selectedStac.value?.["eodash:jsonform"]) {
56
- updatedJsonform = await axios
52
+ updatedJsonform = await fetch(
57
53
  //@ts-expect-error eodash extention
58
- .get(selectedStac.value["eodash:jsonform"])
59
- .then((resp) => resp.data);
54
+ selectedStac.value["eodash:jsonform"],
55
+ ).then((resp) => resp.json());
60
56
  }
61
57
 
62
58
  if (!updatedJsonform && isPoiAlive) {
@@ -72,17 +68,13 @@ export async function initProcess({
72
68
  enableCompare,
73
69
  });
74
70
 
75
- await jsonformEl.value?.editor.destroy();
76
71
  if (updatedJsonform) {
77
72
  // make sure correct target layer id is used in jsonform
78
- let newJsonForm = null;
79
- newJsonForm = await updateJsonformIdentifier({
73
+ const newJsonForm = await updateJsonformIdentifier({
80
74
  jsonformSchema: updatedJsonform,
81
- newLayers: getLayers(),
75
+ newLayers: enableCompare ? getCompareLayers() : getLayers(),
76
+ enableCompare,
82
77
  });
83
- if (enableCompare) {
84
- newJsonForm = updateJsonformSchemaTarget(newJsonForm);
85
- }
86
78
  // trigger jsonform update in next tick
87
79
  jsonformSchema.value = null;
88
80
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -98,62 +90,76 @@ export async function initProcess({
98
90
  * @param {Object} params
99
91
  * @param {Record<string,any> | null} params.jsonformSchema params.jsonformSchema
100
92
  * @param {Record<string, any>[] | undefined} params.newLayers params.newLayers
101
- * @returns {Promise<Record<string,any> | null | undefined>} updated jsonform schema
93
+ * @param { boolean } params.enableCompare params.enableCompare
94
+ * @returns {Promise<Record<string,any> | null>} updated jsonform schema
102
95
  */
103
- export async function updateJsonformIdentifier({ jsonformSchema, newLayers }) {
96
+ export async function updateJsonformIdentifier({
97
+ jsonformSchema,
98
+ newLayers,
99
+ enableCompare,
100
+ }) {
104
101
  const form = jsonformSchema;
105
102
  if (!form) {
106
- return;
103
+ return null;
107
104
  }
108
- const drawToolsProperty = getDrawToolsProperty(form);
109
- if (
110
- drawToolsProperty &&
111
- newLayers &&
112
- form?.properties[drawToolsProperty]?.options?.drawtools?.layerId
113
- ) {
114
- // get partial or full id and try to match with correct eoxmap layer
115
- // check if newLayers is an array or an object with layers property
116
- let layers = newLayers;
117
- // @ts-expect-error TODO payload coming from time update sometimes is not an object with layers property
118
- if (newLayers.layers && Array.isArray(newLayers.layers)) {
119
- // @ts-expect-error TODO payload coming from time update sometimes is not an object with layers property
120
- layers = newLayers.layers;
105
+ const drawToolsProperties = getDrawToolsProperties(form);
106
+ drawToolsProperties.forEach((drawToolsProperty) => {
107
+ if (
108
+ drawToolsProperty &&
109
+ form?.properties[drawToolsProperty]?.options?.drawtools?.for &&
110
+ enableCompare
111
+ ) {
112
+ form.properties[drawToolsProperty].options.drawtools.for =
113
+ "eox-map#compare";
121
114
  }
122
-
123
- const layerId =
124
- form.properties[drawToolsProperty].options.drawtools.layerId.split(
125
- ";:;",
126
- )[0];
127
- let matchedLayerId = null;
128
- // layers are not flat can be grouped, we need to recursively search
129
- const traverseLayers = (
130
- /** @type {Record<string, any>[] | undefined} */ layersArray,
131
- ) => {
132
- if (!layersArray) {
133
- return;
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;
134
127
  }
135
- for (const layer of layersArray) {
136
- if (layer.type === "Group" && Array.isArray(layer.layers)) {
137
- traverseLayers(layer.layers);
138
- } else {
139
- if (layer.properties?.id?.startsWith(layerId)) {
140
- matchedLayerId = layer.properties.id;
141
- break;
128
+
129
+ const layerId =
130
+ form.properties[drawToolsProperty].options.drawtools.layerId.split(
131
+ ";:;",
132
+ )[0];
133
+ let matchedLayerId = null;
134
+ // layers are not flat can be grouped, we need to recursively search
135
+ const traverseLayers = (
136
+ /** @type {Record<string, any>[] | undefined} */ layersArray,
137
+ ) => {
138
+ if (!layersArray) {
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
+ }
142
149
  }
143
150
  }
151
+ };
152
+ traverseLayers(layers);
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
+ );
144
160
  }
145
- };
146
- traverseLayers(layers);
147
- if (matchedLayerId) {
148
- form.properties.feature.options.drawtools.layerId = matchedLayerId;
149
- return form;
150
- } else {
151
- console.warn(
152
- `Could not find matching layer for processing form with id: ${layerId}`,
153
- );
154
- return null;
155
161
  }
156
- }
162
+ });
157
163
  return form;
158
164
  }
159
165
 
@@ -192,6 +198,15 @@ export async function handleProcesses({
192
198
  );
193
199
 
194
200
  const bboxProperty = getBboxProperty(jsonformSchema.value);
201
+ // Preserve raw form value before extractGeometries mutates it.
202
+ // Needed so POST multiQuery can iterate original GeoJSON Feature objects.
203
+ // Uses JSON round-trip instead of structuredClone to handle OL Feature
204
+ // objects from drawtools (which contain non-cloneable methods).
205
+ const rawJsonformValue = JSON.parse(
206
+ JSON.stringify(
207
+ /** @type {Record<string, any>} */ (jsonformEl.value?.value ?? {}),
208
+ ),
209
+ );
195
210
  const jsonformValue = /** @type {Record<string,any>} */ (
196
211
  jsonformEl.value?.value
197
212
  );
@@ -210,6 +225,7 @@ export async function handleProcesses({
210
225
  [tempChartSpec, usedChartData.value] = await processCharts({
211
226
  links: serviceLinks,
212
227
  jsonformValue: { ...(jsonformValue ?? {}) },
228
+ rawJsonformValue,
213
229
  jsonformSchema: jsonformSchema.value,
214
230
  enableCompare,
215
231
  selectedStac: selectedStac.value,
@@ -4,12 +4,21 @@ import axios from "@/plugins/axios";
4
4
  import { createTiffLayerDefinition, separateEndpointLinks } from "./utils";
5
5
  import { useSTAcStore } from "@/store/stac";
6
6
  import { isFirstLoad } from "@/utils/states";
7
+ import {
8
+ getMultiQueryMatches,
9
+ getPostMultiQueryValues,
10
+ getScalarMultiQueryValues,
11
+ createMultiQueryContext,
12
+ aggregateInlineResponses,
13
+ renderJsonBodyTemplate,
14
+ } from "./template-helpers";
7
15
 
8
16
  ////// --- CHARTS --- //////
9
17
  /**
10
18
  * @param {object} options
11
19
  * @param {import("stac-ts").StacLink[] | undefined} options.links
12
20
  * @param {Record<string,any> | undefined} options.jsonformValue
21
+ * @param {Record<string,any> | undefined} [options.rawJsonformValue]
13
22
  * @param {string} options.specUrl
14
23
  * @param {(input:import("^/EodashProcess/types").CustomEnpointInput)=>Promise<Record<string,any>[] | undefined | null>} [options.customEndpointsHandler]
15
24
  * @param {import("vue").Ref<boolean>} options.isPolling
@@ -22,6 +31,7 @@ import { isFirstLoad } from "@/utils/states";
22
31
  export async function processCharts({
23
32
  links,
24
33
  jsonformValue,
34
+ rawJsonformValue,
25
35
  specUrl,
26
36
  customEndpointsHandler,
27
37
  jsonformSchema,
@@ -51,6 +61,7 @@ export async function processCharts({
51
61
  (await customEndpointsHandler({
52
62
  jsonformSchema,
53
63
  jsonformValue,
64
+ rawJsonformValue,
54
65
  links: endpointLinks,
55
66
  selectedStac,
56
67
  isPolling,
@@ -114,6 +125,7 @@ export async function processCharts({
114
125
  await injectVegaInlineData(spec, {
115
126
  url: link.href,
116
127
  jsonformValue: jsonformValue,
128
+ rawJsonformValue,
117
129
  link: link,
118
130
  flatstyleUrl: /** @type string */ (link["eox:flatstyle"]),
119
131
  jsonformSchema,
@@ -153,46 +165,39 @@ export async function processCharts({
153
165
  * @param {object} injectables
154
166
  * @param {string} injectables.url
155
167
  * @param {Record<string,any>} [injectables.jsonformValue]
168
+ * @param {Record<string,any>} [injectables.rawJsonformValue]
156
169
  * @param {import("stac-ts").StacLink} injectables.link
157
170
  * @param {url} [injectables.flatstyleUrl]
158
171
  * @param {import("json-schema").JSONSchema7} [injectables.jsonformSchema]
159
172
  */
160
173
  async function injectVegaInlineData(
161
174
  spec,
162
- { url, jsonformValue, link, flatstyleUrl, jsonformSchema },
175
+ { url, jsonformValue, rawJsonformValue, link, flatstyleUrl, jsonformSchema },
163
176
  ) {
164
177
  if (!spec.data) {
165
178
  return;
166
179
  }
180
+ // @ts-expect-error JSONSchema7 is structurally compatible with Record<string, any>
181
+ const multiQueryMatches = getMultiQueryMatches(jsonformValue, jsonformSchema);
167
182
  if (link.method == "GET") {
168
- // we see if any of the multiQuery values match an array in the jsonformValue
169
- // and if so, we can do multiple requests and merge all data together.
170
- //@ts-expect-error type jsonform Schema
171
- const multiQuery = jsonformSchema?.options?.multiQuery;
172
- const matches = Object.keys(jsonformValue ?? {}).filter((key) => {
173
- return Array.isArray(multiQuery)
174
- ? multiQuery.includes(key)
175
- : multiQuery === key;
176
- });
177
- if (matches.length > 0 && jsonformValue) {
183
+ if (multiQueryMatches.length > 0 && jsonformValue) {
178
184
  const dataValues = [];
179
- for (const match of matches) {
180
- if (Array.isArray(jsonformValue[match])) {
181
- for (const value of jsonformValue[match]) {
182
- const dataUrl = await renderDataUrl(
183
- url,
184
- { ...jsonformValue, [match]: value },
185
- flatstyleUrl,
186
- );
187
- dataValues.push(await axios.get(dataUrl).then((resp) => resp.data));
188
- }
189
- } else {
190
- const dataUrl = await renderDataUrl(url, jsonformValue, flatstyleUrl);
185
+ for (const match of multiQueryMatches) {
186
+ const queryValues = getScalarMultiQueryValues(match, jsonformValue);
187
+ if (!queryValues.length) {
188
+ continue;
189
+ }
190
+ for (const value of queryValues) {
191
+ const dataUrl = await renderDataUrl(
192
+ url,
193
+ { ...jsonformValue, [match]: value },
194
+ flatstyleUrl,
195
+ );
191
196
  dataValues.push(await axios.get(dataUrl).then((resp) => resp.data));
192
197
  }
193
198
  }
194
199
  /** @type {import("vega-lite/build/src/data").InlineData} */
195
- (spec.data).values = dataValues.flat();
200
+ (spec.data).values = aggregateInlineResponses(dataValues);
196
201
  return spec;
197
202
  }
198
203
  // if no array matches, we can just do a single request
@@ -216,11 +221,49 @@ async function injectVegaInlineData(
216
221
  .then((resp) => {
217
222
  return resp.data;
218
223
  });
219
- const body = JSON.parse(
220
- mustache.render(bodyTemplate, {
221
- ...(jsonformValue ?? {}),
222
- }),
223
- );
224
+ if (
225
+ // @ts-expect-error jsonform options extension not in JSONSchema7
226
+ jsonformSchema?.options?.multiQuery &&
227
+ multiQueryMatches.length === 0
228
+ ) {
229
+ /** @type {import("vega-lite/build/src/data").InlineData} */
230
+ (spec.data).values = [];
231
+ return spec;
232
+ }
233
+ if (multiQueryMatches.length > 0 && jsonformValue) {
234
+ const responses = [];
235
+ for (const match of multiQueryMatches) {
236
+ const queryValues = getPostMultiQueryValues(
237
+ match,
238
+ jsonformValue,
239
+ rawJsonformValue,
240
+ /** @type {Record<string, any>} */ (jsonformSchema),
241
+ );
242
+ if (!queryValues.length) {
243
+ continue;
244
+ }
245
+ // Sequential POSTs (not parallel) to avoid rate-limiting on endpoints
246
+ // like TiTiler-PgSTAC. For small selection counts this is fine; if
247
+ // parallelism is needed later, switch to Promise.all.
248
+ for (const [index, value] of queryValues.entries()) {
249
+ const requestContext = createMultiQueryContext(
250
+ jsonformValue,
251
+ match,
252
+ value,
253
+ index,
254
+ );
255
+ const body = renderJsonBodyTemplate(bodyTemplate, requestContext);
256
+ responses.push(await axios.post(url, body).then((resp) => resp.data));
257
+ }
258
+ }
259
+ /** @type {import("vega-lite/build/src/data").InlineData} */
260
+ (spec.data).values = aggregateInlineResponses(responses);
261
+ return spec;
262
+ }
263
+
264
+ const body = renderJsonBodyTemplate(bodyTemplate, {
265
+ ...(jsonformValue ?? {}),
266
+ });
224
267
  /** @type {import("vega-lite/build/src/data").InlineData} */
225
268
  (spec.data).values = await axios.post(url, body).then((resp) => {
226
269
  return resp.data;
@@ -229,6 +272,8 @@ async function injectVegaInlineData(
229
272
  return spec;
230
273
  }
231
274
 
275
+ // MultiQuery helpers and template rendering are in ./template-helpers.js
276
+
232
277
  /**
233
278
  * @param {import("vega-lite").TopLevelSpec} spec
234
279
  * @param {object} injectables
@@ -0,0 +1,246 @@
1
+ import mustache from "mustache";
2
+
3
+ /**
4
+ * Returns form value keys that match the multiQuery configuration.
5
+ * @param {Record<string, any> | undefined} jsonformValue
6
+ * @param {Record<string, any>} jsonformSchema
7
+ * @returns {string[]}
8
+ */
9
+ export function getMultiQueryMatches(jsonformValue, jsonformSchema) {
10
+ const multiQuery = jsonformSchema?.options?.multiQuery;
11
+ return Object.keys(jsonformValue ?? {}).filter((key) => {
12
+ return Array.isArray(multiQuery)
13
+ ? multiQuery.includes(key)
14
+ : multiQuery === key;
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Extracts iterable scalar values for a multiQuery field.
20
+ * Wraps single values in an array; returns empty array for undefined.
21
+ * @param {string} match - The multiQuery field name
22
+ * @param {Record<string, any>} jsonformValue
23
+ * @returns {any[]}
24
+ */
25
+ export function getScalarMultiQueryValues(match, jsonformValue) {
26
+ const value = jsonformValue?.[match];
27
+ if (Array.isArray(value)) {
28
+ return value;
29
+ }
30
+ return value === undefined ? [] : [value];
31
+ }
32
+
33
+ /**
34
+ * Extracts iterable values for a POST multiQuery field.
35
+ * For GeoJSON fields, uses rawJsonformValue to get original Feature objects
36
+ * (before extractGeometries stringifies them). For scalar fields, delegates
37
+ * to getScalarMultiQueryValues.
38
+ * @param {string} match - The multiQuery field name
39
+ * @param {Record<string, any>} jsonformValue - Form values after geometry extraction
40
+ * @param {Record<string, any> | undefined} rawJsonformValue - Form values before geometry extraction
41
+ * @param {Record<string, any>} jsonformSchema
42
+ * @returns {any[]}
43
+ */
44
+ export function getPostMultiQueryValues(
45
+ match,
46
+ jsonformValue,
47
+ rawJsonformValue,
48
+ jsonformSchema,
49
+ ) {
50
+ if (jsonformSchema?.properties?.[match]?.type === "geojson") {
51
+ return getGeoJsonMultiQueryValues(rawJsonformValue?.[match]);
52
+ }
53
+ return getScalarMultiQueryValues(match, jsonformValue);
54
+ }
55
+
56
+ /**
57
+ * Extracts individual GeoJSON Feature objects from a value that may be
58
+ * a FeatureCollection, a single Feature, or an array of Features.
59
+ * Filters for valid Features with geometry and deduplicates.
60
+ * @param {any} value
61
+ * @returns {Record<string, any>[]}
62
+ */
63
+ export function getGeoJsonMultiQueryValues(value) {
64
+ if (value?.type === "FeatureCollection") {
65
+ return dedupeMultiQueryValues(
66
+ (value.features ?? []).filter(
67
+ (/** @type {any} */ feature) =>
68
+ feature?.type === "Feature" && feature.geometry,
69
+ ),
70
+ );
71
+ }
72
+ if (value?.type === "Feature") {
73
+ return [value];
74
+ }
75
+ if (Array.isArray(value)) {
76
+ return dedupeMultiQueryValues(
77
+ value.filter(
78
+ (/** @type {any} */ feature) =>
79
+ feature?.type === "Feature" && feature.geometry,
80
+ ),
81
+ );
82
+ }
83
+ return [];
84
+ }
85
+
86
+ /**
87
+ * Removes duplicate values using JSON serialization as the identity key.
88
+ * @param {any[]} values
89
+ * @returns {any[]}
90
+ */
91
+ export function dedupeMultiQueryValues(values) {
92
+ const seen = new Set();
93
+ const deduped = [];
94
+ for (const value of values ?? []) {
95
+ const key =
96
+ value && typeof value === "object"
97
+ ? JSON.stringify(value)
98
+ : String(value);
99
+ if (seen.has(key)) {
100
+ continue;
101
+ }
102
+ seen.add(key);
103
+ deduped.push(value);
104
+ }
105
+ return deduped;
106
+ }
107
+
108
+ /**
109
+ * Builds the Mustache rendering context for one multiQuery iteration.
110
+ * Spreads the full form value, overrides the matched field with the current
111
+ * iteration value, and adds a 1-based `multiQueryIndex`.
112
+ * @param {Record<string, any>} jsonformValue
113
+ * @param {string} match - The multiQuery field name being iterated
114
+ * @param {any} value - The current iteration value
115
+ * @param {number} index - Zero-based iteration index
116
+ * @returns {Record<string, any>}
117
+ */
118
+ export function createMultiQueryContext(jsonformValue, match, value, index) {
119
+ return {
120
+ ...(jsonformValue ?? {}),
121
+ [match]: value,
122
+ multiQueryIndex: index + 1,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Flattens an array of POST responses into a single values array.
128
+ * Each response that is itself an array is spread; scalars are wrapped.
129
+ * @param {any[]} values
130
+ * @returns {any[]}
131
+ */
132
+ export function aggregateInlineResponses(values) {
133
+ return values.flatMap((value) => (Array.isArray(value) ? value : [value]));
134
+ }
135
+
136
+ /**
137
+ * Renders a JSON body template string with the given context.
138
+ * Primary path: parses the template as JSON, then recursively renders each
139
+ * node (preserving object-valued placeholders as raw JSON).
140
+ * Fallback: treats the template as a Mustache string, renders, then parses.
141
+ * @param {string} bodyTemplate - JSON template string (may contain Mustache placeholders)
142
+ * @param {Record<string, any>} context - Values to substitute into the template
143
+ * @returns {any} Parsed JSON body ready for POST
144
+ */
145
+ export function renderJsonBodyTemplate(bodyTemplate, context) {
146
+ try {
147
+ const parsedTemplate = JSON.parse(bodyTemplate);
148
+ return renderTemplateNode(parsedTemplate, context);
149
+ } catch {
150
+ try {
151
+ return JSON.parse(
152
+ mustache.render(bodyTemplate, {
153
+ ...(context ?? {}),
154
+ }),
155
+ );
156
+ } catch (e) {
157
+ throw new Error(
158
+ `[eodash] Failed to render POST body template: ${e instanceof Error ? e.message : String(e)}`,
159
+ );
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Recursively renders a parsed JSON template node with context values.
166
+ * - Arrays and objects are traversed recursively.
167
+ * - Exact placeholders (where the entire string value is `"{{path}}"` or
168
+ * `"{{{path}}}"`) are resolved to their original typed value (object, array,
169
+ * number, etc.) instead of being stringified. This is critical for embedding
170
+ * GeoJSON geometry objects in POST bodies.
171
+ * - Partial placeholders and mixed strings use standard Mustache interpolation.
172
+ * @param {any} templateNode
173
+ * @param {Record<string, any>} context
174
+ * @returns {any}
175
+ */
176
+ export function renderTemplateNode(templateNode, context) {
177
+ if (Array.isArray(templateNode)) {
178
+ return templateNode.map((value) => renderTemplateNode(value, context));
179
+ }
180
+ if (templateNode && typeof templateNode === "object") {
181
+ return Object.fromEntries(
182
+ Object.entries(templateNode).map(([key, value]) => [
183
+ mustache.render(key, context),
184
+ renderTemplateNode(value, context),
185
+ ]),
186
+ );
187
+ }
188
+ if (typeof templateNode !== "string") {
189
+ return templateNode;
190
+ }
191
+
192
+ // Detect exact placeholder: entire value is "{{path}}" or "{{{path}}}".
193
+ // Matches both double-brace {{...}} and triple-brace {{{...}}} syntax.
194
+ // When matched, the resolved value is returned as its original type (e.g. an
195
+ // object), rather than being coerced to a string by Mustache.
196
+ const exactPlaceholder = templateNode.match(
197
+ /^\{\{\{?\s*([^{}]+?)\s*\}?\}\}$/,
198
+ );
199
+ if (exactPlaceholder) {
200
+ const resolvedValue = resolveTemplatePath(context, exactPlaceholder[1]);
201
+ if (resolvedValue !== undefined) {
202
+ return cloneTemplateValue(resolvedValue);
203
+ }
204
+ }
205
+
206
+ return mustache.render(templateNode, context);
207
+ }
208
+
209
+ /**
210
+ * Resolves a dot/bracket-notation path against a context object.
211
+ * Supports paths like `"feature.geometry"` or `"list[0].name"`.
212
+ * @param {Record<string, any>} context
213
+ * @param {string} path
214
+ * @returns {any}
215
+ */
216
+ export function resolveTemplatePath(context, path) {
217
+ const segments = path
218
+ .replace(/\[(\d+)\]/g, ".$1")
219
+ .split(".")
220
+ .map((segment) => segment.trim())
221
+ .filter(Boolean);
222
+
223
+ let currentValue = context;
224
+ for (const segment of segments) {
225
+ if (currentValue === null || currentValue === undefined) {
226
+ return undefined;
227
+ }
228
+ currentValue = currentValue[segment];
229
+ }
230
+ return currentValue;
231
+ }
232
+
233
+ /**
234
+ * Deep-clones objects via JSON round-trip to prevent template rendering
235
+ * from mutating the original context values. This also strips non-JSON-
236
+ * serializable properties (e.g. OL Feature methods from drawtools),
237
+ * producing clean plain objects. Primitives pass through.
238
+ * @param {any} value
239
+ * @returns {any}
240
+ */
241
+ export function cloneTemplateValue(value) {
242
+ if (value && typeof value === "object") {
243
+ return JSON.parse(JSON.stringify(value));
244
+ }
245
+ return value;
246
+ }
@@ -500,13 +500,17 @@ export function extractAsyncResults(resultItem) {
500
500
  return extracted;
501
501
  }
502
502
 
503
- /** @param {*} jsonformSchema */
504
- export const getDrawToolsProperty = (jsonformSchema) => {
503
+ /** @param {*} jsonformSchema
504
+ * @returns { string[] }
505
+ */
506
+ export const getDrawToolsProperties = (jsonformSchema) => {
507
+ const properties = [];
505
508
  for (const property in jsonformSchema.properties) {
506
509
  if (jsonformSchema.properties[property]?.options?.drawtools) {
507
- return property;
510
+ properties.push(property);
508
511
  }
509
512
  }
513
+ return properties;
510
514
  };
511
515
 
512
516
  /**
@@ -553,17 +557,3 @@ export const applyProcessLayersToMap = (mapElement, processLayers) => {
553
557
  mapElement.layers = layers;
554
558
  }
555
559
  };
556
- /**
557
- * Updates the jsonform schema to target the compare map
558
- * @param {Record<string,any> | null | undefined} jsonformSchema
559
- */
560
- export function updateJsonformSchemaTarget(jsonformSchema) {
561
- if (!jsonformSchema) {
562
- return jsonformSchema;
563
- }
564
- const stringified = JSON.stringify(jsonformSchema).replaceAll(
565
- "eox-map#main",
566
- "eox-map#compare",
567
- );
568
- return JSON.parse(stringified);
569
- }
@@ -5,6 +5,7 @@ export interface CustomEnpointInput {
5
5
  links: StacLink[];
6
6
  jsonformSchema: Record<string, any>;
7
7
  jsonformValue: Record<string, any>;
8
+ rawJsonformValue?: Record<string, any>;
8
9
  selectedStac: StacCollection;
9
10
  isPolling?: Ref<boolean>;
10
11
  enableCompare?: boolean;