@reearth/core 0.0.7-alpha.62 → 0.0.7-alpha.63

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Generic provider configuration for overriding external data sources.
3
+ *
4
+ * - imagery.providers: imagery tile providers keyed by id
5
+ * - layers.providers: 3D tileset providers keyed by id
6
+ *
7
+ * Terrain is configured via viewer/engine terrain settings (see useTerrainProviderPromise)
8
+ * or via each layer's own `url` field.
9
+ */
10
+
11
+ export type ImageryProviderEntry = {
12
+ id: string;
13
+ url: string;
14
+ credit?: string;
15
+ maximumLevel?: number;
16
+ minimumLevel?: number;
17
+ };
18
+
19
+ export type LayerProviderEntry = {
20
+ id: string;
21
+ url: string;
22
+ options?: Record<string, unknown>;
23
+ };
24
+
25
+ export type CustomProviderConfig = {
26
+ imagery?: {
27
+ providers?: ImageryProviderEntry[];
28
+ };
29
+ layers?: {
30
+ providers?: LayerProviderEntry[];
31
+ };
32
+ };
@@ -47,6 +47,7 @@ import {
47
47
  } from "../Sketch/types";
48
48
  import type { TimelineManagerRef } from "../useTimelineManager";
49
49
 
50
+ import type { CustomProviderConfig } from "./customProvider";
50
51
  import type { SceneMode, ViewerProperty } from "./viewerProperty";
51
52
 
