Skip to content
221 changes: 221 additions & 0 deletions src/Map/cesiumIonDetection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { describe, expect, test } from "vitest";

import { computeHasCesiumIonAsset } from "./cesiumIonDetection";

const makeSimple = (data?: object) => ({
id: "layer1",
type: "simple" as const,
...(data ? { data } : {}),
});

const makeGroup = (children: object[]) => ({
id: "group1",
type: "group" as const,
children,
});

describe("computeHasCesiumIonAsset", () => {
describe("tiles", () => {
test("returns false when no tiles", () => {
expect(computeHasCesiumIonAsset({ tiles: [] })).toBe(false);
});

test("returns true for cesium_ion tile type", () => {
expect(computeHasCesiumIonAsset({ tiles: [{ type: "cesium_ion" }] })).toBe(true);
});

test("returns true for cesium_ion_default tile type", () => {
expect(computeHasCesiumIonAsset({ tiles: [{ type: "cesium_ion_default" }] })).toBe(true);
});

test("returns true for legacy tile types", () => {
for (const type of ["default", "default_road", "default_label", "black_marble"]) {
expect(computeHasCesiumIonAsset({ tiles: [{ type }] })).toBe(true);
}
});

test("returns false for non-ion tile type", () => {
expect(computeHasCesiumIonAsset({ tiles: [{ type: "open_street_map" }] })).toBe(false);
});
});

describe("terrain", () => {
test("returns true for cesium terrain type when enabled", () => {
expect(
computeHasCesiumIonAsset({
terrain: { enabled: true, type: "cesium" },
}),
).toBe(true);
});

test("returns true for cesiumion terrain type when enabled", () => {
expect(
computeHasCesiumIonAsset({
terrain: { enabled: true, type: "cesiumion" },
}),
).toBe(true);
});

test("returns false for cesium terrain when disabled", () => {
expect(
computeHasCesiumIonAsset({
terrain: { enabled: false, type: "cesium" },
}),
).toBe(false);
});

test("returns true for ion terrain URL", () => {
expect(
computeHasCesiumIonAsset({
terrain: { enabled: true, type: "url" },
assets: {
cesium: {
terrain: {
ionUrl: "https://assets.ion.cesium.com/1/tileset.json",
},
},
},
} as any),
).toBe(true);
});

test("returns false for non-ion terrain URL", () => {
expect(
computeHasCesiumIonAsset({
terrain: { enabled: true, type: "url" },
assets: {
cesium: { terrain: { ionUrl: "https://example.com/terrain" } },
},
} as any),
).toBe(false);
});
});

describe("layers", () => {
test("returns true for osm-buildings layer", () => {
expect(
computeHasCesiumIonAsset(undefined, [makeSimple({ type: "osm-buildings" })] as any),
).toBe(true);
});

test("returns true for google-photorealistic with cesium-ion provider", () => {
expect(
computeHasCesiumIonAsset(undefined, [
makeSimple({ type: "google-photorealistic", provider: "cesium-ion" }),
] as any),
).toBe(true);
});

test("returns false for google-photorealistic with reearth provider", () => {
expect(
computeHasCesiumIonAsset(undefined, [
makeSimple({ type: "google-photorealistic", provider: "reearth" }),
] as any),
).toBe(false);
});

test("returns false for google-photorealistic with no provider (google API path)", () => {
expect(
computeHasCesiumIonAsset(undefined, [makeSimple({ type: "google-photorealistic" })] as any),
).toBe(false);
});

test("returns true for layer with ion URL", () => {
expect(
computeHasCesiumIonAsset(undefined, [
makeSimple({
type: "3dtiles",
url: "https://assets.ion.cesium.com/123/tileset.json",
}),
] as any),
).toBe(true);
});

test("returns true for any layer type with ion URL", () => {
expect(
computeHasCesiumIonAsset(undefined, [
makeSimple({
type: "geojson",
url: "https://assets.ion.cesium.com/456/data.json",
}),
] as any),
).toBe(true);
});

test("returns false for layer with non-ion URL", () => {
expect(
computeHasCesiumIonAsset(undefined, [
makeSimple({
type: "3dtiles",
url: "https://example.com/tileset.json",
}),
] as any),
).toBe(false);
});

test("returns false for layer with no data", () => {
expect(computeHasCesiumIonAsset(undefined, [makeSimple()] as any)).toBe(false);
});
});

describe("layer groups (recursion)", () => {
test("returns true when nested layer uses ion", () => {
const group = makeGroup([makeSimple({ type: "osm-buildings" })]);
expect(computeHasCesiumIonAsset(undefined, [group] as any)).toBe(true);
});

test("returns false when nested layer does not use ion", () => {
const group = makeGroup([
makeSimple({ type: "geojson", url: "https://example.com/data.json" }),
]);
expect(computeHasCesiumIonAsset(undefined, [group] as any)).toBe(false);
});

test("returns true when deeply nested layer uses ion", () => {
const inner = makeGroup([makeSimple({ type: "osm-buildings" })]);
const outer = makeGroup([inner]);
expect(computeHasCesiumIonAsset(undefined, [outer] as any)).toBe(true);
});
});

describe("combined", () => {
test("returns false when nothing uses ion", () => {
expect(
computeHasCesiumIonAsset(
{
tiles: [{ type: "open_street_map" }],
terrain: { enabled: false, type: "cesium" },
},
[
makeSimple({
type: "geojson",
url: "https://example.com/data.json",
}),
] as any,
),
).toBe(false);
});

test("returns true when only tile uses ion", () => {
expect(
computeHasCesiumIonAsset({ tiles: [{ type: "default" }] }, [
makeSimple({ type: "geojson" }),
] as any),
).toBe(true);
});

test("returns true when only terrain uses ion", () => {
expect(
computeHasCesiumIonAsset({ terrain: { enabled: true, type: "cesium" } }, [
makeSimple({ type: "geojson" }),
] as any),
).toBe(true);
});

test("returns undefined-safe (no property, no layers)", () => {
expect(computeHasCesiumIonAsset()).toBe(false);
expect(computeHasCesiumIonAsset(undefined, undefined)).toBe(false);
expect(computeHasCesiumIonAsset(undefined, [])).toBe(false);
});
});
});
55 changes: 55 additions & 0 deletions src/Map/cesiumIonDetection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Layer, LayerSimple } from "../mantle";

