@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.
Files changed (119) hide show
  1. package/core/client/components/EodashOverlay.vue +27 -4
  2. package/core/client/composables/DefineEodash.js +0 -1
  3. package/core/client/composables/DefineWidgets.js +3 -1
  4. package/core/client/eodashSTAC/EodashCollection.js +87 -24
  5. package/core/client/eodashSTAC/cql.js +64 -0
  6. package/core/client/eodashSTAC/createLayers.js +353 -187
  7. package/core/client/eodashSTAC/helpers.js +694 -110
  8. package/core/client/eodashSTAC/mosaic.js +422 -0
  9. package/core/client/plugins/axios.js +31 -1
  10. package/core/client/store/actions.js +1 -1
  11. package/core/client/store/stac.js +54 -4
  12. package/core/client/store/states.js +20 -2
  13. package/core/client/types.ts +152 -28
  14. package/core/client/utils/bands-editor/exampleSchema.json +50 -50
  15. package/core/client/utils/states.js +10 -0
  16. package/dist/client/{DashboardLayout-kRsmsNW5.js → DashboardLayout-DEs4f_Vy.js} +2 -2
  17. package/dist/client/{DynamicWebComponent-CnKhYo0K.js → DynamicWebComponent-PvnV88BA.js} +1 -1
  18. package/dist/client/EodashChart-D-3AGKZY.js +260 -0
  19. package/dist/client/{EodashDatePicker-CMPDT-ce.js → EodashDatePicker-Dm_goZzs.js} +3 -3
  20. package/dist/client/{EodashItemFilter-BpdA8QQa.js → EodashItemFilter-CJy3zYzD.js} +2 -1
  21. package/dist/client/{EodashLayerControl-BHpDA4_0.js → EodashLayerControl-rL8UnpkA.js} +55 -33
  22. package/dist/client/{EodashLayoutSwitcher-B1fRUt7G.js → EodashLayoutSwitcher-ZRlESI9O.js} +3 -3
  23. package/dist/client/{EodashMapBtns-Cpned0tc.js → EodashMapBtns-CG7u2Aq8.js} +227 -112
  24. package/dist/client/EodashStacInfo-CJhhxthD.js +169 -0
  25. package/dist/client/{EodashTools-1zHz26gv.js → EodashTools-CWyK8QJT.js} +4 -4
  26. package/dist/client/{ExportState-DbOm8hBI.js → ExportState-BXhy5TOU.js} +20 -174
  27. package/dist/client/{Footer-BKg-VVkj.js → Footer-DZPvnhcT.js} +1 -1
  28. package/dist/client/{Header-DCrwCvmY.js → Header-CiGsDE_a.js} +2 -2
  29. package/dist/client/{MobileLayout-vWSHpluo.js → MobileLayout-BJHW7x5V.js} +2 -2
  30. package/dist/client/{PopUp-9uimfK49.js → PopUp-7_XnhvCc.js} +4 -168
  31. package/dist/client/{ProcessList-CTTIZa06.js → ProcessList-LL_UCbvc.js} +5 -5
  32. package/dist/client/VDivider-DWYM0WUr.js +168 -0
  33. package/dist/client/{VImg-CrmB76ZY.js → VImg-B75dgw2K.js} +2 -2
  34. package/dist/client/{VMain-OKZNpcEG.js → VMain-DvMmOL4f.js} +1 -1
  35. package/dist/client/{VTooltip-CJw2FyWk.js → VTooltip-ClWHUmWL.js} +2 -2
  36. package/dist/client/{WidgetsContainer-BaWOnE5R.js → WidgetsContainer-BqaWs4ch.js} +1 -1
  37. package/dist/client/{asWebComponent-BHWpMuE3.js → asWebComponent-IZanwL-A.js} +677 -379
  38. package/dist/client/{async-DSKj6g1f.js → async-CiNIOBVz.js} +31 -29
  39. package/dist/client/{create-layers-config-CBoYedqB.js → create-layers-config-3S3y_kZg.js} +17 -17
  40. package/dist/client/dialog-transition-c4ZSnzac.js +170 -0
  41. package/dist/client/eo-dash.js +1 -1
  42. package/dist/client/{forwardRefs-DH51zPUr.js → forwardRefs-B-c1_6yo.js} +8 -87
  43. package/dist/client/{handling-BrcDDlgS.js → handling-NvY8Csry.js} +3 -4
  44. package/dist/client/{helpers-DjZVl_WF.js → helpers-BFuh19CP.js} +1094 -194
  45. package/dist/client/{index-1JKE68n3.js → index-1wMAyuNL.js} +219 -67
  46. package/dist/client/{index-CSG_JoOA.js → index-B8jGJSTA.js} +62 -56
  47. package/dist/client/{index-Cu3-13Xg.js → index-BKQQJkhy.js} +167 -61
  48. package/dist/client/index-D0jUyol8.js +3167 -0
  49. package/dist/client/{index-CUjl-ZsI.js → index-KGxz5Egv.js} +2 -2
  50. package/dist/client/mosaic-vHvSuih6.js +459 -0
  51. package/dist/client/templates.js +251 -121
  52. package/dist/client/{transition-OHcz5Qq1.js → transition-BkyUsZh_.js} +1 -1
  53. package/dist/node/cli.js +5 -5
  54. package/dist/node/types.d.ts +4 -0
  55. package/dist/types/core/client/composables/DefineEodash.d.ts +1 -1
  56. package/dist/types/core/client/eodashSTAC/EodashCollection.d.ts +57 -25
  57. package/dist/types/core/client/eodashSTAC/cql.d.ts +15 -0
  58. package/dist/types/core/client/eodashSTAC/createLayers.d.ts +42 -23
  59. package/dist/types/core/client/eodashSTAC/helpers.d.ts +118 -19
  60. package/dist/types/core/client/eodashSTAC/mosaic.d.ts +56 -0
  61. package/dist/types/core/client/store/stac.d.ts +38 -8
  62. package/dist/types/core/client/store/states.d.ts +24 -2
  63. package/dist/types/core/client/types.d.ts +128 -24
  64. package/dist/types/core/client/utils/states.d.ts +12 -0
  65. package/dist/types/templates/baseConfig.d.ts +1 -100
  66. package/dist/types/templates/compare.d.ts +32 -3
  67. package/dist/types/templates/expert.d.ts +18 -0
  68. package/dist/types/templates/explore.d.ts +10 -1
  69. package/dist/types/templates/lite.d.ts +11 -0
  70. package/dist/types/widgets/EodashChart.vue.d.ts +5 -3
  71. package/dist/types/widgets/EodashItemCatalog/index.vue.d.ts +129 -2
  72. package/dist/types/widgets/EodashItemCatalog/methods/filters.d.ts +28 -6
  73. package/dist/types/widgets/EodashItemCatalog/methods/handlers.d.ts +17 -4
  74. package/dist/types/widgets/EodashItemCatalog/methods/map.d.ts +16 -4
  75. package/dist/types/widgets/EodashLayerControl.vue.d.ts +35 -16
  76. package/dist/types/widgets/EodashMap/EodashMapBtns.vue.d.ts +14 -10
  77. package/dist/types/widgets/EodashMap/index.vue.d.ts +86 -37
  78. package/dist/types/widgets/EodashMap/methods/create-layers-config.d.ts +1 -1
  79. package/dist/types/widgets/EodashMap/methods/index.d.ts +5 -1
  80. package/dist/types/widgets/EodashMap/methods/use-map-loading.d.ts +1 -0
  81. package/dist/types/widgets/EodashProcess/index.vue.d.ts +5 -3
  82. package/dist/types/widgets/EodashProcess/methods/custom-endpoints/layers/eoxhub-workspaces-endpoint.d.ts +1 -1
  83. package/dist/types/widgets/EodashProcess/states.d.ts +1 -1
  84. package/dist/types/widgets/EodashStacInfo.vue.d.ts +20 -0
  85. package/dist/types/widgets/EodashTimeSlider/index.vue.d.ts +20 -0
  86. package/dist/types/widgets/EodashTimeSlider/methods/index.d.ts +3 -8
  87. package/dist/types/widgets/EodashTimeSlider/types.d.ts +6 -16
  88. package/dist/types/widgets/EodashTools.vue.d.ts +5 -5
  89. package/package.json +34 -34
  90. package/templates/baseConfig.js +37 -6
  91. package/templates/compare.js +61 -7
  92. package/templates/expert.js +24 -2
  93. package/templates/explore.js +12 -5
  94. package/templates/lite.js +13 -0
  95. package/widgets/EodashChart.vue +162 -21
  96. package/widgets/EodashItemCatalog/index.vue +363 -35
  97. package/widgets/EodashItemCatalog/methods/filters.js +134 -70
  98. package/widgets/EodashItemCatalog/methods/handlers.js +113 -19
  99. package/widgets/EodashItemCatalog/methods/map.js +141 -28
  100. package/widgets/EodashLayerControl.vue +51 -25
  101. package/widgets/EodashMap/EodashMapBtns.vue +109 -48
  102. package/widgets/EodashMap/index.vue +91 -105
  103. package/widgets/EodashMap/methods/btns.js +64 -31
  104. package/widgets/EodashMap/methods/create-layers-config.js +16 -16
  105. package/widgets/EodashMap/methods/index.js +71 -8
  106. package/widgets/EodashMap/methods/use-map-loading.js +58 -0
  107. package/widgets/EodashProcess/index.vue +136 -16
  108. package/widgets/EodashProcess/methods/composables.js +13 -0
  109. package/widgets/EodashProcess/methods/outputs.js +0 -1
  110. package/widgets/EodashProcess/methods/utils.js +29 -27
  111. package/widgets/EodashProcess/states.js +1 -1
  112. package/widgets/EodashStacInfo.vue +64 -2
  113. package/widgets/EodashTimeSlider/index.vue +58 -2
  114. package/widgets/EodashTimeSlider/methods/index.js +5 -48
  115. package/widgets/EodashTimeSlider/types.ts +7 -17
  116. package/widgets/ExportState.vue +13 -4
  117. package/dist/client/EodashChart-CLDqE93C.js +0 -144
  118. package/dist/client/EodashStacInfo-BQspAHKH.js +0 -107
  119. 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 { setupCache } from 'axios-cache-interceptor';
3
3
  import log from 'loglevel';
4
- import { ref, shallowRef } from 'vue';
4
+ import { ref, shallowRef, reactive, shallowReactive } from 'vue';
5
+ import mustache from 'mustache';
6
+ import { updateVectorLayerStyle } from '@eox/layercontrol';
7
+ import { mdiWrench, mdiSnowflake, mdiVolleyball, mdiHiking, mdiLightningBolt, mdiLeaf, mdiAccountMultiple, mdiCurrencyEur, mdiWeatherWindy, mdiSetCenter, mdiHospitalBoxOutline, mdiImageFilterHdr, mdiWater, mdiBarley } from '@mdi/js';
5
8
 