52
53
  export type {
@@ -79,6 +80,7 @@ export type {
79
80
  } from "../../mantle";
80
81
  export * from "./event";
81
82
  export * from "./viewerProperty";
83
+ export * from "./customProvider";
82
84
 
83
85
  export type EngineRef = {
84
86
  name: string;
@@ -237,6 +239,7 @@ export type EngineProps = {
237
239
  isLayerDragging?: boolean;
238
240
  shouldRender?: boolean;
239
241
  meta?: Record<string, unknown>;
242
+ customProvider?: CustomProviderConfig;
240
243
  displayCredits?: boolean;
241
244
  layersRef?: RefObject<LayersRef | null>;
242
245
  requestingRenderMode?: MutableRefObject<RequestingRenderMode>;
@@ -74,7 +74,7 @@ export type GlobeAtmosphereProperty = {
74
74
 
75
75
  export type TerrainProperty = {
76
76
  enabled?: boolean;
77
- type?: "cesium" | "arcgis" | "cesiumion";
77
+ type?: "cesium" | "cesiumion" | "reearth_terrain";
78
78
  url?: string;
79
79
  normal?: boolean;
80
80
  elevationHeatMap?: ElevationHeatMapProperty;
@@ -130,6 +130,7 @@ export type TileProperty = {
130
130
  id: string;
131
131
  type?: string;
132
132
  url?: string;
133
+ cesiumIonAssetId?: number;
133
134
  opacity?: number;
134
135
  zoomLevel?: number[];
135
136
  zoomLevelForURL?: number[];
@@ -13,6 +13,7 @@ import {
13
13
  type Cluster,
14
14
  type ComputedLayer,
15
15
  type Credits,
16
+ type CustomProviderConfig,
16
17
  } from "../Map";
17
18
  import { SketchFeature, SketchType } from "../Map/Sketch/types";
18
19
  import type { InteractionModeType } from "../shared/interactionMode";
@@ -42,6 +43,7 @@ export type CoreVisualizerProps = {
42
43
  interactionMode?: InteractionModeType;
43
44
  shouldRender?: boolean;
44
45
  meta?: Record<string, unknown>;
46
+ customProvider?: CustomProviderConfig;
45
47
  style?: CSSProperties;
46
48
  small?: boolean;
47
49
  ready?: boolean;
@@ -85,6 +87,7 @@ export const CoreVisualizer = memo(
85
87
  interactionMode,
86
88
  shouldRender,
87
89
  meta,
90
+ customProvider,
88
91
  displayCredits = true,
89
92
  style,
90
93
  zoomedLayerId,
@@ -165,6 +168,7 @@ export const CoreVisualizer = memo(
165
168
  isLayerDragging={isLayerDragging}
166
169
  isLayerDraggable={isEditable}
167
170
  meta={meta}
171
+ customProvider={customProvider}
168
172
  displayCredits={displayCredits}
169
173
  style={style}
170
174
  featureFlags={featureFlags}
@@ -2,6 +2,7 @@ import {
2
2
  Cesium3DTileset as Cesium3DTilesetType,
3
3
  Cesium3DTileStyle,
4
4
  IonResource,
5
+ Resource,
5
6
  ClippingPlane,
6
7
  ClippingPlaneCollection as CesiumClippingPlaneCollection,
7
8
  Cartesian3,
@@ -75,6 +76,7 @@ const useData = (layer: ComputedLayer | undefined) => {
75
76
  : data?.layers
76
77
  : undefined,
77
78
  googleMapApiKey: data?.serviceTokens?.googleMapApiKey,
79
+ provider: data?.provider,
78
80
  };
79
81
  }, [layer]);
80
82
  };
@@ -457,7 +459,7 @@ export const useHooks = ({
457
459
  }) => {
458
460
  const { viewer } = useCesium();
459
461
  const tilesetRef = useRef<Cesium3DTilesetType>(undefined);
460
- const { onLayerLoad, updateCredits } = useContext();
462
+ const { onLayerLoad, updateCredits, customProvider } = useContext();
461
463
  const layerIdRef = useRef(layer?.id);
462
464
  layerIdRef.current = layer?.id;
463
465
 
@@ -487,7 +489,7 @@ export const useHooks = ({
487
489
  } = useClippingBox({ clipping: experimental_clipping, boxId });
488
490
 
489
491
  const [style, setStyle] = useState<Cesium3DTileStyle>();
490
- const { url, type, idProperty, googleMapApiKey } = useData(layer);
492
+ const { url, type, idProperty, googleMapApiKey, provider } = useData(layer);
491
493
  const shouldUseFeatureIndex = !disableIndexingFeature && !!idProperty;
492
494
 
493
495
  const [isTilesetReady, setIsTilesetReady] = useState(false);
@@ -752,20 +754,30 @@ export const useHooks = ({
752
754
  }
753
755
  }, [style, isTilesetReady]);
754
756
 
755
- const googleMapPhotorealisticResource = useMemo(() => {
757
+ const googleMapPhotorealisticResource = useMemo((): string | Promise<Resource> | null => {
756
758
  if (type !== "google-photorealistic" || !isVisible) return null;
757
759
 
758
- const loadTileset = async () => {
760
+ // For Re:Earth provider, use the custom URL from customProvider or layer data
761
+ if (provider === "reearth") {
762
+ // Try to get URL from customProvider first, then fall back to layer URL
763
+ const customUrl = customProvider?.layers?.providers?.find(
764
+ p => p.id === "reearth_google_photorealistic_3d_tiles",
765
+ )?.url;
766
+
767
+ return customUrl || url || null;
768
+ }
769
+
770
+ // Otherwise load via Google API key or Cesium Ion.
771
+ const loadTileset = async (): Promise<Resource> => {
759
772
  try {
760
- if (googleMapApiKey) {
761
- const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey });
762
- return tileset.resource;
763
- } else {
764
- const resource = IonResource.fromAssetId(2275207, {
773
+ if (provider === "cesium-ion" || !googleMapApiKey) {
774
+ const resource = await IonResource.fromAssetId(2275207, {
765
775
  accessToken: meta?.cesiumIonAccessToken as string | undefined,
766
776
  });
767
777
  return resource;
768
778
  }
779
+ const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey });
780
+ return tileset.resource;
769
781
  } catch (error) {
770
782
  console.error(`Error loading Photorealistic 3D Tiles tileset: ${error}`);
771
783
  throw error;
@@ -773,19 +785,35 @@ export const useHooks = ({
773
785
  };
774
786
 
775
787
  return loadTileset();
776
- }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken]);
777
-
778
- const tilesetUrl = useMemo(() => {
779
- return type === "osm-buildings" && isVisible
780
- ? IonResource.fromAssetId(96188, {
781
- accessToken: meta?.cesiumIonAccessToken as string | undefined,
782
- }) // https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Scene/createOsmBuildings.js#L53
783
- : googleMapPhotorealisticResource && isVisible
784
- ? googleMapPhotorealisticResource
785
- : type === "3dtiles" && isVisible
786
- ? (url ?? tileset)
787
- : null;
788
- }, [type, isVisible, meta?.cesiumIonAccessToken, googleMapPhotorealisticResource, url, tileset]);
788
+ }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, provider, customProvider, url]);
789
+
790
+ const tilesetUrl = useMemo((): string | Resource | Promise<Resource> | null => {
791
+ if (!isVisible) return null;
792
+
793
+ // Google Photorealistic 3D Tiles
794
+ if (googleMapPhotorealisticResource) {
795
+ return googleMapPhotorealisticResource;
796
+ }
797
+
798
+ // Re:Earth Buildings — use layer's own url if provided, otherwise fall back to public service
799
+ if (type === "reearth-buildings") {
800
+ return url ?? "https://buildings.reearth.land/tileset.json";
801
+ }
802
+
803
+ // OSM Buildings — only available via Cesium Ion.
804
+ if (type === "osm-buildings") {
805
+ return IonResource.fromAssetId(96188, {
806
+ accessToken: meta?.cesiumIonAccessToken as string | undefined,
807
+ }); // https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Scene/createOsmBuildings.js#L53
808
+ }
809
+
810
+ // Standard 3D Tiles with explicit URL
811
+ if (type === "3dtiles") {
812
+ return url ?? tileset ?? null;
813
+ }
814
+
815
+ return null;
816
+ }, [type, isVisible, googleMapPhotorealisticResource, url, tileset, meta?.cesiumIonAccessToken]);
789
817
 
790
818
  const imageBasedLighting = useMemo(() => {
791
819
  if (
@@ -3,12 +3,14 @@ import { createContext, useContext as useReactContext } from "react";
3
3
 
4
4
  import type { Camera, LayerSelectionReason } from "../..";
5
5
  import { LayerEditEvent, LayerLoadEvent, LayerVisibilityEvent } from "../../../Map";
6
+ import type { CustomProviderConfig } from "../../../Map/types/customProvider";
6
7
  import { TimelineManagerRef } from "../../../Map/useTimelineManager";
7
8
  import type { FlyTo } from "../../../types";
8
9
 
9
10
  export type Context = {
10
11
  selectionReason?: LayerSelectionReason;
11
12
  timelineManagerRef?: TimelineManagerRef;
13
+ customProvider?: CustomProviderConfig;
12
14
  getCamera?: () => Camera | undefined;
13
15
  flyTo?: FlyTo;
14
16
  onLayerEdit?: (e: LayerEditEvent) => void;
@@ -65,6 +65,7 @@ const displayConfig: Record<DataType, (keyof typeof components)[] | "auto"> = {
65
65
  tms: ["raster"],
66
66
  "3dtiles": ["3dtiles"],
67
67
  "osm-buildings": ["3dtiles"],
68
+ "reearth-buildings": ["3dtiles"],
68
69
  "google-photorealistic": ["3dtiles"],
69
70
  gpx: "auto",
70
71
  shapefile: "auto",
@@ -39,17 +39,13 @@ export default function Globe({
39
39
  providerPromise
40
40
  .then(resolvedProvider => {
41
41
  if (isCancelled) return;
42
-
43
- // Only trigger callback if the resolved provider is actually different
44
42
  if (lastResolvedProviderRef.current !== resolvedProvider) {
45
43
  lastResolvedProviderRef.current = resolvedProvider;
46
44
  onTerrainProviderChange?.();
47
45
  }
48
46
  })
49
47
  .catch(error => {
50
- if (!isCancelled) {
51
- console.warn("Terrain provider failed to load:", error);
52
- }
48
+ if (!isCancelled) console.warn("Terrain provider failed to load:", error);
53
49
  });
54
50
 
55
51
  return () => {
@@ -1,5 +1,4 @@
1
1
  import {
2
- ArcGISTiledElevationTerrainProvider,
3
2
  CesiumTerrainProvider,
4
3
  EllipsoidTerrainProvider,
5
4
  IonResource,
@@ -12,6 +11,8 @@ import { AssetsCesiumProperty } from "../../../../Map";
12
11
 
13
12
  type TerrainType = NonNullable<TerrainProperty["type"]>;
14
13
 
14
+ const REEARTH_TERRAIN_URL = "https://terrain.reearth.land/cesium-mesh/ellipsoid";
15
+
15
16
  type ProviderOpts = Pick<TerrainProperty, "normal"> &
16
17
  AssetsCesiumProperty["terrain"] & {
17
18
  terrain?: boolean;
@@ -20,13 +21,11 @@ type ProviderOpts = Pick<TerrainProperty, "normal"> &
20
21
  };
21
22
 
22
23
  export default function useTerrainProviderPromise(opts: ProviderOpts) {
23
- // Cache promises so we don’t recreate providers on every toggle
24
24
  const cacheRef = useRef(new Map<string, Promise<TerrainProvider>>());
25
25
  const ellipsoidRef = useRef<TerrainProvider>(undefined);
26
26
 
27
27
  return useMemo<Promise<TerrainProvider>>(() => {
28
28
  if (!opts.terrain) {
29
- // single shared ellipsoid provider
30
29
  if (!ellipsoidRef.current) ellipsoidRef.current = new EllipsoidTerrainProvider();
31
30
  return Promise.resolve(ellipsoidRef.current);
32
31
  }
@@ -43,31 +42,30 @@ export default function useTerrainProviderPromise(opts: ProviderOpts) {
43
42
  }
44
43
 
45
44
  function makeKey(type: TerrainType, opts: ProviderOpts) {
46
- // Key should change when output provider would differ
47
45
  const asset = opts.ionAsset ?? "";
48
46
  const url = opts.ionUrl ?? "";
47
+ const ionToken = opts.ionAccessToken ?? "";
49
48
  const normal = String(!!opts.normal);
50
- return `${type}|asset:${asset}|url:${url}|normal:${normal}`;
49
+ return `${type}|asset:${asset}|url:${url}|ion:${ionToken}|normal:${normal}`;
51
50
  }
52
51
 
53
52
  function createProvider(type: TerrainType, opts: ProviderOpts): Promise<TerrainProvider> {
54
53
  switch (type) {
55
- case "cesium":
54
+ case "reearth_terrain":
55
+ return CesiumTerrainProvider.fromUrl(REEARTH_TERRAIN_URL, {
56
+ requestVertexNormals: !!opts.normal,
57
+ requestWaterMask: false,
58
+ }) as Promise<TerrainProvider>;
59
+
60
+ case "cesium": {
56
61
  return CesiumTerrainProvider.fromUrl(
57
62
  IonResource.fromAssetId(1, { accessToken: opts.ionAccessToken }),
58
63
  { requestVertexNormals: !!opts.normal, requestWaterMask: false },
59
64
  ) as Promise<TerrainProvider>;
65
+ }
60
66
 
61
- case "arcgis":
62
- return ArcGISTiledElevationTerrainProvider.fromUrl(
63
- "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer",
64
- ) as Promise<TerrainProvider>;
65
-
66
- case "cesiumion":
67
- if (!opts.ionAsset && !opts.ionUrl) {
68
- // Fallback to ellipsoid when misconfigured
69
- return Promise.resolve(new EllipsoidTerrainProvider());
70
- }
67
+ case "cesiumion": {
68
+ if (!opts.ionAsset && !opts.ionUrl) return Promise.resolve(new EllipsoidTerrainProvider());
71
69
  return CesiumTerrainProvider.fromUrl(
72
70
  opts.ionUrl ??
73
71
  IonResource.fromAssetId(parseInt(String(opts.ionAsset), 10), {
@@ -75,5 +73,9 @@ function createProvider(type: TerrainType, opts: ProviderOpts): Promise<TerrainP
75
73
  }),
76
74
  { requestVertexNormals: !!opts.normal },
77
75
  ) as Promise<TerrainProvider>;
76
+ }
77
+
78
+ default:
79
+ return Promise.resolve(new EllipsoidTerrainProvider());
78
80
  }
79
81
  }
@@ -1,119 +1,154 @@
1
1
  import { renderHook } from "@testing-library/react";
2
+ import { UrlTemplateImageryProvider } from "cesium";
2
3
  import { expect, test, vi } from "vitest";
3
4
 
5
+ import type { CustomProviderConfig } from "../../../Map/types/customProvider";
6
+
4
7
  import { type Tile, useImageryProviders } from "./Imagery";
5
8
 
6
9
  test("useImageryProviders", () => {
7
10
  const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url }));
8
- const provider2 = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge2: url }));
9
- const presets = { default: provider, default_label: provider2 };
11
+ const osmProvider = vi.fn((): any => ({ osm: true }));
12
+
13
+ const presets = {
14
+ cesium_ion_default: provider,
15
+ open_street_map: osmProvider,
16
+ } as any;
17
+
10
18
  const { result, rerender } = renderHook(
11
- ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) =>
19
+ ({
20
+ tiles,
21
+ cesiumIonAccessToken,
22
+ customProvider,
23
+ }: {
24
+ tiles: Tile[];
25
+ cesiumIonAccessToken?: string;
26
+ customProvider?: CustomProviderConfig;
27
+ }) =>
12
28
  useImageryProviders({
13
29
  tiles,
14
30
  presets,
15
31
  cesiumIonAccessToken,
32
+ customProvider,
16
33
  }),
17
- { initialProps: { tiles: [{ id: "1", type: "default" }], cesiumIonAccessToken: undefined } },
34
+ {
35
+ initialProps: {
36
+ tiles: [{ id: "1", type: "cesium_ion_default" }],
37
+ cesiumIonAccessToken: undefined,
38
+ customProvider: undefined,
39
+ },
40
+ },
18
41
  );
19
42
 
20
43
  const typedRerender = rerender as (props: {
21
44
  tiles: Tile[];
22
45
  cesiumIonAccessToken?: string;
46
+ customProvider?: CustomProviderConfig;
23
47
  }) => void;
24
48
 
25
- expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] });
49
+ expect(result.current.providers).toEqual({
50
+ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }],
51
+ });
26
52
  expect(result.current.updated).toBe(true);
27
53
  expect(provider).toBeCalledTimes(1);
28
- const prevImageryProvider = result.current.providers["1"][2];
54
+ const prevImageryProvider = result.current.providers["1"][3];
29
55
 
30
56
  // re-render with same tiles
31
- typedRerender({ tiles: [{ id: "1", type: "default" }] });
57
+ typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default" }] });
32
58
 
33
- expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] });
34
- expect(result.current.providers["1"][2]).toBe(prevImageryProvider); // 1's provider should be reused
59
+ expect(result.current.providers).toEqual({
60
+ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }],
61
+ });
62
+ expect(result.current.providers["1"][3]).toBe(prevImageryProvider); // 1's provider should be reused
35
63
  expect(provider).toBeCalledTimes(1);
36
64
 
37
65
  // update a tile URL
38
- typedRerender({ tiles: [{ id: "1", type: "default", url: "a" }] });
66
+ typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default", url: "a" }] });
39
67
 
40
- expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }] });
41
- expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider);
68
+ expect(result.current.providers).toEqual({
69
+ "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }],
70
+ });
71
+ expect(result.current.providers["1"][3]).not.toBe(prevImageryProvider);
42
72
  expect(result.current.updated).toBe(true);
43
73
  expect(provider).toBeCalledTimes(2);
44
74
  expect(provider).toBeCalledWith({ url: "a" });
45
- const prevImageryProvider2 = result.current.providers["1"][2];
75
+ const prevImageryProvider2 = result.current.providers["1"][3];
46
76
 
47
77
  // add a tile with URL
48
78
  typedRerender({
49
79
  tiles: [
50
- { id: "2", type: "default" },
51
- { id: "1", type: "default", url: "a" },
80
+ { id: "2", type: "cesium_ion_default" },
81
+ { id: "1", type: "cesium_ion_default", url: "a" },
52
82
  ],
53
83
  });
54
84
 
55
85
  expect(result.current.providers).toEqual({
56
- "2": ["default", undefined, { hoge: undefined }],
57
- "1": ["default", "a", { hoge: "a" }],
86
+ "2": ["cesium_ion_default", undefined, undefined, { hoge: undefined }],
87
+ "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }],
58
88
  });
59
89
  expect(result.current.updated).toBe(true);
60
- expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused
90
+ expect(result.current.providers["1"][3]).toBe(prevImageryProvider2); // 1's provider should be reused
61
91
  expect(provider).toBeCalledTimes(3);
62
92
 
63
93
  // sort tiles
64
94
  typedRerender({
65
95
  tiles: [
66
- { id: "1", type: "default", url: "a" },
67
- { id: "2", type: "default" },
96
+ { id: "1", type: "cesium_ion_default", url: "a" },
97
+ { id: "2", type: "cesium_ion_default" },
68
98
  ],
69
99
  });
70
100
 
71
101
  expect(result.current.providers).toEqual({
72
- "1": ["default", "a", { hoge: "a" }],
73
- "2": ["default", undefined, { hoge: undefined }],
102
+ "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }],
103
+ "2": ["cesium_ion_default", undefined, undefined, { hoge: undefined }],
74
104
  });
75
105
  expect(result.current.updated).toBe(true);
76
- expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused
106
+ expect(result.current.providers["1"][3]).toBe(prevImageryProvider2); // 1's provider should be reused
77
107
  expect(provider).toBeCalledTimes(3);
78
108
 
79
- // delete a tile
109
+ // Ion token update triggers provider recreation for cesium_ion_* types
80
110
  typedRerender({
81
- tiles: [{ id: "1", type: "default", url: "a" }],
111
+ tiles: [{ id: "1", type: "cesium_ion_default", url: "a" }],
82
112
  cesiumIonAccessToken: "a",
83
113
  });
84
114
 
85
115
  expect(result.current.providers).toEqual({
86
- "1": ["default", "a", { hoge: "a" }],
116
+ "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }],
87
117
  });
88
118
  expect(result.current.updated).toBe(true);
89
- expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider2);
119
+ expect(result.current.providers["1"][3]).not.toBe(prevImageryProvider2);
90
120
  expect(provider).toBeCalledTimes(4);