import type { TileProperty, ViewerProperty } from "./types/viewerProperty";

const CESIUM_ION_URL_PATTERN = "ion.cesium.com";
const CESIUM_ION_LEGACY_TILE_TYPES = new Set([
"default",
"default_road",
"default_label",
"black_marble",
]);

function isIonUrl(url?: string | null): boolean {
return !!url && url.includes(CESIUM_ION_URL_PATTERN);
}

function tileUsesIon(tile: TileProperty): boolean {
if (!tile.type) return false;
if (tile.type.startsWith("cesium_ion")) return true;
if (CESIUM_ION_LEGACY_TILE_TYPES.has(tile.type)) return true;
return false;
}

function terrainUsesIon(property?: ViewerProperty): boolean {
const terrain = property?.terrain;
if (!terrain?.enabled) return false;
if (terrain.type === "cesium" || terrain.type === "cesiumion") return true;
if (isIonUrl(property?.assets?.cesium?.terrain?.ionUrl)) return true;
return false;
}

function layerUsesIon(layer: LayerSimple): boolean {
const data = layer.data;
if (!data) return false;
if (data.type === "osm-buildings") return true;
if (data.type === "google-photorealistic") {
return data.provider === "cesium-ion";
}
Comment thread
ZTongci marked this conversation as resolved.
if (isIonUrl(data.url)) return true;
return false;
}

function anyLayerUsesIon(layer: Layer): boolean {
if (layer.type === "group") {
return layer.children.some(anyLayerUsesIon);
}
return layerUsesIon(layer);
}

export function computeHasCesiumIonAsset(property?: ViewerProperty, layers?: Layer[]): boolean {
if (property?.tiles?.some(tileUsesIon)) return true;
if (terrainUsesIon(property)) return true;
if (layers?.some(anyLayerUsesIon)) return true;
return false;
Comment thread
ZTongci marked this conversation as resolved.
}
14 changes: 12 additions & 2 deletions src/Map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { forwardRef, useMemo, type Ref, type JSX } from "react";

import { INTERACTION_MODES } from "../shared/interactionMode";

