Skip to content

Commit d371e19

Browse files
ZTongciclaude
andcommitted
fix(web): address Copilot review comments on cesiumIonDetection
- Recurse into LayerGroup children so nested Ion layers are detected - Remove no-explicit-any by using LayerSimple type directly after type narrowing; fix googleMapApiKey access via serviceTokens - Add unit tests covering tiles, terrain, layers, and nested groups Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4ad3412 commit d371e19

2 files changed

Lines changed: 215 additions & 7 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { computeHasCesiumIonAsset } from "./cesiumIonDetection";
4+
5+
const makeSimpleLayer = (data: Record<string, unknown>) => ({
6+
id: "test",
7+
type: "simple" as const,
8+
data,
9+
});
10+
11+
const makeGroupLayer = (children: unknown[]) => ({
12+
id: "group",
13+
type: "group" as const,
14+
children,
15+
});
16+
17+
describe("computeHasCesiumIonAsset", () => {
18+
describe("tiles", () => {
19+
it("returns false when no tiles", () => {
20+
expect(computeHasCesiumIonAsset({})).toBe(false);
21+
});
22+
23+
it("returns true for cesium_ion tile type", () => {
24+
expect(
25+
computeHasCesiumIonAsset({ tiles: [{ id: "1", type: "cesium_ion" }] }),
26+
).toBe(true);
27+
});
28+
29+
it("returns true for cesium_ion_default tile type", () => {
30+
expect(
31+
computeHasCesiumIonAsset({ tiles: [{ id: "1", type: "cesium_ion_default" }] }),
32+
).toBe(true);
33+
});
34+
35+
it("returns true for legacy alias tile types", () => {
36+
for (const type of ["default", "default_road", "default_label", "black_marble"]) {
37+
expect(
38+
computeHasCesiumIonAsset({ tiles: [{ id: "1", type }] }),
39+
).toBe(true);
40+
}
41+
});
42+
43+
it("returns false for open_street_map tile", () => {
44+
expect(
45+
computeHasCesiumIonAsset({ tiles: [{ id: "1", type: "open_street_map" }] }),
46+
).toBe(false);
47+
});
48+
49+
it("returns false for url tile type", () => {
50+
expect(
51+
computeHasCesiumIonAsset({ tiles: [{ id: "1", type: "url", url: "https://example.com/{z}/{x}/{y}.png" }] }),
52+
).toBe(false);
53+
});
54+
});
55+
56+
describe("terrain", () => {
57+
it("returns false when terrain is disabled", () => {
58+
expect(
59+
computeHasCesiumIonAsset({ terrain: { enabled: false, type: "cesium" } }),
60+
).toBe(false);
61+
});
62+
63+
it("returns true for cesium terrain type when enabled", () => {
64+
expect(
65+
computeHasCesiumIonAsset({ terrain: { enabled: true, type: "cesium" } }),
66+
).toBe(true);
67+
});
68+
69+
it("returns true for cesiumion terrain type when enabled", () => {
70+
expect(
71+
computeHasCesiumIonAsset({ terrain: { enabled: true, type: "cesiumion" } }),
72+
).toBe(true);
73+
});
74+
75+
it("returns false for reearth_terrain", () => {
76+
expect(
77+
computeHasCesiumIonAsset({ terrain: { enabled: true, type: "reearth_terrain" } }),
78+
).toBe(false);
79+
});
80+
81+
it("returns true for ionUrl containing ion.cesium.com", () => {
82+
expect(
83+
computeHasCesiumIonAsset({
84+
terrain: { enabled: true, type: "cesiumion" },
85+
assets: { cesium: { terrain: { ionUrl: "https://assets.ion.cesium.com/1/layer.json" } } } as any,
86+
}),
87+
).toBe(true);
88+
});
89+
});
90+
91+
describe("layers", () => {
92+
it("returns true for osm-buildings layer", () => {
93+
expect(
94+
computeHasCesiumIonAsset({}, [makeSimpleLayer({ type: "osm-buildings" }) as any]),
95+
).toBe(true);
96+
});
97+
98+
it("returns false for reearth-buildings layer (migrated from osm-buildings without token)", () => {
99+
expect(
100+
computeHasCesiumIonAsset({}, [makeSimpleLayer({ type: "reearth-buildings" }) as any]),
101+
).toBe(false);
102+
});
103+
104+
it("returns true for google-photorealistic without provider (routed through Ion)", () => {
105+
expect(
106+
computeHasCesiumIonAsset({}, [makeSimpleLayer({ type: "google-photorealistic" }) as any]),
107+
).toBe(true);
108+
});
109+
110+
it("returns true for google-photorealistic with provider=cesium-ion", () => {
111+
expect(
112+
computeHasCesiumIonAsset({}, [
113+
makeSimpleLayer({ type: "google-photorealistic", provider: "cesium-ion" }) as any,
114+
]),
115+
).toBe(true);
116+
});
117+
118+
it("returns false for google-photorealistic with provider=reearth", () => {
119+
expect(
120+
computeHasCesiumIonAsset({}, [
121+
makeSimpleLayer({ type: "google-photorealistic", provider: "reearth" }) as any,
122+
]),
123+
).toBe(false);
124+
});
125+
126+
it("returns false for google-photorealistic with googleMapApiKey and non-cesium-ion provider", () => {
127+
expect(
128+
computeHasCesiumIonAsset({}, [
129+
makeSimpleLayer({
130+
type: "google-photorealistic",
131+
serviceTokens: { googleMapApiKey: "key" },
132+
provider: "google",
133+
}) as any,
134+
]),
135+
).toBe(false);
136+
});
137+
138+
it("returns true for 3dtiles with ion.cesium.com URL", () => {
139+
expect(
140+
computeHasCesiumIonAsset({}, [
141+
makeSimpleLayer({ type: "3dtiles", url: "https://assets.ion.cesium.com/96188/tileset.json" }) as any,
142+
]),
143+
).toBe(true);
144+
});
145+
146+
it("returns false for 3dtiles with non-Ion URL", () => {
147+
expect(
148+
computeHasCesiumIonAsset({}, [
149+
makeSimpleLayer({ type: "3dtiles", url: "https://example.com/tileset.json" }) as any,
150+
]),
151+
).toBe(false);
152+
});
153+
154+
it("returns false for unrelated layer types", () => {
155+
expect(
156+
computeHasCesiumIonAsset({}, [makeSimpleLayer({ type: "geojson" }) as any]),
157+
).toBe(false);
158+
});
159+
});
160+
161+
describe("nested group layers", () => {
162+
it("returns true when Ion layer is nested inside a group", () => {
163+
const group = makeGroupLayer([makeSimpleLayer({ type: "osm-buildings" })]);
164+
expect(computeHasCesiumIonAsset({}, [group as any])).toBe(true);
165+
});
166+
167+
it("returns true for deeply nested Ion layer", () => {
168+
const inner = makeGroupLayer([makeSimpleLayer({ type: "osm-buildings" })]);
169+
const outer = makeGroupLayer([inner]);
170+
expect(computeHasCesiumIonAsset({}, [outer as any])).toBe(true);
171+
});
172+
173+
it("returns false when group contains only non-Ion layers", () => {
174+
const group = makeGroupLayer([makeSimpleLayer({ type: "geojson" })]);
175+
expect(computeHasCesiumIonAsset({}, [group as any])).toBe(false);
176+
});
177+
178+
it("returns false for empty group", () => {
179+
const group = makeGroupLayer([]);
180+
expect(computeHasCesiumIonAsset({}, [group as any])).toBe(false);
181+
});
182+
});
183+
184+
describe("combined", () => {
185+
it("returns false when nothing uses Ion", () => {
186+
expect(
187+
computeHasCesiumIonAsset(
188+
{ tiles: [{ id: "1", type: "open_street_map" }], terrain: { enabled: true, type: "reearth_terrain" } },
189+
[makeSimpleLayer({ type: "geojson" }) as any],
190+
),
191+
).toBe(false);
192+
});
193+
194+
it("returns true when only tiles use Ion", () => {
195+
expect(
196+
computeHasCesiumIonAsset(
197+
{ tiles: [{ id: "1", type: "cesium_ion_default" }], terrain: { enabled: true, type: "reearth_terrain" } },
198+
[makeSimpleLayer({ type: "geojson" }) as any],
199+
),
200+
).toBe(true);
201+
});
202+
});
203+
});