91
121
 
92
- // update a tile type
122
+ // unknown type without customProvider: falls back directly to open_street_map
93
123
  typedRerender({
94
- tiles: [{ id: "1", type: "default_label", url: "u" }],
95
- cesiumIonAccessToken: "a",
124
+ tiles: [{ id: "1", type: "unexpected_type", url: "u" }],
96
125
  });
97
126
 
98
- expect(result.current.providers).toEqual({
99
- "1": ["default_label", "u", { hoge2: "u" }],
100
- });
127
+ expect(result.current.providers["1"][0]).toBe("unexpected_type");
128
+ expect(result.current.providers["1"][3]).toEqual({ osm: true });
101
129
  expect(result.current.updated).toBe(true);
102
- expect(provider).toBeCalledTimes(4);
103
- expect(provider2).toBeCalledTimes(1);
130
+ expect(osmProvider).toBeCalledTimes(1);
104
131
 
105
- // update a tile type to unexpected type
132
+ // unknown type with a matching customProvider entry: uses UrlTemplateImageryProvider
106
133
  typedRerender({
107
- tiles: [{ id: "1", type: "unexpected_type", url: "u" }],
134
+ tiles: [{ id: "1", type: "my_custom_satellite", url: "u" }],
135
+ customProvider: {
136
+ imagery: {
137
+ providers: [
138
+ {
139
+ id: "my_custom_satellite",
140
+ url: "https://example.com/{z}/{x}/{y}.png",
141
+ credit: "© Example",
142
+ },
143
+ ],
144
+ },
145
+ },
108
146
  });
109
147
 
110
- expect(result.current.providers).toEqual({
111
- // unexpected type is treated as "default"
112
- "1": ["unexpected_type", "u", { hoge: "u" }],
113
- });
114
- expect(result.current.updated).toBe(true);
115
- expect(provider).toBeCalledTimes(5);
116
- expect(provider2).toBeCalledTimes(1);
148
+ const dynamicProvider = result.current.providers["1"][3];
149
+ expect(dynamicProvider).toBeDefined();
150
+ expect(dynamicProvider).toBeInstanceOf(UrlTemplateImageryProvider);
151
+ expect(osmProvider).toBeCalledTimes(1); // osm not called again
117
152
 
118
153
  typedRerender({ tiles: [] });
119
154
  expect(result.current.providers).toEqual({});