Skip to content

Commit ee7950c

Browse files
ZTongciclaude
andauthored
fix(cesium): suppress Ion logo when no Ion assets are actively rendering (#155)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 44c060f commit ee7950c

6 files changed

Lines changed: 146 additions & 59 deletions

File tree

src/Map/cesiumIonDetection.test.ts

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,29 @@ describe("computeHasCesiumIonAsset", () => {
2020
expect(computeHasCesiumIonAsset({ tiles: [] })).toBe(false);
2121
});
2222

23-
test("returns true for cesium_ion tile type", () => {
23+
test("returns true for cesium_ion tile type with valid assetId", () => {
2424
expect(
25-
computeHasCesiumIonAsset({ tiles: [{ id: "", type: "cesium_ion" }] }),
25+
computeHasCesiumIonAsset(
26+
{ tiles: [{ id: "", type: "cesium_ion", cesiumIonAssetId: 12345 }] },
27+
undefined,
28+
"my-token",
29+
),
2630
).toBe(true);
2731
});
2832

29-
test("returns true for cesium_ion_default tile type", () => {
33+
test("returns true for cesium_ion_default tile type when token present", () => {
3034
expect(
31-
computeHasCesiumIonAsset({
32-
tiles: [{ id: "", type: "cesium_ion_default" }],
33-
}),
35+
computeHasCesiumIonAsset(
36+
{ tiles: [{ id: "", type: "cesium_ion_default" }] },
37+
undefined,
38+
"my-token",
39+
),
3440
).toBe(true);
3541
});
3642

37-
test("returns true for legacy tile types", () => {
38-
for (const type of [
39-
"default",
40-
"default_road",
41-
"default_label",
42-
"black_marble",
43-
]) {
44-
expect(computeHasCesiumIonAsset({ tiles: [{ id: "", type }] })).toBe(
43+
test("returns true for legacy tile types when token present", () => {
44+
for (const type of ["default", "default_road", "default_label", "black_marble"]) {
45+
expect(computeHasCesiumIonAsset({ tiles: [{ id: "", type }] }, undefined, "my-token")).toBe(
4546
true,
4647
);
4748
}
@@ -65,14 +66,23 @@ describe("computeHasCesiumIonAsset", () => {
6566
).toBe(true);
6667
});
6768

68-
test("returns true for cesiumion terrain type when enabled", () => {
69+
test("returns true for cesiumion terrain type with ionAsset", () => {
6970
expect(
7071
computeHasCesiumIonAsset({
7172
terrain: { enabled: true, type: "cesiumion" },
72-
}),
73+
assets: { cesium: { terrain: { ionAsset: "1" } } },
74+
} as any),
7375
).toBe(true);
7476
});
7577

78+
test("returns false for cesiumion terrain type without ionAsset or ionUrl", () => {
79+
expect(
80+
computeHasCesiumIonAsset({
81+
terrain: { enabled: true, type: "cesiumion" },
82+
}),
83+
).toBe(false);
84+
});
85+
7686
test("returns false for cesium terrain when disabled", () => {
7787
expect(
7888
computeHasCesiumIonAsset({
@@ -111,9 +121,7 @@ describe("computeHasCesiumIonAsset", () => {
111121
describe("layers", () => {
112122
test("returns true for osm-buildings layer", () => {
113123
expect(
114-
computeHasCesiumIonAsset(undefined, [
115-
makeSimple({ type: "osm-buildings" }),
116-
] as any),
124+
computeHasCesiumIonAsset(undefined, [makeSimple({ type: "osm-buildings" })] as any),
117125
).toBe(true);
118126
});
119127

@@ -135,9 +143,7 @@ describe("computeHasCesiumIonAsset", () => {
135143

136144
test("returns false for google-photorealistic with no provider (google API path)", () => {
137145
expect(
138-
computeHasCesiumIonAsset(undefined, [
139-
makeSimple({ type: "google-photorealistic" }),
140-
] as any),
146+
computeHasCesiumIonAsset(undefined, [makeSimple({ type: "google-photorealistic" })] as any),
141147
).toBe(false);
142148
});
143149

@@ -175,9 +181,7 @@ describe("computeHasCesiumIonAsset", () => {
175181
});
176182

177183
test("returns false for layer with no data", () => {
178-
expect(computeHasCesiumIonAsset(undefined, [makeSimple()] as any)).toBe(
179-
false,
180-
);
184+
expect(computeHasCesiumIonAsset(undefined, [makeSimple()] as any)).toBe(false);
181185
});
182186
});
183187

@@ -221,18 +225,19 @@ describe("computeHasCesiumIonAsset", () => {
221225

222226
test("returns true when only tile uses ion", () => {
223227
expect(
224-
computeHasCesiumIonAsset({ tiles: [{ id: "", type: "default" }] }, [
225-
makeSimple({ type: "geojson" }),
226-
] as any),
228+
computeHasCesiumIonAsset(
229+
{ tiles: [{ id: "", type: "default" }] },
230+
[makeSimple({ type: "geojson" })] as any,
231+
"my-token",
232+
),
227233
).toBe(true);
228234
});
229235

230236
test("returns true when only terrain uses ion", () => {
231237
expect(
232-
computeHasCesiumIonAsset(
233-
{ terrain: { enabled: true, type: "cesium" } },
234-
[makeSimple({ type: "geojson" })] as any,
235-
),
238+
computeHasCesiumIonAsset({ terrain: { enabled: true, type: "cesium" } }, [
239+
makeSimple({ type: "geojson" }),
240+
] as any),
236241
).toBe(true);
237242
});
238243

@@ -242,4 +247,48 @@ describe("computeHasCesiumIonAsset", () => {
242247
expect(computeHasCesiumIonAsset(undefined, [])).toBe(false);
243248
});
244249
});
250+
251+
describe("token and assetId gating", () => {
252+
test("returns false for cesium_ion tile with no cesiumIonAssetId", () => {
253+
expect(
254+
computeHasCesiumIonAsset(
255+
{ tiles: [{ id: "", type: "cesium_ion" }] },
256+
undefined,
257+
"my-token",
258+
),
259+
).toBe(false);
260+
});
261+
262+
test("returns false for cesium_ion tile with assetId of 0", () => {
263+
expect(
264+
computeHasCesiumIonAsset(
265+
{ tiles: [{ id: "", type: "cesium_ion", cesiumIonAssetId: 0 }] },
266+
undefined,
267+
"my-token",
268+
),
269+
).toBe(false);
270+
});
271+
272+
test("returns false for cesium_ion_default when no token", () => {
273+
expect(
274+
computeHasCesiumIonAsset({
275+
tiles: [{ id: "", type: "cesium_ion_default" }],
276+
}),
277+
).toBe(false);
278+
});
279+
280+
test("returns false for legacy tile types when no token", () => {
281+
for (const type of ["default", "default_road", "default_label", "black_marble"]) {
282+
expect(computeHasCesiumIonAsset({ tiles: [{ id: "", type }] })).toBe(false);
283+
}
284+
});
285+
286+
test("returns true for cesium_ion with valid assetId (token gate is at engine level)", () => {
287+
expect(
288+
computeHasCesiumIonAsset({
289+
tiles: [{ id: "", type: "cesium_ion", cesiumIonAssetId: 2275207 }],
290+
}),
291+
).toBe(true);
292+
});
293+
});
245294
});

src/Map/cesiumIonDetection.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,28 @@ function isIonUrl(url?: string | null): boolean {
1414
return !!url && url.includes(CESIUM_ION_URL_PATTERN);
1515
}
1616

17-
function tileUsesIon(tile: TileProperty): boolean {
17+
function tileUsesIon(tile: TileProperty, accessToken?: string): boolean {
1818
if (!tile.type) return false;
19-
if (tile.type.startsWith("cesium_ion")) return true;
20-
if (CESIUM_ION_LEGACY_TILE_TYPES.has(tile.type)) return true;
19+
if (tile.type === "cesium_ion") {
20+
const id = tile.cesiumIonAssetId;
21+
return !!id && !isNaN(parseInt(String(id), 10));
22+
}
23+
if (tile.type.startsWith("cesium_ion")) return !!accessToken;
24+
if (CESIUM_ION_LEGACY_TILE_TYPES.has(tile.type)) return !!accessToken;
2125
return false;
2226
}
2327

2428
function terrainUsesIon(property?: ViewerProperty): boolean {
2529
const terrain = property?.terrain;
2630
if (!terrain?.enabled) return false;
27-
if (terrain.type === "cesium" || terrain.type === "cesiumion") return true;
31+
if (terrain.type === "cesium") return true;
32+
if (terrain.type === "cesiumion") {
33+
// Mirrors useTerrainProviderPromise: returns EllipsoidTerrainProvider when neither is set.
34+
return !!(
35+
property?.assets?.cesium?.terrain?.ionAsset ||
36+
isIonUrl(property?.assets?.cesium?.terrain?.ionUrl)
37+
);
38+
}
2839
if (isIonUrl(property?.assets?.cesium?.terrain?.ionUrl)) return true;
2940
return false;
3041
}
@@ -47,8 +58,12 @@ function anyLayerUsesIon(layer: Layer): boolean {
4758
return layerUsesIon(layer);
4859
}
4960

50-
export function computeHasCesiumIonAsset(property?: ViewerProperty, layers?: Layer[]): boolean {
51-
if (property?.tiles?.some(tileUsesIon)) return true;
61+
export function computeHasCesiumIonAsset(
62+
property?: ViewerProperty,
63+
layers?: Layer[],
64+
accessToken?: string,
65+
): boolean {
66+
if (property?.tiles?.some(tile => tileUsesIon(tile, accessToken))) return true;
5267
if (terrainUsesIon(property)) return true;
5368
if (layers?.some(anyLayerUsesIon)) return true;
5469
return false;

src/Map/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,13 @@ function MapFn(
106106
onAPIReady,
107107
});
108108

109-
const hasCesiumIonAsset = useMemo(
110-
() => computeHasCesiumIonAsset(props.property, layers),
111-
[props.property, layers],
112-
);
109+
const hasCesiumIonAsset = useMemo(() => {
110+
const token =
111+
typeof props.meta?.cesiumIonAccessToken === "string" && props.meta.cesiumIonAccessToken
112+
? (props.meta.cesiumIonAccessToken as string)
113+
: undefined;
114+
return computeHasCesiumIonAsset(props.property, layers, token);
115+
}, [props.property, layers, props.meta]);
113116

114117
const selectedLayerIds = useMemo(
115118
() => ({

src/engines/Cesium/common.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -914,24 +914,33 @@ export function getCredits(viewer: Viewer, hasCesiumIonAsset?: boolean) {
914914
screenCredits: { _array: { credit?: CesiumCredit }[] };
915915
};
916916
_currentCesiumCredit: CesiumCredit;
917+
// _cesiumCredit is the per-instance clone that beginFrame() resets _currentCesiumCredit to
918+
// each frame. addCreditToNextFrame(ionCredit) sets _currentCesiumCredit to _defaultCredit
919+
// (a different object) when Ion tiles are actually rendering — see GlobeSurfaceTileProvider
920+
// updateCredits(). If they are equal, no Ion tile rendered in the most-recent frame.
921+
_cesiumCredit: CesiumCredit;
917922
})
918923
| undefined;
919924

920925
if (!creditDisplay) return emptyCredites;
921926

922927
const { lightboxCredits, screenCredits } = creditDisplay?._currentFrameCredits || {};
923-
const cesiumCredits = creditDisplay._currentCesiumCredit;
928+
const currentCesiumCredit = creditDisplay._currentCesiumCredit;
929+
const staticCesiumCredit = creditDisplay._cesiumCredit;
930+
931+
// Ion tiles are truly rendering when _currentCesiumCredit !== _cesiumCredit.
932+
// beginFrame() resets _currentCesiumCredit = _cesiumCredit each frame; only
933+
// addCreditToNextFrame() (called for every ready+visible Ion layer) overrides it.
934+
// hasCesiumIonAsset === false is a fast-path: skip the check when Ion is definitely absent.
935+
const ionIsRendering =
936+
hasCesiumIonAsset !== false &&
937+
currentCesiumCredit !== undefined &&
938+
staticCesiumCredit !== undefined &&
939+
currentCesiumCredit !== staticCesiumCredit;
924940

925941
const credits: Credits = {
926942
engine: {
927-
// Only include Cesium-ion credit when Ion assets are actually in use.
928-
// hasCesiumIonAsset === false means explicitly no Ion assets; undefined preserves existing behavior.
929-
cesium:
930-
hasCesiumIonAsset === false
931-
? undefined
932-
: cesiumCredits?.html
933-
? { html: cesiumCredits.html }
934-
: undefined,
943+
cesium: ionIsRendering ? { html: currentCesiumCredit.html } : undefined,
935944
},
936945
lightbox: Array.from(lightboxCredits?._array ?? []).map(c => ({
937946
html: c?.credit?.html,

src/engines/Cesium/core/Globe/index.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Globe as CesiumGlobeType } from "cesium";
1+
import { EllipsoidTerrainProvider, type Globe as CesiumGlobeType } from "cesium";
22
import { useEffect, useMemo, useRef, type JSX } from "react";
33
import { Globe as CesiumGlobe, type CesiumComponentRef } from "resium";
44

@@ -48,7 +48,13 @@ export default function Globe({
4848
}
4949
})
5050
.catch(() => {
51-
// provider errors are handled by the existing useEffect below
51+
// Fall back to flat terrain so stale Ion terrain doesn't persist and mislead
52+
// the runtime Ion-credit check.
53+
if (cancelled) return;
54+
const cesiumGlobe = cesiumGlobeRef.current?.cesiumElement;
55+
if (cesiumGlobe) {
56+
cesiumGlobe.terrainProvider = new EllipsoidTerrainProvider();
57+
}
5258
});
5359
return () => {
5460
cancelled = true;
@@ -68,7 +74,10 @@ export default function Globe({
6874
}
6975
})
7076
.catch(error => {
71-
if (!isCancelled) console.warn("Terrain provider failed to load:", error);
77+
if (isCancelled) return;
78+
console.warn("Terrain provider failed to load:", error);
79+
// Notify so the engine re-evaluates credits even when terrain fails.
80+
onTerrainProviderChange?.();
7281
});
7382

7483
return () => {

src/engines/Cesium/hooks.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ export default ({
143143
? meta.cesiumIonAccessToken
144144
: undefined;
145145

146-
const hasCesiumIonAssetRef = useRef(hasCesiumIonAsset);
147-
hasCesiumIonAssetRef.current = hasCesiumIonAsset;
146+
const effectiveHasCesiumIonAsset = hasCesiumIonAsset && !!cesiumIonAccessToken;
147+
const hasCesiumIonAssetRef = useRef(effectiveHasCesiumIonAsset);
148+
hasCesiumIonAssetRef.current = effectiveHasCesiumIonAsset;
148149

149150
// expose ref
150151
const engineAPI = useEngineRef(ref, cesium, hasCesiumIonAssetRef);
@@ -665,9 +666,10 @@ export default ({
665666
onCreditsUpdateRef.current = onCreditsUpdate;
666667
const updateCredits = useCallback(() => {
667668
if (!onCreditsUpdateRef.current) return;
668-
// currently we don't have a proper way to get the credits update event
669-
// wait for 3 seconds to get latest credits
670-
// some internal property is been used here.
669+
// Wait for tiles to load/render before checking credits. Cesium's GlobeSurfaceTileProvider
670+
// calls addCreditToNextFrame() each frame for ready+visible Ion layers, which sets
671+
// _currentCesiumCredit !== _cesiumCredit — that comparison is the actual Ion-rendering check
672+
// inside getCredits(). 3 s gives tiles enough time to reach that state.
671673
setTimeout(() => {
672674
if (!onCreditsUpdateRef.current) return;
673675
const viewer = cesium.current?.cesiumElement;

0 commit comments

Comments
 (0)