web/src/app/features/Visualizer/utils/cesiumIonDetection.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Layer } from "@reearth/core";
1+
import type { Layer, LayerSimple } from "@reearth/core";
22

33
import type { ViewerProperty } from "@reearth/app/features/Editor/Visualizer/type";
44

@@ -33,30 +33,35 @@ function terrainUsesIon(viewerProperty?: ViewerProperty): boolean {
3333
return false;
3434
}
3535

36-
function layerUsesIon(layer: Layer): boolean {
37-
if (layer.type !== "simple") return false;
38-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39-
const data = "data" in layer ? (layer as any).data : undefined;
36+
function layerUsesIon(layer: LayerSimple): boolean {
37+
const data = layer.data;
4038
if (!data) return false;
4139
// If still osm-buildings after migration, a valid Ion token exists
4240
if (data.type === "osm-buildings") return true;
4341
if (data.type === "google-photorealistic") {
4442
// provider="reearth" means migrated away from Ion or explicitly using reearth
4543
if (data.provider === "reearth") return false;
4644
// Has a Google Maps API key and not forcing cesium-ion → uses Google API, not Ion
47-
if (data.googleMapApiKey && data.provider !== "cesium-ion") return false;
45+
if (data.serviceTokens?.googleMapApiKey && data.provider !== "cesium-ion") return false;
4846
return true;
4947
}
5048
if (data.type === "3dtiles" && isIonUrl(data.url)) return true;
5149
return false;
5250
}
5351

52+
function anyLayerUsesIon(layer: Layer): boolean {
53+
if (layer.type === "group") {
54+
return layer.children.some(anyLayerUsesIon);
55+
}
56+
return layerUsesIon(layer);
57+
}
58+
5459
export function computeHasCesiumIonAsset(
5560
viewerProperty?: ViewerProperty,
5661
layers?: Layer[],
5762
): boolean {
5863
if (viewerProperty?.tiles?.some(tileUsesIon)) return true;
5964
if (terrainUsesIon(viewerProperty)) return true;
60-
if (layers?.some(layerUsesIon)) return true;
65+
if (layers?.some(anyLayerUsesIon)) return true;
6166
return false;
6267
}

0 commit comments

Comments
 (0)