6
9
  /**
7
10
  * Create a bound version of a function with a specified `this` context
@@ -426,7 +429,9 @@ function merge(...objs) {
426
429
  return;
427
430
  }
428
431
 
429
- const targetKey = (caseless && findKey(result, key)) || key;
432
+ // findKey lowercases the key, so caseless lookup only applies to strings —
433
+ // symbol keys are identity-matched.
434
+ const targetKey = (caseless && typeof key === 'string' && findKey(result, key)) || key;
430
435
  // Read via own-prop only — a bare `result[targetKey]` walks the prototype
431
436
  // chain, so a polluted Object.prototype value could surface here and get
432
437
  // copied into the merged result.
@@ -443,7 +448,24 @@ function merge(...objs) {
443
448
  };
444
449
 
445
450
  for (let i = 0, l = objs.length; i < l; i++) {
446
- objs[i] && forEach(objs[i], assignValue);
451
+ const source = objs[i];
452
+ if (!source || isBuffer(source)) {
453
+ continue;
454
+ }
455
+
456
+ forEach(source, assignValue);
457
+
458
+ if (typeof source !== 'object' || isArray(source)) {
459
+ continue;
460
+ }
461
+
462
+ const symbols = Object.getOwnPropertySymbols(source);
463
+ for (let j = 0; j < symbols.length; j++) {
464
+ const symbol = symbols[j];
465
+ if (propertyIsEnumerable.call(source, symbol)) {
466
+ assignValue(source[symbol], symbol);
467
+ }
468
+ }
447
469
  }
448
470
  return result;
449
471
  }
@@ -672,6 +694,8 @@ const hasOwnProperty = (
672
694
  hasOwnProperty.call(obj, prop)
673
695
  )(Object.prototype);
674
696
 
697
+ const { propertyIsEnumerable } = Object.prototype;
698
+
675
699
  /**
676
700
  * Determine if a value is a RegExp object
677
701
  *
@@ -777,11 +801,11 @@ function isSpecCompliantForm(thing) {
777
801
  * @returns {Object} The JSON-compatible object.
778
802
  */