import { computeHasCesiumIonAsset } from "./cesiumIonDetection";
import Geoid from "./Geoid";
import useHooks, { MapRef } from "./hooks";
import Layers, { type Props as LayersProps } from "./Layers";
Expand Down Expand Up @@ -44,7 +45,10 @@ export type Props = {
| "selectedLayerId"
| "viewerProperty"
> &
Omit<EngineProps, "onLayerSelect" | "layerSelectionReason" | "selectedLayerId"> &
Omit<
EngineProps,
"onLayerSelect" | "layerSelectionReason" | "selectedLayerId" | "hasCesiumIonAsset"
> &
Omit<SketchProps, "layersRef" | "engineRef" | "SketchComponent">;

function MapFn(
Expand Down Expand Up @@ -102,6 +106,11 @@ function MapFn(
onAPIReady,
});

const hasCesiumIonAsset = useMemo(
() => computeHasCesiumIonAsset(props.property, layers),
[props.property, layers],
);

const selectedLayerIds = useMemo(
() => ({
layerId: selectedLayer.layerId,
Expand All @@ -125,7 +134,8 @@ function MapFn(
onLayerSelect={handleEngineLayerSelect}
featureFlags={featureFlags}
onMount={handleEngineMount}
{...props}>
{...props}
hasCesiumIonAsset={hasCesiumIonAsset}>
<Layers
ref={layersRef}
engineRef={engineRef}
Expand Down
1 change: 1 addition & 0 deletions src/Map/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type EngineProps = {
meta?: Record<string, unknown>;
customProvider?: CustomProviderConfig;
displayCredits?: boolean;
hasCesiumIonAsset?: boolean;
layersRef?: RefObject<LayersRef | null>;
requestingRenderMode?: MutableRefObject<RequestingRenderMode>;
timelineManagerRef?: TimelineManagerRef;
Expand Down
11 changes: 9 additions & 2 deletions src/engines/Cesium/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ export function getExtrudedHeight(
return;
}

export function getCredits(viewer: Viewer) {
export function getCredits(viewer: Viewer, hasCesiumIonAsset?: boolean) {
if (!viewer) return emptyCredites;
const creditDisplay = viewer.creditDisplay as
| (CreditDisplay & {
Expand All @@ -924,7 +924,14 @@ export function getCredits(viewer: Viewer) {

const credits: Credits = {
engine: {
cesium: cesiumCredits?.html ? { html: cesiumCredits.html } : undefined,
// Only include Cesium-ion credit when Ion assets are actually in use.
// hasCesiumIonAsset === false means explicitly no Ion assets; undefined preserves existing behavior.
cesium:
hasCesiumIonAsset === false
? undefined
: cesiumCredits?.html
? { html: cesiumCredits.html }
: undefined,
},
lightbox: Array.from(lightboxCredits?._array ?? []).map(c => ({
html: c?.credit?.html,
Expand Down
9 changes: 7 additions & 2 deletions src/engines/Cesium/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default ({
onCameraChange,
onMount,
onCreditsUpdate,
hasCesiumIonAsset,
}: {
ref: React.ForwardedRef<EngineRef>;
property?: ViewerProperty;
Expand Down Expand Up @@ -133,6 +134,7 @@ export default ({
onCameraChange?: (camera: Camera) => void;
onMount?: () => void;
onCreditsUpdate?: (credits: Credits) => void;
hasCesiumIonAsset?: boolean;
}) => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>(null);

Expand All @@ -141,8 +143,11 @@ export default ({
? meta.cesiumIonAccessToken
: undefined;

const hasCesiumIonAssetRef = useRef(hasCesiumIonAsset);
hasCesiumIonAssetRef.current = hasCesiumIonAsset;

// expose ref
const engineAPI = useEngineRef(ref, cesium);
const engineAPI = useEngineRef(ref, cesium, hasCesiumIonAssetRef);

const layerSelectWithRectEventHandlers = useLayerSelectWithRect({
cesium,
Expand Down Expand Up @@ -667,7 +672,7 @@ export default ({
if (!onCreditsUpdateRef.current) return;
const viewer = cesium.current?.cesiumElement;
if (!viewer || viewer.isDestroyed()) return;
const credits = getCredits(viewer);
const credits = getCredits(viewer, hasCesiumIonAssetRef.current);
onCreditsUpdateRef.current(credits);
}, 3000);
}, []);
Expand Down
Loading
Loading