@eodash/eodash 5.7.1 → 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 (66) hide show
  1. package/core/client/eodashSTAC/EodashCollection.js +14 -5
  2. package/core/client/eodashSTAC/createLayers.js +193 -80
  3. package/core/client/eodashSTAC/helpers.js +330 -39
  4. package/core/client/eodashSTAC/mosaic.js +10 -4
  5. package/core/client/store/stac.js +29 -0
  6. package/core/client/types.ts +5 -0
  7. package/dist/client/{DashboardLayout-ePbdvUTE.js → DashboardLayout-DEs4f_Vy.js} +2 -2
  8. package/dist/client/{DynamicWebComponent-g5SdRbjY.js → DynamicWebComponent-PvnV88BA.js} +1 -1
  9. package/dist/client/EodashChart-D-3AGKZY.js +260 -0
  10. package/dist/client/{EodashDatePicker-CY1TrsDo.js → EodashDatePicker-Dm_goZzs.js} +3 -3
  11. package/dist/client/{EodashItemFilter-DzshHDlS.js → EodashItemFilter-CJy3zYzD.js} +2 -2
  12. package/dist/client/{EodashLayerControl-DMCeMkSd.js → EodashLayerControl-rL8UnpkA.js} +11 -19
  13. package/dist/client/{EodashLayoutSwitcher-iJ8mX1rF.js → EodashLayoutSwitcher-ZRlESI9O.js} +3 -3
  14. package/dist/client/{EodashMapBtns-C7Cr7q5Y.js → EodashMapBtns-CG7u2Aq8.js} +34 -33
  15. package/dist/client/{EodashStacInfo-K7-iuXa7.js → EodashStacInfo-CJhhxthD.js} +2 -2
  16. package/dist/client/{EodashTools-BEK7e7JM.js → EodashTools-CWyK8QJT.js} +4 -4
  17. package/dist/client/{ExportState-CqIzfMYQ.js → ExportState-BXhy5TOU.js} +19 -11
  18. package/dist/client/{Footer-1P0cmBOZ.js → Footer-DZPvnhcT.js} +1 -1
  19. package/dist/client/{Header-CkkBaBNh.js → Header-CiGsDE_a.js} +2 -2
  20. package/dist/client/{MobileLayout-BPf49kDU.js → MobileLayout-BJHW7x5V.js} +2 -2
  21. package/dist/client/{PopUp-DPZq5SWI.js → PopUp-7_XnhvCc.js} +3 -3
  22. package/dist/client/{ProcessList-DuI6WpS1.js → ProcessList-LL_UCbvc.js} +4 -4
  23. package/dist/client/{VDivider-Bj9LG_JV.js → VDivider-DWYM0WUr.js} +2 -2
  24. package/dist/client/{VImg-Ddv5OyMz.js → VImg-B75dgw2K.js} +2 -2
  25. package/dist/client/{VMain-7yqk_qC3.js → VMain-DvMmOL4f.js} +1 -1
  26. package/dist/client/{VTooltip-2hAJyEoa.js → VTooltip-ClWHUmWL.js} +2 -2
  27. package/dist/client/{WidgetsContainer-2DOrlwCB.js → WidgetsContainer-BqaWs4ch.js} +1 -1
  28. package/dist/client/{asWebComponent-BlFN2lkB.js → asWebComponent-IZanwL-A.js} +239 -91
  29. package/dist/client/{async-B2H8ZwUK.js → async-CiNIOBVz.js} +31 -29
  30. package/dist/client/{create-layers-config-u_vn5hPk.js → create-layers-config-3S3y_kZg.js} +1 -1
  31. package/dist/client/{dialog-transition-BrMkMiNa.js → dialog-transition-c4ZSnzac.js} +2 -2
  32. package/dist/client/eo-dash.js +1 -1
  33. package/dist/client/{forwardRefs-DAlror6d.js → forwardRefs-B-c1_6yo.js} +2 -2
  34. package/dist/client/{handling-B6I1V5eG.js → handling-NvY8Csry.js} +3 -4
  35. package/dist/client/{helpers-CBqIUSXJ.js → helpers-BFuh19CP.js} +328 -40
  36. package/dist/client/{index-DhKU6KqM.js → index-1wMAyuNL.js} +17 -9
  37. package/dist/client/{index-DXLz4sFR.js → index-B8jGJSTA.js} +4 -4
  38. package/dist/client/{index-DjQlbWbJ.js → index-BKQQJkhy.js} +166 -61
  39. package/dist/client/{index-BWq1ZKaV.js → index-D0jUyol8.js} +10 -11
  40. package/dist/client/{index-CJ9UI3ln.js → index-KGxz5Egv.js} +2 -2
  41. package/dist/client/{mosaic-BFYLEwyx.js → mosaic-vHvSuih6.js} +10 -6
  42. package/dist/client/templates.js +6 -7
  43. package/dist/client/{transition-D_W6Wn9v.js → transition-BkyUsZh_.js} +1 -1
  44. package/dist/types/core/client/eodashSTAC/helpers.d.ts +47 -7
  45. package/dist/types/core/client/store/stac.d.ts +8 -2
  46. package/dist/types/core/client/types.d.ts +5 -0
  47. package/dist/types/templates/explore.d.ts +1 -0
  48. package/dist/types/widgets/EodashProcess/methods/custom-endpoints/layers/eoxhub-workspaces-endpoint.d.ts +1 -1
  49. package/package.json +5 -5
  50. package/templates/baseConfig.js +2 -1
  51. package/templates/compare.js +0 -1
  52. package/templates/expert.js +2 -3
  53. package/templates/explore.js +1 -0
  54. package/templates/lite.js +0 -1
  55. package/widgets/EodashChart.vue +161 -21
  56. package/widgets/EodashItemCatalog/methods/map.js +0 -1
  57. package/widgets/EodashLayerControl.vue +10 -15
  58. package/widgets/EodashMap/index.vue +3 -1
  59. package/widgets/EodashMap/methods/btns.js +27 -26
  60. package/widgets/EodashMap/methods/use-map-loading.js +8 -2
  61. package/widgets/EodashProcess/index.vue +135 -16
  62. package/widgets/EodashProcess/methods/composables.js +13 -0
  63. package/widgets/EodashProcess/methods/outputs.js +0 -1
  64. package/widgets/EodashProcess/methods/utils.js +29 -27
  65. package/widgets/ExportState.vue +13 -4
  66. package/dist/client/EodashChart-ChrkA5oS.js +0 -145