779
803
  const toJSONObject = (obj) => {
780
- const stack = new Array(10);
804
+ const visited = new WeakSet();
781
805
 
782
- const visit = (source, i) => {
806
+ const visit = (source) => {
783
807
  if (isObject(source)) {
784
- if (stack.indexOf(source) >= 0) {
808
+ if (visited.has(source)) {
785
809
  return;
786
810
  }
787
811
 
@@ -791,15 +815,16 @@ const toJSONObject = (obj) => {
791
815
  }
792
816
 
793
817
  if (!('toJSON' in source)) {
794
- stack[i] = source;
818
+ // add-on descent / delete-on-ascent: preserves path semantics, so DAG nodes serialise at every occurrence (see #7230).
819
+ visited.add(source);
795
820
  const target = isArray(source) ? [] : {};
796
821
 
797
822
  forEach(source, (value, key) => {
798
- const reducedValue = visit(value, i + 1);
823
+ const reducedValue = visit(value);
799
824
  !isUndefined(reducedValue) && (target[key] = reducedValue);
800
825
  });
801
826
 
802
- stack[i] = undefined;
827
+ visited.delete(source);
803
828
 
804
829
  return target;
805
830
  }
@@ -808,7 +833,7 @@ const toJSONObject = (obj) => {
808
833
  return source;
809
834
  };
810
835
 
811
- return visit(obj, 0);
836
+ return visit(obj);
812
837
  };
813
838
 
814
839
  /**
@@ -1010,10 +1035,6 @@ const parseHeaders = (rawHeaders) => {
1010
1035
  return parsed;
1011
1036
  };
1012
1037
 
1013
- const $internals = Symbol('internals');
1014
-
1015
- const INVALID_HEADER_VALUE_CHARS_RE = /[^\x09\x20-\x7E\x80-\xFF]/g;
1016
-
1017
1038
  function trimSPorHTAB(str) {
1018
1039
  let start = 0;
1019
1040
  let end = str.length;
@@ -1041,12 +1062,40 @@ function trimSPorHTAB(str) {
1041
1062
  return start === 0 && end === str.length ? str : str.slice(start, end);
1042
1063
  }
1043
1064
 
1044
- function normalizeHeader(header) {
1045
- return header && String(header).trim().toLowerCase();
1065
+ // The control-code ranges are intentional: header sanitization strips C0/DEL bytes.
1066
+ // eslint-disable-next-line no-control-regex
1067
+ const INVALID_UNICODE_HEADER_VALUE_CHARS = new RegExp('[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+', 'g');
1068
+ // eslint-disable-next-line no-control-regex
1069
+ const INVALID_BYTE_STRING_HEADER_VALUE_CHARS = new RegExp('[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+', 'g');
1070
+
1071
+ function sanitizeValue(value, invalidChars) {
1072
+ if (utils$1.isArray(value)) {
1073
+ return value.map((item) => sanitizeValue(item, invalidChars));
1074
+ }
1075
+
1076
+ return trimSPorHTAB(String(value).replace(invalidChars, ''));
1046
1077
  }
1047
1078
 
1048
- function sanitizeHeaderValue(str) {
1049
- return trimSPorHTAB(str.replace(INVALID_HEADER_VALUE_CHARS_RE, ''));
1079
+ const sanitizeHeaderValue = (value) =>
1080
+ sanitizeValue(value, INVALID_UNICODE_HEADER_VALUE_CHARS);
1081
+
1082
+ const sanitizeByteStringHeaderValue = (value) =>
1083
+ sanitizeValue(value, INVALID_BYTE_STRING_HEADER_VALUE_CHARS);
1084
+
1085
+ function toByteStringHeaderObject(headers) {
1086
+ const byteStringHeaders = Object.create(null);
1087
+
1088
+ utils$1.forEach(headers.toJSON(), (value, header) => {
1089
+ byteStringHeaders[header] = sanitizeByteStringHeaderValue(value);
1090
+ });
1091
+
1092
+ return byteStringHeaders;
1093
+ }
1094
+
1095
+ const $internals = Symbol('internals');
1096
+
1097
+ function normalizeHeader(header) {
1098
+ return header && String(header).trim().toLowerCase();
1050
1099
  }
1051
1100
 
1052
1101
  function normalizeValue(value) {
@@ -1128,7 +1177,7 @@ class AxiosHeaders {
1128
1177
  const lHeader = normalizeHeader(_header);
1129
1178
 
1130
1179
  if (!lHeader) {
1131
- throw new Error('header name must be a non-empty string');
1180
+ return;
1132
1181
  }
1133
1182
 
1134
1183
  const key = utils$1.findKey(self, lHeader);
@@ -1156,7 +1205,7 @@ class AxiosHeaders {
1156
1205
  key;
1157
1206
  for (const entry of header) {
1158
1207
  if (!utils$1.isArray(entry)) {
1159
- throw TypeError('Object iterator must return a key-value pair');
1208
+ throw new TypeError('Object iterator must return a key-value pair');
1160
1209
  }
1161
1210
 
1162
1211
  obj[(key = entry[0])] = (dest = obj[key])
@@ -1771,7 +1820,7 @@ function toFormData(obj, formData, options) {
1771
1820
  }
1772
1821
 
1773
1822
  if (stack.indexOf(value) !== -1) {
1774
- throw Error('Circular reference detected in ' + path.join('.'));
1823
+ throw new Error('Circular reference detected in ' + path.join('.'));
1775
1824
  }
1776
1825
 
1777
1826
  stack.push(value);
@@ -1988,6 +2037,7 @@ const transitionalDefaults = {
1988
2037
  forcedJSONParsing: true,
1989
2038
  clarifyTimeoutError: false,
1990
2039
  legacyInterceptorReqResOrdering: true,
2040
+ advertiseZstdAcceptEncoding: false,
1991
2041
  };
1992
2042
 
1993
2043
  const URLSearchParams$1 = typeof URLSearchParams !== 'undefined' ? URLSearchParams : AxiosURLSearchParams;
@@ -2145,7 +2195,7 @@ function formDataToJSON(formData) {
2145
2195
  return !isNumericKey;
2146
2196
  }
2147
2197
 
2148
- if (!target[name] || !utils$1.isObject(target[name])) {
2198
+ if (!utils$1.hasOwnProp(target, name) || !utils$1.isObject(target[name])) {
2149
2199
  target[name] = [];
2150
2200
  }
2151
2201
 
@@ -2510,6 +2560,9 @@ const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
2510
2560
  const _speedometer = speedometer(50, 250);
2511
2561
 
2512
2562
  return throttle((e) => {
2563
+ if (!e || typeof e.loaded !== 'number') {
2564
+ return;
2565
+ }
2513
2566
  const rawLoaded = e.loaded;
2514
2567
  const total = e.lengthComputable ? e.total : undefined;
2515
2568
  const loaded = total != null ? Math.min(rawLoaded, total) : rawLoaded;
@@ -2819,12 +2872,12 @@ function setFormDataHeaders(headers, formHeaders, policy) {
2819
2872
  *
2820
2873
  * @returns {string} UTF-8 bytes as a Latin-1 string
2821
2874
  */
2822
- const encodeUTF8 = (str) =>
2875
+ const encodeUTF8$1 = (str) =>
2823
2876
  encodeURIComponent(str).replace(/%([0-9A-F]{2})/gi, (_, hex) =>
2824
2877
  String.fromCharCode(parseInt(hex, 16))
2825
2878
  );
2826
2879
 
2827
- const resolveConfig = (config) => {
2880
+ function resolveConfig(config) {
2828
2881
  const newConfig = mergeConfig({}, config);
2829
2882
 
2830
2883
  // Read only own properties to prevent prototype pollution gadgets
@@ -2845,8 +2898,8 @@ const resolveConfig = (config) => {
2845
2898
 
2846
2899
  newConfig.url = buildURL(
2847
2900
  buildFullPath(baseURL, url, allowAbsoluteUrls),
2848
- config.params,
2849
- config.paramsSerializer
2901
+ own('params'),
2902
+ own('paramsSerializer')
2850
2903
  );
2851
2904
 
2852
2905
  // HTTP basic authentication
@@ -2854,13 +2907,17 @@ const resolveConfig = (config) => {
2854
2907
  headers.set(
2855
2908
  'Authorization',
2856
2909
  'Basic ' +
2857
- btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
2910
+ btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8$1(auth.password) : ''))
2858
2911
  );
2859
2912
  }
2860
2913
 
2861
2914
  if (utils$1.isFormData(data)) {
2862
- if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
2863
- headers.setContentType(undefined); // browser handles it
2915
+ if (
2916
+ platform.hasStandardBrowserEnv ||
2917
+ platform.hasStandardBrowserWebWorkerEnv ||
2918
+ utils$1.isReactNative(data)
2919
+ ) {
2920
+ headers.setContentType(undefined); // browser/web worker/RN handles it
2864
2921
  } else if (utils$1.isFunction(data.getHeaders)) {
2865
2922
  // Node.js FormData (like form-data package)
2866
2923
  setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy'));
@@ -2892,7 +2949,7 @@ const resolveConfig = (config) => {
2892
2949
  }
2893
2950
 
2894
2951
  return newConfig;
2895
- };
2952
+ }
2896
2953
 
2897
2954
  const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
2898
2955
 
@@ -3041,7 +3098,7 @@ const xhrAdapter = isXHRAdapterSupported &&
3041
3098
 
3042
3099
  // Add headers to the request
3043
3100
  if ('setRequestHeader' in request) {
3044
- utils$1.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
3101
+ utils$1.forEach(toByteStringHeaderObject(requestHeaders), function setRequestHeader(val, key) {
3045
3102
  request.setRequestHeader(key, val);
3046
3103
  });
3047
3104
  }
@@ -3111,54 +3168,55 @@ const xhrAdapter = isXHRAdapterSupported &&
3111
3168
  };
3112
3169
 
3113
3170
  const composeSignals = (signals, timeout) => {
3114
- const { length } = (signals = signals ? signals.filter(Boolean) : []);
3115
-
3116
- if (timeout || length) {
3117
- let controller = new AbortController();
3118
-
3119
- let aborted;
3120
-
3121
- const onabort = function (reason) {
3122
- if (!aborted) {
3123
- aborted = true;
3124
- unsubscribe();
3125
- const err = reason instanceof Error ? reason : this.reason;
3126
- controller.abort(
3127
- err instanceof AxiosError
3128
- ? err
3129
- : new CanceledError(err instanceof Error ? err.message : err)
3130
- );
3131
- }
3132
- };
3171
+ signals = signals ? signals.filter(Boolean) : [];
3133
3172
 
3134
- let timer =
3135
- timeout &&
3136
- setTimeout(() => {
3137
- timer = null;
3138
- onabort(new AxiosError(`timeout of ${timeout}ms exceeded`, AxiosError.ETIMEDOUT));
3139
- }, timeout);
3140
-
3141
- const unsubscribe = () => {
3142
- if (signals) {
3143
- timer && clearTimeout(timer);
3144
- timer = null;
3145
- signals.forEach((signal) => {
3146
- signal.unsubscribe
3147
- ? signal.unsubscribe(onabort)
3148
- : signal.removeEventListener('abort', onabort);
3149
- });
3150
- signals = null;
3151
- }
3152
- };
3173
+ if (!timeout && !signals.length) {
3174
+ return;
3175
+ }
3153
3176
 
3154
- signals.forEach((signal) => signal.addEventListener('abort', onabort));
3177
+ const controller = new AbortController();
3155
3178
 
3156
- const { signal } = controller;
3179
+ let aborted = false;
3157
3180
 
3158
- signal.unsubscribe = () => utils$1.asap(unsubscribe);
3181
+ const onabort = function (reason) {
3182
+ if (!aborted) {
3183
+ aborted = true;
3184
+ unsubscribe();
3185
+ const err = reason instanceof Error ? reason : this.reason;
3186
+ controller.abort(
3187
+ err instanceof AxiosError
3188
+ ? err
3189
+ : new CanceledError(err instanceof Error ? err.message : err)
3190
+ );
3191
+ }
3192
+ };
3159
3193
 
3160
- return signal;
3161
- }
3194
+ let timer =
3195
+ timeout &&
3196
+ setTimeout(() => {
3197
+ timer = null;
3198
+ onabort(new AxiosError(`timeout of ${timeout}ms exceeded`, AxiosError.ETIMEDOUT));
3199
+ }, timeout);
3200
+
3201
+ const unsubscribe = () => {
3202
+ if (!signals) { return; }
3203
+ timer && clearTimeout(timer);
3204
+ timer = null;
3205
+ signals.forEach((signal) => {
3206
+ signal.unsubscribe
3207
+ ? signal.unsubscribe(onabort)
3208
+ : signal.removeEventListener('abort', onabort);
3209
+ });
3210
+ signals = null;
3211
+ };
3212
+
3213
+ signals.forEach((signal) => signal.addEventListener('abort', onabort));
3214
+
3215
+ const { signal } = controller;
3216
+
3217
+ signal.unsubscribe = () => utils$1.asap(unsubscribe);
3218
+
3219
+ return signal;
3162
3220
  };
3163
3221
 
3164
3222
  const streamChunk = function* (chunk, chunkSize) {
@@ -3352,12 +3410,41 @@ function estimateDataURLDecodedBytes(url) {
3352
3410
  return bytes;
3353
3411
  }
3354
3412
 
3355
- const VERSION = "1.16.0";
3413
+ const VERSION = "1.17.0";
3356
3414
 
3357
3415
  const DEFAULT_CHUNK_SIZE = 64 * 1024;
3358
3416
 
3359
3417
  const { isFunction } = utils$1;
3360
3418
 
3419
+ /**
3420
+ * Encode a UTF-8 string to a Latin-1 byte string for use with btoa().
3421
+ * This is a modern replacement for the deprecated unescape(encodeURIComponent(str)) pattern.
3422
+ *
3423
+ * @param {string} str The string to encode
3424
+ *
3425
+ * @returns {string} UTF-8 bytes as a Latin-1 string
3426
+ */
3427
+ const encodeUTF8 = (str) =>
3428
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/gi, (_, hex) =>
3429
+ String.fromCharCode(parseInt(hex, 16))
3430
+ );
3431
+
3432
+ // Node's WHATWG URL parser returns `username` and `password` percent-encoded.
3433
+ // Decode before composing the `auth` option so credentials such as
3434
+ // `my%40email.com:pass` are sent as `my@email.com:pass`. Falls back to the
3435
+ // original value for malformed input so a bad encoding never throws.
3436
+ const decodeURIComponentSafe = (value) => {
3437
+ if (!utils$1.isString(value)) {
3438
+ return value;
3439
+ }
3440
+
3441
+ try {
3442
+ return decodeURIComponent(value);
3443
+ } catch (error) {
3444
+ return value;
3445
+ }
3446
+ };
3447
+
3361
3448
  const test = (fn, ...args) => {
3362
3449
  try {
3363
3450
  return !!fn(...args);
@@ -3366,8 +3453,20 @@ const test = (fn, ...args) => {
3366
3453
  }
3367
3454
  };
3368
3455
 
3456
+ const maybeWithAuthCredentials = (url) => {
3457
+ const protocolIndex = url.indexOf('://');
3458
+ let urlToCheck = url;
3459
+ if (protocolIndex !== -1) {
3460
+ urlToCheck = urlToCheck.slice(protocolIndex + 3);
3461
+ }
3462
+ return urlToCheck.includes('@') || urlToCheck.includes(':');
3463
+ };
3464
+
3369
3465
  const factory = (env) => {
3370
- const globalObject = utils$1.global ?? globalThis;
3466
+ const globalObject =
3467
+ utils$1.global !== undefined && utils$1.global !== null
3468
+ ? utils$1.global
3469
+ : globalThis;
3371
3470
  const { ReadableStream, TextEncoder } = globalObject;
3372
3471
 
3373
3472
  env = utils$1.merge.call(
@@ -3510,6 +3609,7 @@ const factory = (env) => {
3510
3609
 
3511
3610
  const hasMaxContentLength = utils$1.isNumber(maxContentLength) && maxContentLength > -1;
3512
3611
  const hasMaxBodyLength = utils$1.isNumber(maxBodyLength) && maxBodyLength > -1;
3612
+ const own = (key) => (utils$1.hasOwnProp(config, key) ? config[key] : undefined);
3513
3613
 
3514
3614
  let _fetch = envFetch || fetch;
3515
3615
 
@@ -3532,6 +3632,46 @@ const factory = (env) => {
3532
3632
  let requestContentLength;
3533
3633
 
3534
3634
  try {
3635
+ // HTTP basic authentication
3636
+ let auth = undefined;
3637
+ const configAuth = own('auth');
3638
+
3639
+ if (configAuth) {
3640
+ const username = configAuth.username || '';
3641
+ const password = configAuth.password || '';
3642
+ auth = {
3643
+ username,
3644
+ password
3645
+ };
3646
+ }
3647
+
3648
+ if (maybeWithAuthCredentials(url)) {
3649
+ const parsedURL = new URL(url, platform.origin);
3650
+
3651
+ if (!auth && (parsedURL.username || parsedURL.password)) {
3652
+ const urlUsername = decodeURIComponentSafe(parsedURL.username);
3653
+ const urlPassword = decodeURIComponentSafe(parsedURL.password);
3654
+ auth = {
3655
+ username: urlUsername,
3656
+ password: urlPassword
3657
+ };
3658
+ }
3659
+
3660
+ if (parsedURL.username || parsedURL.password) {
3661
+ parsedURL.username = '';
3662
+ parsedURL.password = '';
3663
+ url = parsedURL.href;
3664
+ }
3665
+ }
3666
+
3667
+ if (auth) {
3668
+ headers.delete('authorization');
3669
+ headers.set(
3670
+ 'Authorization',
3671
+ 'Basic ' + btoa(encodeUTF8((auth.username || '') + ':' + (auth.password || '')))
3672
+ );
3673
+ }
3674
+
3535
3675
  // Enforce maxContentLength for data: URLs up-front so we never materialize
3536
3676
  // an oversized payload. The HTTP adapter applies the same check (see http.js
3537
3677
  // "if (protocol === 'data:')" branch).
@@ -3624,7 +3764,7 @@ const factory = (env) => {
3624
3764
  ...fetchOptions,
3625
3765
  signal: composedSignal,
3626
3766
  method: method.toUpperCase(),
3627
- headers: headers.normalize().toJSON(),
3767
+ headers: toByteStringHeaderObject(headers.normalize()),
3628
3768
  body: data,
3629
3769
  duplex: 'half',
3630
3770
  credentials: isCredentialsSupported ? withCredentials : undefined,
@@ -4213,6 +4353,7 @@ class Axios {
4213
4353
  forcedJSONParsing: validators.transitional(validators.boolean),
4214
4354
  clarifyTimeoutError: validators.transitional(validators.boolean),
4215
4355
  legacyInterceptorReqResOrdering: validators.transitional(validators.boolean),
4356
+ advertiseZstdAcceptEncoding: validators.transitional(validators.boolean),
4216
4357
  },
4217
4358
  false
4218
4359
  );
@@ -4699,10 +4840,6 @@ axios$1.HttpStatusCode = HttpStatusCode;
4699
4840
 
4700
4841
  axios$1.default = axios$1;
4701
4842
 
4702
- const instance = axios$1.create();
4703
-
4704
- const axios = setupCache(instance, { cacheTakeover: false });
4705
-
4706
4843
  /** setting default log level globally to warning */
4707
4844
  log.setLevel(log.levels.WARN, true);
4708
4845
 
@@ -4732,7 +4869,7 @@ const registeredProjections = ["EPSG:4326", "EPSG:3857"];
4732
4869
  /** available projection to be rendered by `EodashMap` */
4733
4870
  const availableMapProjection = ref("EPSG:3857");
4734
4871
 
4735
- /** @type {import("vue").Ref<import("@eox/map").EOxMap | null>} */
4872
+ /** @type {import("vue").Ref<import("@eox/map").EOxMap & { mapUpdateId?: number } | null>} */
4736
4873
  const mapEl = shallowRef(null);
4737
4874
 
4738
4875
  /** @type {import("vue").Ref<import("@eox/map").EOxMap | null>} */
@@ -4776,6 +4913,24 @@ const chartSpec = ref(null);
4776
4913
  */
4777
4914
  const compareChartSpec = ref(null);
4778
4915
 
4916
+ /**
4917
+ * Global loading state.
4918
+ * - `loading.activeLoads` — increment/decrement to track concurrent loads
4919
+ * - `loading.value` — derived boolean, true when `activeLoads > 0`
4920
+ */
4921
+ const loading = reactive({
4922
+ activeLoads: 0,
4923
+ get value() {
4924
+ return this.activeLoads > 0;
4925
+ },
4926
+ });
4927
+
4928
+ /**
4929
+ * Adapter allows external widgets to hook into tooltip property transformation
4930
+ * @type {import("vue").Ref<((param: {key: string, value: any}, map?: string) => {key: string, value: any} | undefined) | null>}
4931
+ */
4932
+ const tooltipAdapter = shallowRef(null);
4933
+
4779
4934
  const states = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
4780
4935
  __proto__: null,
4781
4936
  activeTemplate,
@@ -4792,13 +4947,180 @@ const states = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
4792
4947
  datetime,
4793
4948
  indicator,
4794
4949
  isGlobe,
4950
+ loading,
4795
4951
  mapCompareEl,
4796
4952
  mapEl,
4797
4953
  mapPosition,
4798
4954
  poi,
4799
- registeredProjections
4955
+ registeredProjections,
4956
+ tooltipAdapter
4800
4957
  }, Symbol.toStringTag, { value: 'Module' }));
4801
4958
 
4959
+ const instance = axios$1.create();
4960
+ const axios = setupCache(instance, { cacheTakeover: false });
4961
+ function installLoadingInterceptors() {
4962
+ const reqId = axios.interceptors.request.use((config) => {
4963
+ loading.activeLoads++;
4964
+ return config;
4965
+ });
4966
+ const resId = axios.interceptors.response.use(
4967
+ (response) => {
4968
+ loading.activeLoads = Math.max(0, loading.activeLoads - 1);
4969
+ return response;
4970
+ },
4971
+ (error) => {
4972
+ loading.activeLoads = Math.max(0, loading.activeLoads - 1);
4973
+ return Promise.reject(error);
4974
+ }
4975
+ );
4976
+ return () => {
4977
+ axios.interceptors.request.eject(reqId);
4978
+ axios.interceptors.response.eject(resId);
4979
+ };
4980
+ }
4981
+ installLoadingInterceptors();
4982
+
4983
+ /**
4984
+ * Array of eodash STAC Collections extracted from the current selected indicator.
4985
+ * Updated in {@link file://./../store/stac.js `loadSelectedSTAC`} widget
4986
+ *
4987
+ * @type {import('../eodashSTAC/EodashCollection').EodashCollection[]}
4988
+ * @private
4989
+ */
4990
+ const eodashCollections = shallowReactive([]);
4991
+
4992
+ /**
4993
+ * Array of eodash STAC Collections extracted from the current selected COMPARE indicator.
4994
+ * Updated in {@link file://./../store/stac.js ` loadSelectedCompareSTAC`} widget
4995
+ *
4996
+ * @type {import('../eodashSTAC/EodashCollection').EodashCollection[]}
4997
+ * @private
4998
+ */
4999
+ const eodashCompareCollections = shallowReactive([]);
5000
+
5001
+ /** whether it's the first load of the app, used to track if there are params in the url */
5002
+ const isFirstLoad = ref(false);
5003
+
5004
+ /**
5005
+ * Current value of the layer control JSON form for the latest layer the user interacted with.
5006
+ * @type {import("vue").Ref<Record<string, any> | undefined>}
5007
+ */
5008
+ const layerControlFormValue = ref({});
5009
+
5010
+ /**
5011
+ * Current value of the layer control JSON form for the latest layer the user interacted with.
5012
+ * @type {import("vue").Ref<Record<string, any> | undefined>}
5013
+ */
5014
+ const layerControlFormValueCompare = ref({});
5015
+
5016
+ /**
5017
+ * STAC indicators color palette, defaults to Bank-Wong palette
5018
+ * @type {string[]} */
5019
+ const collectionsPalette = reactive([
5020
+ "#009E73",
5021
+ "#0072B2",
5022
+ "#E69F00",
5023
+ "#CC79A7",
5024
+ "#56B4E9",
5025
+ "#D55E00",
5026
+ ]);
5027
+
5028
+ /**
5029
+ * Object containing data themes and their associated brand properties (icon and color).
5030
+ */
5031
+ const dataThemesBrands = {
5032
+ agriculture: {
5033
+ icon: mdiBarley,
5034
+ color: "#F2AF25",
5035
+ },
5036
+ water: {
5037
+ icon: mdiWater,
5038
+ color: "#73A6C7",
5039
+ },
5040
+ oceans: {
5041
+ icon: mdiWater,
5042
+ color: "#6DA2C5",
5043
+ },
5044
+ land: {
5045
+ icon: mdiImageFilterHdr,
5046
+ color: "#019E73",
5047
+ },
5048
+ health: {
5049
+ icon: mdiHospitalBoxOutline,
5050
+ color: "#32322C",
5051
+ },
5052
+ "covid-19": {
5053
+ icon: mdiHospitalBoxOutline,
5054
+ color: "#32322C",
5055
+ },
5056
+ combined: {
5057
+ icon: mdiSetCenter,
5058
+ color: "#56B4E9",
5059
+ },
5060
+ air: {
5061
+ icon: mdiWeatherWindy,
5062
+ color: "#475faf",
5063
+ },
5064
+ atmosphere: {
5065
+ icon: mdiWeatherWindy,
5066
+ color: "#475faf",
5067
+ },
5068
+ climate: {
5069
+ icon: mdiWeatherWindy,
5070
+ color: "#475faf",
5071
+ },
5072
+ economy: {
5073
+ icon: mdiCurrencyEur,
5074
+ color: "#8E81AF",
5075
+ },
5076
+ commerce: {
5077
+ icon: mdiCurrencyEur,
5078
+ color: "#8E81AF",
5079
+ },
5080
+ society: {
5081
+ icon: mdiAccountMultiple,
5082
+ color: "#8ac501",
5083
+ },
5084
+ biomass: {
5085
+ icon: mdiLeaf,
5086
+ color: "#009E73",
5087
+ },
5088
+ extremes: {
5089
+ icon: mdiLightningBolt,
5090
+ color: "#a1280a",
5091
+ },
5092
+ energy: {
5093
+ icon: mdiLightningBolt,
5094
+ color: "#475faf",
5095
+ },
5096
+ tourism: {
5097
+ icon: mdiHiking,
5098
+ color: "#80510aff",
5099
+ },
5100
+ sport: {
5101
+ icon: mdiVolleyball,
5102
+ color: "#e98e65ff",
5103
+ },
5104
+ cryosphere: {
5105
+ icon: mdiSnowflake,
5106
+ color: "#42C7B8",
5107
+ },
5108
+ industry: {
5109
+ icon: mdiWrench,
5110
+ color: "#8d845cff",
5111
+ },
5112
+ };
5113
+
5114
+ /** @type {Map<string, import("stac-ts").StacItem[]>} */
5115
+ const itemsCache = new Map();
5116
+ /**
5117
+ * Split items cache stores items with their time range metadata
5118
+ * @type {Map<string, {items: import("stac-ts").StacItem[], minTime: number, maxTime: number}>}
5119
+ */
5120
+ const splitItemsCache = new Map();
5121
+
5122
+ ref(0);
5123
+
4802
5124
  /**
4803
5125
  * Returns the current layers of {@link mapEl}
4804
5126
  * @returns {import("@eox/map").EoxLayer[]}
@@ -4807,7 +5129,7 @@ const getLayers = () => mapEl.value?.layers ?? [];
4807
5129
 
4808
5130
  /**
4809
5131
  * Returns the current layers of {@link mapCompareEl}
4810
- * * @returns {import("@eox/map").EoxLayer[]}
5132
+ * @returns {import("@eox/map").EoxLayer[]}
4811
5133
  */
4812
5134
  const getCompareLayers = () => mapCompareEl.value?.layers ?? [];
4813
5135
 
@@ -5043,6 +5365,26 @@ function generateFeatures(links, extraProperties = {}, rel = "item") {
5043
5365
  return geojsonObject;
5044
5366
  }
5045
5367
 
5368
+ /**
5369
+ * Fetches or extracts the raster form configuration for a STAC object.
5370
+ * Supports direct JSON objects, data URIs, and URL strings.
5371
+ *
5372
+ * @param {string|object|undefined} rasterform - The rasterform property from the STAC object.
5373
+ * @returns {Promise<import("@/types").EodashRasterJSONForm|undefined>}
5374
+ */
5375
+ async function fetchRasterForm(rasterform) {
5376
+ if (!rasterform) {
5377
+ return undefined;
5378
+ }
5379
+ if (typeof rasterform === "object") {
5380
+ return /** @type {import("@/types").EodashRasterJSONForm} */ (rasterform);
5381
+ }
5382
+ if (typeof rasterform === "string") {
5383
+ return await axios.get(rasterform).then((resp) => resp.data);
5384
+ }
5385
+ return undefined;
5386
+ }
5387
+
5046
5388
  /**
5047
5389
  * Spearates and extracts layerConfig (jsonform schema & legend) from a style json
5048
5390
  *
@@ -5099,6 +5441,39 @@ function extractLayerConfig(
5099
5441
  return { layerConfig, style };
5100
5442
  }
5101
5443
 
5444
+ /**
5445
+ * Recursively extracts URL keys from a JSON Schema.
5446
+ * Maps schema property names to their defined `url_key`.
5447
+ * @param {Record<string, any> | null | undefined} schema
5448
+ * @returns {Record<string, string>}
5449
+ */
5450
+ function extractUrlKeys(schema) {
5451
+ /** @type {Record<string, string>} */
5452
+ const keys = {};
5453
+ if (!schema || typeof schema !== "object") return keys;
5454
+
5455
+ if (schema.properties) {
5456
+ for (const [key, propDef] of Object.entries(schema.properties)) {
5457
+ if (propDef && typeof propDef === "object") {
5458
+ if (typeof propDef.url_key === "string") {
5459
+ keys[key] = propDef.url_key;
5460
+ }
5461
+ Object.assign(keys, extractUrlKeys(propDef));
5462
+ }
5463
+ }
5464
+ }
5465
+
5466
+ for (const combinator of ["oneOf", "allOf", "anyOf"]) {
5467
+ if (Array.isArray(schema[combinator])) {
5468
+ for (const sub of schema[combinator]) {
5469
+ Object.assign(keys, extractUrlKeys(sub));
5470
+ }
5471
+ }
5472
+ }
5473
+
5474
+ return keys;
5475
+ }
5476
+
5102
5477
  /**
5103
5478
  *
5104
5479
  * @param {number[]} bbox
@@ -5173,7 +5548,7 @@ const extractRoles = (properties, linkOrAsset) => {
5173
5548
 
5174
5549
  /**
5175
5550
  * Extracts a single non-link style JSON from a STAC Item optionally for a selected key mapping
5176
- * @param { import("stac-ts").StacItem | import("stac-ts").StacCollection } stacObject
5551
+ * @param { import("stac-ts").StacItem | import("stac-ts").StacCollection | null | undefined} stacObject
5177
5552
  * @param {string | undefined} linkKey
5178
5553
  * @param {string | undefined} assetKey
5179
5554
  * @returns
@@ -5183,6 +5558,7 @@ const fetchStyle = async (
5183
5558
  linkKey = undefined,
5184
5559
  assetKey = undefined,
5185
5560
  ) => {
5561
+ if (!stacObject) return undefined;
5186
5562
  let styleLink = null;
5187
5563
  if (linkKey) {
5188
5564
  styleLink = stacObject.links.find(
@@ -5214,6 +5590,20 @@ const fetchStyle = async (
5214
5590
  }
5215
5591
  };
5216
5592
 
5593
+ /**
5594
+ * Resolves a style by preferring the item's own `style` link and falling back
5595
+ * to the collection's. Takes the same key arguments as `fetchStyle`.
5596
+ *
5597
+ * @param {import("stac-ts").StacItem | import("stac-ts").StacCollection} item
5598
+ * @param {import("stac-ts").StacCollection | null | undefined} collection
5599
+ * @param {string} [linkKey]
5600
+ * @param {string} [assetKey]
5601
+ * @returns {Promise<import("@/types").EodashStyleJson | undefined>}
5602
+ */
5603
+ const resolveStyle = async (item, collection, linkKey, assetKey) =>
5604
+ (await fetchStyle(item, linkKey, assetKey)) ??
5605
+ (await fetchStyle(collection, linkKey, assetKey));
5606
+
5217
5607
  /**
5218
5608
  * Fetches all style JSONs from a STAC Item and returns an array with style objects
5219
5609
  * @param {import("stac-ts").StacItem | import("stac-ts").StacCollection} stacObject
@@ -5224,6 +5614,7 @@ const fetchAllStyles = async (stacObject) => {
5224
5614
  link.rel.includes("style"),
5225
5615
  );
5226
5616
  const fetchPromises = styleLinks.map(async (link) => {
5617
+ /** @type {import("@/types").EodashStyleJson} */
5227
5618
  const styleJson = await axios.get(link.href).then((resp) => resp.data);
5228
5619
  log.debug("fetched styles JSON", JSON.parse(JSON.stringify(styleJson)));
5229
5620
  return styleJson;
@@ -5253,94 +5644,74 @@ const getProjectionCode = (projection) => {
5253
5644
  };
5254
5645
 
5255
5646
  /**
5256
- * Extracts layercontrol LayerDatetime property from STAC Links
5257
- * @param {import("stac-ts").StacLink[] | import("stac-ts").StacItem[] | undefined} [items]
5258
- * @param {string|null} [currentStep]
5259
- **/
5260
- const extractLayerTimeValues = (items, currentStep) => {
5261
- if (!currentStep || !items?.length) {
5262
- return { layerDatetime: undefined, timeControlValues: undefined };
5263
- }
5264
-
5265
- // check if items has a datetime value
5266
- const dateProperty = getDatetimeProperty(items);
5267
-
5268
- if (!dateProperty) {
5647
+ * Builds layercontrol LayerDatetime + timeControlValues from a list of dates.
5648
+ *
5649
+ * @param {Date[] | undefined} dates
5650
+ * @param {string | null} [currentStep] - target datetime; snapped to the closest available date
5651
+ * @returns {{ layerDatetime: Record<string, any> | undefined, timeControlValues: { date: string }[] | undefined }}
5652
+ */
5653
+ const extractLayerTimeValues = (dates, currentStep) => {
5654
+ if (!currentStep || !dates?.length || dates.length <= 1) {
5269
5655
  return { layerDatetime: undefined, timeControlValues: undefined };
5270
5656
  }
5271
- /** @type {{date:string;itemId:string}[]} */
5272
- const timeValues = [];
5273
- try {
5274
- /**
5275
- * @param {typeof timeValues} vals
5276
- * @param {import("stac-ts").StacLink} link
5277
- */
5278
- const reduceLinks = (vals, link) => {
5279
- if (link[dateProperty] && link.rel === "item") {
5280
- vals.push({
5281
- itemId: /** @type {string} */ (link.id),
5282
- date: new Date(
5283
- /** @type {string} */ (link[dateProperty]),
5284
- ).toISOString(),
5285
- });
5286
- }
5287
- return vals;
5288
- };
5289
5657
 
5290
- /**
5291
- *
5292
- * @param {typeof timeValues} vals
5293
- * @param {import("stac-ts").StacItem} item
5294
- */
5295
- const reduceItems = (vals, item) => {
5296
- const date = item.properties?.[dateProperty];
5297
- if (date) {
5298
- vals.push({
5299
- itemId: /** @type {string} */ (item.id),
5300
- date: new Date(/** @type {string} */ (date)).toISOString(),
5301
- ...item.properties,
5302
- });
5303
- }
5304
- return vals;
5305
- };
5306
- currentStep = new Date(currentStep).toISOString();
5307
- //@ts-expect-error TODO
5308
- items.reduce(isSTACItem(items[0]) ? reduceItems : reduceLinks, timeValues);
5309
- } catch (e) {
5310
- console.warn("[eodash] not supported datetime format was provided", e);
5311
- return { layerDatetime: undefined, timeControlValues: undefined };
5312
- }
5313
- // not enough timeValues
5314
- if (timeValues.length <= 1) {
5315
- return { layerDatetime: undefined, timeControlValues: undefined };
5316
- }
5658
+ const controlValues = dates.map((d) => d.toISOString()).sort();
5659
+ const timeControlValues = controlValues.map((date) => ({ date }));
5317
5660
 
5318
- // item datetime is not included in the item links datetime
5319
- if (!timeValues.some((val) => val.date === currentStep)) {
5320
- const currentStepTime = new Date(currentStep).getTime();
5321
- currentStep = timeValues.reduce((time, step) => {
5322
- const aDiff = Math.abs(new Date(time).getTime() - currentStepTime);
5323
- const bDiff = Math.abs(new Date(step.date).getTime() - currentStepTime);
5324
- return bDiff < aDiff ? step.date : time;
5325
- }, timeValues[0].date);
5661
+ currentStep = new Date(currentStep).toISOString();
5662
+ if (!controlValues.includes(currentStep)) {
5663
+ const target = new Date(currentStep).getTime();
5664
+ currentStep = controlValues.reduce((best, d) =>
5665
+ Math.abs(new Date(d).getTime() - target) <
5666
+ Math.abs(new Date(best).getTime() - target)
5667
+ ? d
5668
+ : best,
5669
+ );
5326
5670
  }
5327
5671
 
5328
5672
  const layerDatetime = {
5329
- controlValues: timeValues.map((d) => d.date).sort(),
5673
+ controlValues,
5330
5674
  currentStep,
5331
5675
  slider: true,
5332
5676
  navigation: true,
5333
5677
  play: false,
5334
5678
  displayFormat: "DD.MM.YYYY HH:mm",
5335
5679
  animateOnClickInterval: false,
5680
+ showUTC: true,
5336
5681
  };
5337
5682
 
5338
- return {
5339
- layerDatetime,
5340
- timeControlValues: timeValues,
5341
- };
5683
+ return { layerDatetime, timeControlValues };
5342
5684
  };
5343
5685
 
5686
+ /**
5687
+ * Fetches the daily pre-aggregation data for a STAC collection if available.
5688
+ * Returns the raw AggregationCollection,
5689
+ * or null if no daily pre-aggregation link exists, or the fetch fails.
5690
+ *
5691
+ * @param {import("stac-ts").StacCollection | undefined} stacCollection
5692
+ * @param {string} fallbackBaseUrl - base for relative href resolution when no `self` link is present
5693
+ * @returns {Promise<any | null>}
5694
+ */
5695
+ async function fetchPreAggregations(stacCollection, fallbackBaseUrl) {
5696
+ if (!stacCollection) return null;
5697
+ const preAggregationLink = stacCollection.links?.find(
5698
+ (l) => l.rel === "pre-aggregation" && l["aggregation:interval"] === "daily",
5699
+ );
5700
+ if (!preAggregationLink) return null;
5701
+
5702
+ try {
5703
+ const selfLink = stacCollection.links?.find((l) => l.rel === "self")?.href;
5704
+ const url = toAbsolute(
5705
+ preAggregationLink.href,
5706
+ selfLink || fallbackBaseUrl,
5707
+ );
5708
+ return await axios.get(url).then((resp) => resp.data);
5709
+ } catch (e) {
5710
+ console.warn("[eodash] Failed to fetch pre-aggregation", e);
5711
+ return null;
5712
+ }
5713
+ }
5714
+
5344
5715
  /**
5345
5716
  * Recursively find all layers whose ID up to the first ; is same as given layer
5346
5717
  *
@@ -5349,6 +5720,9 @@ const extractLayerTimeValues = (items, currentStep) => {
5349
5720
  * @returns {import("@eox/map").EoxLayer[]} Matching layer objects.
5350
5721
  */
5351
5722
  const findLayersByLayerPrefix = (layers, referenceLayer) => {
5723
+ if (!layers || !referenceLayer) {
5724
+ return [];
5725
+ }
5352
5726
  const refId = referenceLayer?.properties?.id;
5353
5727
 
5354
5728
  if (typeof refId !== "string" || !refId.includes(";:;")) {
@@ -5535,20 +5909,147 @@ const createAssetID = (collectionId, itemId, index) => {
5535
5909
  *
5536
5910
  * @param {Record<string,any>[]} layers
5537
5911
  */
5538
- const removeUnneededProperties = (layers) => {
5539
- const cloned = structuredClone(layers);
5540
- cloned.forEach((layer) => {
5541
- const id = layer.properties.id;
5542
- const title = layer.properties.title;
5543
- layer.properties = { id, title };
5544
- if (layer["interactions"]) {
5545
- delete layer["interactions"];
5912
+ const removeUnneededProperties = (layers, formValues = {}) => {
5913
+ /**
5914
+ * @param {Record<string,any>} layer
5915
+ * @returns {Record<string,any>[]}
5916
+ */
5917
+ const processLayer = (layer) => {
5918
+ // If the layer (or group) is explicitly marked as not visible, skip it and all children
5919
+ if (layer.properties?.visible === false) {
5920
+ return [];
5921
+ }
5922
+
5923
+ // If it's a Group, we just want its children
5924
+ if (layer.type === "Group" && Array.isArray(layer.layers)) {
5925
+ return layer.layers.flatMap(processLayer);
5546
5926
  }
5547
- if (layer.type === "Group") {
5548
- layer.layers = removeUnneededProperties(layer.layers);
5927
+
5928
+ // Break any Vue Proxies/OpenLayers getters by stringifying first
5929
+ let clonedLayer;
5930
+ try {
5931
+ clonedLayer = JSON.parse(JSON.stringify(layer));
5932
+ } catch (_e) {
5933
+ clonedLayer = structuredClone(layer);
5549
5934
  }
5550
- });
5551
- return cloned;
5935
+
5936
+ // Flatten formValues to handle nested properties (e.g., vminmax: { vmin, vmax })
5937
+ /**
5938
+ * @param {Record<string,any>} obj
5939
+ * @returns {Record<string,any>}
5940
+ */
5941
+ const flattenFormValues = (obj) => {
5942
+ /** @type {Record<string,any>} */
5943
+ let result = {};
5944
+ for (const key in obj) {
5945
+ if (
5946
+ obj[key] !== null &&
5947
+ typeof obj[key] === "object" &&
5948
+ !Array.isArray(obj[key])
5949
+ ) {
5950
+ Object.assign(result, flattenFormValues(obj[key]));
5951
+ } else {
5952
+ result[key] = obj[key];
5953
+ }
5954
+ }
5955
+ return result;
5956
+ };
5957
+ const flatFormValues = flattenFormValues(formValues);
5958
+
5959
+ // Burn in style variables using Mustache if formValues are provided
5960
+ if (Object.keys(flatFormValues).length > 0) {
5961
+ // Stringify, render mustache, then parse back
5962
+ try {
5963
+ const renderedString = mustache.render(
5964
+ JSON.stringify(clonedLayer),
5965
+ flatFormValues,
5966
+ );
5967
+ clonedLayer = JSON.parse(renderedString);
5968
+ } catch (e) {
5969
+ log.warn(
5970
+ "[eodash] Failed to apply mustache templating during export cleanup:",
5971
+ e,
5972
+ );
5973
+ }
5974
+ }
5975
+
5976
+ const {
5977
+ id,
5978
+ title,
5979
+ mapboxStyle,
5980
+ projection,
5981
+ applyOptions,
5982
+ layerConfig,
5983
+ visible,
5984
+ } = clonedLayer.properties || {};
5985
+
5986
+ // If style was not at root but in properties (layerConfig), move it to root early
5987
+ if (!clonedLayer.style && layerConfig?.style) {
5988
+ clonedLayer.style = layerConfig.style;
5989
+ }
5990
+
5991
+ // Burn in OpenLayers ["var", "name"] variables using flatFormValues overriding style.variables
5992
+ const styleVariables = {
5993
+ ...(clonedLayer.style?.variables || {}),
5994
+ ...flatFormValues,
5995
+ };
5996
+
5997
+ if (clonedLayer.style && Object.keys(styleVariables).length > 0) {
5998
+ clonedLayer.style = updateVectorLayerStyle({
5999
+ ...clonedLayer.style,
6000
+ variables: styleVariables,
6001
+ });
6002
+ }
6003
+
6004
+ clonedLayer.properties = {
6005
+ id,
6006
+ title,
6007
+ ...(mapboxStyle && { mapboxStyle }),
6008
+ ...(projection && { projection }),
6009
+ ...(applyOptions && { applyOptions }),
6010
+ ...(visible !== undefined && { visible }),
6011
+ };
6012
+
6013
+ if (clonedLayer["interactions"]) {
6014
+ delete clonedLayer["interactions"];
6015
+ }
6016
+
6017
+ // Cleanup unnecessary properties
6018
+ /**
6019
+ * @param {any} obj
6020
+ */
6021
+ const cleanupProperties = (obj) => {
6022
+ if (!obj || typeof obj !== "object") return;
6023
+
6024
+ for (const key in obj) {
6025
+ if (obj[key] === null) {
6026
+ delete obj[key];
6027
+ } else if (Array.isArray(obj[key])) {
6028
+ if (obj[key].length === 0) {
6029
+ // Remove empty arrays like empty interactions
6030
+ delete obj[key];
6031
+ } else {
6032
+ obj[key].forEach(cleanupProperties);
6033
+ }
6034
+ } else if (typeof obj[key] === "object") {
6035
+ cleanupProperties(obj[key]);
6036
+ }
6037
+ }
6038
+ };
6039
+ cleanupProperties(clonedLayer);
6040
+
6041
+ return [clonedLayer];
6042
+ };
6043
+
6044
+ // ensure the top level array is also deeply un-proxied if needed
6045
+ let rawLayers = layers;
6046
+ try {
6047
+ rawLayers = JSON.parse(JSON.stringify(layers));
6048
+ } catch (_e) {
6049
+ rawLayers = structuredClone(layers);
6050
+ }
6051
+
6052
+ return rawLayers.flatMap(processLayer);
5552
6053
  };
5553
6054
 
5554
6055
  /**
@@ -5708,7 +6209,12 @@ function getDatetimeProperty(linksOrItems) {
5708
6209
  }
5709
6210
 
5710
6211
  // TODO: consider other properties for datetime ranges
5711
- const datetimeProperties = ["datetime", "start_datetime", "end_datetime"];
6212
+
6213
+ const datetimeProperties = /** @type {const} */ ([
6214
+ "datetime",
6215
+ "start_datetime",
6216
+ "end_datetime",
6217
+ ]);
5712
6218
  if (checkProperties) {
5713
6219
  for (const prop of datetimeProperties) {
5714
6220
  const propExists = linksOrItems.some(
@@ -5734,6 +6240,7 @@ function getDatetimeProperty(linksOrItems) {
5734
6240
  return prop;
5735
6241
  }
5736
6242
  }
6243
+
5737
6244
  /**
5738
6245
  *
5739
6246
  * @param {*} stacObject
@@ -5750,6 +6257,65 @@ function isSTACItem(stacObject) {
5750
6257
  );
5751
6258
  }
5752
6259
 
6260
+ /**
6261
+ * Fetches items using a split strategy (past/future) around a center date
6262
+ * @param {string} itemsUrl
6263
+ * @param {string} query
6264
+ * @param {string | Date} centerDatetime
6265
+ * @param {number} maxNumber
6266
+ * @returns {Promise<import("stac-ts").StacItem[]>}
6267
+ */
6268
+ const fetchSplitItems = async (
6269
+ itemsUrl,
6270
+ query,
6271
+ centerDatetime,
6272
+ maxNumber,
6273
+ ) => {
6274
+ const center = new Date(centerDatetime).toISOString();
6275
+ const limit = Math.ceil(maxNumber / 2);
6276
+
6277
+ /** @param {"past"|"future"} direction */
6278
+ const fetchSide = async (direction) => {
6279
+ const isPast = direction === "past";
6280
+ const datetimeRange = isPast ? `../${center}` : `${center}/..`;
6281
+ const sortOrder = isPast ? "-datetime" : "+datetime";
6282
+ const queryParams = new URLSearchParams(query);
6283
+
6284
+ queryParams.set("limit", limit.toString());
6285
+ queryParams.set("datetime", datetimeRange);
6286
+ queryParams.set("sortby", sortOrder);
6287
+
6288
+ const splitUrl = itemsUrl.split("/");
6289
+ const collectionId = /** @type {string} */ (splitUrl.at(-2));
6290
+ queryParams.set("collection", collectionId);
6291
+
6292
+ const searchEndpoint = `${splitUrl.slice(0, -3).join("/")}/search`;
6293
+ const items = await axios
6294
+ .get(searchEndpoint, { params: queryParams })
6295
+ .then((res) => res.data.features);
6296
+ return items;
6297
+ };
6298
+
6299
+ const [pastItems, futureItems] = await Promise.all([
6300
+ fetchSide("past"),
6301
+ fetchSide("future"),
6302
+ ]);
6303
+
6304
+ const allItems = [...pastItems, ...futureItems];
6305
+
6306
+ // check for duplicates by ids
6307
+ const seen = new Set();
6308
+ const uniqueItems = [];
6309
+ for (const item of allItems) {
6310
+ if (!seen.has(item.id)) {
6311
+ seen.add(item.id);
6312
+ uniqueItems.push(item);
6313
+ }
6314
+ }
6315
+
6316
+ return uniqueItems;
6317
+ };
6318
+
5753
6319
  /**
5754
6320
  * Fetch all STAC items from a STAC API endpoint.
5755
6321
  * @param {string} itemsUrl
@@ -5757,6 +6323,8 @@ function isSTACItem(stacObject) {
5757
6323
  * @param {number} [limit=100] - The maximum number of items to fetch per request.
5758
6324
  * @param {boolean} [returnFirst] - If true, only the first page of results will be returned.
5759
6325
  * @param {number} [maxNumber=1000] - if the matched number of items exceed this, only the first page will be returned.
6326
+ * @param {string | Date} [centerDatetime] - Date to center the search around if items exceed maxNumber
6327
+ * @returns {Promise<import("stac-ts").StacItem[]>}
5760
6328
  */
5761
6329
  async function fetchApiItems(
5762
6330
  itemsUrl,
@@ -5764,23 +6332,42 @@ async function fetchApiItems(
5764
6332
  limit = 100,
5765
6333
  returnFirst = false,
5766
6334
  maxNumber = 1000,
6335
+ centerDatetime,
5767
6336
  ) {
5768
- itemsUrl = itemsUrl.includes("?") ? itemsUrl.split("?")[0] : itemsUrl;
5769
- itemsUrl += query ? `?limit=${limit}&${query}` : `?limit=${limit}`;
6337
+ // Exclude centerDatetime from cache key - it's only used for split search fallback
6338
+ const cacheKey = JSON.stringify({
6339
+ itemsUrl,
6340
+ query,
6341
+ limit,
6342
+ returnFirst,
6343
+ maxNumber,
6344
+ });
6345
+
6346
+ if (itemsCache.has(cacheKey)) {
6347
+ return itemsCache.get(cacheKey) ?? [];
6348
+ }
6349
+
6350
+ const urlQuery = new URLSearchParams(query);
6351
+ urlQuery.set("limit", limit.toString());
6352
+
6353
+ let finalItemsUrl = itemsUrl.includes("?")
6354
+ ? itemsUrl.split("?")[0]
6355
+ : itemsUrl;
6356
+ finalItemsUrl += urlQuery.keys().toArray().length
6357
+ ? `?${urlQuery.toString()}`
6358
+ : "";
5770
6359
 
5771
6360
  const itemsFeatureCollection = await axios
5772
- .get(itemsUrl)
6361
+ .get(finalItemsUrl)
5773
6362
  .then((resp) => resp.data);
6363
+
5774
6364
  /** @type {import("stac-ts").StacItem[]} */
5775
6365
  const items = itemsFeatureCollection.features;
5776
- const nextLink = itemsFeatureCollection.links?.find(
5777
- //@ts-expect-error TODO: itemsFeatureCollection is not typed
5778
- (link) => link.rel === "next",
5779
- );
5780
-
5781
- if (!nextLink || returnFirst) {
6366
+ if (returnFirst) {
6367
+ itemsCache.set(cacheKey, items);
5782
6368
  return items;
5783
6369
  }
6370
+
5784
6371
  /** @type {number} */
5785
6372
  const matchedItems = itemsFeatureCollection.numberMatched;
5786
6373
  // Avoid fetching too many items
@@ -5788,26 +6375,70 @@ async function fetchApiItems(
5788
6375
  console.warn(
5789
6376
  `[eodash] The number of items matched (${matchedItems}) exceeds the maximum allowed (${maxNumber})`,
5790
6377
  );
5791
- return items;
5792
- }
6378
+ // we try to narrow down the search around the center datetime
6379
+ if (centerDatetime) {
6380
+ // Check if we have a cached split result that covers this centerDatetime
6381
+ const splitCacheKey = JSON.stringify({ itemsUrl, query, maxNumber });
6382
+ const cachedSplit = splitItemsCache.get(splitCacheKey);
6383
+ const centerTime = new Date(centerDatetime).getTime();
5793
6384
 
5794
- let [nextLinkURL, nextLinkQuery] = nextLink.href.split("?");
5795
- nextLinkQuery = nextLinkQuery.replace(/limit=\d+/, "");
5796
- if (query) {
5797
- const queryParams = new URLSearchParams(query);
5798
- const nextLinkParams = new URLSearchParams(nextLinkQuery);
6385
+ if (
6386
+ cachedSplit &&
6387
+ centerTime >= cachedSplit.minTime &&
6388
+ centerTime <= cachedSplit.maxTime
6389
+ ) {
6390
+ return cachedSplit.items;
6391
+ }
5799
6392
 
5800
- for (const key of nextLinkParams.keys()) {
5801
- queryParams.delete(key);
5802
- }
5803
- const remainingQuery = queryParams.toString();
5804
- if (remainingQuery) {
5805
- nextLinkQuery += `&${remainingQuery}`;
6393
+ const narrowedItems = await fetchSplitItems(
6394
+ itemsUrl,
6395
+ query ?? "",
6396
+ centerDatetime,
6397
+ maxNumber,
6398
+ );
6399
+
6400
+ if (!narrowedItems.length) {
6401
+ return narrowedItems;
6402
+ }
6403
+ const datetimeProperty = getDatetimeProperty(narrowedItems);
6404
+ if (!datetimeProperty) {
6405
+ return narrowedItems;
6406
+ }
6407
+
6408
+ const times = narrowedItems
6409
+ .map((i) =>
6410
+ i.properties[datetimeProperty]
6411
+ ? new Date(i.properties[datetimeProperty]).getTime()
6412
+ : null,
6413
+ )
6414
+ .filter((t) => t !== null);
6415
+ if (!times.length) {
6416
+ return narrowedItems;
6417
+ }
6418
+ splitItemsCache.set(splitCacheKey, {
6419
+ items: narrowedItems,
6420
+ minTime: Math.min(...times),
6421
+ maxTime: Math.max(...times),
6422
+ });
6423
+
6424
+ return narrowedItems;
5806
6425
  }
6426
+
6427
+ itemsCache.set(cacheKey, items);
6428
+ return items;
5807
6429
  }
5808
6430
 
5809
- const nextPage = await fetchApiItems(nextLinkURL, nextLinkQuery);
5810
- items.push(...nextPage);
6431
+ urlQuery.set("limit", maxNumber.toString());
6432
+
6433
+ const allItems = await axios
6434
+ .get(itemsUrl + "?" + urlQuery.toString())
6435
+ .then((resp) => resp.data.features)
6436
+ .catch((err) => {
6437
+ console.error(err);
6438
+ return [];
6439
+ });
6440
+ items.splice(0, items.length, ...allItems);
6441
+ itemsCache.set(cacheKey, items);
5811
6442
  return items;
5812
6443
  }
5813
6444
  /**
@@ -5846,4 +6477,273 @@ function extractEoxLegendLink(link) {
5846
6477
  return extraProperties;
5847
6478
  }
5848
6479
 
5849
- export { axios$1 as $, datetime as A, poi as B, activeTemplate as C, mapPosition as D, indicator as E, currentUrl as F, currentCompareUrl as G, compareIndicator as H, comparePoi as I, states as J, actions as K, areChartsSeparateLayout as L, chartSpec as M, compareChartSpec as N, includesProcess as O, shouldShowChartWidget as P, sanitizeBbox as Q, mapCompareEl as R, getColFromLayer as S, setMapProjFromCol as T, isGlobe as U, setActiveTemplate as V, availableMapProjection as W, changeMapProjection as X, getCompareLayers as Y, compareChartData as Z, chartData as _, extractEoxLegendLink as a, removeLayers as a0, removeUnneededProperties as a1, extractLayerConfig as b, createLayerID as c, axios as d, extractRoles as e, fetchStyle as f, addTooltipInteraction as g, createAssetID as h, mapEl as i, getProjectionCode as j, isSTACItem as k, extractLayerTimeValues as l, mergeGeojsons as m, fetchApiItems as n, getDatetimeProperty as o, fetchAllStyles as p, findLayer as q, registerProjection as r, findLayersByLayerPrefix as s, replaceLayer as t, generateFeatures as u, getLayers as v, extractLayerLegend as w, extractCollectionUrls as x, generateLinksFromItems as y, revokeCollectionBlobUrls as z };
6480
+ /**
6481
+ * Locate the first sub-schema whose `format` matches by walking `properties`
6482
+ * and the `oneOf` / `allOf` / `anyOf` combinators. Returns the schema path
6483
+ * (array of keys/indices) from the root schema to the matched node, or
6484
+ * undefined if not found. Empty array means the input schema itself matched.
6485
+ *
6486
+ * @param {Record<string, any> | null | undefined} schema
6487
+ * @param {string} [format="bands"]
6488
+ * @returns {(string | number)[] | undefined}
6489
+ */
6490
+ function getBandsProperty(schema, format = "bands") {
6491
+ if (!schema || typeof schema !== "object") return undefined;
6492
+ if (schema.format === format) return [];
6493
+
6494
+ if (schema.properties) {
6495
+ for (const key of Object.keys(schema.properties)) {
6496
+ const sub = getBandsProperty(schema.properties[key], format);
6497
+ if (sub) return ["properties", key, ...sub];
6498
+ }
6499
+ }
6500
+
6501
+ for (const combinator of ["oneOf", "allOf", "anyOf"]) {
6502
+ if (!Array.isArray(schema[combinator])) continue;
6503
+ for (let i = 0; i < schema[combinator].length; i++) {
6504
+ const sub = getBandsProperty(schema[combinator][i], format);
6505
+ if (sub) return [combinator, i, ...sub];
6506
+ }
6507
+ }
6508
+
6509
+ return undefined;
6510
+ }
6511
+
6512
+ /**
6513
+ * Checks whether a GeoZarr layer's bands changed in the jsonform output and,
6514
+ * if so, rebuilds the source with the new 3 selected bands.
6515
+ * Gamma and rescale are handled automatically by `applyUpdatedStyles` via
6516
+ * `updateStyleVariables` — this function only manages source reconstruction.
6517
+ * Uses the existing source constructor to avoid import-version mismatches.
6518
+ *
6519
+ * @param {import("ol/layer/Layer").default} olLayer - Layer from layerConfig:change event
6520
+ * @param {Record<string, any>} jsonformValue - Current jsonform output
6521
+ * @returns {boolean} true if the source was rebuilt
6522
+ */
6523
+ function updateGeoZarrBands(olLayer, jsonformValue) {
6524
+ /** @type {import("@eox/map/src/layers").EOxLayerType<"WebGLTile","GeoZarr">} */
6525
+ const jsonLayer = olLayer.get("_jsonDefinition");
6526
+ const updatedBands = jsonformValue.bands;
6527
+ const isGeoZarr =
6528
+ jsonLayer?.type === "WebGLTile" && jsonLayer?.source?.type === "GeoZarr";
6529
+ if (!jsonLayer || !jsonLayer.source || !isGeoZarr || !updatedBands) {
6530
+ return false;
6531
+ }
6532
+
6533
+ const oldBands = jsonLayer.source?.bands;
6534
+ if (JSON.stringify(updatedBands) === JSON.stringify(oldBands)) {
6535
+ return false;
6536
+ }
6537
+ jsonLayer.source.bands = [...updatedBands];
6538
+ olLayer.setSource(
6539
+ new window.eoxMapAdvancedOlSources.GeoZarr(jsonLayer.source),
6540
+ );
6541
+ return true;
6542
+ }
6543
+
6544
+ /**
6545
+ * Safely appends query parameters to a URL string, preserving templates like {z}/{x}/{y}
6546
+ * @param {string} url
6547
+ * @param {Record<string, string>} params
6548
+ * @returns {string}
6549
+ */
6550
+ function appendQueryParams(url, params) {
6551
+ const [base, query] = url.split("?");
6552
+ const searchParams = new URLSearchParams(query || "");
6553
+
6554
+ for (const [key, val] of Object.entries(params)) {
6555
+ if (val !== undefined && val !== null && val !== "") {
6556
+ searchParams.set(key, val);
6557
+ } else {
6558
+ searchParams.delete(key);
6559
+ }
6560
+ }
6561
+
6562
+ const newQuery = searchParams.toString();
6563
+ return newQuery ? `${base}?${newQuery}` : base;
6564
+ }
6565
+
6566
+ /**
6567
+ * Checks whether a VectorTile layer's URL needs to be updated based on jsonform output.
6568
+ * If the style's jsonform schema has properties with `url_key` defined, their values
6569
+ * are injected as query parameters into the source URL.
6570
+ *
6571
+ * @param {import("ol/layer/Layer").default} olLayer - Layer from layerConfig:change event
6572
+ * @param {Record<string, any>} jsonformValue - Current jsonform output
6573
+ * @returns {boolean} true if the URL was updated
6574
+ */
6575
+ function updateLayerUrl(olLayer, jsonformValue) {
6576
+ const jsonLayer = olLayer.get("_jsonDefinition");
6577
+ if (!jsonLayer || jsonLayer.type !== "VectorTile") {
6578
+ return false;
6579
+ }
6580
+
6581
+ const schema = jsonLayer.properties?.layerConfig?.schema;
6582
+ const urlKeys = extractUrlKeys(schema);
6583
+
6584
+ if (Object.keys(urlKeys).length === 0) {
6585
+ return false;
6586
+ }
6587
+
6588
+ let originalUrl = olLayer.get("originalUrl") || jsonLayer.source?.url;
6589
+
6590
+ if (!originalUrl || typeof originalUrl !== "string") {
6591
+ return false;
6592
+ }
6593
+
6594
+ if (!olLayer.get("originalUrl")) {
6595
+ olLayer.set("originalUrl", originalUrl);
6596
+ }
6597
+
6598
+ /** @type {Record<string, string>} */
6599
+ const queryParamsToInject = {};
6600
+ for (const [propName, urlKey] of Object.entries(urlKeys)) {
6601
+ queryParamsToInject[urlKey] = jsonformValue[propName];
6602
+ }
6603
+
6604
+ const newUrl = appendQueryParams(originalUrl, queryParamsToInject);
6605
+
6606
+ if (jsonLayer.source?.url) {
6607
+ if (jsonLayer.source.url === newUrl) {
6608
+ return false;
6609
+ }
6610
+ jsonLayer.source.url = newUrl;
6611
+ if (olLayer.get("injectedUrl") === newUrl) {
6612
+ return false;
6613
+ }
6614
+ const source = olLayer.getSource();
6615
+ olLayer.set("injectedUrl", newUrl);
6616
+ if (source && "setUrl" in source) {
6617
+ /** @type {any} */ (source).setUrl(newUrl);
6618
+ return true;
6619
+ }
6620
+ if (source && "setUrls" in source) {
6621
+ /** @type {any} */ (source).setUrls([newUrl]);
6622
+ return true;
6623
+ }
6624
+ }
6625
+
6626
+ return false;
6627
+ }
6628
+
6629
+ /**
6630
+ * Applies titiler upscaling to an XYZ tile URL based on the matched endpoint config.
6631
+ * - titiler v1: appends `@2x` to the `{y}` tile coordinate
6632
+ * - titiler v2: adds `tilesize=512` query parameter (v2 removed the `@2x` suffix)
6633
+ * Plain strings in the config default to v1 behavior for backward compatibility.
6634
+ *
6635
+ * @param {string} url - The XYZ tile URL template
6636
+ * @param {Array<string | { url: string; titilerVersion?: 1 | 2 }>} upscalingEndpoints
6637
+ * @returns {{ url: string; tileSize: [number, number] } | null} null if no endpoint matches
6638
+ */
6639
+ function applyTitilerUpscaling(url, upscalingEndpoints) {
6640
+ const match = upscalingEndpoints.find((entry) => {
6641
+ const endpointUrl = typeof entry === "string" ? entry : entry.url;
6642
+ return url.includes(endpointUrl);
6643
+ });
6644
+
6645
+ if (!match) {
6646
+ return null;
6647
+ }
6648
+
6649
+ const version = typeof match === "string" ? 1 : (match.titilerVersion ?? 1);
6650
+
6651
+ if (version === 2) {
6652
+ const [base, query] = url.split("?");
6653
+ const params = new URLSearchParams(query);
6654
+ params.set("tilesize", "512");
6655
+ return { url: `${base}?${params.toString()}`, tileSize: [512, 512] };
6656
+ }
6657
+
6658
+ return { url: url.replace("{y}", "{y}@2x"), tileSize: [512, 512] };
6659
+ }
6660
+
6661
+ /**
6662
+ * Picks the render presets for a collection, preferring client-provided config
6663
+ * (`options.renders[collectionId]`) and falling back to the collection's own STAC
6664
+ * `renders` extension when no config entry exists.
6665
+ * @param {import("stac-ts").StacCollection | null | undefined} collection
6666
+ * @param {Record<string, Record<string, import("@/types").Render>> | undefined} [configRenders]
6667
+ * @returns {Record<string, import("@/types").Render> | undefined}
6668
+ */
6669
+ function resolveRenders(collection, configRenders) {
6670
+ const config = collection?.id ? configRenders?.[collection.id] : undefined;
6671
+ if (config) return config;
6672
+ return /** @type {Record<string, import("@/types").Render>|undefined} */ (
6673
+ collection?.renders ?? undefined
6674
+ );
6675
+ }
6676
+
6677
+ /**
6678
+ * TiTiler expects rescale as [min,max] pairs; chunks a flat numeric list
6679
+ * into pairs (e.g. [0,0.4,0,0.1] -> [[0,0.4],[0,0.1]]). Nested input passes through.
6680
+ * @param {number[]|number[][]|undefined} rescale - flat or nested rescale values
6681
+ * @returns {number[][]|undefined} rescale as [min,max] pairs
6682
+ */
6683
+ function normalizeRescale(rescale) {
6684
+ if (!rescale?.length || Array.isArray(rescale[0])) {
6685
+ return /** @type {number[][]|undefined} */ (rescale);
6686
+ }
6687
+ const pairs = [];
6688
+ for (let i = 0; i < rescale.length; i += 2) {
6689
+ pairs.push(/** @type {number[]} */ (rescale).slice(i, i + 2));
6690
+ }
6691
+ return pairs;
6692
+ }
6693
+
6694
+ /**
6695
+ * Drops NaN nodata values; NaN is already the implicit fill for float data,
6696
+ * so forwarding `nodata=nan` to TiTiler is redundant.
6697
+ * @param {string|number|undefined} nodata - nodata value from render/asset metadata
6698
+ * @returns {string|number|undefined} nodata, or undefined when it is NaN
6699
+ */
6700
+ function normalizeNodata(nodata) {
6701
+ if (typeof nodata === "number" && Number.isNaN(nodata)) return undefined;
6702
+ if (typeof nodata === "string" && nodata.trim().toLowerCase() === "nan")
6703
+ return undefined;
6704
+ return nodata;
6705
+ }
6706
+
6707
+ /**
6708
+ * Serializes an object into a TiTiler query string. Arrays repeat the key per
6709
+ * element (TiTiler list params, e.g. `assets=a&assets=b`); nested elements
6710
+ * comma-join (`rescale: [[0,1],[0,2]]` -> `rescale=0,1&rescale=0,2`); objects
6711
+ * are JSON-encoded. Shared by the render-extension and mosaic paths.
6712
+ * @param {Record<string,any>} obj
6713
+ * @returns {string}
6714
+ */
6715
+ function encodeURLObject(obj) {
6716
+ let str = "";
6717
+ for (const key in obj) {
6718
+ const value = obj[key];
6719
+ if (value === null || value === undefined || value === "") {
6720
+ continue;
6721
+ }
6722
+
6723
+ const valueType = Array.isArray(value) ? "array" : typeof value;
6724
+
6725
+ switch (valueType) {
6726
+ case "array": {
6727
+ for (const val of value) {
6728
+ if (Array.isArray(val)) {
6729
+ str += `${key}=${val.join(",")}&`;
6730
+ } else {
6731
+ str += `${key}=${encodeURIComponent(val)}&`;
6732
+ }
6733
+ }
6734
+ break;
6735
+ }
6736
+ case "object": {
6737
+ str += `${key}=${encodeURI(JSON.stringify(value))}&`;
6738
+ break;
6739
+ }
6740
+ default: {
6741
+ str += `${key}=${encodeURIComponent(value)}&`;
6742
+ break;
6743
+ }
6744
+ }
6745
+ }
6746
+ return str;
6747
+ }
6748
+
6749
+ export { compareChartSpec as $, findLayer as A, findLayersByLayerPrefix as B, replaceLayer as C, generateFeatures as D, dataThemesBrands as E, getLayers as F, extractLayerLegend as G, extractCollectionUrls as H, generateLinksFromItems as I, revokeCollectionBlobUrls as J, collectionsPalette as K, datetime as L, poi as M, activeTemplate as N, mapPosition as O, isFirstLoad as P, indicator as Q, currentUrl as R, currentCompareUrl as S, eodashCompareCollections as T, compareIndicator as U, comparePoi as V, eodashCollections as W, states as X, actions as Y, areChartsSeparateLayout as Z, chartSpec as _, extractRoles as a, loading as a0, includesProcess as a1, shouldShowChartWidget as a2, sanitizeBbox as a3, tooltipAdapter as a4, mapCompareEl as a5, updateGeoZarrBands as a6, updateLayerUrl as a7, layerControlFormValueCompare as a8, layerControlFormValue as a9, getColFromLayer as aa, isGlobe as ab, setMapProjFromCol as ac, setActiveTemplate as ad, availableMapProjection as ae, changeMapProjection as af, getCompareLayers as ag, compareChartData as ah, chartData as ai, axios$1 as aj, removeLayers as ak, removeUnneededProperties as al, extractEoxLegendLink as b, createLayerID as c, applyTitilerUpscaling as d, extractLayerConfig as e, fetchRasterForm as f, axios as g, resolveStyle as h, addTooltipInteraction as i, createAssetID as j, getBandsProperty as k, resolveRenders as l, mergeGeojsons as m, normalizeRescale as n, normalizeNodata as o, encodeURLObject as p, mapEl as q, registerProjection as r, getProjectionCode as s, isSTACItem as t, extractLayerTimeValues as u, fetchStyle as v, fetchApiItems as w, getDatetimeProperty as x, fetchPreAggregations as y, fetchAllStyles as z };