@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,7 +1,10 @@
|
|
|
1
1
|
import { toAbsolute } from "stac-js/src/http.js";
|
|
2
2
|
import axios from "@/plugins/axios";
|
|
3
3
|
import log from "loglevel";
|
|
4
|
+
import mustache from "mustache";
|
|
5
|
+
import { updateVectorLayerStyle } from "@eox/layercontrol";
|
|
4
6
|
import { getStyleVariablesState } from "./triggers.js";
|
|
7
|
+
import { itemsCache, splitItemsCache } from "@/utils/states.js";
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* @param {import("stac-ts").StacLink[]} [links]
|
|
@@ -41,6 +44,26 @@ export function generateFeatures(links, extraProperties = {}, rel = "item") {
|
|
|
41
44
|
return geojsonObject;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Fetches or extracts the raster form configuration for a STAC object.
|
|
49
|
+
* Supports direct JSON objects, data URIs, and URL strings.
|
|
50
|
+
*
|
|
51
|
+
* @param {string|object|undefined} rasterform - The rasterform property from the STAC object.
|
|
52
|
+
* @returns {Promise<import("@/types").EodashRasterJSONForm|undefined>}
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchRasterForm(rasterform) {
|
|
55
|
+
if (!rasterform) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (typeof rasterform === "object") {
|
|
59
|
+
return /** @type {import("@/types").EodashRasterJSONForm} */ (rasterform);
|
|
60
|
+
}
|
|
61
|
+
if (typeof rasterform === "string") {
|
|
62
|
+
return await axios.get(rasterform).then((resp) => resp.data);
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
/**
|
|
45
68
|
* Spearates and extracts layerConfig (jsonform schema & legend) from a style json
|
|
46
69
|
*
|
|
@@ -97,6 +120,39 @@ export function extractLayerConfig(
|
|
|
97
120
|
return { layerConfig, style };
|
|
98
121
|
}
|
|
99
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Recursively extracts URL keys from a JSON Schema.
|
|
125
|
+
* Maps schema property names to their defined `url_key`.
|
|
126
|
+
* @param {Record<string, any> | null | undefined} schema
|
|
127
|
+
* @returns {Record<string, string>}
|
|
128
|
+
*/
|
|
129
|
+
export function extractUrlKeys(schema) {
|
|
130
|
+
/** @type {Record<string, string>} */
|
|
131
|
+
const keys = {};
|
|
132
|
+
if (!schema || typeof schema !== "object") return keys;
|
|
133
|
+
|
|
134
|
+
if (schema.properties) {
|
|
135
|
+
for (const [key, propDef] of Object.entries(schema.properties)) {
|
|
136
|
+
if (propDef && typeof propDef === "object") {
|
|
137
|
+
if (typeof propDef.url_key === "string") {
|
|
138
|
+
keys[key] = propDef.url_key;
|
|
139
|
+
}
|
|
140
|
+
Object.assign(keys, extractUrlKeys(propDef));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const combinator of ["oneOf", "allOf", "anyOf"]) {
|
|
146
|
+
if (Array.isArray(schema[combinator])) {
|
|
147
|
+
for (const sub of schema[combinator]) {
|
|
148
|
+
Object.assign(keys, extractUrlKeys(sub));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return keys;
|
|
154
|
+
}
|
|
155
|
+
|
|
100
156
|
/**
|
|
101
157
|
*
|
|
102
158
|
* @param {number[]} bbox
|
|
@@ -171,7 +227,7 @@ export const extractRoles = (properties, linkOrAsset) => {
|
|
|
171
227
|
|
|
172
228
|
/**
|
|
173
229
|
* Extracts a single non-link style JSON from a STAC Item optionally for a selected key mapping
|
|
174
|
-
* @param { import("stac-ts").StacItem | import("stac-ts").StacCollection } stacObject
|
|
230
|
+
* @param { import("stac-ts").StacItem | import("stac-ts").StacCollection | null | undefined} stacObject
|
|
175
231
|
* @param {string | undefined} linkKey
|
|
176
232
|
* @param {string | undefined} assetKey
|
|
177
233
|
* @returns
|
|
@@ -181,6 +237,7 @@ export const fetchStyle = async (
|
|
|
181
237
|
linkKey = undefined,
|
|
182
238
|
assetKey = undefined,
|
|
183
239
|
) => {
|
|
240
|
+
if (!stacObject) return undefined;
|
|
184
241
|
let styleLink = null;
|
|
185
242
|
if (linkKey) {
|
|
186
243
|
styleLink = stacObject.links.find(
|
|
@@ -212,6 +269,20 @@ export const fetchStyle = async (
|
|
|
212
269
|
}
|
|
213
270
|
};
|
|
214
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Resolves a style by preferring the item's own `style` link and falling back
|
|
274
|
+
* to the collection's. Takes the same key arguments as `fetchStyle`.
|
|
275
|
+
*
|
|
276
|
+
* @param {import("stac-ts").StacItem | import("stac-ts").StacCollection} item
|
|
277
|
+
* @param {import("stac-ts").StacCollection | null | undefined} collection
|
|
278
|
+
* @param {string} [linkKey]
|
|
279
|
+
* @param {string} [assetKey]
|
|
280
|
+
* @returns {Promise<import("@/types").EodashStyleJson | undefined>}
|
|
281
|
+
*/
|
|
282
|
+
export const resolveStyle = async (item, collection, linkKey, assetKey) =>
|
|
283
|
+
(await fetchStyle(item, linkKey, assetKey)) ??
|
|
284
|
+
(await fetchStyle(collection, linkKey, assetKey));
|
|
285
|
+
|
|
215
286
|
/**
|
|
216
287
|
* Fetches all style JSONs from a STAC Item and returns an array with style objects
|
|
217
288
|
* @param {import("stac-ts").StacItem | import("stac-ts").StacCollection} stacObject
|
|
@@ -222,6 +293,7 @@ export const fetchAllStyles = async (stacObject) => {
|
|
|
222
293
|
link.rel.includes("style"),
|
|
223
294
|
);
|
|
224
295
|
const fetchPromises = styleLinks.map(async (link) => {
|
|
296
|
+
/** @type {import("@/types").EodashStyleJson} */
|
|
225
297
|
const styleJson = await axios.get(link.href).then((resp) => resp.data);
|
|
226
298
|
log.debug("fetched styles JSON", JSON.parse(JSON.stringify(styleJson)));
|
|
227
299
|
return styleJson;
|
|
@@ -251,94 +323,74 @@ export const getProjectionCode = (projection) => {
|
|
|
251
323
|
};
|
|
252
324
|
|
|
253
325
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* @param {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// check if items has a datetime value
|
|
264
|
-
const dateProperty = getDatetimeProperty(items);
|
|
265
|
-
|
|
266
|
-
if (!dateProperty) {
|
|
267
|
-
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
268
|
-
}
|
|
269
|
-
/** @type {{date:string;itemId:string}[]} */
|
|
270
|
-
const timeValues = [];
|
|
271
|
-
try {
|
|
272
|
-
/**
|
|
273
|
-
* @param {typeof timeValues} vals
|
|
274
|
-
* @param {import("stac-ts").StacLink} link
|
|
275
|
-
*/
|
|
276
|
-
const reduceLinks = (vals, link) => {
|
|
277
|
-
if (link[dateProperty] && link.rel === "item") {
|
|
278
|
-
vals.push({
|
|
279
|
-
itemId: /** @type {string} */ (link.id),
|
|
280
|
-
date: new Date(
|
|
281
|
-
/** @type {string} */ (link[dateProperty]),
|
|
282
|
-
).toISOString(),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return vals;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
*
|
|
290
|
-
* @param {typeof timeValues} vals
|
|
291
|
-
* @param {import("stac-ts").StacItem} item
|
|
292
|
-
*/
|
|
293
|
-
const reduceItems = (vals, item) => {
|
|
294
|
-
const date = item.properties?.[dateProperty];
|
|
295
|
-
if (date) {
|
|
296
|
-
vals.push({
|
|
297
|
-
itemId: /** @type {string} */ (item.id),
|
|
298
|
-
date: new Date(/** @type {string} */ (date)).toISOString(),
|
|
299
|
-
...item.properties,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
return vals;
|
|
303
|
-
};
|
|
304
|
-
currentStep = new Date(currentStep).toISOString();
|
|
305
|
-
//@ts-expect-error TODO
|
|
306
|
-
items.reduce(isSTACItem(items[0]) ? reduceItems : reduceLinks, timeValues);
|
|
307
|
-
} catch (e) {
|
|
308
|
-
console.warn("[eodash] not supported datetime format was provided", e);
|
|
309
|
-
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
310
|
-
}
|
|
311
|
-
// not enough timeValues
|
|
312
|
-
if (timeValues.length <= 1) {
|
|
326
|
+
* Builds layercontrol LayerDatetime + timeControlValues from a list of dates.
|
|
327
|
+
*
|
|
328
|
+
* @param {Date[] | undefined} dates
|
|
329
|
+
* @param {string | null} [currentStep] - target datetime; snapped to the closest available date
|
|
330
|
+
* @returns {{ layerDatetime: Record<string, any> | undefined, timeControlValues: { date: string }[] | undefined }}
|
|
331
|
+
*/
|
|
332
|
+
export const extractLayerTimeValues = (dates, currentStep) => {
|
|
333
|
+
if (!currentStep || !dates?.length || dates.length <= 1) {
|
|
313
334
|
return { layerDatetime: undefined, timeControlValues: undefined };
|
|
314
335
|
}
|
|
315
336
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
337
|
+
const controlValues = dates.map((d) => d.toISOString()).sort();
|
|
338
|
+
const timeControlValues = controlValues.map((date) => ({ date }));
|
|
339
|
+
|
|
340
|
+
currentStep = new Date(currentStep).toISOString();
|
|
341
|
+
if (!controlValues.includes(currentStep)) {
|
|
342
|
+
const target = new Date(currentStep).getTime();
|
|
343
|
+
currentStep = controlValues.reduce((best, d) =>
|
|
344
|
+
Math.abs(new Date(d).getTime() - target) <
|
|
345
|
+
Math.abs(new Date(best).getTime() - target)
|
|
346
|
+
? d
|
|
347
|
+
: best,
|
|
348
|
+
);
|
|
324
349
|
}
|
|
325
350
|
|
|
326
351
|
const layerDatetime = {
|
|
327
|
-
controlValues
|
|
352
|
+
controlValues,
|
|
328
353
|
currentStep,
|
|
329
354
|
slider: true,
|
|
330
355
|
navigation: true,
|
|
331
356
|
play: false,
|
|
332
357
|
displayFormat: "DD.MM.YYYY HH:mm",
|
|
333
358
|
animateOnClickInterval: false,
|
|
359
|
+
showUTC: true,
|
|
334
360
|
};
|
|
335
361
|
|
|
336
|
-
return {
|
|
337
|
-
layerDatetime,
|
|
338
|
-
timeControlValues: timeValues,
|
|
339
|
-
};
|
|
362
|
+
return { layerDatetime, timeControlValues };
|
|
340
363
|
};
|
|
341
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Fetches the daily pre-aggregation data for a STAC collection if available.
|
|
367
|
+
* Returns the raw AggregationCollection,
|
|
368
|
+
* or null if no daily pre-aggregation link exists, or the fetch fails.
|
|
369
|
+
*
|
|
370
|
+
* @param {import("stac-ts").StacCollection | undefined} stacCollection
|
|
371
|
+
* @param {string} fallbackBaseUrl - base for relative href resolution when no `self` link is present
|
|
372
|
+
* @returns {Promise<any | null>}
|
|
373
|
+
*/
|
|
374
|
+
export async function fetchPreAggregations(stacCollection, fallbackBaseUrl) {
|
|
375
|
+
if (!stacCollection) return null;
|
|
376
|
+
const preAggregationLink = stacCollection.links?.find(
|
|
377
|
+
(l) => l.rel === "pre-aggregation" && l["aggregation:interval"] === "daily",
|
|
378
|
+
);
|
|
379
|
+
if (!preAggregationLink) return null;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const selfLink = stacCollection.links?.find((l) => l.rel === "self")?.href;
|
|
383
|
+
const url = toAbsolute(
|
|
384
|
+
preAggregationLink.href,
|
|
385
|
+
selfLink || fallbackBaseUrl,
|
|
386
|
+
);
|
|
387
|
+
return await axios.get(url).then((resp) => resp.data);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.warn("[eodash] Failed to fetch pre-aggregation", e);
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
342
394
|
/**
|
|
343
395
|
* Recursively find all layers whose ID up to the first ; is same as given layer
|
|
344
396
|
*
|
|
@@ -347,6 +399,9 @@ export const extractLayerTimeValues = (items, currentStep) => {
|
|
|
347
399
|
* @returns {import("@eox/map").EoxLayer[]} Matching layer objects.
|
|
348
400
|
*/
|
|
349
401
|
export const findLayersByLayerPrefix = (layers, referenceLayer) => {
|
|
402
|
+
if (!layers || !referenceLayer) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
350
405
|
const refId = referenceLayer?.properties?.id;
|
|
351
406
|
|
|
352
407
|
if (typeof refId !== "string" || !refId.includes(";:;")) {
|
|
@@ -561,20 +616,150 @@ export function assignProjID(item, linkOrAsset, id, layer) {
|
|
|
561
616
|
*
|
|
562
617
|
* @param {Record<string,any>[]} layers
|
|
563
618
|
*/
|
|
564
|
-
export const removeUnneededProperties = (layers) => {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
619
|
+
export const removeUnneededProperties = (layers, formValues = {}) => {
|
|
620
|
+
/**
|
|
621
|
+
* @param {Record<string,any>} layer
|
|
622
|
+
* @returns {Record<string,any>[]}
|
|
623
|
+
*/
|
|
624
|
+
const processLayer = (layer) => {
|
|
625
|
+
// If the layer (or group) is explicitly marked as not visible, skip it and all children
|
|
626
|
+
if (layer.properties?.visible === false) {
|
|
627
|
+
return [];
|
|
572
628
|
}
|
|
573
|
-
|
|
574
|
-
|
|
629
|
+
|
|
630
|
+
// If it's a Group, we just want its children
|
|
631
|
+
if (layer.type === "Group" && Array.isArray(layer.layers)) {
|
|
632
|
+
return layer.layers.flatMap(processLayer);
|
|
575
633
|
}
|
|
576
|
-
|
|
577
|
-
|
|
634
|
+
|
|
635
|
+
// Break any Vue Proxies/OpenLayers getters by stringifying first
|
|
636
|
+
let clonedLayer;
|
|
637
|
+
try {
|
|
638
|
+
clonedLayer = JSON.parse(JSON.stringify(layer));
|
|
639
|
+
} catch (_e) {
|
|
640
|
+
clonedLayer = structuredClone(layer);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Flatten formValues to handle nested properties (e.g., vminmax: { vmin, vmax })
|
|
644
|
+
/**
|
|
645
|
+
* @param {Record<string,any>} obj
|
|
646
|
+
* @returns {Record<string,any>}
|
|
647
|
+
*/
|
|
648
|
+
const flattenFormValues = (obj) => {
|
|
649
|
+
/** @type {Record<string,any>} */
|
|
650
|
+
let result = {};
|
|
651
|
+
for (const key in obj) {
|
|
652
|
+
if (
|
|
653
|
+
obj[key] !== null &&
|
|
654
|
+
typeof obj[key] === "object" &&
|
|
655
|
+
!Array.isArray(obj[key])
|
|
656
|
+
) {
|
|
657
|
+
Object.assign(result, flattenFormValues(obj[key]));
|
|
658
|
+
} else {
|
|
659
|
+
result[key] = obj[key];
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
};
|
|
664
|
+
const flatFormValues = flattenFormValues(formValues);
|
|
665
|
+
|
|
666
|
+
// Burn in style variables using Mustache if formValues are provided
|
|
667
|
+
if (Object.keys(flatFormValues).length > 0) {
|
|
668
|
+
// Stringify, render mustache, then parse back
|
|
669
|
+
try {
|
|
670
|
+
const renderedString = mustache.render(
|
|
671
|
+
JSON.stringify(clonedLayer),
|
|
672
|
+
flatFormValues,
|
|
673
|
+
);
|
|
674
|
+
clonedLayer = JSON.parse(renderedString);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
log.warn(
|
|
677
|
+
"[eodash] Failed to apply mustache templating during export cleanup:",
|
|
678
|
+
e,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const {
|
|
684
|
+
id,
|
|
685
|
+
title,
|
|
686
|
+
mapboxStyle,
|
|
687
|
+
projection,
|
|
688
|
+
applyOptions,
|
|
689
|
+
layerConfig,
|
|
690
|
+
visible,
|
|
691
|
+
} = clonedLayer.properties || {};
|
|
692
|
+
|
|
693
|
+
// If style was not at root but in properties (layerConfig), move it to root early
|
|
694
|
+
if (!clonedLayer.style && layerConfig?.style) {
|
|
695
|
+
clonedLayer.style = layerConfig.style;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Burn in OpenLayers ["var", "name"] variables using flatFormValues overriding style.variables
|
|
699
|
+
const styleVariables = {
|
|
700
|
+
...(clonedLayer.style?.variables || {}),
|
|
701
|
+
...flatFormValues,
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
if (clonedLayer.style && Object.keys(styleVariables).length > 0) {
|
|
705
|
+
clonedLayer.style = updateVectorLayerStyle({
|
|
706
|
+
...clonedLayer.style,
|
|
707
|
+
variables: styleVariables,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
clonedLayer.properties = {
|
|
712
|
+
id,
|
|
713
|
+
title,
|
|
714
|
+
...(mapboxStyle && { mapboxStyle }),
|
|
715
|
+
...(projection && { projection }),
|
|
716
|
+
...(applyOptions && { applyOptions }),
|
|
717
|
+
...(visible !== undefined && { visible }),
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
if (clonedLayer["interactions"]) {
|
|
721
|
+
delete clonedLayer["interactions"];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Cleanup unnecessary properties
|
|
725
|
+
/**
|
|
726
|
+
* @param {any} obj
|
|
727
|
+
*/
|
|
728
|
+
const cleanupProperties = (obj) => {
|
|
729
|
+
if (!obj || typeof obj !== "object") return;
|
|
730
|
+
|
|
731
|
+
for (const key in obj) {
|
|
732
|
+
if (obj[key] === null) {
|
|
733
|
+
delete obj[key];
|
|
734
|
+
} else if (Array.isArray(obj[key])) {
|
|
735
|
+
if (obj[key].length === 0) {
|
|
736
|
+
// Remove empty arrays like empty interactions
|
|
737
|
+
delete obj[key];
|
|
738
|
+
} else {
|
|
739
|
+
obj[key].forEach(cleanupProperties);
|
|
740
|
+
}
|
|
741
|
+
} else if (typeof obj[key] === "object") {
|
|
742
|
+
cleanupProperties(obj[key]);
|
|
743
|
+
if (Object.keys(obj[key]).length === 0) {
|
|
744
|
+
// we don't delete empty objects for now as it might break schema requirements
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
cleanupProperties(clonedLayer);
|
|
750
|
+
|
|
751
|
+
return [clonedLayer];
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// ensure the top level array is also deeply un-proxied if needed
|
|
755
|
+
let rawLayers = layers;
|
|
756
|
+
try {
|
|
757
|
+
rawLayers = JSON.parse(JSON.stringify(layers));
|
|
758
|
+
} catch (_e) {
|
|
759
|
+
rawLayers = structuredClone(layers);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return rawLayers.flatMap(processLayer);
|
|
578
763
|
};
|
|
579
764
|
|
|
580
765
|
/**
|
|
@@ -734,7 +919,12 @@ export function getDatetimeProperty(linksOrItems) {
|
|
|
734
919
|
}
|
|
735
920
|
|
|
736
921
|
// TODO: consider other properties for datetime ranges
|
|
737
|
-
|
|
922
|
+
|
|
923
|
+
const datetimeProperties = /** @type {const} */ ([
|
|
924
|
+
"datetime",
|
|
925
|
+
"start_datetime",
|
|
926
|
+
"end_datetime",
|
|
927
|
+
]);
|
|
738
928
|
if (checkProperties) {
|
|
739
929
|
for (const prop of datetimeProperties) {
|
|
740
930
|
const propExists = linksOrItems.some(
|
|
@@ -760,6 +950,7 @@ export function getDatetimeProperty(linksOrItems) {
|
|
|
760
950
|
return prop;
|
|
761
951
|
}
|
|
762
952
|
}
|
|
953
|
+
|
|
763
954
|
/**
|
|
764
955
|
*
|
|
765
956
|
* @param {*} stacObject
|
|
@@ -776,6 +967,65 @@ export function isSTACItem(stacObject) {
|
|
|
776
967
|
);
|
|
777
968
|
}
|
|
778
969
|
|
|
970
|
+
/**
|
|
971
|
+
* Fetches items using a split strategy (past/future) around a center date
|
|
972
|
+
* @param {string} itemsUrl
|
|
973
|
+
* @param {string} query
|
|
974
|
+
* @param {string | Date} centerDatetime
|
|
975
|
+
* @param {number} maxNumber
|
|
976
|
+
* @returns {Promise<import("stac-ts").StacItem[]>}
|
|
977
|
+
*/
|
|
978
|
+
export const fetchSplitItems = async (
|
|
979
|
+
itemsUrl,
|
|
980
|
+
query,
|
|
981
|
+
centerDatetime,
|
|
982
|
+
maxNumber,
|
|
983
|
+
) => {
|
|
984
|
+
const center = new Date(centerDatetime).toISOString();
|
|
985
|
+
const limit = Math.ceil(maxNumber / 2);
|
|
986
|
+
|
|
987
|
+
/** @param {"past"|"future"} direction */
|
|
988
|
+
const fetchSide = async (direction) => {
|
|
989
|
+
const isPast = direction === "past";
|
|
990
|
+
const datetimeRange = isPast ? `../${center}` : `${center}/..`;
|
|
991
|
+
const sortOrder = isPast ? "-datetime" : "+datetime";
|
|
992
|
+
const queryParams = new URLSearchParams(query);
|
|
993
|
+
|
|
994
|
+
queryParams.set("limit", limit.toString());
|
|
995
|
+
queryParams.set("datetime", datetimeRange);
|
|
996
|
+
queryParams.set("sortby", sortOrder);
|
|
997
|
+
|
|
998
|
+
const splitUrl = itemsUrl.split("/");
|
|
999
|
+
const collectionId = /** @type {string} */ (splitUrl.at(-2));
|
|
1000
|
+
queryParams.set("collection", collectionId);
|
|
1001
|
+
|
|
1002
|
+
const searchEndpoint = `${splitUrl.slice(0, -3).join("/")}/search`;
|
|
1003
|
+
const items = await axios
|
|
1004
|
+
.get(searchEndpoint, { params: queryParams })
|
|
1005
|
+
.then((res) => res.data.features);
|
|
1006
|
+
return items;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const [pastItems, futureItems] = await Promise.all([
|
|
1010
|
+
fetchSide("past"),
|
|
1011
|
+
fetchSide("future"),
|
|
1012
|
+
]);
|
|
1013
|
+
|
|
1014
|
+
const allItems = [...pastItems, ...futureItems];
|
|
1015
|
+
|
|
1016
|
+
// check for duplicates by ids
|
|
1017
|
+
const seen = new Set();
|
|
1018
|
+
const uniqueItems = [];
|
|
1019
|
+
for (const item of allItems) {
|
|
1020
|
+
if (!seen.has(item.id)) {
|
|
1021
|
+
seen.add(item.id);
|
|
1022
|
+
uniqueItems.push(item);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return uniqueItems;
|
|
1027
|
+
};
|
|
1028
|
+
|
|
779
1029
|
/**
|
|
780
1030
|
* Fetch all STAC items from a STAC API endpoint.
|
|
781
1031
|
* @param {string} itemsUrl
|
|
@@ -783,6 +1033,8 @@ export function isSTACItem(stacObject) {
|
|
|
783
1033
|
* @param {number} [limit=100] - The maximum number of items to fetch per request.
|
|
784
1034
|
* @param {boolean} [returnFirst] - If true, only the first page of results will be returned.
|
|
785
1035
|
* @param {number} [maxNumber=1000] - if the matched number of items exceed this, only the first page will be returned.
|
|
1036
|
+
* @param {string | Date} [centerDatetime] - Date to center the search around if items exceed maxNumber
|
|
1037
|
+
* @returns {Promise<import("stac-ts").StacItem[]>}
|
|
786
1038
|
*/
|
|
787
1039
|
export async function fetchApiItems(
|
|
788
1040
|
itemsUrl,
|
|
@@ -790,23 +1042,42 @@ export async function fetchApiItems(
|
|
|
790
1042
|
limit = 100,
|
|
791
1043
|
returnFirst = false,
|
|
792
1044
|
maxNumber = 1000,
|
|
1045
|
+
centerDatetime,
|
|
793
1046
|
) {
|
|
794
|
-
|
|
795
|
-
|
|
1047
|
+
// Exclude centerDatetime from cache key - it's only used for split search fallback
|
|
1048
|
+
const cacheKey = JSON.stringify({
|
|
1049
|
+
itemsUrl,
|
|
1050
|
+
query,
|
|
1051
|
+
limit,
|
|
1052
|
+
returnFirst,
|
|
1053
|
+
maxNumber,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
if (itemsCache.has(cacheKey)) {
|
|
1057
|
+
return itemsCache.get(cacheKey) ?? [];
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const urlQuery = new URLSearchParams(query);
|
|
1061
|
+
urlQuery.set("limit", limit.toString());
|
|
1062
|
+
|
|
1063
|
+
let finalItemsUrl = itemsUrl.includes("?")
|
|
1064
|
+
? itemsUrl.split("?")[0]
|
|
1065
|
+
: itemsUrl;
|
|
1066
|
+
finalItemsUrl += urlQuery.keys().toArray().length
|
|
1067
|
+
? `?${urlQuery.toString()}`
|
|
1068
|
+
: "";
|
|
796
1069
|
|
|
797
1070
|
const itemsFeatureCollection = await axios
|
|
798
|
-
.get(
|
|
1071
|
+
.get(finalItemsUrl)
|
|
799
1072
|
.then((resp) => resp.data);
|
|
1073
|
+
|
|
800
1074
|
/** @type {import("stac-ts").StacItem[]} */
|
|
801
1075
|
const items = itemsFeatureCollection.features;
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
(link) => link.rel === "next",
|
|
805
|
-
);
|
|
806
|
-
|
|
807
|
-
if (!nextLink || returnFirst) {
|
|
1076
|
+
if (returnFirst) {
|
|
1077
|
+
itemsCache.set(cacheKey, items);
|
|
808
1078
|
return items;
|
|
809
1079
|
}
|
|
1080
|
+
|
|
810
1081
|
/** @type {number} */
|
|
811
1082
|
const matchedItems = itemsFeatureCollection.numberMatched;
|
|
812
1083
|
// Avoid fetching too many items
|
|
@@ -814,26 +1085,70 @@ export async function fetchApiItems(
|
|
|
814
1085
|
console.warn(
|
|
815
1086
|
`[eodash] The number of items matched (${matchedItems}) exceeds the maximum allowed (${maxNumber})`,
|
|
816
1087
|
);
|
|
817
|
-
|
|
818
|
-
|
|
1088
|
+
// we try to narrow down the search around the center datetime
|
|
1089
|
+
if (centerDatetime) {
|
|
1090
|
+
// Check if we have a cached split result that covers this centerDatetime
|
|
1091
|
+
const splitCacheKey = JSON.stringify({ itemsUrl, query, maxNumber });
|
|
1092
|
+
const cachedSplit = splitItemsCache.get(splitCacheKey);
|
|
1093
|
+
const centerTime = new Date(centerDatetime).getTime();
|
|
1094
|
+
|
|
1095
|
+
if (
|
|
1096
|
+
cachedSplit &&
|
|
1097
|
+
centerTime >= cachedSplit.minTime &&
|
|
1098
|
+
centerTime <= cachedSplit.maxTime
|
|
1099
|
+
) {
|
|
1100
|
+
return cachedSplit.items;
|
|
1101
|
+
}
|
|
819
1102
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1103
|
+
const narrowedItems = await fetchSplitItems(
|
|
1104
|
+
itemsUrl,
|
|
1105
|
+
query ?? "",
|
|
1106
|
+
centerDatetime,
|
|
1107
|
+
maxNumber,
|
|
1108
|
+
);
|
|
825
1109
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1110
|
+
if (!narrowedItems.length) {
|
|
1111
|
+
return narrowedItems;
|
|
1112
|
+
}
|
|
1113
|
+
const datetimeProperty = getDatetimeProperty(narrowedItems);
|
|
1114
|
+
if (!datetimeProperty) {
|
|
1115
|
+
return narrowedItems;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const times = narrowedItems
|
|
1119
|
+
.map((i) =>
|
|
1120
|
+
i.properties[datetimeProperty]
|
|
1121
|
+
? new Date(i.properties[datetimeProperty]).getTime()
|
|
1122
|
+
: null,
|
|
1123
|
+
)
|
|
1124
|
+
.filter((t) => t !== null);
|
|
1125
|
+
if (!times.length) {
|
|
1126
|
+
return narrowedItems;
|
|
1127
|
+
}
|
|
1128
|
+
splitItemsCache.set(splitCacheKey, {
|
|
1129
|
+
items: narrowedItems,
|
|
1130
|
+
minTime: Math.min(...times),
|
|
1131
|
+
maxTime: Math.max(...times),
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
return narrowedItems;
|
|
832
1135
|
}
|
|
1136
|
+
|
|
1137
|
+
itemsCache.set(cacheKey, items);
|
|
1138
|
+
return items;
|
|
833
1139
|
}
|
|
834
1140
|
|
|
835
|
-
|
|
836
|
-
|
|
1141
|
+
urlQuery.set("limit", maxNumber.toString());
|
|
1142
|
+
|
|
1143
|
+
const allItems = await axios
|
|
1144
|
+
.get(itemsUrl + "?" + urlQuery.toString())
|
|
1145
|
+
.then((resp) => resp.data.features)
|
|
1146
|
+
.catch((err) => {
|
|
1147
|
+
console.error(err);
|
|
1148
|
+
return [];
|
|
1149
|
+
});
|
|
1150
|
+
items.splice(0, items.length, ...allItems);
|
|
1151
|
+
itemsCache.set(cacheKey, items);
|
|
837
1152
|
return items;
|
|
838
1153
|
}
|
|
839
1154
|
/**
|
|
@@ -871,3 +1186,272 @@ export function extractEoxLegendLink(link) {
|
|
|
871
1186
|
}
|
|
872
1187
|
return extraProperties;
|
|
873
1188
|
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Locate the first sub-schema whose `format` matches by walking `properties`
|
|
1192
|
+
* and the `oneOf` / `allOf` / `anyOf` combinators. Returns the schema path
|
|
1193
|
+
* (array of keys/indices) from the root schema to the matched node, or
|
|
1194
|
+
* undefined if not found. Empty array means the input schema itself matched.
|
|
1195
|
+
*
|
|
1196
|
+
* @param {Record<string, any> | null | undefined} schema
|
|
1197
|
+
* @param {string} [format="bands"]
|
|
1198
|
+
* @returns {(string | number)[] | undefined}
|
|
1199
|
+
*/
|
|
1200
|
+
export function getBandsProperty(schema, format = "bands") {
|
|
1201
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
1202
|
+
if (schema.format === format) return [];
|
|
1203
|
+
|
|
1204
|
+
if (schema.properties) {
|
|
1205
|
+
for (const key of Object.keys(schema.properties)) {
|
|
1206
|
+
const sub = getBandsProperty(schema.properties[key], format);
|
|
1207
|
+
if (sub) return ["properties", key, ...sub];
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
for (const combinator of ["oneOf", "allOf", "anyOf"]) {
|
|
1212
|
+
if (!Array.isArray(schema[combinator])) continue;
|
|
1213
|
+
for (let i = 0; i < schema[combinator].length; i++) {
|
|
1214
|
+
const sub = getBandsProperty(schema[combinator][i], format);
|
|
1215
|
+
if (sub) return [combinator, i, ...sub];
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return undefined;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Checks whether a GeoZarr layer's bands changed in the jsonform output and,
|
|
1224
|
+
* if so, rebuilds the source with the new 3 selected bands.
|
|
1225
|
+
* Gamma and rescale are handled automatically by `applyUpdatedStyles` via
|
|
1226
|
+
* `updateStyleVariables` — this function only manages source reconstruction.
|
|
1227
|
+
* Uses the existing source constructor to avoid import-version mismatches.
|
|
1228
|
+
*
|
|
1229
|
+
* @param {import("ol/layer/Layer").default} olLayer - Layer from layerConfig:change event
|
|
1230
|
+
* @param {Record<string, any>} jsonformValue - Current jsonform output
|
|
1231
|
+
* @returns {boolean} true if the source was rebuilt
|
|
1232
|
+
*/
|
|
1233
|
+
export function updateGeoZarrBands(olLayer, jsonformValue) {
|
|
1234
|
+
/** @type {import("@eox/map/src/layers").EOxLayerType<"WebGLTile","GeoZarr">} */
|
|
1235
|
+
const jsonLayer = olLayer.get("_jsonDefinition");
|
|
1236
|
+
const updatedBands = jsonformValue.bands;
|
|
1237
|
+
const isGeoZarr =
|
|
1238
|
+
jsonLayer?.type === "WebGLTile" && jsonLayer?.source?.type === "GeoZarr";
|
|
1239
|
+
if (!jsonLayer || !jsonLayer.source || !isGeoZarr || !updatedBands) {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const oldBands = jsonLayer.source?.bands;
|
|
1244
|
+
if (JSON.stringify(updatedBands) === JSON.stringify(oldBands)) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
jsonLayer.source.bands = [...updatedBands];
|
|
1248
|
+
olLayer.setSource(
|
|
1249
|
+
new window.eoxMapAdvancedOlSources.GeoZarr(jsonLayer.source),
|
|
1250
|
+
);
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Safely appends query parameters to a URL string, preserving templates like {z}/{x}/{y}
|
|
1256
|
+
* @param {string} url
|
|
1257
|
+
* @param {Record<string, string>} params
|
|
1258
|
+
* @returns {string}
|
|
1259
|
+
*/
|
|
1260
|
+
function appendQueryParams(url, params) {
|
|
1261
|
+
const [base, query] = url.split("?");
|
|
1262
|
+
const searchParams = new URLSearchParams(query || "");
|
|
1263
|
+
|
|
1264
|
+
for (const [key, val] of Object.entries(params)) {
|
|
1265
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
1266
|
+
searchParams.set(key, val);
|
|
1267
|
+
} else {
|
|
1268
|
+
searchParams.delete(key);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const newQuery = searchParams.toString();
|
|
1273
|
+
return newQuery ? `${base}?${newQuery}` : base;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Checks whether a VectorTile layer's URL needs to be updated based on jsonform output.
|
|
1278
|
+
* If the style's jsonform schema has properties with `url_key` defined, their values
|
|
1279
|
+
* are injected as query parameters into the source URL.
|
|
1280
|
+
*
|
|
1281
|
+
* @param {import("ol/layer/Layer").default} olLayer - Layer from layerConfig:change event
|
|
1282
|
+
* @param {Record<string, any>} jsonformValue - Current jsonform output
|
|
1283
|
+
* @returns {boolean} true if the URL was updated
|
|
1284
|
+
*/
|
|
1285
|
+
export function updateLayerUrl(olLayer, jsonformValue) {
|
|
1286
|
+
const jsonLayer = olLayer.get("_jsonDefinition");
|
|
1287
|
+
if (!jsonLayer || jsonLayer.type !== "VectorTile") {
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const schema = jsonLayer.properties?.layerConfig?.schema;
|
|
1292
|
+
const urlKeys = extractUrlKeys(schema);
|
|
1293
|
+
|
|
1294
|
+
if (Object.keys(urlKeys).length === 0) {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
let originalUrl = olLayer.get("originalUrl") || jsonLayer.source?.url;
|
|
1299
|
+
|
|
1300
|
+
if (!originalUrl || typeof originalUrl !== "string") {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (!olLayer.get("originalUrl")) {
|
|
1305
|
+
olLayer.set("originalUrl", originalUrl);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** @type {Record<string, string>} */
|
|
1309
|
+
const queryParamsToInject = {};
|
|
1310
|
+
for (const [propName, urlKey] of Object.entries(urlKeys)) {
|
|
1311
|
+
queryParamsToInject[urlKey] = jsonformValue[propName];
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const newUrl = appendQueryParams(originalUrl, queryParamsToInject);
|
|
1315
|
+
|
|
1316
|
+
if (jsonLayer.source?.url) {
|
|
1317
|
+
if (jsonLayer.source.url === newUrl) {
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
jsonLayer.source.url = newUrl;
|
|
1321
|
+
if (olLayer.get("injectedUrl") === newUrl) {
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
const source = olLayer.getSource();
|
|
1325
|
+
olLayer.set("injectedUrl", newUrl);
|
|
1326
|
+
if (source && "setUrl" in source) {
|
|
1327
|
+
/** @type {any} */ (source).setUrl(newUrl);
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
if (source && "setUrls" in source) {
|
|
1331
|
+
/** @type {any} */ (source).setUrls([newUrl]);
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Applies titiler upscaling to an XYZ tile URL based on the matched endpoint config.
|
|
1341
|
+
* - titiler v1: appends `@2x` to the `{y}` tile coordinate
|
|
1342
|
+
* - titiler v2: adds `tilesize=512` query parameter (v2 removed the `@2x` suffix)
|
|
1343
|
+
* Plain strings in the config default to v1 behavior for backward compatibility.
|
|
1344
|
+
*
|
|
1345
|
+
* @param {string} url - The XYZ tile URL template
|
|
1346
|
+
* @param {Array<string | { url: string; titilerVersion?: 1 | 2 }>} upscalingEndpoints
|
|
1347
|
+
* @returns {{ url: string; tileSize: [number, number] } | null} null if no endpoint matches
|
|
1348
|
+
*/
|
|
1349
|
+
export function applyTitilerUpscaling(url, upscalingEndpoints) {
|
|
1350
|
+
const match = upscalingEndpoints.find((entry) => {
|
|
1351
|
+
const endpointUrl = typeof entry === "string" ? entry : entry.url;
|
|
1352
|
+
return url.includes(endpointUrl);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
if (!match) {
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const version = typeof match === "string" ? 1 : (match.titilerVersion ?? 1);
|
|
1360
|
+
|
|
1361
|
+
if (version === 2) {
|
|
1362
|
+
const [base, query] = url.split("?");
|
|
1363
|
+
const params = new URLSearchParams(query);
|
|
1364
|
+
params.set("tilesize", "512");
|
|
1365
|
+
return { url: `${base}?${params.toString()}`, tileSize: [512, 512] };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return { url: url.replace("{y}", "{y}@2x"), tileSize: [512, 512] };
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Picks the render presets for a collection, preferring client-provided config
|
|
1373
|
+
* (`options.renders[collectionId]`) and falling back to the collection's own STAC
|
|
1374
|
+
* `renders` extension when no config entry exists.
|
|
1375
|
+
* @param {import("stac-ts").StacCollection | null | undefined} collection
|
|
1376
|
+
* @param {Record<string, Record<string, import("@/types").Render>> | undefined} [configRenders]
|
|
1377
|
+
* @returns {Record<string, import("@/types").Render> | undefined}
|
|
1378
|
+
*/
|
|
1379
|
+
export function resolveRenders(collection, configRenders) {
|
|
1380
|
+
const config = collection?.id ? configRenders?.[collection.id] : undefined;
|
|
1381
|
+
if (config) return config;
|
|
1382
|
+
return /** @type {Record<string, import("@/types").Render>|undefined} */ (
|
|
1383
|
+
collection?.renders ?? undefined
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* TiTiler expects rescale as [min,max] pairs; chunks a flat numeric list
|
|
1389
|
+
* into pairs (e.g. [0,0.4,0,0.1] -> [[0,0.4],[0,0.1]]). Nested input passes through.
|
|
1390
|
+
* @param {number[]|number[][]|undefined} rescale - flat or nested rescale values
|
|
1391
|
+
* @returns {number[][]|undefined} rescale as [min,max] pairs
|
|
1392
|
+
*/
|
|
1393
|
+
export function normalizeRescale(rescale) {
|
|
1394
|
+
if (!rescale?.length || Array.isArray(rescale[0])) {
|
|
1395
|
+
return /** @type {number[][]|undefined} */ (rescale);
|
|
1396
|
+
}
|
|
1397
|
+
const pairs = [];
|
|
1398
|
+
for (let i = 0; i < rescale.length; i += 2) {
|
|
1399
|
+
pairs.push(/** @type {number[]} */ (rescale).slice(i, i + 2));
|
|
1400
|
+
}
|
|
1401
|
+
return pairs;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Drops NaN nodata values; NaN is already the implicit fill for float data,
|
|
1406
|
+
* so forwarding `nodata=nan` to TiTiler is redundant.
|
|
1407
|
+
* @param {string|number|undefined} nodata - nodata value from render/asset metadata
|
|
1408
|
+
* @returns {string|number|undefined} nodata, or undefined when it is NaN
|
|
1409
|
+
*/
|
|
1410
|
+
export function normalizeNodata(nodata) {
|
|
1411
|
+
if (typeof nodata === "number" && Number.isNaN(nodata)) return undefined;
|
|
1412
|
+
if (typeof nodata === "string" && nodata.trim().toLowerCase() === "nan")
|
|
1413
|
+
return undefined;
|
|
1414
|
+
return nodata;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Serializes an object into a TiTiler query string. Arrays repeat the key per
|
|
1419
|
+
* element (TiTiler list params, e.g. `assets=a&assets=b`); nested elements
|
|
1420
|
+
* comma-join (`rescale: [[0,1],[0,2]]` -> `rescale=0,1&rescale=0,2`); objects
|
|
1421
|
+
* are JSON-encoded. Shared by the render-extension and mosaic paths.
|
|
1422
|
+
* @param {Record<string,any>} obj
|
|
1423
|
+
* @returns {string}
|
|
1424
|
+
*/
|
|
1425
|
+
export function encodeURLObject(obj) {
|
|
1426
|
+
let str = "";
|
|
1427
|
+
for (const key in obj) {
|
|
1428
|
+
const value = obj[key];
|
|
1429
|
+
if (value === null || value === undefined || value === "") {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const valueType = Array.isArray(value) ? "array" : typeof value;
|
|
1434
|
+
|
|
1435
|
+
switch (valueType) {
|
|
1436
|
+
case "array": {
|
|
1437
|
+
for (const val of value) {
|
|
1438
|
+
if (Array.isArray(val)) {
|
|
1439
|
+
str += `${key}=${val.join(",")}&`;
|
|
1440
|
+
} else {
|
|
1441
|
+
str += `${key}=${encodeURIComponent(val)}&`;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
case "object": {
|
|
1447
|
+
str += `${key}=${encodeURI(JSON.stringify(value))}&`;
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
default: {
|
|
1451
|
+
str += `${key}=${encodeURIComponent(value)}&`;
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return str;
|
|
1457
|
+
}
|