@@ -192,9 +192,14 @@ export class EodashCollection {
192
192
 
193
193
  const isSupported =
194
194
  item.links.some((link) =>
195
- ["wms", "xyz", "wmts", "vector-tile", "mapbox-style-document"].includes(
196
- link.rel,
197
- ),
195
+ [
196
+ "wms",
197
+ "xyz",
198
+ "wmts",
199
+ "vector-tile",
200
+ "mapbox-style-document",
201
+ "tilejson",
202
+ ].includes(link.rel),
198
203
  ) || Object.keys(dataAssets).length;
199
204
 
200
205
  if (isSupported) {
@@ -356,7 +361,9 @@ export class EodashCollection {
356
361
  (/** @type {any} */ a) => a.key?.startsWith("datetime_") || a.interval,
357
362
  );
358
363
  if (datetimeAgg?.buckets) {
359
- return datetimeAgg.buckets.map((/** @type {any} */ b) => new Date(b.key));
364
+ return datetimeAgg.buckets
365
+ .map((/** @type {any} */ b) => new Date(b.key))
366
+ .filter((/** @type {Date} */ d) => !isNaN(d.getTime()));
360
367
  }
361
368
 
362
369
  const items = await this.getItems(true, false, centerDatetime);
@@ -371,7 +378,9 @@ export class EodashCollection {
371
378
  new Date(/** @type {string} */ (i.properties?.[datetimeProperty]))
372
379
  : //@ts-expect-error todo
373
380
  (i) => new Date(/** @type {string} */ (i[datetimeProperty]));
374
- return items?.map(mapToDates) || [];
381
+ return (items ?? [])
382
+ .map(mapToDates)
383
+ .filter((/** @type {Date} */ d) => !isNaN(d.getTime()));
375
384
  }
376
385
 
377
386
  async getExtent() {
@@ -1,6 +1,6 @@
1
1
  import { registerProjection } from "@/store/actions";
2
2
  import { mapEl } from "@/store/states";
3
- import axios from "@/plugins/axios";
3
+ import { useEodash } from "@/composables";
4
4
 
5
5
  import {
6
6
  extractRoles,
@@ -11,14 +11,18 @@ import {
11
11
  extractLayerConfig,
12
12
  extractEoxLegendLink,
13
13
  addTooltipInteraction,
14
- fetchStyle,
14
+ fetchRasterForm,
15
15
  resolveStyle,
16
16
  getBandsProperty,
17
17
  applyTitilerUpscaling,
18
18
  encodeURLObject,
19
+ normalizeRescale,
20
+ normalizeNodata,
21
+ resolveRenders,
19
22
  } from "./helpers";
20
23
  import { handleAuthenticationOfLink } from "./auth";
21
24
  import log from "loglevel";
25
+ import axios from "@/plugins/axios";
22
26
  import { useSTAcStore } from "@/store/stac";
23
27
 
24
28
  /**
@@ -438,6 +442,10 @@ export const createLayersFromLinks = async (
438
442
  item.links.filter((l) => l.rel === "vector-tile") ?? [];
439
443
  const mapboxStyleDocumentArray =
440
444
  item.links.filter((l) => l.rel === "mapbox-style-document") ?? [];
445
+ // An xyz link takes precedence over a tilejson link;
446
+ const tilejsonArray = xyzArray.length
447
+ ? []
448
+ : (item.links.filter((l) => l.rel === "tilejson") ?? []);
441
449
  // Taking projection code from main map view, as main view defines
442
450
  // projection for comparison map
443
451
  const viewProjectionCode = mapEl?.value?.projection || "EPSG:3857";
@@ -460,6 +468,20 @@ export const createLayersFromLinks = async (
460
468
  wmsLink,
461
469
  viewProjectionCode,
462
470
  );
471
+ const rasterForm = await fetchRasterForm(
472
+ /** @type {string|object|undefined} */ (
473
+ wmsLink?.["eodash:rasterform"] ||
474
+ item?.["eodash:rasterform"] ||
475
+ collection?.["eodash:rasterform"]
476
+ ),
477
+ );
478
+ let { layerConfig } = extractLayerConfig(
479
+ collectionId,
480
+ {},
481
+ rasterForm,
482
+ "tileUrl",
483
+ );
484
+
463
485
  log.debug("WMS Layer added", linkId);
464
486
  const tileSize = /** @type {number[]} */ (
465
487
  "wms:tilesize" in wmsLink
@@ -472,6 +494,7 @@ export const createLayersFromLinks = async (
472
494
  id: linkId,
473
495
  title: wmsLink.title || title || item.id,
474
496
  layerDatetime,
497
+ layerConfig,
475
498
  },
476
499
  source: {
477
500
  type: "TileWMS",
@@ -527,15 +550,18 @@ export const createLayersFromLinks = async (
527
550
  (wmtsLink?.["proj:epsg"] || wmtsLink?.["eodash:proj4_def"]);
528
551
 
529
552
  await registerProjection(wmtsLinkProjection);
530
- const key =
531
- /** @type {string | undefined} */ (wmtsLink["key"]) || undefined;
532
553
 
533
- const styles = await resolveStyle(item, collection, key);
534
- // get the correct style which is attached to a link
554
+ const rasterForm = await fetchRasterForm(
555
+ /** @type {string|object|undefined} */ (
556
+ wmtsLink?.["eodash:rasterform"] ||
557
+ item?.["eodash:rasterform"] ||
558
+ collection?.["eodash:rasterform"]
559
+ ),
560
+ );
535
561
  const returnedLayerConfig = extractLayerConfig(
536
562
  collectionId,
537
- styles,
538
- undefined,
563
+ {},
564
+ rasterForm,
539
565
  "tileUrl",
540
566
  );
541
567
  const projectionCode = getProjectionCode(wmtsLinkProjection || "EPSG:3857");
@@ -558,14 +584,6 @@ export const createLayersFromLinks = async (
558
584
  // TODO, this does not yet work between layer time changes because we do not get
559
585
  // updated variables from OL layer due to usage of tileurlfunction
560
586
 
561
- // update dimensions with current value of style variables if applicable
562
- const variables = returnedLayerConfig?.style?.variables;
563
- if (variables) {
564
- for (const [kk, vv] of Object.entries(variables)) {
565
- dimensionsWithoutStyle[kk] = vv;
566
- }
567
- }
568
-
569
587
  if (wmtsLink.href.includes("marine.copernicus")) {
570
588
  log.debug(
571
589
  "Warning: WMTS Layer from capabilities added, function needs to be updated",
@@ -606,6 +624,7 @@ export const createLayersFromLinks = async (
606
624
  id: linkId,
607
625
  title: wmtsLink.title || title || item.id,
608
626
  layerDatetime,
627
+ layerConfig: returnedLayerConfig.layerConfig,
609
628
  },
610
629
  source: {
611
630
  type: "WMTSCapabilities",
@@ -635,19 +654,16 @@ export const createLayersFromLinks = async (
635
654
  const xyzLinkProjection =
636
655
  /** @type {number | string | {name: string, def: string} | undefined} */
637
656
  (xyzLink?.["proj:epsg"] || xyzLink?.["eodash:proj4_def"]);
638
- const key = /** @type {string | undefined} */ (xyzLink["key"]) || undefined;
639
- const rasterformURL = /** @type {string|undefined} */ (
640
- collection?.["eodash:rasterform"]
657
+ const rasterForm = await fetchRasterForm(
658
+ /** @type {string|object|undefined} */ (
659
+ xyzLink?.["eodash:rasterform"] ||
660
+ item?.["eodash:rasterform"] ||
661
+ collection?.["eodash:rasterform"]
662
+ ),
641
663
  );
642
- /** @type {import("@/types").EodashRasterJSONForm|undefined} */
643
- const rasterForm = rasterformURL
644
- ? await axios.get(rasterformURL).then((resp) => resp.data)
645
- : undefined;
646
- const styles = await resolveStyle(item, collection, key);
647
- // get the correct style which is attached to a link
648
- let { layerConfig, style } = extractLayerConfig(
664
+ let { layerConfig } = extractLayerConfig(
649
665
  collectionId,
650
- styles,
666
+ {},
651
667
  rasterForm,
652
668
  "tileUrl",
653
669
  );
@@ -660,19 +676,6 @@ export const createLayersFromLinks = async (
660
676
  viewProjectionCode,
661
677
  );
662
678
  let xyzUrl = xyzLink.href;
663
- // TODO, this does not yet work between layer time changes because we do not get
664
- // updated variables from OL layer due to usage of tileurlfunction
665
-
666
- // update url query params with current value of style variables if applicable
667
- const variables = style?.variables;
668
- if (variables) {
669
- const [base, query] = xyzUrl.split("?");
670
- const params = new URLSearchParams(query);
671
- for (const [kk, vv] of Object.entries(variables)) {
672
- params.set(kk, JSON.stringify(vv));
673
- }
674
- xyzUrl = `${base}?${params.toString()}`;
675
- }
676
679
  const { supportedUpscalingEndpoints } = useSTAcStore();
677
680
  const upscaling = applyTitilerUpscaling(
678
681
  xyzUrl,
@@ -731,6 +734,96 @@ export const createLayersFromLinks = async (
731
734
  jsonArray.push(json);
732
735
  }
733
736
 
737
+ for (const tilejsonLink of tilejsonArray) {
738
+ // The tilejson href is a complete URL with the render params baked in by the
739
+ // STAC producer; fetch it and use its `tiles[0]` template as an XYZ source.
740
+ const tileJSON = await axios
741
+ .get(tilejsonLink.href)
742
+ .then((res) => res.data)
743
+ .catch((err) => {
744
+ console.error("[eodash] Failed to fetch item TileJSON", err);
745
+ return null;
746
+ });
747
+ if (!tileJSON?.tiles?.[0]) {
748
+ console.warn(
749
+ "[eodash] No tile URL in item TileJSON response",
750
+ tilejsonLink.href,
751
+ );
752
+ continue;
753
+ }
754
+ // Only raster XYZ tiles are supported; skip vector & TMS scheme TileJSON
755
+ if (tileJSON.vector_layers || tileJSON.scheme === "tms") {
756
+ console.warn(
757
+ "[eodash] Unsupported TileJSON (only raster XYZ is supported)",
758
+ tilejsonLink.href,
759
+ );
760
+ continue;
761
+ }
762
+
763
+ const tilejsonProjection =
764
+ /** @type {number | string | {name: string, def: string} | undefined} */
765
+ (tilejsonLink?.["proj:epsg"] || tilejsonLink?.["eodash:proj4_def"]);
766
+ await registerProjection(tilejsonProjection);
767
+ const projectionCode = getProjectionCode(tilejsonProjection || "EPSG:3857");
768
+ const rasterForm = await fetchRasterForm(
769
+ /** @type {string|object|undefined} */ (
770
+ tilejsonLink?.["eodash:rasterform"] ||
771
+ item?.["eodash:rasterform"] ||
772
+ collection?.["eodash:rasterform"]
773
+ ),
774
+ );
775
+ const { layerConfig } = extractLayerConfig(
776
+ collectionId,
777
+ {},
778
+ rasterForm,
779
+ "tileUrl",
780
+ );
781
+ const linkId = createLayerID(
782
+ collectionId,
783
+ item.id,
784
+ tilejsonLink,
785
+ viewProjectionCode,
786
+ );
787
+
788
+ log.debug("TileJSON layer added", linkId);
789
+ /** @type {Record<string, any>} */
790
+ const json = {
791
+ type: "Tile",
792
+ properties: {
793
+ id: linkId,
794
+ title: tilejsonLink.title || title || item.id,
795
+ roles: tilejsonLink.roles,
796
+ layerDatetime,
797
+ layerConfig,
798
+ },
799
+ source: {
800
+ type: "XYZ",
801
+ ...(tileJSON.tiles.length > 1
802
+ ? { urls: tileJSON.tiles }
803
+ : { url: tileJSON.tiles[0] }),
804
+ projection: projectionCode,
805
+ // Link attribution wins; the TileJSON document's own is the fallback.
806
+ ...(tilejsonLink.attribution || tileJSON.attribution
807
+ ? {
808
+ attributions: tilejsonLink.attribution || tileJSON.attribution,
809
+ }
810
+ : {}),
811
+ },
812
+ };
813
+ if (Number.isFinite(tileJSON.minzoom)) json.minZoom = tileJSON.minzoom;
814
+ if (Number.isFinite(tileJSON.maxzoom)) json.maxZoom = tileJSON.maxzoom;
815
+
816
+ extractRoles(json.properties, tilejsonLink);
817
+ if (extraProperties !== null) {
818
+ json.properties = {
819
+ ...json.properties,
820
+ ...extraProperties,
821
+ ...extractEoxLegendLink(tilejsonLink),
822
+ };
823
+ }
824
+ jsonArray.push(json);
825
+ }
826
+
734
827
  for (const vectorTileLink of vectorTileArray ?? []) {
735
828
  const vectorTileLinkProjection =
736
829
  /** @type {number | string | {name: string, def: string} | undefined} */
@@ -882,72 +975,85 @@ export const createLayerFromRender = async (
882
975
  item,
883
976
  extraProperties,
884
977
  ) => {
885
- if (!collection || !collection.renders || !item) {
978
+ // config renders > collection STAC renders > item renders
979
+ const renders = /** @type {Record<string,import("@/types").Render>} */ (
980
+ resolveRenders(collection, useEodash()?.options?.renders) ?? item?.renders
981
+ );
982
+ if (!collection || !item || !renders) {
886
983
  return [];
887
984
  }
888
985
 
889
- // Skip when an explicit xyz link already targets the collection on the same endpoint
890
- const hasMatchingXyzLink = item.links?.some(
986
+ // Yield to a raster xyz/tilejson link createLayersFromLinks renders it.
987
+ const hasMatchingTileLink = item.links?.some(
891
988
  (link) =>
892
- link.rel === "xyz" &&
893
- link.href?.includes(rasterURL) &&
894
- link.href?.includes(`/collections/${collection.id}/`),
989
+ (link.rel === "xyz" || link.rel === "tilejson") &&
990
+ link.href?.includes(rasterURL),
895
991
  );
896
- if (hasMatchingXyzLink) {
992
+ if (hasMatchingTileLink) {
897
993
  return [];
898
994
  }
899
995
 
900
- const rasterformURL = /** @type {string|undefined} */ (
901
- collection?.["eodash:rasterform"]
902
- );
903
- /** @type {import("@/types").EodashRasterJSONForm|undefined} */
904
- const rasterForm = rasterformURL
905
- ? await axios.get(rasterformURL).then((resp) => resp.data)
906
- : undefined;
907
- let { layerConfig } = extractLayerConfig(
908
- collection.id,
909
- await fetchStyle(item),
910
- rasterForm,
996
+ const rasterForm = await fetchRasterForm(
997
+ /** @type {string|object|undefined} */ (
998
+ item?.["eodash:rasterform"] || collection?.["eodash:rasterform"]
999
+ ),
911
1000
  );
1001
+ let { layerConfig } = extractLayerConfig(collection.id, {}, rasterForm);
1002
+
1003
+ /**
1004
+ * Resolves the first defined value of a property across a render's assets,
1005
+ * checking item assets before falling back to collection assets.
1006
+ * @param {import("@/types").Render} render
1007
+ * @param {string} propertyName
1008
+ * @returns {any}
1009
+ */
1010
+ const getRenderAssetProperty = (render, propertyName) => {
1011
+ for (const assetKey of render.assets ?? []) {
1012
+ const asset = item?.assets?.[assetKey] ?? collection?.assets?.[assetKey];
1013
+ const value = asset?.[propertyName];
1014
+ if (value !== undefined) {
1015
+ return value;
1016
+ }
1017
+ }
1018
+ return undefined;
1019
+ };
912
1020
 
913
- const renders = /** @type {Record<string,import("@/types").Render>} */ (
914
- collection.renders ?? item?.renders
915
- );
916
1021
  const layers = [];
917
- // special case for rescale
918
1022
  for (const key in renders) {
919
1023
  const title = renders[key].title;
920
1024
 
921
- const assetsCollection =
922
- renders[key].assets[0] in item["assets"] ? item : collection;
1025
+ const expression =
1026
+ renders[key].expression ??
1027
+ getRenderAssetProperty(renders[key], "expression");
923
1028
 
924
1029
  const paramsObject = {
925
- assets: renders[key].assets,
926
- expression:
927
- renders[key].expression ??
928
- assetsCollection["assets"]?.[renders[key].assets[0]]?.expression,
929
- nodata:
930
- renders[key].nodata ??
931
- assetsCollection["assets"]?.[renders[key].assets[0]]?.nodata,
1030
+ // TiTiler treats assets and expression as mutually exclusive band selection
1031
+ assets: expression ? undefined : renders[key].assets,
1032
+ expression,
1033
+ nodata: normalizeNodata(
1034
+ renders[key].nodata ?? getRenderAssetProperty(renders[key], "nodata"),
1035
+ ),
932
1036
  resampling:
933
1037
  renders[key].resampling ??
934
- assetsCollection["assets"]?.[renders[key].assets[0]]?.resampling,
1038
+ getRenderAssetProperty(renders[key], "resampling"),
935
1039
  color_formula:
936
1040
  renders[key].color_formula ??
937
- assetsCollection["assets"]?.[renders[key].assets[0]]?.color_formula,
1041
+ getRenderAssetProperty(renders[key], "color_formula"),
938
1042
  colormap:
939
1043
  renders[key].colormap ??
940
- assetsCollection["assets"]?.[renders[key].assets[0]]?.colormap,
1044
+ getRenderAssetProperty(renders[key], "colormap"),
941
1045
  colormap_name:
942
1046
  renders[key].colormap_name ??
943
- assetsCollection["assets"]?.[renders[key].assets[0]]?.colormap_name,
944
- rescale:
945
- renders[key].rescale ??
946
- assetsCollection["assets"]?.[renders[key].assets[0]]?.rescale,
1047
+ getRenderAssetProperty(renders[key], "colormap_name"),
1048
+ rescale: normalizeRescale(
1049
+ renders[key].rescale ?? getRenderAssetProperty(renders[key], "rescale"),
1050
+ ),
1051
+ bidx: renders[key].bidx,
1052
+ tilesize: renders[key].tilesize,
947
1053
  };
948
1054
  const paramsStr = encodeURLObject(paramsObject);
949
1055
  const url = `${rasterURL}/collections/${collection.id}/items/${item.id}/tiles/WebMercatorQuad/{z}/{x}/{y}?${paramsStr}`;
950
- layers.push({
1056
+ const json = {
951
1057
  /** @type {"Tile"} */
952
1058
  type: "Tile",
953
1059
  properties: {
@@ -970,7 +1076,14 @@ export const createLayerFromRender = async (
970
1076
  url,
971
1077
  projection: "EPSG:3857",
972
1078
  },
973
- });
1079
+ };
1080
+ if (renders[key].tilesize) {
1081
+ // @ts-expect-error tileGrid is added here and supported in eox-map layer definition
1082
+ json.source.tileGrid = {
1083
+ tileSize: [renders[key].tilesize, renders[key].tilesize],
1084
+ };
1085
+ }
1086
+ layers.push(json);
974
1087
  }
975
1088
 
976
1089
  return layers;