From ae73e97b2cad6afe5e4b96943a75ca1e58833b32 Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Mon, 18 May 2026 11:12:05 +0900 Subject: [PATCH 01/19] feat: migrate to self-hosted tile server --- .env.example | 20 ++- example/buildExampleMeta.ts | 86 +++++++++++ example/constants.ts | 3 - example/hooks.ts | 37 +++-- src/Map/index.tsx | 1 + src/Map/tileProviderDefaults.ts | 106 +++++++++++++ src/Map/types/index.ts | 1 + src/Map/types/tileProvider.ts | 97 ++++++++++++ src/Map/types/viewerProperty.ts | 10 +- src/engines/Cesium/Feature/Tileset/hooks.ts | 65 +++++--- src/engines/Cesium/core/Globe/index.tsx | 4 + .../core/Globe/useTerrainProviderPromise.ts | 61 ++++++-- src/engines/Cesium/core/Imagery.test.ts | 17 +- src/engines/Cesium/core/Imagery.tsx | 125 +++++++++------ src/engines/Cesium/core/presets.ts | 106 ++++++++++--- .../Cesium/core/tileProviderResolver.ts | 72 +++++++++ src/engines/Cesium/hooks.ts | 146 +++++++++++++----- src/engines/Cesium/index.tsx | 5 +- vite.config.example.ts | 11 ++ 19 files changed, 807 insertions(+), 166 deletions(-) create mode 100644 example/buildExampleMeta.ts create mode 100644 src/Map/tileProviderDefaults.ts create mode 100644 src/Map/types/tileProvider.ts create mode 100644 src/engines/Cesium/core/tileProviderResolver.ts diff --git a/.env.example b/.env.example index c4643c0..21f5771 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,18 @@ -EXAMPLE_CESIUM_ION_ACCESS_TOKEN=your_access_token_here -EXAMPLE_GOOGLE_MAP_API_KEY=your_google_map_api_key \ No newline at end of file +EXAMPLE_GOOGLE_MAP_API_KEY=your_google_map_api_key + +# Terravista tile provider (optional) +# Get token: gcloud secrets versions access latest --secret=terravista-auth-token +# In yarn dev the token is appended as ?token= query param (no Sentinel needed). +# Without a token, the example falls back to public OSM tiles. +EXAMPLE_TERRAVISTA_ACCESS_TOKEN= +EXAMPLE_TERRAVISTA_BASE_URL= + +# Individual tile URLs — required when using Terravista. +EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_URL= +EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_CREDIT= +EXAMPLE_TERRAVISTA_IMAGERY_ROAD_URL= +EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT= +EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_URL= +EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT= +EXAMPLE_REEARTH_TERRAIN_URL= +EXAMPLE_TERRAVISTA_GOOGLE3D_URL= diff --git a/example/buildExampleMeta.ts b/example/buildExampleMeta.ts new file mode 100644 index 0000000..b5310e0 --- /dev/null +++ b/example/buildExampleMeta.ts @@ -0,0 +1,86 @@ +import { + createCustomConfig, + createTerravistaConfig, + TileProviderConfig, +} from "@reearth/core"; + +// terrain.reearth.land doesn't send CORS headers, so in dev we route through the +// Vite proxy (/terrain-proxy) defined in vite.config.example.ts. +function toTerrainUrl(url: string | undefined): string | undefined { + if (!url) return undefined; + if (import.meta.env.DEV) return url.replace("https://terrain.reearth.land", "/terrain-proxy"); + return url; +} + +/** + * Build TileProviderConfig from example env vars. + * + * In dev (yarn dev) we append ?token= so tiles work without Sentinel. + * In production the visualizer registers Sentinel which injects Bearer headers instead. + * + * Returns a Terravista config when a token is present, otherwise a public OSM fallback. + */ +export function buildExampleTileProvider(): TileProviderConfig { + const token: string | undefined = + import.meta.env.EXAMPLE_TERRAVISTA_ACCESS_TOKEN || undefined; + const t = token ? `?token=${token}` : ""; + + if (token) { + return createTerravistaConfig(token, { + baseUrl: import.meta.env.EXAMPLE_TERRAVISTA_BASE_URL || undefined, + imageryTileOverrides: [ + { + id: "default", + url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_URL}${t}`, + credit: + import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_CREDIT || + "© Google", + }, + { + id: "default_road", + url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_URL}${t}`, + credit: + import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT || + "© Google", + }, + { + id: "black_marble", + url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_URL}${t}`, + credit: + import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT || + "NASA GIBS VIIRS", + maxZoomLevel: 8, + }, + ], + terrainOverrides: [ + { + id: "default", + url: toTerrainUrl(import.meta.env.EXAMPLE_REEARTH_TERRAIN_URL), + }, + ], + layerSourceOverrides: [ + { + id: "googlePhotorealistic", + url: `${import.meta.env.EXAMPLE_TERRAVISTA_GOOGLE3D_URL}${t}`, + }, + ], + }); + } + + // No token — fall back to public OSM tiles. + return createCustomConfig({ + imageryTileOverrides: [ + { + id: "default", + url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + credit: "© OpenStreetMap contributors", + }, + { + id: "default_road", + url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + credit: "© OpenStreetMap contributors", + }, + ], + terrainOverrides: [], + }); +} diff --git a/example/constants.ts b/example/constants.ts index 36ae0ee..428018b 100644 --- a/example/constants.ts +++ b/example/constants.ts @@ -14,13 +14,10 @@ export const DEFAULT_CAMERA = { export const TILES = [ "default", - "default_label", "default_road", "open_street_map", - "esri_world_topo", "black_marble", "japan_gsi_standard", - "test_unexpected_type", ]; export const SKETCH_TOOLS: SketchType[] = [ diff --git a/example/hooks.ts b/example/hooks.ts index 1650ab1..bbd607c 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -12,6 +12,7 @@ import { ViewerProperty, } from "@reearth/core"; +import { buildExampleTileProvider } from "./buildExampleMeta"; import { DEFAULT_CAMERA, DEFAULT_LAYERS, TILES } from "./constants"; import { DEFAULT_VIEWER_PROPERTY } from "./scene"; import { TEST_LAYERS } from "./testLayers"; @@ -34,7 +35,9 @@ export default () => { // TODO: use onLayerSelect props (core should export a type for selection). const [selectedLayer, setSelectedLayer] = useState(); - const [selectedFeature, setSelectedFeature] = useState(); + const [selectedFeature, setSelectedFeature] = useState< + ComputedFeature | undefined + >(); const handleSelect: ( layerId: string | undefined, layer: (() => Promise) | undefined, @@ -46,12 +49,10 @@ export default () => { setSelectedFeature(ref.current?.layers.selectedFeature() ?? feature); }, []); - const meta = useMemo( - () => ({ - cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, - }), - [], - ); + const meta = useMemo(() => { + const tileProvider = buildExampleTileProvider(); + return { tileProvider }; + }, []); const [currentTile, setCurrentTile] = useState( TILES.includes(DEFAULT_VIEWER_PROPERTY.tiles?.[0]?.type ?? "") @@ -61,7 +62,8 @@ export default () => { const [currentCamera, setCurrentCamera] = useState(DEFAULT_CAMERA); const [terrainEnabled, setTerrainEnabled] = useState(true); const [hideUnderground, setHideUnderground] = useState(false); - const [activeLayerIds, setActiveLayerIds] = useState(DEFAULT_LAYERS); + const [activeLayerIds, setActiveLayerIds] = + useState(DEFAULT_LAYERS); const viewerProperty: ViewerProperty = useMemo( () => ({ @@ -88,11 +90,13 @@ export default () => { ); const layers = useMemo( - () => TEST_LAYERS.filter(layer => activeLayerIds.includes(layer.id)), + () => TEST_LAYERS.filter((layer) => activeLayerIds.includes(layer.id)), [activeLayerIds], ); - const [sketchTool, setSketchTool] = useState(undefined); + const [sketchTool, setSketchTool] = useState( + undefined, + ); useEffect(() => { ref.current?.sketch?.setType(sketchTool); }, [ref, sketchTool]); @@ -113,12 +117,17 @@ export default () => { const handleEditSketchFeature = useCallback(() => { if ( - !(selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer) || + !( + selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer + ) || !selectedLayer.id || !selectedFeature?.id ) return; - ref.current?.sketch.editFeature({ layerId: selectedLayer.id, feature: selectedFeature }); + ref.current?.sketch.editFeature({ + layerId: selectedLayer.id, + feature: selectedFeature, + }); }, [selectedLayer, selectedFeature]); const handleCancelEditSketchFeature = useCallback(() => { @@ -131,7 +140,9 @@ export default () => { const handleDeleteSketchFeature = useCallback(() => { if ( - !(selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer) || + !( + selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer + ) || !selectedLayer.id || !selectedFeature?.id ) diff --git a/src/Map/index.tsx b/src/Map/index.tsx index 1325257..2b2eae4 100644 --- a/src/Map/index.tsx +++ b/src/Map/index.tsx @@ -10,6 +10,7 @@ import SpatialId from "./SpatialId"; import type { Engine, EngineProps } from "./types"; export * from "./types"; +export * from "./tileProviderDefaults"; export { useGet, type WrappedRef, type Undefinable } from "./utils"; export type { diff --git a/src/Map/tileProviderDefaults.ts b/src/Map/tileProviderDefaults.ts new file mode 100644 index 0000000..281a42a --- /dev/null +++ b/src/Map/tileProviderDefaults.ts @@ -0,0 +1,106 @@ +/** + * Tile Provider Default Configurations + * ------------------------------------- + * Factory functions for creating TileProviderConfig objects. + * All tile URLs must be provided by the caller — this module + * does not generate or hard-code any URLs. + */ + +import type { TileOverride, TileProviderConfig } from "./types/tileProvider"; + +export type { TileOverride }; + +/** + * Create a TileProviderConfig for Terravista. + * The caller (e.g. visualizer) is responsible for building the override arrays + * from its own env vars or configuration. + * + * @example + * ```typescript + * import { createTerravistaConfig } from '@reearth/core'; + * + * const baseUrl = "https://tiles.eukarya.io"; + * const config = createTerravistaConfig(token, { + * imageryTileOverrides: [ + * { id: "default", url: `${baseUrl}/imagery/google-satellite/{z}/{x}/{y}.webp`, credit: "© Google" }, + * ], + * terrainOverrides: [ + * { id: "default", url: `${baseUrl}/cesium-mesh/ellipsoid/layer.json` }, + * ], + * layerSourceOverrides: [ + * { id: "googlePhotorealistic", url: `${baseUrl}/google3d/root.json` }, + * ], + * onTokenExpired: async () => ({ accessToken: await refreshToken() }), + * }); + * ``` + */ +export function createTerravistaConfig( + accessToken: string, + options?: { + baseUrl?: string; + expiresAt?: number; + onTokenExpired?: () => Promise<{ accessToken: string; expiresAt?: number }>; + imageryTileOverrides?: TileOverride[]; + terrainOverrides?: TileOverride[]; + layerSourceOverrides?: TileOverride[]; + }, +): TileProviderConfig { + return { + type: "terravista", + baseUrl: options?.baseUrl, + auth: { + type: "bearer", + accessToken, + expiresAt: options?.expiresAt, + onTokenExpired: options?.onTokenExpired, + }, + imageryTileOverrides: options?.imageryTileOverrides, + terrainOverrides: options?.terrainOverrides, + layerSourceOverrides: options?.layerSourceOverrides, + }; +} + +/** + * Create a TileProviderConfig for a fully custom tile server. + * + * @example + * ```typescript + * import { createCustomConfig } from '@reearth/core'; + * + * const config = createCustomConfig({ + * auth: { type: 'query-param', accessToken: 'my-api-key', queryParamName: 'api_key' }, + * imageryTileOverrides: [ + * { id: "default", url: "https://my-tiles.com/{z}/{x}/{y}.png" }, + * ], + * terrainOverrides: [ + * { id: "default", url: "https://my-terrain.com/layer.json" }, + * ], + * }); + * ``` + */ +export function createCustomConfig(options: { + baseUrl?: string; + auth?: { + type: "bearer" | "query-param"; + accessToken?: string; + queryParamName?: string; + }; + imageryTileOverrides?: TileOverride[]; + terrainOverrides?: TileOverride[]; + layerSourceOverrides?: TileOverride[]; +}): TileProviderConfig { + return { + type: "custom", + baseUrl: options.baseUrl, + auth: options.auth + ? { + type: options.auth.type, + accessToken: options.auth.accessToken, + queryParamName: options.auth.queryParamName, + } + : undefined, + imageryTileOverrides: options.imageryTileOverrides, + terrainOverrides: options.terrainOverrides, + layerSourceOverrides: options.layerSourceOverrides, + }; +} diff --git a/src/Map/types/index.ts b/src/Map/types/index.ts index 4c453d9..e99dab6 100644 --- a/src/Map/types/index.ts +++ b/src/Map/types/index.ts @@ -79,6 +79,7 @@ export type { } from "../../mantle"; export * from "./event"; export * from "./viewerProperty"; +export * from "./tileProvider"; export type EngineRef = { name: string; diff --git a/src/Map/types/tileProvider.ts b/src/Map/types/tileProvider.ts new file mode 100644 index 0000000..063dcf8 --- /dev/null +++ b/src/Map/types/tileProvider.ts @@ -0,0 +1,97 @@ +/** + * Tile Provider Configuration Types + * --------------------------------- + * These types allow configuring different tile service providers + * in a provider-agnostic way, making @reearth/core work with + * various backends (Terravista or custom servers). + * + * Note: Cesium Ion is not configured here. When users select cesiumion + * terrain in the editor, the token flows through meta.cesiumIonAccessToken. + */ + +/** + * A single tile source override — the unit passed from visualizer to core. + * Visualizer constructs these from env vars; core only consumes them. + */ +export interface TileOverride { + /** Identifier matched against preset names (e.g. 'default', 'black_marble', 'googlePhotorealistic') */ + id: string; + url: string; + credit?: string; + maxZoomLevel?: number; +} + +/** + * Tile Provider Configuration + * All URL content comes from the consuming application (visualizer) via override arrays. + * Core never generates or hard-codes tile URLs. + */ +export interface TileProviderConfig { + /** + * Provider type + * - 'terravista': Use Terravista (requires Sentinel authentication) + * - 'custom': Fully custom URLs + */ + type: "terravista" | "custom"; + + /** + * Base URL reference (informational, not used for URL generation inside core) + * @example 'https://tiles.eukarya.io' + */ + baseUrl?: string; + + /** + * Authentication configuration + */ + auth?: TileProviderAuth; + + /** + * Imagery tile overrides — keyed by preset id (e.g. 'default', 'default_road', 'black_marble') + */ + imageryTileOverrides?: TileOverride[]; + + /** + * Terrain overrides — typically a single entry with id 'default' + */ + terrainOverrides?: TileOverride[]; + + /** + * Layer source overrides — for 3D tilesets (e.g. id 'googlePhotorealistic') + */ + layerSourceOverrides?: TileOverride[]; +} + +/** + * Authentication configuration + */ +export interface TileProviderAuth { + /** + * Authentication type + * - 'bearer': Bearer token (Terravista style) + * - 'query-param': URL query parameter + */ + type: "bearer" | "query-param"; + + /** + * Access Token + * - For 'bearer': Terravista token + * - For 'query-param': Will be appended to URL + */ + accessToken?: string; + + /** + * Token expiration timestamp (milliseconds since epoch) + */ + expiresAt?: number; + + /** + * Token refresh callback + */ + onTokenExpired?: () => Promise<{ accessToken: string; expiresAt?: number }>; + + /** + * Query parameter name (for 'query-param' type) + * @default 'token' + */ + queryParamName?: string; +} diff --git a/src/Map/types/viewerProperty.ts b/src/Map/types/viewerProperty.ts index 0b1b62e..81ea260 100644 --- a/src/Map/types/viewerProperty.ts +++ b/src/Map/types/viewerProperty.ts @@ -74,7 +74,7 @@ export type GlobeAtmosphereProperty = { export type TerrainProperty = { enabled?: boolean; - type?: "cesium" | "arcgis" | "cesiumion"; + type?: "cesium" | "arcgis" | "cesiumion" | "reearth"; url?: string; normal?: boolean; elevationHeatMap?: ElevationHeatMapProperty; @@ -209,6 +209,7 @@ export type IndicatorProperty = { export type AssetsProperty = { cesium?: AssetsCesiumProperty; + reearthTerrain?: AssetsReearthTerrainProperty; }; export type AssetsCesiumProperty = { @@ -219,6 +220,13 @@ export type AssetsCesiumProperty = { }; }; +export type AssetsReearthTerrainProperty = { + terrain?: { + /** Reearth terrain base URL — do NOT include '/layer.json', Cesium appends it automatically. e.g. 'https://terrain.reearth.land/' */ + url?: string; + }; +}; + export type DebugProperty = { showGlobeWireframe?: boolean; showFramesPerSecond?: boolean; diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index 80228ce..cda5670 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -2,6 +2,7 @@ import { Cesium3DTileset as Cesium3DTilesetType, Cesium3DTileStyle, IonResource, + Resource, ClippingPlane, ClippingPlaneCollection as CesiumClippingPlaneCollection, Cartesian3, @@ -34,8 +35,9 @@ import type { Cesium3DTilesAppearance, } from "../../.."; import { useRefValue } from "../../../../hooks"; -import { LayerSimple } from "../../../../Map"; +import { LayerSimple, TileProviderConfig } from "../../../../Map"; import { layerIdField, sampleTerrainHeightFromCartesian } from "../../common"; +import { resolveTilesetUrl } from "../../core/tileProviderResolver"; import { arrayToCartecian3 } from "../../helpers/sphericalHaromic"; import type { InternalCesium3DTileFeature } from "../../types"; import { @@ -752,20 +754,32 @@ export const useHooks = ({ } }, [style, isTilesetReady]); - const googleMapPhotorealisticResource = useMemo(() => { + const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - const loadTileset = async () => { + const tileProvider = meta?.tileProvider as TileProviderConfig | undefined; + + // First, try to use TileProviderConfig for Terravista URL (returns string directly) + const terravistaUrl = resolveTilesetUrl(tileProvider, "googlePhotorealistic"); + if (terravistaUrl) { + // Terravista or custom URL - Sentinel will inject auth header + // Return string directly (not wrapped in Promise) + return terravistaUrl; + } + + // For async resource loading (Google API or Cesium Ion), return Promise + const loadTileset = async (): Promise => { try { if (googleMapApiKey) { const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey }); return tileset.resource; - } else { - const resource = IonResource.fromAssetId(2275207, { - accessToken: meta?.cesiumIonAccessToken as string | undefined, - }); - return resource; } + + // Fallback: use user-configured Cesium Ion token + const resource = await IonResource.fromAssetId(2275207, { + accessToken: meta?.cesiumIonAccessToken as string | undefined, + }); + return resource; } catch (error) { console.error(`Error loading Photorealistic 3D Tiles tileset: ${error}`); throw error; @@ -773,19 +787,30 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken]); + }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, meta?.tileProvider]); - const tilesetUrl = useMemo(() => { - return type === "osm-buildings" && isVisible - ? IonResource.fromAssetId(96188, { - accessToken: meta?.cesiumIonAccessToken as string | undefined, - }) // https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Scene/createOsmBuildings.js#L53 - : googleMapPhotorealisticResource && isVisible - ? googleMapPhotorealisticResource - : type === "3dtiles" && isVisible - ? (url ?? tileset) - : null; - }, [type, isVisible, meta?.cesiumIonAccessToken, googleMapPhotorealisticResource, url, tileset]); + const tilesetUrl = useMemo((): string | Resource | Promise | null => { + if (!isVisible) return null; + + // Google Photorealistic 3D Tiles + if (googleMapPhotorealisticResource) { + return googleMapPhotorealisticResource; + } + + // OSM Buildings — only available via Cesium Ion (Terravista does not host this dataset). + if (type === "osm-buildings") { + return IonResource.fromAssetId(96188, { + accessToken: meta?.cesiumIonAccessToken as string | undefined, + }); // https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Scene/createOsmBuildings.js#L53 + } + + // Standard 3D Tiles with explicit URL + if (type === "3dtiles") { + return url ?? tileset ?? null; + } + + return null; + }, [type, isVisible, googleMapPhotorealisticResource, url, tileset, meta?.cesiumIonAccessToken]); const imageBasedLighting = useMemo(() => { if ( diff --git a/src/engines/Cesium/core/Globe/index.tsx b/src/engines/Cesium/core/Globe/index.tsx index 667d545..11114b4 100644 --- a/src/engines/Cesium/core/Globe/index.tsx +++ b/src/engines/Cesium/core/Globe/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, type JSX } from "react"; import { Globe as CesiumGlobe } from "resium"; import type { ViewerProperty } from "../../.."; +import type { TileProviderConfig } from "../../../../Map/types/tileProvider"; import { toColor } from "../../common"; import useTerrainProviderPromise from "./useTerrainProviderPromise"; @@ -9,12 +10,14 @@ import useTerrainProviderPromise from "./useTerrainProviderPromise"; export type Props = { property?: ViewerProperty; cesiumIonAccessToken?: string; + tileProvider?: TileProviderConfig; onTerrainProviderChange?: () => void; }; export default function Globe({ property, cesiumIonAccessToken, + tileProvider, onTerrainProviderChange, }: Props): JSX.Element | null { const providerPromise = useTerrainProviderPromise({ @@ -24,6 +27,7 @@ export default function Globe({ ionAccessToken: property?.assets?.cesium?.terrain?.ionAccessToken || cesiumIonAccessToken, ionAsset: property?.assets?.cesium?.terrain?.ionAsset, ionUrl: property?.assets?.cesium?.terrain?.ionUrl, + tileProvider, }); const baseColor = useMemo( diff --git a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts index 002f142..704d790 100644 --- a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts +++ b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts @@ -8,26 +8,33 @@ import { import { useMemo, useRef } from "react"; import { TerrainProperty } from "../../.."; -import { AssetsCesiumProperty } from "../../../../Map"; +import { AssetsCesiumProperty, TileProviderConfig } from "../../../../Map"; +import { resolveTerrainUrl } from "../tileProviderResolver"; -type TerrainType = NonNullable; +// Extended terrain types to include 'reearth' +type TerrainType = NonNullable | "reearth"; type ProviderOpts = Pick & AssetsCesiumProperty["terrain"] & { terrain?: boolean; terrainType?: TerrainType | null | undefined; ionAccessToken?: string | undefined; + /** Reearth terrain URL (for type='reearth') */ + reearthTerrainUrl?: string | undefined; + /** TileProviderConfig for provider-agnostic configuration */ + tileProvider?: TileProviderConfig | undefined; }; export default function useTerrainProviderPromise(opts: ProviderOpts) { - // Cache promises so we don’t recreate providers on every toggle + // Cache promises so we don't recreate providers on every toggle const cacheRef = useRef(new Map>()); const ellipsoidRef = useRef(undefined); return useMemo>(() => { if (!opts.terrain) { // single shared ellipsoid provider - if (!ellipsoidRef.current) ellipsoidRef.current = new EllipsoidTerrainProvider(); + if (!ellipsoidRef.current) + ellipsoidRef.current = new EllipsoidTerrainProvider(); return Promise.resolve(ellipsoidRef.current); } @@ -43,29 +50,60 @@ export default function useTerrainProviderPromise(opts: ProviderOpts) { } function makeKey(type: TerrainType, opts: ProviderOpts) { - // Key should change when output provider would differ const asset = opts.ionAsset ?? ""; const url = opts.ionUrl ?? ""; + const reearthTerrainUrl = opts.reearthTerrainUrl ?? resolveTerrainUrl(opts.tileProvider) ?? ""; + const ionToken = opts.ionAccessToken ?? ""; const normal = String(!!opts.normal); - return `${type}|asset:${asset}|url:${url}|normal:${normal}`; + return `${type}|asset:${asset}|url:${url}|reearth:${reearthTerrainUrl}|ion:${ionToken}|normal:${normal}`; } function createProvider(type: TerrainType, opts: ProviderOpts): Promise { + // First, try to use TileProviderConfig if available + const tileProviderUrl = resolveTerrainUrl(opts.tileProvider); + switch (type) { - case "cesium": + case "reearth": { + // Explicit Reearth terrain — must have a URL configured. + const terrainUrl = opts.reearthTerrainUrl ?? tileProviderUrl; + if (terrainUrl) { + return CesiumTerrainProvider.fromUrl(terrainUrl, { + requestVertexNormals: !!opts.normal, + requestWaterMask: false, + }) as Promise; + } + console.warn( + "[Terrain] type='reearth' requires tileProvider.terrainOverrides to be configured. " + + "Falling back to ellipsoid.", + ); + return Promise.resolve(new EllipsoidTerrainProvider()); + } + + case "cesium": { + // "cesium" preserves its original meaning: CesiumWorld Terrain (Ion Asset 1). + // If a tileProvider URL is configured it takes precedence — this allows reearth terrain + // deployments to upgrade without requiring a scene-data migration from "cesium" → "reearth". + const terrainUrl = opts.reearthTerrainUrl ?? tileProviderUrl; + if (terrainUrl) { + return CesiumTerrainProvider.fromUrl(terrainUrl, { + requestVertexNormals: !!opts.normal, + requestWaterMask: false, + }) as Promise; + } + // Legacy: fall back to CesiumWorld Terrain via Ion. return CesiumTerrainProvider.fromUrl( IonResource.fromAssetId(1, { accessToken: opts.ionAccessToken }), { requestVertexNormals: !!opts.normal, requestWaterMask: false }, ) as Promise; + } case "arcgis": return ArcGISTiledElevationTerrainProvider.fromUrl( "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer", ) as Promise; - case "cesiumion": + case "cesiumion": { if (!opts.ionAsset && !opts.ionUrl) { - // Fallback to ellipsoid when misconfigured return Promise.resolve(new EllipsoidTerrainProvider()); } return CesiumTerrainProvider.fromUrl( @@ -75,5 +113,10 @@ function createProvider(type: TerrainType, opts: ProviderOpts): Promise; + } + + default: + // Unknown type, return ellipsoid + return Promise.resolve(new EllipsoidTerrainProvider()); } } diff --git a/src/engines/Cesium/core/Imagery.test.ts b/src/engines/Cesium/core/Imagery.test.ts index 6e6d436..7873e13 100644 --- a/src/engines/Cesium/core/Imagery.test.ts +++ b/src/engines/Cesium/core/Imagery.test.ts @@ -5,8 +5,7 @@ import { type Tile, useImageryProviders } from "./Imagery"; test("useImageryProviders", () => { const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url })); - const provider2 = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge2: url })); - const presets = { default: provider, default_label: provider2 }; + const presets = { default: provider }; const { result, rerender } = renderHook( ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => useImageryProviders({ @@ -89,19 +88,6 @@ test("useImageryProviders", () => { expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider2); expect(provider).toBeCalledTimes(4); - // update a tile type - typedRerender({ - tiles: [{ id: "1", type: "default_label", url: "u" }], - cesiumIonAccessToken: "a", - }); - - expect(result.current.providers).toEqual({ - "1": ["default_label", "u", { hoge2: "u" }], - }); - expect(result.current.updated).toBe(true); - expect(provider).toBeCalledTimes(4); - expect(provider2).toBeCalledTimes(1); - // update a tile type to unexpected type typedRerender({ tiles: [{ id: "1", type: "unexpected_type", url: "u" }], @@ -113,7 +99,6 @@ test("useImageryProviders", () => { }); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(5); - expect(provider2).toBeCalledTimes(1); typedRerender({ tiles: [] }); expect(result.current.providers).toEqual({}); diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index bdf5acc..4636854 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -1,12 +1,15 @@ import { Color, + ImageryLayer as CesiumImageryLayer, ImageryProvider, TextureMagnificationFilter, TextureMinificationFilter, } from "cesium"; import { isEqual } from "lodash-es"; -import { useCallback, useMemo, useRef, useLayoutEffect, useState, useEffect } from "react"; -import { ImageryLayer } from "resium"; +import { useCallback, useMemo, useRef, useEffect } from "react"; +import { useCesium } from "resium"; + +import type { TileProviderConfig } from "../../../Map/types/tileProvider"; import { isValidPresetTileType, PresetTileType, tiles as tilePresets } from "./presets"; @@ -31,53 +34,77 @@ export type Tile = { export type Props = { tiles?: Tile[]; cesiumIonAccessToken?: string; + tileProvider?: TileProviderConfig; onTilesChange?: () => void; }; -export default function ImageryLayers({ tiles, cesiumIonAccessToken, onTilesChange }: Props) { - const { providers, updated } = useImageryProviders({ +// NOTE: This component intentionally bypasses Resium's declarative . +// Resium wraps provider creation in queueMicrotask; under React 18 Strict Mode the effect +// cleanup runs before that microtask resolves, so the layer reference is undefined at cleanup +// time and old layers are never removed — they accumulate silently behind new ones. +// Direct imageryLayerCollection management with a `cancelled` flag is the only safe fix. +export default function ImageryLayers({ + tiles, + cesiumIonAccessToken, + tileProvider, + onTilesChange, +}: Props) { + const { imageryLayerCollection, scene } = useCesium(); + + const { providers } = useImageryProviders({ tiles, cesiumIonAccessToken, + tileProvider, presets: tilePresets, }); - // force rerendering all layers when any provider is updated - // since Resium does not sort layers according to ImageryLayer component order - const [counter, setCounter] = useState(0); - useLayoutEffect(() => { - if (updated) setCounter(c => c + 1); - }, [providers, updated]); - useEffect(() => { + if (!imageryLayerCollection || !scene) return; + + let cancelled = false; + const addedLayers: CesiumImageryLayer[] = []; + + tiles?.forEach(({ id, zoomLevel, opacity, heatmap }, i) => { + const providerOrPromise = providers[id]?.[2]; + if (!providerOrPromise) return; + + const doAdd = (provider: ImageryProvider) => { + if (cancelled || scene.isDestroyed()) return; + const layer = new CesiumImageryLayer(provider, { + minimumTerrainLevel: zoomLevel?.[0], + maximumTerrainLevel: zoomLevel?.[1], + alpha: opacity, + colorToAlpha: heatmap ? Color.WHITE : undefined, + colorToAlphaThreshold: heatmap ? 1 : undefined, + magnificationFilter: heatmap ? TextureMagnificationFilter.LINEAR : undefined, + minificationFilter: heatmap ? TextureMinificationFilter.NEAREST : undefined, + }); + imageryLayerCollection.add(layer, i); + addedLayers.push(layer); + scene.requestRender(); + }; + + if (providerOrPromise instanceof Promise) { + providerOrPromise.then(doAdd); + } else { + doAdd(providerOrPromise as ImageryProvider); + } + }); + + scene.requestRender(); onTilesChange?.(); - }, [tiles, counter, onTilesChange]); - - return ( - <> - {tiles - ?.map(({ id, ...tile }) => ({ - ...tile, - id, - provider: providers[id]?.[2], - })) - .map(({ id, opacity, zoomLevel, provider, heatmap }, i) => - provider ? ( - - ) : null, - )} - - ); + + return () => { + cancelled = true; + for (const layer of addedLayers) { + if (!scene.isDestroyed() && imageryLayerCollection.contains(layer)) { + imageryLayerCollection.remove(layer); + } + } + }; + }, [providers, tiles, imageryLayerCollection, scene, onTilesChange]); + + return null; } type Providers = { [id: string]: [string | undefined, string | undefined, ImageryProvider] }; @@ -85,31 +112,37 @@ type Providers = { [id: string]: [string | undefined, string | undefined, Imager export function useImageryProviders({ tiles = [], cesiumIonAccessToken, + tileProvider, presets, }: { tiles?: Tile[]; cesiumIonAccessToken?: string; + tileProvider?: TileProviderConfig; presets: { [K in PresetTileType]: (opts?: { url?: string; cesiumIonAccessToken?: string; heatmap?: boolean; zoomLevel?: number[]; + tileProvider?: TileProviderConfig; }) => Promise | ImageryProvider | null; }; }): { providers: Providers; updated: boolean } { const newTile = useCallback( - (t: Tile, ciat?: string) => - presets[isValidPresetTileType(t.type) ? t.type : "default"]({ + (t: Tile, ciat?: string, tp?: TileProviderConfig) => { + return presets[isValidPresetTileType(t.type) ? t.type : "default"]({ url: t.url, cesiumIonAccessToken: ciat, heatmap: t.heatmap, zoomLevel: t.zoomLevelForURL, - }), + tileProvider: tp, + }); + }, [presets], ); const prevCesiumIonAccessToken = useRef(cesiumIonAccessToken); + const prevTileProvider = useRef(tileProvider); const tileKeys = tiles.map(t => t.id).join(","); const prevTileKeys = useRef(tileKeys); const prevProviders = useRef({}); @@ -126,6 +159,7 @@ export function useImageryProviders({ // Manage TileProviders so that TileProvider does not need to be recreated each time tiles are updated. const { providers, updated } = useMemo(() => { const isCesiumAccessTokenUpdated = prevCesiumIonAccessToken.current !== cesiumIonAccessToken; + const isTileProviderUpdated = prevTileProvider.current !== tileProvider; const prevProvidersKeys = Object.keys(prevProviders.current); const added = tiles.map(t => t.id).filter(t => t && !prevProvidersKeys.includes(t)); @@ -168,8 +202,9 @@ export function useImageryProviders({ added || prevType !== tile.type || prevUrl !== tile.url || + isTileProviderUpdated || (isCesiumAccessTokenUpdated && (!tile.type || tile.type === "default")) - ? [tile.type, tile.url, newTile(tile, cesiumIonAccessToken)] + ? [tile.type, tile.url, newTile(tile, cesiumIonAccessToken, tileProvider)] : [prevType, prevUrl, prevProvider], ], ) @@ -182,6 +217,7 @@ export function useImageryProviders({ const updated = !!added.length || !!isCesiumAccessTokenUpdated || + !!isTileProviderUpdated || !isEqual(prevTileKeys.current, tileKeys) || !isEqual(prevZoomLevels.current, zoomLevels) || rawProviders.some(p => p.tile && (p.prevType !== p.tile.type || p.prevUrl !== p.tile.url)); @@ -189,9 +225,10 @@ export function useImageryProviders({ prevTileKeys.current = tileKeys; prevZoomLevels.current = zoomLevels; prevCesiumIonAccessToken.current = cesiumIonAccessToken; + prevTileProvider.current = tileProvider; return { providers, updated }; - }, [cesiumIonAccessToken, tiles, tileKeys, newTile, zoomLevels]); + }, [cesiumIonAccessToken, tileProvider, tiles, tileKeys, newTile, zoomLevels]); prevProviders.current = providers; return { providers, updated }; diff --git a/src/engines/Cesium/core/presets.ts b/src/engines/Cesium/core/presets.ts index 713be4d..dc47c59 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -8,12 +8,19 @@ import { DiscardEmptyTileImagePolicy, } from "cesium"; +import type { TileProviderConfig } from "../../../Map/types/tileProvider"; + import { JapanGSIOptimalBVmapLabelImageryProvider } from "./labels/JapanGSIOptimalBVmapVectorMapLabel/JapanGSIOptimalBVmapLabelImageryProvider"; +import { + resolveImageryUrl, + resolveImageryCredit, + resolveImageryMaxZoom, +} from "./tileProviderResolver"; const PRESET_TILE_TYPES = [ "default", - "default_label", "default_road", + "default_label", "open_street_map", "black_marble", "japan_gsi_standard", @@ -26,35 +33,97 @@ export const isValidPresetTileType = (type: string | undefined): type is PresetT return PRESET_TILE_TYPES.includes(type as PresetTileType); }; +export type TileOptions = { + url?: string; + cesiumIonAccessToken?: string; + heatmap?: boolean; + tile_zoomLevel?: number[]; + tileProvider?: TileProviderConfig; +}; + +/** + * Create imagery provider from TileProviderConfig URL (terravista or custom). + * Returns null if no URL is configured — callers fall back to legacy Ion path. + */ +function createPresetImageryProvider( + presetName: string, + opts?: TileOptions, +): Promise | ImageryProvider | null { + const { tileProvider } = opts ?? {}; + + const url = resolveImageryUrl(tileProvider, presetName); + if (!url) return null; + + return new UrlTemplateImageryProvider({ + url, + credit: resolveImageryCredit(tileProvider, presetName), + maximumLevel: resolveImageryMaxZoom(tileProvider, presetName), + }); +} + export const tiles = { - default: ({ cesiumIonAccessToken } = {}) => - IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL, { - accessToken: cesiumIonAccessToken, - }).catch(console.error), + // Standard presets - now support TileProviderConfig + default: (opts?: TileOptions) => { + const result = createPresetImageryProvider("default", opts); + if (result) return result; + + // Legacy fallback + return IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }); + }, + + default_road: (opts?: TileOptions) => { + const result = createPresetImageryProvider("default_road", opts); + if (result) return result; + + // Legacy fallback + return IonImageryProvider.fromAssetId(IonWorldImageryStyle.ROAD, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }); + }, + default_label: ({ cesiumIonAccessToken } = {}) => IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL_WITH_LABELS, { accessToken: cesiumIonAccessToken, - }).catch(console.error), - default_road: ({ cesiumIonAccessToken } = {}) => - IonImageryProvider.fromAssetId(IonWorldImageryStyle.ROAD, { - accessToken: cesiumIonAccessToken, - }).catch(console.error), + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }), + open_street_map: () => new OpenStreetMapImageryProvider({ url: "https://tile.openstreetmap.org", credit: '© OpenStreetMap contributors', }), - black_marble: ({ cesiumIonAccessToken } = {}) => - IonImageryProvider.fromAssetId(3812, { - accessToken: cesiumIonAccessToken, - }).catch(console.error), + + black_marble: (opts?: TileOptions) => { + const result = createPresetImageryProvider("black_marble", opts); + if (result) return result; + + // Legacy fallback + return IonImageryProvider.fromAssetId(3812, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }); + }, + japan_gsi_standard: () => new OpenStreetMapImageryProvider({ url: "https://cyberjapandata.gsi.go.jp/xyz/std/", credit: "国土地理院, Shoreline data is derived from: United States. National Imagery and Mapping Agency. \"Vector Map Level 0 (VMAP0).\" Bethesda, MD: Denver, CO: The Agency; USGS Information Services, 1997.", }), - url: ({ url, heatmap, tile_zoomLevel } = {}) => + + url: ({ url, heatmap, tile_zoomLevel }: TileOptions = {}) => url ? new UrlTemplateImageryProvider({ url, @@ -64,12 +133,7 @@ export const tiles = { }) : null, } as { - [K in PresetTileType]: (opts?: { - url?: string; - cesiumIonAccessToken?: string; - heatmap?: boolean; - tile_zoomLevel?: number[]; - }) => Promise | ImageryProvider | null; + [K in PresetTileType]: (opts?: TileOptions) => Promise | ImageryProvider | null; }; export const labelTiles = { diff --git a/src/engines/Cesium/core/tileProviderResolver.ts b/src/engines/Cesium/core/tileProviderResolver.ts new file mode 100644 index 0000000..af65349 --- /dev/null +++ b/src/engines/Cesium/core/tileProviderResolver.ts @@ -0,0 +1,72 @@ +/** + * Tile Provider Resolver + * ---------------------- + * Utility functions to look up URLs and metadata + * from TileProviderConfig override arrays. + */ + +import type { TileProviderConfig } from "../../../Map/types/tileProvider"; + +/** + * Resolve terrain URL from TileProviderConfig. + * Uses the first entry with id 'default', or the first entry if none matches. + */ +export function resolveTerrainUrl(config: TileProviderConfig | undefined): string | null { + if (!config) return null; + const overrides = config.terrainOverrides; + if (!overrides?.length) return null; + return (overrides.find(t => t.id === "default") ?? overrides[0]).url; +} + +/** + * Resolve imagery URL from TileProviderConfig by preset id. + * @param presetName - e.g. 'default', 'default_road', 'black_marble' + */ +export function resolveImageryUrl( + config: TileProviderConfig | undefined, + presetName: string, +): string | null { + if (!config) return null; + return config.imageryTileOverrides?.find(o => o.id === presetName)?.url ?? null; +} + +/** + * Resolve imagery credit/attribution by preset id. + */ +export function resolveImageryCredit( + config: TileProviderConfig | undefined, + presetName: string, +): string | undefined { + if (!config) return undefined; + return config.imageryTileOverrides?.find(o => o.id === presetName)?.credit; +} + +/** + * Resolve imagery maximum zoom level by preset id. + */ +export function resolveImageryMaxZoom( + config: TileProviderConfig | undefined, + presetName: string, +): number | undefined { + if (!config) return undefined; + return config.imageryTileOverrides?.find(o => o.id === presetName)?.maxZoomLevel; +} + +/** + * Resolve 3D tileset URL from TileProviderConfig by tileset id. + * @param tilesetId - e.g. 'googlePhotorealistic' + */ +export function resolveTilesetUrl( + config: TileProviderConfig | undefined, + tilesetId: string, +): string | null { + if (!config) return null; + return config.layerSourceOverrides?.find(o => o.id === tilesetId)?.url ?? null; +} + +/** + * Get base URL from config (informational reference only). + */ +export function getBaseUrl(config: TileProviderConfig | undefined): string | undefined { + return config?.baseUrl; +} diff --git a/src/engines/Cesium/hooks.ts b/src/engines/Cesium/hooks.ts index e0d015c..d7c0c14 100644 --- a/src/engines/Cesium/hooks.ts +++ b/src/engines/Cesium/hooks.ts @@ -1,7 +1,6 @@ import { Entity, Cesium3DTileFeature, - Ion, Cesium3DTileset, JulianDate, Cesium3DTilePointFeature, @@ -13,8 +12,19 @@ import { ShadowMap, ImageryLayer, } from "cesium"; -import { MutableRefObject, RefObject, useCallback, useEffect, useMemo, useRef } from "react"; -import type { CesiumComponentRef, CesiumMovementEvent, RootEventTarget } from "resium"; +import { + MutableRefObject, + RefObject, + useCallback, + useEffect, + useMemo, + useRef, +} from "react"; +import type { + CesiumComponentRef, + CesiumMovementEvent, + RootEventTarget, +} from "resium"; import type { LayerSelectionReason, @@ -25,7 +35,13 @@ import type { LayerVisibilityEvent, } from ".."; import { e2eAccessToken, setE2ECesiumViewer } from "../../e2eConfig"; -import { ComputedFeature, DataType, SelectedFeatureInfo, LatLng, Camera } from "../../mantle"; +import { + ComputedFeature, + DataType, + SelectedFeatureInfo, + LatLng, + Camera, +} from "../../mantle"; import { Credits, LayerLoadEvent, @@ -35,6 +51,7 @@ import { LayersRef, RequestingRenderMode, } from "../../Map"; +import type { TileProviderConfig } from "../../Map/types/tileProvider"; import { TimelineManagerRef } from "../../Map/useTimelineManager"; import { FEATURE_FLAGS } from "../../shared/featureFlags"; @@ -60,7 +77,10 @@ interface CustomGlobeSurface { }; } -type CesiumMouseEvent = (movement: CesiumMovementEvent, target: RootEventTarget) => void; +type CesiumMouseEvent = ( + movement: CesiumMovementEvent, + target: RootEventTarget, +) => void; type CesiumMouseWheelEvent = (delta: number) => void; export default ({ @@ -116,7 +136,11 @@ export default ({ options?: LayerSelectionReason, info?: SelectedFeatureInfo, ) => void; - onLayerDrag?: (layerId: string, featureId: string | undefined, position: LatLng) => void; + onLayerDrag?: ( + layerId: string, + featureId: string | undefined, + position: LatLng, + ) => void; onLayerDrop?: ( layerId: string, featureId: string | undefined, @@ -137,7 +161,10 @@ export default ({ const cesiumIonAccessToken = typeof meta?.cesiumIonAccessToken === "string" && meta.cesiumIonAccessToken ? meta.cesiumIonAccessToken - : Ion.defaultAccessToken; + : undefined; + + // Extract TileProviderConfig from meta + const tileProvider = meta?.tileProvider as TileProviderConfig | undefined; // expose ref const engineAPI = useEngineRef(ref, cesium); @@ -170,10 +197,12 @@ export default ({ }) | undefined; if (!shadowMap) return; - shadowMap.softShadows = property?.scene?.shadow?.shadowMap?.softShadows ?? false; + shadowMap.softShadows = + property?.scene?.shadow?.shadowMap?.softShadows ?? false; shadowMap.darkness = property?.scene?.shadow?.shadowMap?.darkness ?? 0.3; shadowMap.size = property?.scene?.shadow?.shadowMap?.size ?? 2048; - shadowMap.maximumDistance = property?.scene?.shadow?.shadowMap?.maximumDistance ?? 5000; + shadowMap.maximumDistance = + property?.scene?.shadow?.shadowMap?.maximumDistance ?? 5000; shadowMap.fadingEnabled = true; shadowMap.normalOffset = true; @@ -286,7 +315,7 @@ export default ({ // Find ImageryLayerFeature const ImageryLayerDataTypes: DataType[] = []; const layers = layersRef?.current?.findAll( - layer => + (layer) => layer.type === "simple" && !!layer.data?.type && ImageryLayerDataTypes.includes(layer.data?.type), @@ -298,7 +327,7 @@ export default ({ ((): [ComputedFeature, string] | void => { for (const layer of layers) { const f = layer.computed?.features.find( - feature => feature.id !== selectedLayerId?.featureId, + (feature) => feature.id !== selectedLayerId?.featureId, ); if (f) { return [f, layer.id]; @@ -337,7 +366,9 @@ export default ({ if (entity) { const layer = tag?.layerId - ? (layersRef?.current?.overriddenLayers().find(l => l.id === tag.layerId) ?? + ? (layersRef?.current + ?.overriddenLayers() + .find((l) => l.id === tag.layerId) ?? layersRef?.current?.findById(tag.layerId)) : undefined; // Sometimes only featureId is specified, so we need to sync entity tag. @@ -350,8 +381,11 @@ export default ({ title: entity.name, content: getEntityContent( entity, - cesium.current?.cesiumElement?.clock.currentTime ?? new JulianDate(), - tag?.layerId ? layer?.infobox?.property?.defaultContent : undefined, + cesium.current?.cesiumElement?.clock.currentTime ?? + new JulianDate(), + tag?.layerId + ? layer?.infobox?.property?.defaultContent + : undefined, ), }, } @@ -385,7 +419,11 @@ export default ({ }); const handleMouseEvent = useCallback( - (type: keyof MouseEvents, e: CesiumMovementEvent, target: RootEventTarget) => { + ( + type: keyof MouseEvents, + e: CesiumMovementEvent, + target: RootEventTarget, + ) => { if (engineAPI.mouseEventCallbacks[type]?.length > 0) { const viewer = cesium.current?.cesiumElement; if (!viewer || viewer.isDestroyed()) return; @@ -393,7 +431,7 @@ export default ({ if (!props) return; const layerId = getLayerId(target); if (layerId) props.layerId = layerId; - engineAPI.mouseEventCallbacks[type].forEach(cb => cb(props)); + engineAPI.mouseEventCallbacks[type].forEach((cb) => cb(props)); } }, [engineAPI], @@ -402,7 +440,7 @@ export default ({ const handleMouseWheel = useCallback( (delta: number) => { if (engineAPI.mouseEventCallbacks.wheel.length > 0) { - engineAPI.mouseEventCallbacks.wheel.forEach(cb => cb({ delta })); + engineAPI.mouseEventCallbacks.wheel.forEach((cb) => cb({ delta })); } }, [engineAPI], @@ -431,9 +469,12 @@ export default ({ handleMouseWheel(delta); }, }; - (Object.keys(mouseEvents) as (keyof MouseEvents)[]).forEach(type => { + (Object.keys(mouseEvents) as (keyof MouseEvents)[]).forEach((type) => { if (type !== "wheel") - mouseEvents[type] = (e: CesiumMovementEvent, target: RootEventTarget) => { + mouseEvents[type] = ( + e: CesiumMovementEvent, + target: RootEventTarget, + ) => { handleMouseEvent(type as keyof MouseEvents, e, target); }; }); @@ -462,10 +503,17 @@ export default ({ viewer.selectedEntity = undefined; } - if (target && "id" in target && target.id instanceof Entity && isSelectable(target.id)) { + if ( + target && + "id" in target && + target.id instanceof Entity && + isSelectable(target.id) + ) { const tag = getTag(target.id); const layer = tag?.layerId - ? (layersRef?.current?.overriddenLayers().find(l => l.id === tag.layerId) ?? + ? (layersRef?.current + ?.overriddenLayers() + .find((l) => l.id === tag.layerId) ?? layersRef?.current?.findById(tag.layerId)) : undefined; onLayerSelect?.( @@ -478,7 +526,9 @@ export default ({ content: getEntityContent( target.id, viewer.clock.currentTime ?? new JulianDate(), - tag?.layerId ? layer?.infobox?.property?.defaultContent : undefined, + tag?.layerId + ? layer?.infobox?.property?.defaultContent + : undefined, ), }, } @@ -495,7 +545,8 @@ export default ({ if ( target && - (target instanceof Cesium3DTileFeature || target instanceof Cesium3DTilePointFeature) + (target instanceof Cesium3DTileFeature || + target instanceof Cesium3DTilePointFeature) ) { const tag = getTag(target); if (tag) { @@ -540,7 +591,8 @@ export default ({ if ( target?.primitive && - (target.primitive instanceof Primitive || target.primitive instanceof GroundPrimitive) + (target.primitive instanceof Primitive || + target.primitive instanceof GroundPrimitive) ) { const primitive = target.primitive; const tag = getTag(primitive); @@ -558,17 +610,26 @@ export default ({ const pickRay = scene.camera.getPickRay(e.position); if (pickRay) { - const l = await scene.imageryLayers.pickImageryLayerFeatures(pickRay, scene); + const l = await scene.imageryLayers.pickImageryLayerFeatures( + pickRay, + scene, + ); // Find the topmost overlaid feature. - const f = l?.findLast(f => { + const f = l?.findLast((f) => { const appearanceType = f?.data?.appearanceType; - return appearanceType && f?.data?.feature?.[appearanceType]?.show !== false; + return ( + appearanceType && + f?.data?.feature?.[appearanceType]?.show !== false + ); }); const appearanceType = f?.data?.appearanceType; - if (appearanceType && f?.data?.feature?.[appearanceType]?.show !== false) { + if ( + appearanceType && + f?.data?.feature?.[appearanceType]?.show !== false + ) { const tag = getTag(f.imageryLayer); const pos = f.position; @@ -587,13 +648,17 @@ export default ({ } const layer = tag?.layerId - ? (layersRef?.current?.overriddenLayers().find(l => l.id === tag.layerId) ?? + ? (layersRef?.current + ?.overriddenLayers() + .find((l) => l.id === tag.layerId) ?? layersRef?.current?.findById(tag.layerId)) : undefined; const content = getEntityContent( f.data.feature ?? f, viewer.clock.currentTime ?? new JulianDate(), - tag?.layerId ? layer?.infobox?.property?.defaultContent : undefined, + tag?.layerId + ? layer?.infobox?.property?.defaultContent + : undefined, ); prevSelectedImageryFeatureId.current = f.data.featureId; onLayerSelect?.( @@ -702,7 +767,8 @@ export default ({ if (globe) { const surface = (globe as any)._surface as CustomGlobeSurface; if (surface) { - surface.tileProvider._debug.wireframe = property?.debug?.showGlobeWireframe ?? false; + surface.tileProvider._debug.wireframe = + property?.debug?.showGlobeWireframe ?? false; } } }, [property?.debug?.showGlobeWireframe]); @@ -723,14 +789,21 @@ export default ({ }); }, [time, timelineManagerRef]); - const { sceneLight, sceneBackgroundColor, sceneMsaaSamples, sceneMode } = useViewerProperty({ - property, - cesium, - }); + const { sceneLight, sceneBackgroundColor, sceneMsaaSamples, sceneMode } = + useViewerProperty({ + property, + cesium, + }); useLayerDragDrop({ cesium, onLayerDrag, onLayerDrop, isLayerDraggable }); - useExplicitRender({ cesium, requestingRenderMode, isLayerDragging, shouldRender, property }); + useExplicitRender({ + cesium, + requestingRenderMode, + isLayerDragging, + shouldRender, + property, + }); const { cameraViewBoundaries, @@ -770,6 +843,7 @@ export default ({ return { cesium, cesiumIonAccessToken, + tileProvider, mouseEventHandles, layerSelectWithRectEventHandlers, context, diff --git a/src/engines/Cesium/index.tsx b/src/engines/Cesium/index.tsx index 8228650..a65a190 100644 --- a/src/engines/Cesium/index.tsx +++ b/src/engines/Cesium/index.tsx @@ -72,6 +72,7 @@ const Cesium: React.ForwardRefRenderFunction = ( cesium, mouseEventHandles, cesiumIonAccessToken, + tileProvider, context, layerSelectWithRectEventHandlers, sceneLight, @@ -167,6 +168,7 @@ const Cesium: React.ForwardRefRenderFunction = ( @@ -265,6 +267,7 @@ const Cesium: React.ForwardRefRenderFunction = ( {ready ? children : null} @@ -293,5 +296,5 @@ export const engine: Engine = { featureComponent: Feature, clusterComponent: Cluster, sketchComponent: Sketch, - delegatedDataTypes: ["czml", "wms", "mvt", "3dtiles", "osm-buildings", "kml"], + delegatedDataTypes: ["czml", "wms", "mvt", "3dtiles", "osm-buildings", "google-photorealistic", "kml"], }; diff --git a/vite.config.example.ts b/vite.config.example.ts index fd226a9..d61359f 100644 --- a/vite.config.example.ts +++ b/vite.config.example.ts @@ -23,4 +23,15 @@ export default defineConfig({ }, }, envPrefix: "EXAMPLE_", + server: { + proxy: { + // Proxy terrain requests to avoid CORS in dev — terrain.reearth.land + // does not send Access-Control-Allow-Origin headers. + "/terrain-proxy": { + target: "https://terrain.reearth.land", + changeOrigin: true, + rewrite: path => path.replace(/^\/terrain-proxy/, ""), + }, + }, + }, }); From 7ca34ce567875b043f7de0a1f30c146a15592ec8 Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Mon, 18 May 2026 19:05:34 +0900 Subject: [PATCH 02/19] refactor: seperate Ion and terravista and add reearth osm building --- .env.example | 5 +- example/buildExampleMeta.ts | 46 ++++---- example/constants.ts | 13 ++- example/hooks.ts | 5 +- example/testLayers/index.ts | 2 + example/testLayers/reearth_buildings.ts | 10 ++ src/engines/Cesium/Feature/Tileset/hooks.ts | 24 +++- src/engines/Cesium/Feature/index.tsx | 1 + src/engines/Cesium/core/Imagery.test.ts | 55 +++++---- src/engines/Cesium/core/Imagery.tsx | 12 +- src/engines/Cesium/core/presets.ts | 117 ++++++++++---------- src/mantle/types/index.ts | 1 + 12 files changed, 180 insertions(+), 111 deletions(-) create mode 100644 example/testLayers/reearth_buildings.ts diff --git a/.env.example b/.env.example index 21f5771..cc3ca99 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,9 @@ +# Used by the google-photorealistic layer as a fallback when no Terravista Google 3D Tiles URL is configured EXAMPLE_GOOGLE_MAP_API_KEY=your_google_map_api_key +# Cesium Ion access token — required to test cesium_ion_* imagery tiles and osm-buildings layer +EXAMPLE_CESIUM_ION_ACCESS_TOKEN= + # Terravista tile provider (optional) # Get token: gcloud secrets versions access latest --secret=terravista-auth-token # In yarn dev the token is appended as ?token= query param (no Sentinel needed). @@ -14,5 +18,4 @@ EXAMPLE_TERRAVISTA_IMAGERY_ROAD_URL= EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT= EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_URL= EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT= -EXAMPLE_REEARTH_TERRAIN_URL= EXAMPLE_TERRAVISTA_GOOGLE3D_URL= diff --git a/example/buildExampleMeta.ts b/example/buildExampleMeta.ts index b5310e0..26ec855 100644 --- a/example/buildExampleMeta.ts +++ b/example/buildExampleMeta.ts @@ -30,34 +30,38 @@ export function buildExampleTileProvider(): TileProviderConfig { baseUrl: import.meta.env.EXAMPLE_TERRAVISTA_BASE_URL || undefined, imageryTileOverrides: [ { - id: "default", + id: "terravista_google_satellite", url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_URL}${t}`, - credit: - import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_CREDIT || - "© Google", + credit: import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_CREDIT || "© Google", }, { - id: "default_road", + id: "terravista_google_roadmap", url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_URL}${t}`, - credit: - import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT || - "© Google", + credit: import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT || "© Google", }, { - id: "black_marble", + id: "terravista_black_marble", url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_URL}${t}`, - credit: - import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT || - "NASA GIBS VIIRS", + credit: import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT || "NASA GIBS VIIRS", maxZoomLevel: 8, }, ], - terrainOverrides: [ - { - id: "default", - url: toTerrainUrl(import.meta.env.EXAMPLE_REEARTH_TERRAIN_URL), - }, - ], + terrainOverrides: (() => { + const terrainUrl = toTerrainUrl( + import.meta.env.EXAMPLE_REEARTH_TERRAIN_URL || + "https://terrain.reearth.land/cesium-mesh/ellipsoid", + ); + return terrainUrl + ? [ + { + id: "default", + url: terrainUrl, + credit: + import.meta.env.EXAMPLE_REEARTH_TERRAIN_CREDIT || "Re:Earth Terrain, Mapterhorn", + }, + ] + : []; + })(), layerSourceOverrides: [ { id: "googlePhotorealistic", @@ -67,16 +71,16 @@ export function buildExampleTileProvider(): TileProviderConfig { }); } - // No token — fall back to public OSM tiles. + // No token — map terravista preset IDs to public OSM so unknown-type fallback still shows tiles. return createCustomConfig({ imageryTileOverrides: [ { - id: "default", + id: "terravista_google_satellite", url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", credit: "© OpenStreetMap contributors", }, { - id: "default_road", + id: "terravista_google_roadmap", url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", credit: "© OpenStreetMap contributors", }, diff --git a/example/constants.ts b/example/constants.ts index 428018b..c5861c5 100644 --- a/example/constants.ts +++ b/example/constants.ts @@ -13,11 +13,18 @@ export const DEFAULT_CAMERA = { }; export const TILES = [ - "default", - "default_road", + // Terravista (requires TileProviderConfig) + "terravista_google_satellite", + "terravista_google_roadmap", + "terravista_black_marble", + // Public (always available) "open_street_map", - "black_marble", "japan_gsi_standard", + // Cesium Ion (requires user-provided Ion token in scene settings) + "cesium_ion_default", + "cesium_ion_labelled", + "cesium_ion_road", + "cesium_ion_earth_at_night", ]; export const SKETCH_TOOLS: SketchType[] = [ diff --git a/example/hooks.ts b/example/hooks.ts index bbd607c..1ecba0e 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -51,7 +51,10 @@ export default () => { const meta = useMemo(() => { const tileProvider = buildExampleTileProvider(); - return { tileProvider }; + return { + tileProvider, + cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, + }; }, []); const [currentTile, setCurrentTile] = useState( diff --git a/example/testLayers/index.ts b/example/testLayers/index.ts index b76ed1b..c7695f6 100644 --- a/example/testLayers/index.ts +++ b/example/testLayers/index.ts @@ -6,6 +6,7 @@ import { GEOJSON_SIMPLE } from "./geojson_simple"; import { GOOGLE_PHOTOREALISTIC_3DTILES } from "./google_photorealistic_3dtiles"; import { LAND_USE, LSLD_SAPPORO, LSLD_NIJIMA } from "./mvt"; import { OSM_BUILDINGS } from "./osm_buildings"; +import { REEARTH_BUILDINGS } from "./reearth_buildings"; import { THREEDTILES_KUMAGAYA_LOD2 } from "./threedtiles_lod2"; import { THREEDTILES_SIMPLE } from "./threedtiles_simple"; import { THREEDTILES_SIMPLE_WITH_STYLE_URL } from "./threedtiles_simple_with_style_url"; @@ -18,6 +19,7 @@ export const TEST_LAYERS: Layer[] = [ GEOJSON_SIMPLE, GOOGLE_PHOTOREALISTIC_3DTILES, OSM_BUILDINGS, + REEARTH_BUILDINGS, THREEDTILES_SIMPLE, THREEDTILES_SIMPLE_WITH_STYLE_URL, THREEDTILES_KUMAGAYA_LOD2, diff --git a/example/testLayers/reearth_buildings.ts b/example/testLayers/reearth_buildings.ts new file mode 100644 index 0000000..d06dc2b --- /dev/null +++ b/example/testLayers/reearth_buildings.ts @@ -0,0 +1,10 @@ +import { Layer } from "@reearth/core"; + +export const REEARTH_BUILDINGS: Layer = { + id: "reearth_buildings", + type: "simple", + data: { + type: "reearth-buildings", + }, + "3dtiles": {}, +}; diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index cda5670..c646bcd 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -754,11 +754,11 @@ export const useHooks = ({ } }, [style, isTilesetReady]); + const tileProvider = meta?.tileProvider as TileProviderConfig | undefined; + const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - const tileProvider = meta?.tileProvider as TileProviderConfig | undefined; - // First, try to use TileProviderConfig for Terravista URL (returns string directly) const terravistaUrl = resolveTilesetUrl(tileProvider, "googlePhotorealistic"); if (terravistaUrl) { @@ -787,7 +787,7 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, meta?.tileProvider]); + }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, tileProvider]); const tilesetUrl = useMemo((): string | Resource | Promise | null => { if (!isVisible) return null; @@ -797,6 +797,14 @@ export const useHooks = ({ return googleMapPhotorealisticResource; } + // Re:Earth Buildings — public service; overridable via TileProviderConfig for self-hosted mirrors + if (type === "reearth-buildings") { + return ( + resolveTilesetUrl(tileProvider, "reearthBuildings") ?? + "https://buildings.reearth.land/tileset.json" + ); + } + // OSM Buildings — only available via Cesium Ion (Terravista does not host this dataset). if (type === "osm-buildings") { return IonResource.fromAssetId(96188, { @@ -810,7 +818,15 @@ export const useHooks = ({ } return null; - }, [type, isVisible, googleMapPhotorealisticResource, url, tileset, meta?.cesiumIonAccessToken]); + }, [ + type, + isVisible, + googleMapPhotorealisticResource, + url, + tileset, + meta?.cesiumIonAccessToken, + tileProvider, + ]); const imageBasedLighting = useMemo(() => { if ( diff --git a/src/engines/Cesium/Feature/index.tsx b/src/engines/Cesium/Feature/index.tsx index bb427a8..81bc604 100644 --- a/src/engines/Cesium/Feature/index.tsx +++ b/src/engines/Cesium/Feature/index.tsx @@ -65,6 +65,7 @@ const displayConfig: Record = { tms: ["raster"], "3dtiles": ["3dtiles"], "osm-buildings": ["3dtiles"], + "reearth-buildings": ["3dtiles"], "google-photorealistic": ["3dtiles"], gpx: "auto", shapefile: "auto", diff --git a/src/engines/Cesium/core/Imagery.test.ts b/src/engines/Cesium/core/Imagery.test.ts index 7873e13..03ec3d0 100644 --- a/src/engines/Cesium/core/Imagery.test.ts +++ b/src/engines/Cesium/core/Imagery.test.ts @@ -5,7 +5,17 @@ import { type Tile, useImageryProviders } from "./Imagery"; test("useImageryProviders", () => { const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url })); - const presets = { default: provider }; + const osmProvider = vi.fn((): any => ({ osm: true })); + const terravistaProvider = vi.fn((): null => null); // returns null when no TileProviderConfig + + // cesium_ion_default is the primary tile type used in this test (was "default"). + // terravista_google_satellite and open_street_map are needed for the unknown-type fallback path. + const presets = { + cesium_ion_default: provider, + terravista_google_satellite: terravistaProvider, + open_street_map: osmProvider, + } as any; + const { result, rerender } = renderHook( ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => useImageryProviders({ @@ -13,7 +23,7 @@ test("useImageryProviders", () => { presets, cesiumIonAccessToken, }), - { initialProps: { tiles: [{ id: "1", type: "default" }], cesiumIonAccessToken: undefined } }, + { initialProps: { tiles: [{ id: "1", type: "cesium_ion_default" }], cesiumIonAccessToken: undefined } }, ); const typedRerender = rerender as (props: { @@ -21,22 +31,22 @@ test("useImageryProviders", () => { cesiumIonAccessToken?: string; }) => void; - expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] }); + expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, { hoge: undefined }] }); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(1); const prevImageryProvider = result.current.providers["1"][2]; // re-render with same tiles - typedRerender({ tiles: [{ id: "1", type: "default" }] }); + typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default" }] }); - expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] }); + expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, { hoge: undefined }] }); expect(result.current.providers["1"][2]).toBe(prevImageryProvider); // 1's provider should be reused expect(provider).toBeCalledTimes(1); // update a tile URL - typedRerender({ tiles: [{ id: "1", type: "default", url: "a" }] }); + typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default", url: "a" }] }); - expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }] }); + expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", "a", { hoge: "a" }] }); expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(2); @@ -46,14 +56,14 @@ test("useImageryProviders", () => { // add a tile with URL typedRerender({ tiles: [ - { id: "2", type: "default" }, - { id: "1", type: "default", url: "a" }, + { id: "2", type: "cesium_ion_default" }, + { id: "1", type: "cesium_ion_default", url: "a" }, ], }); expect(result.current.providers).toEqual({ - "2": ["default", undefined, { hoge: undefined }], - "1": ["default", "a", { hoge: "a" }], + "2": ["cesium_ion_default", undefined, { hoge: undefined }], + "1": ["cesium_ion_default", "a", { hoge: "a" }], }); expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused @@ -62,43 +72,44 @@ test("useImageryProviders", () => { // sort tiles typedRerender({ tiles: [ - { id: "1", type: "default", url: "a" }, - { id: "2", type: "default" }, + { id: "1", type: "cesium_ion_default", url: "a" }, + { id: "2", type: "cesium_ion_default" }, ], }); expect(result.current.providers).toEqual({ - "1": ["default", "a", { hoge: "a" }], - "2": ["default", undefined, { hoge: undefined }], + "1": ["cesium_ion_default", "a", { hoge: "a" }], + "2": ["cesium_ion_default", undefined, { hoge: undefined }], }); expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused expect(provider).toBeCalledTimes(3); - // delete a tile + // Ion token update triggers provider recreation for cesium_ion_* types typedRerender({ - tiles: [{ id: "1", type: "default", url: "a" }], + tiles: [{ id: "1", type: "cesium_ion_default", url: "a" }], cesiumIonAccessToken: "a", }); expect(result.current.providers).toEqual({ - "1": ["default", "a", { hoge: "a" }], + "1": ["cesium_ion_default", "a", { hoge: "a" }], }); expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider2); expect(provider).toBeCalledTimes(4); - // update a tile type to unexpected type + // unknown type: falls back to terravista_google_satellite (returns null) then open_street_map typedRerender({ tiles: [{ id: "1", type: "unexpected_type", url: "u" }], }); expect(result.current.providers).toEqual({ - // unexpected type is treated as "default" - "1": ["unexpected_type", "u", { hoge: "u" }], + "1": ["unexpected_type", "u", { osm: true }], }); expect(result.current.updated).toBe(true); - expect(provider).toBeCalledTimes(5); + expect(provider).toBeCalledTimes(4); // cesium_ion_default provider not called again + expect(terravistaProvider).toBeCalledTimes(1); // tried first + expect(osmProvider).toBeCalledTimes(1); // used as fallback typedRerender({ tiles: [] }); expect(result.current.providers).toEqual({}); diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index 4636854..6fa6326 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -130,13 +130,19 @@ export function useImageryProviders({ }): { providers: Providers; updated: boolean } { const newTile = useCallback( (t: Tile, ciat?: string, tp?: TileProviderConfig) => { - return presets[isValidPresetTileType(t.type) ? t.type : "default"]({ + const opts = { url: t.url, cesiumIonAccessToken: ciat, heatmap: t.heatmap, zoomLevel: t.zoomLevelForURL, tileProvider: tp, - }); + }; + if (isValidPresetTileType(t.type)) { + return presets[t.type](opts); + } + // Unknown type: try terravista_google_satellite first (returns null if no TileProviderConfig), + // then fall back to open_street_map which is always available. + return presets["terravista_google_satellite"](opts) ?? presets["open_street_map"](opts); }, [presets], ); @@ -203,7 +209,7 @@ export function useImageryProviders({ prevType !== tile.type || prevUrl !== tile.url || isTileProviderUpdated || - (isCesiumAccessTokenUpdated && (!tile.type || tile.type === "default")) + (isCesiumAccessTokenUpdated && tile.type?.startsWith("cesium_ion_")) ? [tile.type, tile.url, newTile(tile, cesiumIonAccessToken, tileProvider)] : [prevType, prevUrl, prevProvider], ], diff --git a/src/engines/Cesium/core/presets.ts b/src/engines/Cesium/core/presets.ts index dc47c59..e20de8d 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -18,13 +18,21 @@ import { } from "./tileProviderResolver"; const PRESET_TILE_TYPES = [ - "default", - "default_road", - "default_label", + // Terravista presets — require TileProviderConfig; no Ion fallback + "terravista_google_satellite", + "terravista_google_roadmap", + "terravista_black_marble", + + // Public presets — always available, no auth required "open_street_map", - "black_marble", "japan_gsi_standard", "url", + + // Cesium Ion presets — require user-provided Ion token (scene.default.ion) + "cesium_ion_default", + "cesium_ion_labelled", + "cesium_ion_road", + "cesium_ion_earth_at_night", ]; export type PresetTileType = (typeof PRESET_TILE_TYPES)[number]; @@ -42,80 +50,43 @@ export type TileOptions = { }; /** - * Create imagery provider from TileProviderConfig URL (terravista or custom). - * Returns null if no URL is configured — callers fall back to legacy Ion path. + * Create imagery provider from TileProviderConfig override array. + * Returns null if no matching override is configured. */ function createPresetImageryProvider( presetName: string, opts?: TileOptions, ): Promise | ImageryProvider | null { - const { tileProvider } = opts ?? {}; - - const url = resolveImageryUrl(tileProvider, presetName); + const url = resolveImageryUrl(opts?.tileProvider, presetName); if (!url) return null; return new UrlTemplateImageryProvider({ url, - credit: resolveImageryCredit(tileProvider, presetName), - maximumLevel: resolveImageryMaxZoom(tileProvider, presetName), + credit: resolveImageryCredit(opts?.tileProvider, presetName), + maximumLevel: resolveImageryMaxZoom(opts?.tileProvider, presetName), }); } export const tiles = { - // Standard presets - now support TileProviderConfig - default: (opts?: TileOptions) => { - const result = createPresetImageryProvider("default", opts); - if (result) return result; + // --- Terravista presets --- + // Each resolves by its own name as the override ID in TileProviderConfig. + // Returns null when TileProviderConfig is not configured. + terravista_google_satellite: (opts?: TileOptions) => + createPresetImageryProvider("terravista_google_satellite", opts), - // Legacy fallback - return IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL, { - accessToken: opts?.cesiumIonAccessToken, - }).catch(err => { - console.error(err); - return undefined as unknown as ImageryProvider; - }); - }, + terravista_google_roadmap: (opts?: TileOptions) => + createPresetImageryProvider("terravista_google_roadmap", opts), - default_road: (opts?: TileOptions) => { - const result = createPresetImageryProvider("default_road", opts); - if (result) return result; - - // Legacy fallback - return IonImageryProvider.fromAssetId(IonWorldImageryStyle.ROAD, { - accessToken: opts?.cesiumIonAccessToken, - }).catch(err => { - console.error(err); - return undefined as unknown as ImageryProvider; - }); - }, - - default_label: ({ cesiumIonAccessToken } = {}) => - IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL_WITH_LABELS, { - accessToken: cesiumIonAccessToken, - }).catch(err => { - console.error(err); - return undefined as unknown as ImageryProvider; - }), + terravista_black_marble: (opts?: TileOptions) => + createPresetImageryProvider("terravista_black_marble", opts), + // --- Public presets --- open_street_map: () => new OpenStreetMapImageryProvider({ url: "https://tile.openstreetmap.org", credit: '© OpenStreetMap contributors', }), - black_marble: (opts?: TileOptions) => { - const result = createPresetImageryProvider("black_marble", opts); - if (result) return result; - - // Legacy fallback - return IonImageryProvider.fromAssetId(3812, { - accessToken: opts?.cesiumIonAccessToken, - }).catch(err => { - console.error(err); - return undefined as unknown as ImageryProvider; - }); - }, - japan_gsi_standard: () => new OpenStreetMapImageryProvider({ url: "https://cyberjapandata.gsi.go.jp/xyz/std/", @@ -132,6 +103,40 @@ export const tiles = { maximumLevel: tile_zoomLevel?.[1], }) : null, + + // --- Cesium Ion presets --- + // Require the user to configure a Cesium Ion access token in their scene (scene.default.ion). + cesium_ion_default: (opts?: TileOptions) => + IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }), + + cesium_ion_labelled: (opts?: TileOptions) => + IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL_WITH_LABELS, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }), + + cesium_ion_road: (opts?: TileOptions) => + IonImageryProvider.fromAssetId(IonWorldImageryStyle.ROAD, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }), + + cesium_ion_earth_at_night: (opts?: TileOptions) => + IonImageryProvider.fromAssetId(3812, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }), } as { [K in PresetTileType]: (opts?: TileOptions) => Promise | ImageryProvider | null; }; diff --git a/src/mantle/types/index.ts b/src/mantle/types/index.ts index 013ad70..e67fc47 100644 --- a/src/mantle/types/index.ts +++ b/src/mantle/types/index.ts @@ -107,6 +107,7 @@ export type DataType = | "geojson" | "3dtiles" | "osm-buildings" + | "reearth-buildings" | "google-photorealistic" | "czml" | "csv" From 49f52c56d4d5cc3eb766455dcf4e406cbf010c80 Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Mon, 18 May 2026 20:28:48 +0900 Subject: [PATCH 03/19] feat: add cesiumIonAssetId tiles --- example/App.tsx | 4 +++ example/components/OptionsPanel/index.tsx | 21 +++++++++++++++ example/constants.ts | 4 ++- example/hooks.ts | 6 ++++- src/Map/types/viewerProperty.ts | 1 + src/engines/Cesium/core/Imagery.test.ts | 32 +++++++++++------------ src/engines/Cesium/core/Imagery.tsx | 26 +++++++++++------- src/engines/Cesium/core/presets.ts | 16 ++++++++++++ 8 files changed, 83 insertions(+), 27 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index 0aad9e1..1ec2aee 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -16,6 +16,8 @@ function App() { meta, currentTile, setCurrentTile, + cesiumIonAssetId, + setCesiumIonAssetId, currentCamera, setCurrentCamera, terrainEnabled, @@ -48,6 +50,8 @@ function App() { void; + cesiumIonAssetId?: number; + setCesiumIonAssetId: (v: number | undefined) => void; terrainEnabled: boolean; setTerrainEnabled: (v: boolean) => void; hideUnderground: boolean; @@ -48,6 +50,8 @@ type OptionsPanelProps = { const OptionsPanel: FC = ({ currentTile, setCurrentTile, + cesiumIonAssetId, + setCesiumIonAssetId, terrainEnabled, setTerrainEnabled, hideUnderground, @@ -99,6 +103,23 @@ const OptionsPanel: FC = ({ ))} + {currentTile === "cesium_ion" && ( +
+ + { + const v = parseInt(e.target.value, 10); + setCesiumIonAssetId(isNaN(v) ? undefined : v); + }} + /> +
+ )} diff --git a/example/constants.ts b/example/constants.ts index c5861c5..568dbbf 100644 --- a/example/constants.ts +++ b/example/constants.ts @@ -20,7 +20,9 @@ export const TILES = [ // Public (always available) "open_street_map", "japan_gsi_standard", - // Cesium Ion (requires user-provided Ion token in scene settings) + // Cesium Ion — custom asset ID (enter asset ID in the input below) + "cesium_ion", + // Cesium Ion — preset assets (requires user-provided Ion token in scene settings) "cesium_ion_default", "cesium_ion_labelled", "cesium_ion_road", diff --git a/example/hooks.ts b/example/hooks.ts index 1ecba0e..600fd53 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -62,6 +62,7 @@ export default () => { ? DEFAULT_VIEWER_PROPERTY.tiles?.[0]?.type : undefined, ); + const [cesiumIonAssetId, setCesiumIonAssetId] = useState(undefined); const [currentCamera, setCurrentCamera] = useState(DEFAULT_CAMERA); const [terrainEnabled, setTerrainEnabled] = useState(true); const [hideUnderground, setHideUnderground] = useState(false); @@ -76,6 +77,7 @@ export default () => { { id: "default", type: currentTile, + ionAssetId: currentTile === "cesium_ion" ? cesiumIonAssetId : undefined, opacity: 1, }, ] @@ -89,7 +91,7 @@ export default () => { depthTestAgainstTerrain: hideUnderground, }, }), - [currentTile, terrainEnabled, hideUnderground], + [currentTile, cesiumIonAssetId, terrainEnabled, hideUnderground], ); const layers = useMemo( @@ -181,6 +183,8 @@ export default () => { meta, currentTile, setCurrentTile, + cesiumIonAssetId, + setCesiumIonAssetId, currentCamera, setCurrentCamera, terrainEnabled, diff --git a/src/Map/types/viewerProperty.ts b/src/Map/types/viewerProperty.ts index 81ea260..b821c2f 100644 --- a/src/Map/types/viewerProperty.ts +++ b/src/Map/types/viewerProperty.ts @@ -130,6 +130,7 @@ export type TileProperty = { id: string; type?: string; url?: string; + ionAssetId?: number; opacity?: number; zoomLevel?: number[]; zoomLevelForURL?: number[]; diff --git a/src/engines/Cesium/core/Imagery.test.ts b/src/engines/Cesium/core/Imagery.test.ts index 03ec3d0..0fc8b43 100644 --- a/src/engines/Cesium/core/Imagery.test.ts +++ b/src/engines/Cesium/core/Imagery.test.ts @@ -31,27 +31,27 @@ test("useImageryProviders", () => { cesiumIonAccessToken?: string; }) => void; - expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, { hoge: undefined }] }); + expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }] }); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(1); - const prevImageryProvider = result.current.providers["1"][2]; + const prevImageryProvider = result.current.providers["1"][3]; // re-render with same tiles typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default" }] }); - expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, { hoge: undefined }] }); - expect(result.current.providers["1"][2]).toBe(prevImageryProvider); // 1's provider should be reused + expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }] }); + expect(result.current.providers["1"][3]).toBe(prevImageryProvider); // 1's provider should be reused expect(provider).toBeCalledTimes(1); // update a tile URL typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default", url: "a" }] }); - expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", "a", { hoge: "a" }] }); - expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider); + expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }] }); + expect(result.current.providers["1"][3]).not.toBe(prevImageryProvider); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(2); expect(provider).toBeCalledWith({ url: "a" }); - const prevImageryProvider2 = result.current.providers["1"][2]; + const prevImageryProvider2 = result.current.providers["1"][3]; // add a tile with URL typedRerender({ @@ -62,11 +62,11 @@ test("useImageryProviders", () => { }); expect(result.current.providers).toEqual({ - "2": ["cesium_ion_default", undefined, { hoge: undefined }], - "1": ["cesium_ion_default", "a", { hoge: "a" }], + "2": ["cesium_ion_default", undefined, undefined, { hoge: undefined }], + "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }], }); expect(result.current.updated).toBe(true); - expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused + expect(result.current.providers["1"][3]).toBe(prevImageryProvider2); // 1's provider should be reused expect(provider).toBeCalledTimes(3); // sort tiles @@ -78,11 +78,11 @@ test("useImageryProviders", () => { }); expect(result.current.providers).toEqual({ - "1": ["cesium_ion_default", "a", { hoge: "a" }], - "2": ["cesium_ion_default", undefined, { hoge: undefined }], + "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }], + "2": ["cesium_ion_default", undefined, undefined, { hoge: undefined }], }); expect(result.current.updated).toBe(true); - expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused + expect(result.current.providers["1"][3]).toBe(prevImageryProvider2); // 1's provider should be reused expect(provider).toBeCalledTimes(3); // Ion token update triggers provider recreation for cesium_ion_* types @@ -92,10 +92,10 @@ test("useImageryProviders", () => { }); expect(result.current.providers).toEqual({ - "1": ["cesium_ion_default", "a", { hoge: "a" }], + "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }], }); expect(result.current.updated).toBe(true); - expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider2); + expect(result.current.providers["1"][3]).not.toBe(prevImageryProvider2); expect(provider).toBeCalledTimes(4); // unknown type: falls back to terravista_google_satellite (returns null) then open_street_map @@ -104,7 +104,7 @@ test("useImageryProviders", () => { }); expect(result.current.providers).toEqual({ - "1": ["unexpected_type", "u", { osm: true }], + "1": ["unexpected_type", "u", undefined, { osm: true }], }); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(4); // cesium_ion_default provider not called again diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index 6fa6326..a36c08d 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -25,6 +25,7 @@ export type Tile = { id: string; url?: string; type?: string; + ionAssetId?: number; opacity?: number; zoomLevel?: number[]; zoomLevelForURL?: number[]; @@ -65,7 +66,7 @@ export default function ImageryLayers({ const addedLayers: CesiumImageryLayer[] = []; tiles?.forEach(({ id, zoomLevel, opacity, heatmap }, i) => { - const providerOrPromise = providers[id]?.[2]; + const providerOrPromise = providers[id]?.[3]; if (!providerOrPromise) return; const doAdd = (provider: ImageryProvider) => { @@ -107,7 +108,7 @@ export default function ImageryLayers({ return null; } -type Providers = { [id: string]: [string | undefined, string | undefined, ImageryProvider] }; +type Providers = { [id: string]: [string | undefined, string | undefined, number | undefined, ImageryProvider] }; export function useImageryProviders({ tiles = [], @@ -133,6 +134,7 @@ export function useImageryProviders({ const opts = { url: t.url, cesiumIonAccessToken: ciat, + ionAssetId: t.ionAssetId, heatmap: t.heatmap, zoomLevel: t.zoomLevelForURL, tileProvider: tp, @@ -177,7 +179,8 @@ export function useImageryProviders({ added: added.includes(k), prevType: v?.[0], prevUrl: v?.[1], - prevProvider: v?.[2], + prevIonAssetId: v?.[2], + prevProvider: v?.[3], tile: tiles.find(t => t.id === k), })); @@ -189,6 +192,7 @@ export function useImageryProviders({ added, prevType, prevUrl, + prevIonAssetId, prevProvider, tile, }): @@ -197,6 +201,7 @@ export function useImageryProviders({ [ string | undefined, string | undefined, + number | undefined, Promise | ImageryProvider | null | undefined, ], ] @@ -208,15 +213,16 @@ export function useImageryProviders({ added || prevType !== tile.type || prevUrl !== tile.url || + prevIonAssetId !== tile.ionAssetId || isTileProviderUpdated || - (isCesiumAccessTokenUpdated && tile.type?.startsWith("cesium_ion_")) - ? [tile.type, tile.url, newTile(tile, cesiumIonAccessToken, tileProvider)] - : [prevType, prevUrl, prevProvider], + (isCesiumAccessTokenUpdated && tile.type?.startsWith("cesium_ion")) + ? [tile.type, tile.url, tile.ionAssetId, newTile(tile, cesiumIonAccessToken, tileProvider)] + : [prevType, prevUrl, prevIonAssetId, prevProvider], ], ) .filter( - (e): e is [string, [string | undefined, string | undefined, ImageryProvider]] => - !!e?.[1][2], + (e): e is [string, [string | undefined, string | undefined, number | undefined, ImageryProvider]] => + !!e?.[1][3], ), ); @@ -226,7 +232,9 @@ export function useImageryProviders({ !!isTileProviderUpdated || !isEqual(prevTileKeys.current, tileKeys) || !isEqual(prevZoomLevels.current, zoomLevels) || - rawProviders.some(p => p.tile && (p.prevType !== p.tile.type || p.prevUrl !== p.tile.url)); + rawProviders.some( + p => p.tile && (p.prevType !== p.tile.type || p.prevUrl !== p.tile.url || p.prevIonAssetId !== p.tile.ionAssetId), + ); prevTileKeys.current = tileKeys; prevZoomLevels.current = zoomLevels; diff --git a/src/engines/Cesium/core/presets.ts b/src/engines/Cesium/core/presets.ts index e20de8d..22667a7 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -18,6 +18,9 @@ import { } from "./tileProviderResolver"; const PRESET_TILE_TYPES = [ + // Dynamic Cesium Ion asset — asset ID supplied per-tile via ionAssetId field + "cesium_ion", + // Terravista presets — require TileProviderConfig; no Ion fallback "terravista_google_satellite", "terravista_google_roadmap", @@ -44,6 +47,7 @@ export const isValidPresetTileType = (type: string | undefined): type is PresetT export type TileOptions = { url?: string; cesiumIonAccessToken?: string; + ionAssetId?: number; heatmap?: boolean; tile_zoomLevel?: number[]; tileProvider?: TileProviderConfig; @@ -68,6 +72,18 @@ function createPresetImageryProvider( } export const tiles = { + // --- Dynamic Cesium Ion asset --- + // Uses per-tile ionAssetId; requires cesiumIonAccessToken. + cesium_ion: (opts?: TileOptions) => { + if (!opts?.ionAssetId) return null; + return IonImageryProvider.fromAssetId(opts.ionAssetId, { + accessToken: opts?.cesiumIonAccessToken, + }).catch(err => { + console.error(err); + return undefined as unknown as ImageryProvider; + }); + }, + // --- Terravista presets --- // Each resolves by its own name as the override ID in TileProviderConfig. // Returns null when TileProviderConfig is not configured. From b0f6c9df27768df43289ce73e02b5f02a759c36f Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Tue, 19 May 2026 19:34:13 +0900 Subject: [PATCH 04/19] refactor: refactor props and add classic tile --- .env.example | 31 ++-- example/App.tsx | 4 + example/buildExampleMeta.ts | 133 ++++++++---------- example/components/OptionsPanel/index.tsx | 7 + example/constants.ts | 8 +- example/hooks.ts | 19 +-- src/Map/index.tsx | 1 - src/Map/tileProviderDefaults.ts | 106 -------------- src/Map/types/customProvider.ts | 51 +++++++ src/Map/types/index.ts | 4 +- src/Map/types/tileProvider.ts | 97 ------------- src/Visualizer/index.tsx | 4 + src/engines/Cesium/Feature/Tileset/hooks.ts | 27 ++-- src/engines/Cesium/core/Globe/index.tsx | 8 +- .../core/Globe/useTerrainProviderPromise.ts | 16 +-- src/engines/Cesium/core/Imagery.test.ts | 54 +++++-- src/engines/Cesium/core/Imagery.tsx | 73 +++++++--- .../Cesium/core/customProviderResolver.ts | 34 +++++ src/engines/Cesium/core/presets.ts | 77 +++++----- .../Cesium/core/tileProviderResolver.ts | 72 ---------- src/engines/Cesium/hooks.ts | 5 - src/engines/Cesium/index.tsx | 6 +- 22 files changed, 340 insertions(+), 497 deletions(-) delete mode 100644 src/Map/tileProviderDefaults.ts create mode 100644 src/Map/types/customProvider.ts delete mode 100644 src/Map/types/tileProvider.ts create mode 100644 src/engines/Cesium/core/customProviderResolver.ts delete mode 100644 src/engines/Cesium/core/tileProviderResolver.ts diff --git a/.env.example b/.env.example index cc3ca99..269a9b3 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,20 @@ -# Used by the google-photorealistic layer as a fallback when no Terravista Google 3D Tiles URL is configured -EXAMPLE_GOOGLE_MAP_API_KEY=your_google_map_api_key +# Used by the google-photorealistic layer +EXAMPLE_GOOGLE_MAP_API_KEY= # Cesium Ion access token — required to test cesium_ion_* imagery tiles and osm-buildings layer EXAMPLE_CESIUM_ION_ACCESS_TOKEN= -# Terravista tile provider (optional) -# Get token: gcloud secrets versions access latest --secret=terravista-auth-token -# In yarn dev the token is appended as ?token= query param (no Sentinel needed). -# Without a token, the example falls back to public OSM tiles. -EXAMPLE_TERRAVISTA_ACCESS_TOKEN= -EXAMPLE_TERRAVISTA_BASE_URL= +# Tile server auth token — appended as ?token= to imagery and layer provider URLs in code. +# Leave empty to use the URLs as-is (e.g. for public tile servers). +EXAMPLE_TILE_SERVER_TOKEN= -# Individual tile URLs — required when using Terravista. -EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_URL= -EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_CREDIT= -EXAMPLE_TERRAVISTA_IMAGERY_ROAD_URL= -EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT= -EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_URL= -EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT= -EXAMPLE_TERRAVISTA_GOOGLE3D_URL= +# Imagery providers — JSON array of ImageryProviderEntry objects (URLs without token) +# Each entry: { "id": string, "url": string, "credit"?: string, "maximumLevel"?: number, "minimumLevel"?: number } +# The token from EXAMPLE_TILE_SERVER_TOKEN is appended automatically. +# Leave empty to use only the built-in preset tiles (OSM, Cesium Ion, etc.) +EXAMPLE_IMAGERY_PROVIDERS= + +# Layer (3D tileset) providers — JSON array of LayerProviderEntry objects (URLs without token) +# Each entry: { "id": string, "url": string } +# The token from EXAMPLE_TILE_SERVER_TOKEN is appended automatically. +EXAMPLE_LAYER_PROVIDERS= \ No newline at end of file diff --git a/example/App.tsx b/example/App.tsx index 1ec2aee..1595aae 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -14,6 +14,8 @@ function App() { handleAPIReady, handleSelect, meta, + customProvider, + customTileIds, currentTile, setCurrentTile, cesiumIonAssetId, @@ -48,6 +50,7 @@ function App() { return (
(envVar: string | undefined): T[] { + if (!envVar) return []; + try { + const parsed = JSON.parse(envVar); + return Array.isArray(parsed) ? parsed : []; + } catch { + console.warn(`[buildExampleTileProvider] Failed to parse env JSON: ${envVar}`); + return []; + } +} + +function appendToken(url: string, token: string): string { + const sep = url.includes("?") ? "&" : "?"; + return `${url}${sep}token=${token}`; +} // terrain.reearth.land doesn't send CORS headers, so in dev we route through the // Vite proxy (/terrain-proxy) defined in vite.config.example.ts. -function toTerrainUrl(url: string | undefined): string | undefined { - if (!url) return undefined; - if (import.meta.env.DEV) return url.replace("https://terrain.reearth.land", "/terrain-proxy"); - return url; -} +const REEARTH_TERRAIN_URL = "https://terrain.reearth.land/cesium-mesh/ellipsoid"; +const REEARTH_TERRAIN_CREDIT = "Re:Earth Terrain, Mapterhorn"; /** - * Build TileProviderConfig from example env vars. + * Build CustomProviderConfig from env vars. * - * In dev (yarn dev) we append ?token= so tiles work without Sentinel. - * In production the visualizer registers Sentinel which injects Bearer headers instead. + * Env vars: + * EXAMPLE_TILE_SERVER_TOKEN — auth token appended as ?token= to imagery and layer URLs + * EXAMPLE_IMAGERY_PROVIDERS — JSON array of ImageryProviderEntry (URLs without token) + * EXAMPLE_LAYER_PROVIDERS — JSON array of LayerProviderEntry (URLs without token) * - * Returns a Terravista config when a token is present, otherwise a public OSM fallback. + * Terrain uses the public Re:Earth terrain service (hardcoded, no token needed). + * Returns undefined when no imagery or layer providers are configured. */ -export function buildExampleTileProvider(): TileProviderConfig { - const token: string | undefined = - import.meta.env.EXAMPLE_TERRAVISTA_ACCESS_TOKEN || undefined; - const t = token ? `?token=${token}` : ""; +export function buildExampleTileProvider(): CustomProviderConfig | undefined { + const token: string | undefined = import.meta.env.EXAMPLE_TILE_SERVER_TOKEN || undefined; + const rawImageryProviders = parseEnvJson( + import.meta.env.EXAMPLE_IMAGERY_PROVIDERS, + ); + const rawLayerProviders = parseEnvJson( + import.meta.env.EXAMPLE_LAYER_PROVIDERS, + ); - if (token) { - return createTerravistaConfig(token, { - baseUrl: import.meta.env.EXAMPLE_TERRAVISTA_BASE_URL || undefined, - imageryTileOverrides: [ - { - id: "terravista_google_satellite", - url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_URL}${t}`, - credit: import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_DEFAULT_CREDIT || "© Google", - }, - { - id: "terravista_google_roadmap", - url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_URL}${t}`, - credit: import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_ROAD_CREDIT || "© Google", - }, - { - id: "terravista_black_marble", - url: `${import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_URL}${t}`, - credit: import.meta.env.EXAMPLE_TERRAVISTA_IMAGERY_BLACK_MARBLE_CREDIT || "NASA GIBS VIIRS", - maxZoomLevel: 8, - }, - ], - terrainOverrides: (() => { - const terrainUrl = toTerrainUrl( - import.meta.env.EXAMPLE_REEARTH_TERRAIN_URL || - "https://terrain.reearth.land/cesium-mesh/ellipsoid", - ); - return terrainUrl - ? [ - { - id: "default", - url: terrainUrl, - credit: - import.meta.env.EXAMPLE_REEARTH_TERRAIN_CREDIT || "Re:Earth Terrain, Mapterhorn", - }, - ] - : []; - })(), - layerSourceOverrides: [ - { - id: "googlePhotorealistic", - url: `${import.meta.env.EXAMPLE_TERRAVISTA_GOOGLE3D_URL}${t}`, - }, - ], - }); + if (rawImageryProviders.length === 0 && rawLayerProviders.length === 0) { + return undefined; } - // No token — map terravista preset IDs to public OSM so unknown-type fallback still shows tiles. - return createCustomConfig({ - imageryTileOverrides: [ - { - id: "terravista_google_satellite", - url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - credit: "© OpenStreetMap contributors", - }, - { - id: "terravista_google_roadmap", - url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - credit: "© OpenStreetMap contributors", - }, - ], - terrainOverrides: [], - }); + const terrainUrl = import.meta.env.DEV + ? REEARTH_TERRAIN_URL.replace("https://terrain.reearth.land", "/terrain-proxy") + : REEARTH_TERRAIN_URL; + + const imageryProviders = token + ? rawImageryProviders.map(p => ({ ...p, url: appendToken(p.url, token) })) + : rawImageryProviders; + + const layerProviders = token + ? rawLayerProviders.map(p => ({ ...p, url: appendToken(p.url, token) })) + : rawLayerProviders; + + return { + imagery: imageryProviders.length > 0 ? { providers: imageryProviders } : undefined, + terrain: { + providers: [{ id: "default", url: terrainUrl, credit: REEARTH_TERRAIN_CREDIT }], + }, + layers: layerProviders.length > 0 ? { providers: layerProviders } : undefined, + }; } diff --git a/example/components/OptionsPanel/index.tsx b/example/components/OptionsPanel/index.tsx index e2a108d..38b36da 100644 --- a/example/components/OptionsPanel/index.tsx +++ b/example/components/OptionsPanel/index.tsx @@ -21,6 +21,7 @@ import { SKETCH_TOOLS, TILES } from "@/constants"; import { TEST_LAYERS } from "@/testLayers"; type OptionsPanelProps = { + customTileIds?: string[]; currentTile?: string; setCurrentTile: (v: string) => void; cesiumIonAssetId?: number; @@ -48,6 +49,7 @@ type OptionsPanelProps = { }; const OptionsPanel: FC = ({ + customTileIds, currentTile, setCurrentTile, cesiumIonAssetId, @@ -96,6 +98,11 @@ const OptionsPanel: FC = ({ + {customTileIds?.map(tile => ( + + {tile} + + ))} {TILES.map(tile => ( {tile} diff --git a/example/constants.ts b/example/constants.ts index 568dbbf..072c3dc 100644 --- a/example/constants.ts +++ b/example/constants.ts @@ -12,14 +12,14 @@ export const DEFAULT_CAMERA = { roll: 6.2830631767616465, }; +// Static preset tile types — always available regardless of CustomProviderConfig. +// Dynamic provider IDs (from EXAMPLE_IMAGERY_PROVIDERS) are appended at runtime in hooks.ts. export const TILES = [ - // Terravista (requires TileProviderConfig) - "terravista_google_satellite", - "terravista_google_roadmap", - "terravista_black_marble", // Public (always available) "open_street_map", "japan_gsi_standard", + "stamen_watercolor", + "carto_light", // Cesium Ion — custom asset ID (enter asset ID in the input below) "cesium_ion", // Cesium Ion — preset assets (requires user-provided Ion token in scene settings) diff --git a/example/hooks.ts b/example/hooks.ts index 600fd53..9de8758 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -49,16 +49,17 @@ export default () => { setSelectedFeature(ref.current?.layers.selectedFeature() ?? feature); }, []); - const meta = useMemo(() => { - const tileProvider = buildExampleTileProvider(); - return { - tileProvider, - cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, - }; - }, []); + const customProvider = useMemo(() => buildExampleTileProvider(), []); + + const meta = useMemo(() => ({ + cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, + }), []); + + const customTileIds = customProvider?.imagery?.providers?.map(p => p.id) ?? []; + const allTileIds = [...TILES, ...customTileIds]; const [currentTile, setCurrentTile] = useState( - TILES.includes(DEFAULT_VIEWER_PROPERTY.tiles?.[0]?.type ?? "") + allTileIds.includes(DEFAULT_VIEWER_PROPERTY.tiles?.[0]?.type ?? "") ? DEFAULT_VIEWER_PROPERTY.tiles?.[0]?.type : undefined, ); @@ -181,6 +182,8 @@ export default () => { handleAPIReady, handleSelect, meta, + customProvider, + customTileIds, currentTile, setCurrentTile, cesiumIonAssetId, diff --git a/src/Map/index.tsx b/src/Map/index.tsx index 2b2eae4..1325257 100644 --- a/src/Map/index.tsx +++ b/src/Map/index.tsx @@ -10,7 +10,6 @@ import SpatialId from "./SpatialId"; import type { Engine, EngineProps } from "./types"; export * from "./types"; -export * from "./tileProviderDefaults"; export { useGet, type WrappedRef, type Undefinable } from "./utils"; export type { diff --git a/src/Map/tileProviderDefaults.ts b/src/Map/tileProviderDefaults.ts deleted file mode 100644 index 281a42a..0000000 --- a/src/Map/tileProviderDefaults.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Tile Provider Default Configurations - * ------------------------------------- - * Factory functions for creating TileProviderConfig objects. - * All tile URLs must be provided by the caller — this module - * does not generate or hard-code any URLs. - */ - -import type { TileOverride, TileProviderConfig } from "./types/tileProvider"; - -export type { TileOverride }; - -/** - * Create a TileProviderConfig for Terravista. - * The caller (e.g. visualizer) is responsible for building the override arrays - * from its own env vars or configuration. - * - * @example - * ```typescript - * import { createTerravistaConfig } from '@reearth/core'; - * - * const baseUrl = "https://tiles.eukarya.io"; - * const config = createTerravistaConfig(token, { - * imageryTileOverrides: [ - * { id: "default", url: `${baseUrl}/imagery/google-satellite/{z}/{x}/{y}.webp`, credit: "© Google" }, - * ], - * terrainOverrides: [ - * { id: "default", url: `${baseUrl}/cesium-mesh/ellipsoid/layer.json` }, - * ], - * layerSourceOverrides: [ - * { id: "googlePhotorealistic", url: `${baseUrl}/google3d/root.json` }, - * ], - * onTokenExpired: async () => ({ accessToken: await refreshToken() }), - * }); - * ``` - */ -export function createTerravistaConfig( - accessToken: string, - options?: { - baseUrl?: string; - expiresAt?: number; - onTokenExpired?: () => Promise<{ accessToken: string; expiresAt?: number }>; - imageryTileOverrides?: TileOverride[]; - terrainOverrides?: TileOverride[]; - layerSourceOverrides?: TileOverride[]; - }, -): TileProviderConfig { - return { - type: "terravista", - baseUrl: options?.baseUrl, - auth: { - type: "bearer", - accessToken, - expiresAt: options?.expiresAt, - onTokenExpired: options?.onTokenExpired, - }, - imageryTileOverrides: options?.imageryTileOverrides, - terrainOverrides: options?.terrainOverrides, - layerSourceOverrides: options?.layerSourceOverrides, - }; -} - -/** - * Create a TileProviderConfig for a fully custom tile server. - * - * @example - * ```typescript - * import { createCustomConfig } from '@reearth/core'; - * - * const config = createCustomConfig({ - * auth: { type: 'query-param', accessToken: 'my-api-key', queryParamName: 'api_key' }, - * imageryTileOverrides: [ - * { id: "default", url: "https://my-tiles.com/{z}/{x}/{y}.png" }, - * ], - * terrainOverrides: [ - * { id: "default", url: "https://my-terrain.com/layer.json" }, - * ], - * }); - * ``` - */ -export function createCustomConfig(options: { - baseUrl?: string; - auth?: { - type: "bearer" | "query-param"; - accessToken?: string; - queryParamName?: string; - }; - imageryTileOverrides?: TileOverride[]; - terrainOverrides?: TileOverride[]; - layerSourceOverrides?: TileOverride[]; -}): TileProviderConfig { - return { - type: "custom", - baseUrl: options.baseUrl, - auth: options.auth - ? { - type: options.auth.type, - accessToken: options.auth.accessToken, - queryParamName: options.auth.queryParamName, - } - : undefined, - imageryTileOverrides: options.imageryTileOverrides, - terrainOverrides: options.terrainOverrides, - layerSourceOverrides: options.layerSourceOverrides, - }; -} diff --git a/src/Map/types/customProvider.ts b/src/Map/types/customProvider.ts new file mode 100644 index 0000000..2391849 --- /dev/null +++ b/src/Map/types/customProvider.ts @@ -0,0 +1,51 @@ +/** + * Generic provider configuration — auth-agnostic, URL-first design. + * + * These types allow configuring different tile service providers + * in a provider-agnostic way, making @reearth/core work with + * various backends without coupling to any specific vendor. + * + * Note: Cesium Ion is not configured here. When users select cesiumion + * terrain in the editor, the token flows through meta.cesiumIonAccessToken. + */ + +export interface ImageryProviderEntry { + id: string; + name?: string; + nameJa?: string; + url: string; + credit?: string; + maximumLevel?: number; + minimumLevel?: number; +} + +export interface TerrainProviderEntry { + id: string; + name?: string; + nameJa?: string; + url: string; + requestVertexNormals?: boolean; + requestWaterMask?: boolean; + credit?: string; +} + +export interface LayerProviderEntry { + id: string; + url: string; + options?: Record; +} + +export interface CustomProviderConfig { + imagery?: { + providers?: ImageryProviderEntry[]; + remove?: string[]; + }; + terrain?: { + providers?: TerrainProviderEntry[]; + remove?: string[]; + }; + layers?: { + providers?: LayerProviderEntry[]; + remove?: string[]; + }; +} diff --git a/src/Map/types/index.ts b/src/Map/types/index.ts index e99dab6..00d448b 100644 --- a/src/Map/types/index.ts +++ b/src/Map/types/index.ts @@ -47,6 +47,7 @@ import { } from "../Sketch/types"; import type { TimelineManagerRef } from "../useTimelineManager"; +import type { CustomProviderConfig } from "./customProvider"; import type { SceneMode, ViewerProperty } from "./viewerProperty"; export type { @@ -79,7 +80,7 @@ export type { } from "../../mantle"; export * from "./event"; export * from "./viewerProperty"; -export * from "./tileProvider"; +export * from "./customProvider"; export type EngineRef = { name: string; @@ -238,6 +239,7 @@ export type EngineProps = { isLayerDragging?: boolean; shouldRender?: boolean; meta?: Record; + customProvider?: CustomProviderConfig; displayCredits?: boolean; layersRef?: RefObject; requestingRenderMode?: MutableRefObject; diff --git a/src/Map/types/tileProvider.ts b/src/Map/types/tileProvider.ts deleted file mode 100644 index 063dcf8..0000000 --- a/src/Map/types/tileProvider.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Tile Provider Configuration Types - * --------------------------------- - * These types allow configuring different tile service providers - * in a provider-agnostic way, making @reearth/core work with - * various backends (Terravista or custom servers). - * - * Note: Cesium Ion is not configured here. When users select cesiumion - * terrain in the editor, the token flows through meta.cesiumIonAccessToken. - */ - -/** - * A single tile source override — the unit passed from visualizer to core. - * Visualizer constructs these from env vars; core only consumes them. - */ -export interface TileOverride { - /** Identifier matched against preset names (e.g. 'default', 'black_marble', 'googlePhotorealistic') */ - id: string; - url: string; - credit?: string; - maxZoomLevel?: number; -} - -/** - * Tile Provider Configuration - * All URL content comes from the consuming application (visualizer) via override arrays. - * Core never generates or hard-codes tile URLs. - */ -export interface TileProviderConfig { - /** - * Provider type - * - 'terravista': Use Terravista (requires Sentinel authentication) - * - 'custom': Fully custom URLs - */ - type: "terravista" | "custom"; - - /** - * Base URL reference (informational, not used for URL generation inside core) - * @example 'https://tiles.eukarya.io' - */ - baseUrl?: string; - - /** - * Authentication configuration - */ - auth?: TileProviderAuth; - - /** - * Imagery tile overrides — keyed by preset id (e.g. 'default', 'default_road', 'black_marble') - */ - imageryTileOverrides?: TileOverride[]; - - /** - * Terrain overrides — typically a single entry with id 'default' - */ - terrainOverrides?: TileOverride[]; - - /** - * Layer source overrides — for 3D tilesets (e.g. id 'googlePhotorealistic') - */ - layerSourceOverrides?: TileOverride[]; -} - -/** - * Authentication configuration - */ -export interface TileProviderAuth { - /** - * Authentication type - * - 'bearer': Bearer token (Terravista style) - * - 'query-param': URL query parameter - */ - type: "bearer" | "query-param"; - - /** - * Access Token - * - For 'bearer': Terravista token - * - For 'query-param': Will be appended to URL - */ - accessToken?: string; - - /** - * Token expiration timestamp (milliseconds since epoch) - */ - expiresAt?: number; - - /** - * Token refresh callback - */ - onTokenExpired?: () => Promise<{ accessToken: string; expiresAt?: number }>; - - /** - * Query parameter name (for 'query-param' type) - * @default 'token' - */ - queryParamName?: string; -} diff --git a/src/Visualizer/index.tsx b/src/Visualizer/index.tsx index fd4405c..873866b 100644 --- a/src/Visualizer/index.tsx +++ b/src/Visualizer/index.tsx @@ -13,6 +13,7 @@ import { type Cluster, type ComputedLayer, type Credits, + type CustomProviderConfig, } from "../Map"; import { SketchFeature, SketchType } from "../Map/Sketch/types"; import type { InteractionModeType } from "../shared/interactionMode"; @@ -42,6 +43,7 @@ export type CoreVisualizerProps = { interactionMode?: InteractionModeType; shouldRender?: boolean; meta?: Record; + customProvider?: CustomProviderConfig; style?: CSSProperties; small?: boolean; ready?: boolean; @@ -85,6 +87,7 @@ export const CoreVisualizer = memo( interactionMode, shouldRender, meta, + customProvider, displayCredits = true, style, zoomedLayerId, @@ -165,6 +168,7 @@ export const CoreVisualizer = memo( isLayerDragging={isLayerDragging} isLayerDraggable={isEditable} meta={meta} + customProvider={customProvider} displayCredits={displayCredits} style={style} featureFlags={featureFlags} diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index c646bcd..99ee05c 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -35,9 +35,9 @@ import type { Cesium3DTilesAppearance, } from "../../.."; import { useRefValue } from "../../../../hooks"; -import { LayerSimple, TileProviderConfig } from "../../../../Map"; +import { LayerSimple, CustomProviderConfig } from "../../../../Map"; import { layerIdField, sampleTerrainHeightFromCartesian } from "../../common"; -import { resolveTilesetUrl } from "../../core/tileProviderResolver"; +import { resolveTilesetUrl } from "../../core/customProviderResolver"; import { arrayToCartecian3 } from "../../helpers/sphericalHaromic"; import type { InternalCesium3DTileFeature } from "../../types"; import { @@ -754,17 +754,16 @@ export const useHooks = ({ } }, [style, isTilesetReady]); - const tileProvider = meta?.tileProvider as TileProviderConfig | undefined; + const customProvider = meta?.customProvider as CustomProviderConfig | undefined; const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - // First, try to use TileProviderConfig for Terravista URL (returns string directly) - const terravistaUrl = resolveTilesetUrl(tileProvider, "googlePhotorealistic"); - if (terravistaUrl) { - // Terravista or custom URL - Sentinel will inject auth header - // Return string directly (not wrapped in Promise) - return terravistaUrl; + // First, try to use CustomProviderConfig URL (returns string directly) + const customUrl = resolveTilesetUrl(customProvider, "googlePhotorealistic"); + if (customUrl) { + // Custom provider URL — return string directly (not wrapped in Promise) + return customUrl; } // For async resource loading (Google API or Cesium Ion), return Promise @@ -787,7 +786,7 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, tileProvider]); + }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, customProvider]); const tilesetUrl = useMemo((): string | Resource | Promise | null => { if (!isVisible) return null; @@ -797,15 +796,15 @@ export const useHooks = ({ return googleMapPhotorealisticResource; } - // Re:Earth Buildings — public service; overridable via TileProviderConfig for self-hosted mirrors + // Re:Earth Buildings — public service; overridable via CustomProviderConfig for self-hosted mirrors if (type === "reearth-buildings") { return ( - resolveTilesetUrl(tileProvider, "reearthBuildings") ?? + resolveTilesetUrl(customProvider, "reearthBuildings") ?? "https://buildings.reearth.land/tileset.json" ); } - // OSM Buildings — only available via Cesium Ion (Terravista does not host this dataset). + // OSM Buildings — only available via Cesium Ion. if (type === "osm-buildings") { return IonResource.fromAssetId(96188, { accessToken: meta?.cesiumIonAccessToken as string | undefined, @@ -825,7 +824,7 @@ export const useHooks = ({ url, tileset, meta?.cesiumIonAccessToken, - tileProvider, + customProvider, ]); const imageBasedLighting = useMemo(() => { diff --git a/src/engines/Cesium/core/Globe/index.tsx b/src/engines/Cesium/core/Globe/index.tsx index 11114b4..5cf0146 100644 --- a/src/engines/Cesium/core/Globe/index.tsx +++ b/src/engines/Cesium/core/Globe/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, type JSX } from "react"; import { Globe as CesiumGlobe } from "resium"; import type { ViewerProperty } from "../../.."; -import type { TileProviderConfig } from "../../../../Map/types/tileProvider"; +import type { CustomProviderConfig } from "../../../../Map/types/customProvider"; import { toColor } from "../../common"; import useTerrainProviderPromise from "./useTerrainProviderPromise"; @@ -10,14 +10,14 @@ import useTerrainProviderPromise from "./useTerrainProviderPromise"; export type Props = { property?: ViewerProperty; cesiumIonAccessToken?: string; - tileProvider?: TileProviderConfig; + customProvider?: CustomProviderConfig; onTerrainProviderChange?: () => void; }; export default function Globe({ property, cesiumIonAccessToken, - tileProvider, + customProvider, onTerrainProviderChange, }: Props): JSX.Element | null { const providerPromise = useTerrainProviderPromise({ @@ -27,7 +27,7 @@ export default function Globe({ ionAccessToken: property?.assets?.cesium?.terrain?.ionAccessToken || cesiumIonAccessToken, ionAsset: property?.assets?.cesium?.terrain?.ionAsset, ionUrl: property?.assets?.cesium?.terrain?.ionUrl, - tileProvider, + customProvider, }); const baseColor = useMemo( diff --git a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts index 704d790..0c2e39e 100644 --- a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts +++ b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts @@ -8,8 +8,8 @@ import { import { useMemo, useRef } from "react"; import { TerrainProperty } from "../../.."; -import { AssetsCesiumProperty, TileProviderConfig } from "../../../../Map"; -import { resolveTerrainUrl } from "../tileProviderResolver"; +import { AssetsCesiumProperty, CustomProviderConfig } from "../../../../Map"; +import { resolveTerrainUrl } from "../customProviderResolver"; // Extended terrain types to include 'reearth' type TerrainType = NonNullable | "reearth"; @@ -21,8 +21,8 @@ type ProviderOpts = Pick & ionAccessToken?: string | undefined; /** Reearth terrain URL (for type='reearth') */ reearthTerrainUrl?: string | undefined; - /** TileProviderConfig for provider-agnostic configuration */ - tileProvider?: TileProviderConfig | undefined; + /** CustomProviderConfig for provider-agnostic configuration */ + customProvider?: CustomProviderConfig | undefined; }; export default function useTerrainProviderPromise(opts: ProviderOpts) { @@ -52,15 +52,15 @@ export default function useTerrainProviderPromise(opts: ProviderOpts) { function makeKey(type: TerrainType, opts: ProviderOpts) { const asset = opts.ionAsset ?? ""; const url = opts.ionUrl ?? ""; - const reearthTerrainUrl = opts.reearthTerrainUrl ?? resolveTerrainUrl(opts.tileProvider) ?? ""; + const reearthTerrainUrl = opts.reearthTerrainUrl ?? resolveTerrainUrl(opts.customProvider) ?? ""; const ionToken = opts.ionAccessToken ?? ""; const normal = String(!!opts.normal); return `${type}|asset:${asset}|url:${url}|reearth:${reearthTerrainUrl}|ion:${ionToken}|normal:${normal}`; } function createProvider(type: TerrainType, opts: ProviderOpts): Promise { - // First, try to use TileProviderConfig if available - const tileProviderUrl = resolveTerrainUrl(opts.tileProvider); + // First, try to use CustomProviderConfig if available + const tileProviderUrl = resolveTerrainUrl(opts.customProvider); switch (type) { case "reearth": { @@ -73,7 +73,7 @@ function createProvider(type: TerrainType, opts: ProviderOpts): Promise; } console.warn( - "[Terrain] type='reearth' requires tileProvider.terrainOverrides to be configured. " + + "[Terrain] type='reearth' requires customProvider.terrain.providers to be configured. " + "Falling back to ellipsoid.", ); return Promise.resolve(new EllipsoidTerrainProvider()); diff --git a/src/engines/Cesium/core/Imagery.test.ts b/src/engines/Cesium/core/Imagery.test.ts index 0fc8b43..a003a0d 100644 --- a/src/engines/Cesium/core/Imagery.test.ts +++ b/src/engines/Cesium/core/Imagery.test.ts @@ -1,34 +1,48 @@ +import { UrlTemplateImageryProvider } from "cesium"; import { renderHook } from "@testing-library/react"; import { expect, test, vi } from "vitest"; +import type { CustomProviderConfig } from "../../../Map/types/customProvider"; import { type Tile, useImageryProviders } from "./Imagery"; test("useImageryProviders", () => { const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url })); const osmProvider = vi.fn((): any => ({ osm: true })); - const terravistaProvider = vi.fn((): null => null); // returns null when no TileProviderConfig - // cesium_ion_default is the primary tile type used in this test (was "default"). - // terravista_google_satellite and open_street_map are needed for the unknown-type fallback path. const presets = { cesium_ion_default: provider, - terravista_google_satellite: terravistaProvider, open_street_map: osmProvider, } as any; const { result, rerender } = renderHook( - ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => + ({ + tiles, + cesiumIonAccessToken, + customProvider, + }: { + tiles: Tile[]; + cesiumIonAccessToken?: string; + customProvider?: CustomProviderConfig; + }) => useImageryProviders({ tiles, presets, cesiumIonAccessToken, + customProvider, }), - { initialProps: { tiles: [{ id: "1", type: "cesium_ion_default" }], cesiumIonAccessToken: undefined } }, + { + initialProps: { + tiles: [{ id: "1", type: "cesium_ion_default" }], + cesiumIonAccessToken: undefined, + customProvider: undefined, + }, + }, ); const typedRerender = rerender as (props: { tiles: Tile[]; cesiumIonAccessToken?: string; + customProvider?: CustomProviderConfig; }) => void; expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }] }); @@ -98,18 +112,32 @@ test("useImageryProviders", () => { expect(result.current.providers["1"][3]).not.toBe(prevImageryProvider2); expect(provider).toBeCalledTimes(4); - // unknown type: falls back to terravista_google_satellite (returns null) then open_street_map + // unknown type without customProvider: falls back directly to open_street_map typedRerender({ tiles: [{ id: "1", type: "unexpected_type", url: "u" }], }); - expect(result.current.providers).toEqual({ - "1": ["unexpected_type", "u", undefined, { osm: true }], - }); + expect(result.current.providers["1"][0]).toBe("unexpected_type"); + expect(result.current.providers["1"][3]).toEqual({ osm: true }); expect(result.current.updated).toBe(true); - expect(provider).toBeCalledTimes(4); // cesium_ion_default provider not called again - expect(terravistaProvider).toBeCalledTimes(1); // tried first - expect(osmProvider).toBeCalledTimes(1); // used as fallback + expect(osmProvider).toBeCalledTimes(1); + + // unknown type with a matching customProvider entry: uses UrlTemplateImageryProvider + typedRerender({ + tiles: [{ id: "1", type: "my_custom_satellite", url: "u" }], + customProvider: { + imagery: { + providers: [ + { id: "my_custom_satellite", url: "https://example.com/{z}/{x}/{y}.png", credit: "© Example" }, + ], + }, + }, + }); + + const dynamicProvider = result.current.providers["1"][3]; + expect(dynamicProvider).toBeDefined(); + expect(dynamicProvider).toBeInstanceOf(UrlTemplateImageryProvider); + expect(osmProvider).toBeCalledTimes(1); // osm not called again typedRerender({ tiles: [] }); expect(result.current.providers).toEqual({}); diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index a36c08d..f12fc8f 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -4,12 +4,13 @@ import { ImageryProvider, TextureMagnificationFilter, TextureMinificationFilter, + UrlTemplateImageryProvider, } from "cesium"; import { isEqual } from "lodash-es"; import { useCallback, useMemo, useRef, useEffect } from "react"; import { useCesium } from "resium"; -import type { TileProviderConfig } from "../../../Map/types/tileProvider"; +import type { CustomProviderConfig } from "../../../Map/types/customProvider"; import { isValidPresetTileType, PresetTileType, tiles as tilePresets } from "./presets"; @@ -35,7 +36,7 @@ export type Tile = { export type Props = { tiles?: Tile[]; cesiumIonAccessToken?: string; - tileProvider?: TileProviderConfig; + customProvider?: CustomProviderConfig; onTilesChange?: () => void; }; @@ -47,7 +48,7 @@ export type Props = { export default function ImageryLayers({ tiles, cesiumIonAccessToken, - tileProvider, + customProvider, onTilesChange, }: Props) { const { imageryLayerCollection, scene } = useCesium(); @@ -55,7 +56,7 @@ export default function ImageryLayers({ const { providers } = useImageryProviders({ tiles, cesiumIonAccessToken, - tileProvider, + customProvider, presets: tilePresets, }); @@ -108,49 +109,57 @@ export default function ImageryLayers({ return null; } -type Providers = { [id: string]: [string | undefined, string | undefined, number | undefined, ImageryProvider] }; +type Providers = { + [id: string]: [string | undefined, string | undefined, number | undefined, ImageryProvider]; +}; export function useImageryProviders({ tiles = [], cesiumIonAccessToken, - tileProvider, + customProvider, presets, }: { tiles?: Tile[]; cesiumIonAccessToken?: string; - tileProvider?: TileProviderConfig; + customProvider?: CustomProviderConfig; presets: { [K in PresetTileType]: (opts?: { url?: string; cesiumIonAccessToken?: string; heatmap?: boolean; zoomLevel?: number[]; - tileProvider?: TileProviderConfig; }) => Promise | ImageryProvider | null; }; }): { providers: Providers; updated: boolean } { const newTile = useCallback( - (t: Tile, ciat?: string, tp?: TileProviderConfig) => { + (t: Tile, ciat?: string, tp?: CustomProviderConfig) => { const opts = { url: t.url, cesiumIonAccessToken: ciat, ionAssetId: t.ionAssetId, heatmap: t.heatmap, zoomLevel: t.zoomLevelForURL, - tileProvider: tp, }; if (isValidPresetTileType(t.type)) { return presets[t.type](opts); } - // Unknown type: try terravista_google_satellite first (returns null if no TileProviderConfig), - // then fall back to open_street_map which is always available. - return presets["terravista_google_satellite"](opts) ?? presets["open_street_map"](opts); + // Dynamic: check customProvider.imagery.providers for a matching id + const customEntry = tp?.imagery?.providers?.find(p => p.id === t.type); + if (customEntry) { + return new UrlTemplateImageryProvider({ + url: customEntry.url, + credit: customEntry.credit, + maximumLevel: customEntry.maximumLevel, + minimumLevel: customEntry.minimumLevel, + }); + } + return presets["open_street_map"](opts); }, [presets], ); const prevCesiumIonAccessToken = useRef(cesiumIonAccessToken); - const prevTileProvider = useRef(tileProvider); + const prevCustomProvider = useRef(customProvider); const tileKeys = tiles.map(t => t.id).join(","); const prevTileKeys = useRef(tileKeys); const prevProviders = useRef({}); @@ -164,10 +173,10 @@ export function useImageryProviders({ ); const prevZoomLevels = useRef(zoomLevels); - // Manage TileProviders so that TileProvider does not need to be recreated each time tiles are updated. + // Manage CustomProviders so that CustomProvider does not need to be recreated each time tiles are updated. const { providers, updated } = useMemo(() => { const isCesiumAccessTokenUpdated = prevCesiumIonAccessToken.current !== cesiumIonAccessToken; - const isTileProviderUpdated = prevTileProvider.current !== tileProvider; + const isTileProviderUpdated = prevCustomProvider.current !== customProvider; const prevProvidersKeys = Object.keys(prevProviders.current); const added = tiles.map(t => t.id).filter(t => t && !prevProvidersKeys.includes(t)); @@ -215,14 +224,28 @@ export function useImageryProviders({ prevUrl !== tile.url || prevIonAssetId !== tile.ionAssetId || isTileProviderUpdated || - (isCesiumAccessTokenUpdated && tile.type?.startsWith("cesium_ion")) - ? [tile.type, tile.url, tile.ionAssetId, newTile(tile, cesiumIonAccessToken, tileProvider)] + (isCesiumAccessTokenUpdated && + (tile.type?.startsWith("cesium_ion") || + tile.type === "default" || + tile.type === "default_road" || + tile.type === "default_label" || + tile.type === "black_marble")) + ? [ + tile.type, + tile.url, + tile.ionAssetId, + newTile(tile, cesiumIonAccessToken, customProvider), + ] : [prevType, prevUrl, prevIonAssetId, prevProvider], ], ) .filter( - (e): e is [string, [string | undefined, string | undefined, number | undefined, ImageryProvider]] => - !!e?.[1][3], + ( + e, + ): e is [ + string, + [string | undefined, string | undefined, number | undefined, ImageryProvider], + ] => !!e?.[1][3], ), ); @@ -233,16 +256,20 @@ export function useImageryProviders({ !isEqual(prevTileKeys.current, tileKeys) || !isEqual(prevZoomLevels.current, zoomLevels) || rawProviders.some( - p => p.tile && (p.prevType !== p.tile.type || p.prevUrl !== p.tile.url || p.prevIonAssetId !== p.tile.ionAssetId), + p => + p.tile && + (p.prevType !== p.tile.type || + p.prevUrl !== p.tile.url || + p.prevIonAssetId !== p.tile.ionAssetId), ); prevTileKeys.current = tileKeys; prevZoomLevels.current = zoomLevels; prevCesiumIonAccessToken.current = cesiumIonAccessToken; - prevTileProvider.current = tileProvider; + prevCustomProvider.current = customProvider; return { providers, updated }; - }, [cesiumIonAccessToken, tileProvider, tiles, tileKeys, newTile, zoomLevels]); + }, [cesiumIonAccessToken, customProvider, tiles, tileKeys, newTile, zoomLevels]); prevProviders.current = providers; return { providers, updated }; diff --git a/src/engines/Cesium/core/customProviderResolver.ts b/src/engines/Cesium/core/customProviderResolver.ts new file mode 100644 index 0000000..07c5b01 --- /dev/null +++ b/src/engines/Cesium/core/customProviderResolver.ts @@ -0,0 +1,34 @@ +/** + * Custom Provider Resolver + * ---------------------- + * Utility functions to look up URLs and metadata + * from CustomProviderConfig nested structure. + */ + +import type { CustomProviderConfig, TerrainProviderEntry } from "../../../Map/types/customProvider"; + +export function resolveTerrainEntry(config: CustomProviderConfig | undefined): TerrainProviderEntry | null { + const providers = config?.terrain?.providers; + if (!providers?.length) return null; + return providers.find(p => p.id === "default") ?? providers[0]; +} + +export function resolveTerrainUrl(config: CustomProviderConfig | undefined): string | null { + return resolveTerrainEntry(config)?.url ?? null; +} + +export function resolveImageryUrl(config: CustomProviderConfig | undefined, presetName: string): string | null { + return config?.imagery?.providers?.find(p => p.id === presetName)?.url ?? null; +} + +export function resolveImageryCredit(config: CustomProviderConfig | undefined, presetName: string): string | undefined { + return config?.imagery?.providers?.find(p => p.id === presetName)?.credit; +} + +export function resolveImageryMaxZoom(config: CustomProviderConfig | undefined, presetName: string): number | undefined { + return config?.imagery?.providers?.find(p => p.id === presetName)?.maximumLevel; +} + +export function resolveTilesetUrl(config: CustomProviderConfig | undefined, tilesetId: string): string | null { + return config?.layers?.providers?.find(p => p.id === tilesetId)?.url ?? null; +} diff --git a/src/engines/Cesium/core/presets.ts b/src/engines/Cesium/core/presets.ts index 22667a7..04ca0b3 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -8,27 +8,17 @@ import { DiscardEmptyTileImagePolicy, } from "cesium"; -import type { TileProviderConfig } from "../../../Map/types/tileProvider"; - import { JapanGSIOptimalBVmapLabelImageryProvider } from "./labels/JapanGSIOptimalBVmapVectorMapLabel/JapanGSIOptimalBVmapLabelImageryProvider"; -import { - resolveImageryUrl, - resolveImageryCredit, - resolveImageryMaxZoom, -} from "./tileProviderResolver"; const PRESET_TILE_TYPES = [ // Dynamic Cesium Ion asset — asset ID supplied per-tile via ionAssetId field "cesium_ion", - // Terravista presets — require TileProviderConfig; no Ion fallback - "terravista_google_satellite", - "terravista_google_roadmap", - "terravista_black_marble", - // Public presets — always available, no auth required "open_street_map", "japan_gsi_standard", + "stamen_watercolor", + "carto_light", "url", // Cesium Ion presets — require user-provided Ion token (scene.default.ion) @@ -36,6 +26,12 @@ const PRESET_TILE_TYPES = [ "cesium_ion_labelled", "cesium_ion_road", "cesium_ion_earth_at_night", + + // Legacy aliases — kept for backward compatibility with existing apps + "default", // → cesium_ion_default + "default_road", // → cesium_ion_road + "default_label", // → cesium_ion_labelled + "black_marble", // → cesium_ion_earth_at_night ]; export type PresetTileType = (typeof PRESET_TILE_TYPES)[number]; @@ -50,27 +46,8 @@ export type TileOptions = { ionAssetId?: number; heatmap?: boolean; tile_zoomLevel?: number[]; - tileProvider?: TileProviderConfig; }; -/** - * Create imagery provider from TileProviderConfig override array. - * Returns null if no matching override is configured. - */ -function createPresetImageryProvider( - presetName: string, - opts?: TileOptions, -): Promise | ImageryProvider | null { - const url = resolveImageryUrl(opts?.tileProvider, presetName); - if (!url) return null; - - return new UrlTemplateImageryProvider({ - url, - credit: resolveImageryCredit(opts?.tileProvider, presetName), - maximumLevel: resolveImageryMaxZoom(opts?.tileProvider, presetName), - }); -} - export const tiles = { // --- Dynamic Cesium Ion asset --- // Uses per-tile ionAssetId; requires cesiumIonAccessToken. @@ -84,18 +61,6 @@ export const tiles = { }); }, - // --- Terravista presets --- - // Each resolves by its own name as the override ID in TileProviderConfig. - // Returns null when TileProviderConfig is not configured. - terravista_google_satellite: (opts?: TileOptions) => - createPresetImageryProvider("terravista_google_satellite", opts), - - terravista_google_roadmap: (opts?: TileOptions) => - createPresetImageryProvider("terravista_google_roadmap", opts), - - terravista_black_marble: (opts?: TileOptions) => - createPresetImageryProvider("terravista_black_marble", opts), - // --- Public presets --- open_street_map: () => new OpenStreetMapImageryProvider({ @@ -110,6 +75,20 @@ export const tiles = { "国土地理院, Shoreline data is derived from: United States. National Imagery and Mapping Agency. \"Vector Map Level 0 (VMAP0).\" Bethesda, MD: Denver, CO: The Agency; USGS Information Services, 1997.", }), + stamen_watercolor: () => + new UrlTemplateImageryProvider({ + url: "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg", + credit: "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.", + maximumLevel: 16, + }), + + carto_light: () => + new UrlTemplateImageryProvider({ + url: "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", + credit: + "© OpenStreetMap contributors, © CARTO", + }), + url: ({ url, heatmap, tile_zoomLevel }: TileOptions = {}) => url ? new UrlTemplateImageryProvider({ @@ -153,10 +132,20 @@ export const tiles = { console.error(err); return undefined as unknown as ImageryProvider; }), -} as { + // --- Legacy aliases --- + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as any as { [K in PresetTileType]: (opts?: TileOptions) => Promise | ImageryProvider | null; }; +// Populate legacy aliases after object literal to allow forward references. +Object.assign(tiles, { + default: tiles.cesium_ion_default, + default_road: tiles.cesium_ion_road, + default_label: tiles.cesium_ion_labelled, + black_marble: tiles.cesium_ion_earth_at_night, +}); + export const labelTiles = { japan_gsi_optimal_bvmap: (params: { url: string; diff --git a/src/engines/Cesium/core/tileProviderResolver.ts b/src/engines/Cesium/core/tileProviderResolver.ts deleted file mode 100644 index af65349..0000000 --- a/src/engines/Cesium/core/tileProviderResolver.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Tile Provider Resolver - * ---------------------- - * Utility functions to look up URLs and metadata - * from TileProviderConfig override arrays. - */ - -import type { TileProviderConfig } from "../../../Map/types/tileProvider"; - -/** - * Resolve terrain URL from TileProviderConfig. - * Uses the first entry with id 'default', or the first entry if none matches. - */ -export function resolveTerrainUrl(config: TileProviderConfig | undefined): string | null { - if (!config) return null; - const overrides = config.terrainOverrides; - if (!overrides?.length) return null; - return (overrides.find(t => t.id === "default") ?? overrides[0]).url; -} - -/** - * Resolve imagery URL from TileProviderConfig by preset id. - * @param presetName - e.g. 'default', 'default_road', 'black_marble' - */ -export function resolveImageryUrl( - config: TileProviderConfig | undefined, - presetName: string, -): string | null { - if (!config) return null; - return config.imageryTileOverrides?.find(o => o.id === presetName)?.url ?? null; -} - -/** - * Resolve imagery credit/attribution by preset id. - */ -export function resolveImageryCredit( - config: TileProviderConfig | undefined, - presetName: string, -): string | undefined { - if (!config) return undefined; - return config.imageryTileOverrides?.find(o => o.id === presetName)?.credit; -} - -/** - * Resolve imagery maximum zoom level by preset id. - */ -export function resolveImageryMaxZoom( - config: TileProviderConfig | undefined, - presetName: string, -): number | undefined { - if (!config) return undefined; - return config.imageryTileOverrides?.find(o => o.id === presetName)?.maxZoomLevel; -} - -/** - * Resolve 3D tileset URL from TileProviderConfig by tileset id. - * @param tilesetId - e.g. 'googlePhotorealistic' - */ -export function resolveTilesetUrl( - config: TileProviderConfig | undefined, - tilesetId: string, -): string | null { - if (!config) return null; - return config.layerSourceOverrides?.find(o => o.id === tilesetId)?.url ?? null; -} - -/** - * Get base URL from config (informational reference only). - */ -export function getBaseUrl(config: TileProviderConfig | undefined): string | undefined { - return config?.baseUrl; -} diff --git a/src/engines/Cesium/hooks.ts b/src/engines/Cesium/hooks.ts index d7c0c14..2c2c2b5 100644 --- a/src/engines/Cesium/hooks.ts +++ b/src/engines/Cesium/hooks.ts @@ -51,7 +51,6 @@ import { LayersRef, RequestingRenderMode, } from "../../Map"; -import type { TileProviderConfig } from "../../Map/types/tileProvider"; import { TimelineManagerRef } from "../../Map/useTimelineManager"; import { FEATURE_FLAGS } from "../../shared/featureFlags"; @@ -163,9 +162,6 @@ export default ({ ? meta.cesiumIonAccessToken : undefined; - // Extract TileProviderConfig from meta - const tileProvider = meta?.tileProvider as TileProviderConfig | undefined; - // expose ref const engineAPI = useEngineRef(ref, cesium); @@ -843,7 +839,6 @@ export default ({ return { cesium, cesiumIonAccessToken, - tileProvider, mouseEventHandles, layerSelectWithRectEventHandlers, context, diff --git a/src/engines/Cesium/index.tsx b/src/engines/Cesium/index.tsx index a65a190..a718eda 100644 --- a/src/engines/Cesium/index.tsx +++ b/src/engines/Cesium/index.tsx @@ -47,6 +47,7 @@ const Cesium: React.ForwardRefRenderFunction = ( shouldRender, layerSelectionReason, meta, + customProvider, displayCredits, layersRef, featureFlags, @@ -72,7 +73,6 @@ const Cesium: React.ForwardRefRenderFunction = ( cesium, mouseEventHandles, cesiumIonAccessToken, - tileProvider, context, layerSelectWithRectEventHandlers, sceneLight, @@ -168,7 +168,7 @@ const Cesium: React.ForwardRefRenderFunction = ( @@ -267,7 +267,7 @@ const Cesium: React.ForwardRefRenderFunction = ( {ready ? children : null} From bc2dcabefb551e92fde457bfb4711dc9563dacfc Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Wed, 20 May 2026 19:08:53 +0900 Subject: [PATCH 05/19] refactor: remove customProvider for tiles and remove remove string type --- example/buildExampleMeta.ts | 34 +++-------- src/Map/types/customProvider.ts | 36 ++---------- src/engines/Cesium/Feature/Tileset/hooks.ts | 36 +++--------- src/engines/Cesium/core/Globe/index.tsx | 10 +--- .../core/Globe/useTerrainProviderPromise.ts | 56 +++++-------------- .../Cesium/core/customProviderResolver.ts | 38 +++++-------- src/engines/Cesium/index.tsx | 1 - 7 files changed, 47 insertions(+), 164 deletions(-) diff --git a/example/buildExampleMeta.ts b/example/buildExampleMeta.ts index 8508602..bc4c806 100644 --- a/example/buildExampleMeta.ts +++ b/example/buildExampleMeta.ts @@ -1,4 +1,4 @@ -import type { CustomProviderConfig, ImageryProviderEntry, LayerProviderEntry } from "@reearth/core"; +import type { CustomProviderConfig, ImageryProviderEntry } from "@reearth/core"; function parseEnvJson(envVar: string | undefined): T[] { if (!envVar) return []; @@ -16,52 +16,32 @@ function appendToken(url: string, token: string): string { return `${url}${sep}token=${token}`; } -// terrain.reearth.land doesn't send CORS headers, so in dev we route through the -// Vite proxy (/terrain-proxy) defined in vite.config.example.ts. -const REEARTH_TERRAIN_URL = "https://terrain.reearth.land/cesium-mesh/ellipsoid"; -const REEARTH_TERRAIN_CREDIT = "Re:Earth Terrain, Mapterhorn"; - /** * Build CustomProviderConfig from env vars. * * Env vars: - * EXAMPLE_TILE_SERVER_TOKEN — auth token appended as ?token= to imagery and layer URLs + * EXAMPLE_TILE_SERVER_TOKEN — auth token appended as ?token= to imagery URLs * EXAMPLE_IMAGERY_PROVIDERS — JSON array of ImageryProviderEntry (URLs without token) - * EXAMPLE_LAYER_PROVIDERS — JSON array of LayerProviderEntry (URLs without token) * - * Terrain uses the public Re:Earth terrain service (hardcoded, no token needed). - * Returns undefined when no imagery or layer providers are configured. + * Terrain URL is hardcoded in core (useTerrainProviderPromise). + * Layer URLs are set directly on individual layer data.url fields. + * Returns undefined when no imagery providers are configured. */ export function buildExampleTileProvider(): CustomProviderConfig | undefined { const token: string | undefined = import.meta.env.EXAMPLE_TILE_SERVER_TOKEN || undefined; const rawImageryProviders = parseEnvJson( import.meta.env.EXAMPLE_IMAGERY_PROVIDERS, ); - const rawLayerProviders = parseEnvJson( - import.meta.env.EXAMPLE_LAYER_PROVIDERS, - ); - if (rawImageryProviders.length === 0 && rawLayerProviders.length === 0) { + if (rawImageryProviders.length === 0) { return undefined; } - const terrainUrl = import.meta.env.DEV - ? REEARTH_TERRAIN_URL.replace("https://terrain.reearth.land", "/terrain-proxy") - : REEARTH_TERRAIN_URL; - const imageryProviders = token ? rawImageryProviders.map(p => ({ ...p, url: appendToken(p.url, token) })) : rawImageryProviders; - const layerProviders = token - ? rawLayerProviders.map(p => ({ ...p, url: appendToken(p.url, token) })) - : rawLayerProviders; - return { - imagery: imageryProviders.length > 0 ? { providers: imageryProviders } : undefined, - terrain: { - providers: [{ id: "default", url: terrainUrl, credit: REEARTH_TERRAIN_CREDIT }], - }, - layers: layerProviders.length > 0 ? { providers: layerProviders } : undefined, + imagery: { providers: imageryProviders }, }; } diff --git a/src/Map/types/customProvider.ts b/src/Map/types/customProvider.ts index 2391849..06a924a 100644 --- a/src/Map/types/customProvider.ts +++ b/src/Map/types/customProvider.ts @@ -1,12 +1,9 @@ /** - * Generic provider configuration — auth-agnostic, URL-first design. + * Generic provider configuration — imagery overrides only. * - * These types allow configuring different tile service providers - * in a provider-agnostic way, making @reearth/core work with - * various backends without coupling to any specific vendor. - * - * Note: Cesium Ion is not configured here. When users select cesiumion - * terrain in the editor, the token flows through meta.cesiumIonAccessToken. + * Allows configuring custom imagery tile providers in a vendor-agnostic way. + * Terrain and layer (3D tileset) URLs are configured directly on the engine + * the engine (hardcoded in useTerrainProviderPromise) or via the layer's own url field. */ export interface ImageryProviderEntry { @@ -19,33 +16,8 @@ export interface ImageryProviderEntry { minimumLevel?: number; } -export interface TerrainProviderEntry { - id: string; - name?: string; - nameJa?: string; - url: string; - requestVertexNormals?: boolean; - requestWaterMask?: boolean; - credit?: string; -} - -export interface LayerProviderEntry { - id: string; - url: string; - options?: Record; -} - export interface CustomProviderConfig { imagery?: { providers?: ImageryProviderEntry[]; - remove?: string[]; - }; - terrain?: { - providers?: TerrainProviderEntry[]; - remove?: string[]; - }; - layers?: { - providers?: LayerProviderEntry[]; - remove?: string[]; }; } diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index 99ee05c..edf8ea1 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -35,9 +35,8 @@ import type { Cesium3DTilesAppearance, } from "../../.."; import { useRefValue } from "../../../../hooks"; -import { LayerSimple, CustomProviderConfig } from "../../../../Map"; +import { LayerSimple } from "../../../../Map"; import { layerIdField, sampleTerrainHeightFromCartesian } from "../../common"; -import { resolveTilesetUrl } from "../../core/customProviderResolver"; import { arrayToCartecian3 } from "../../helpers/sphericalHaromic"; import type { InternalCesium3DTileFeature } from "../../types"; import { @@ -754,27 +753,19 @@ export const useHooks = ({ } }, [style, isTilesetReady]); - const customProvider = meta?.customProvider as CustomProviderConfig | undefined; - const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - // First, try to use CustomProviderConfig URL (returns string directly) - const customUrl = resolveTilesetUrl(customProvider, "googlePhotorealistic"); - if (customUrl) { - // Custom provider URL — return string directly (not wrapped in Promise) - return customUrl; - } + // If the layer has an explicit URL (e.g. a self-hosted tile server), use it directly. + if (url) return url; - // For async resource loading (Google API or Cesium Ion), return Promise + // Otherwise load via Google API key or Cesium Ion. const loadTileset = async (): Promise => { try { if (googleMapApiKey) { const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey }); return tileset.resource; } - - // Fallback: use user-configured Cesium Ion token const resource = await IonResource.fromAssetId(2275207, { accessToken: meta?.cesiumIonAccessToken as string | undefined, }); @@ -786,7 +777,7 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, customProvider]); + }, [type, isVisible, url, googleMapApiKey, meta?.cesiumIonAccessToken]); const tilesetUrl = useMemo((): string | Resource | Promise | null => { if (!isVisible) return null; @@ -796,12 +787,9 @@ export const useHooks = ({ return googleMapPhotorealisticResource; } - // Re:Earth Buildings — public service; overridable via CustomProviderConfig for self-hosted mirrors + // Re:Earth Buildings — use layer's own url if provided, otherwise fall back to public service if (type === "reearth-buildings") { - return ( - resolveTilesetUrl(customProvider, "reearthBuildings") ?? - "https://buildings.reearth.land/tileset.json" - ); + return url ?? "https://buildings.reearth.land/tileset.json"; } // OSM Buildings — only available via Cesium Ion. @@ -817,15 +805,7 @@ export const useHooks = ({ } return null; - }, [ - type, - isVisible, - googleMapPhotorealisticResource, - url, - tileset, - meta?.cesiumIonAccessToken, - customProvider, - ]); + }, [type, isVisible, googleMapPhotorealisticResource, url, tileset, meta?.cesiumIonAccessToken]); const imageBasedLighting = useMemo(() => { if ( diff --git a/src/engines/Cesium/core/Globe/index.tsx b/src/engines/Cesium/core/Globe/index.tsx index 5cf0146..a445e76 100644 --- a/src/engines/Cesium/core/Globe/index.tsx +++ b/src/engines/Cesium/core/Globe/index.tsx @@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, type JSX } from "react"; import { Globe as CesiumGlobe } from "resium"; import type { ViewerProperty } from "../../.."; -import type { CustomProviderConfig } from "../../../../Map/types/customProvider"; import { toColor } from "../../common"; import useTerrainProviderPromise from "./useTerrainProviderPromise"; @@ -10,14 +9,12 @@ import useTerrainProviderPromise from "./useTerrainProviderPromise"; export type Props = { property?: ViewerProperty; cesiumIonAccessToken?: string; - customProvider?: CustomProviderConfig; onTerrainProviderChange?: () => void; }; export default function Globe({ property, cesiumIonAccessToken, - customProvider, onTerrainProviderChange, }: Props): JSX.Element | null { const providerPromise = useTerrainProviderPromise({ @@ -27,7 +24,6 @@ export default function Globe({ ionAccessToken: property?.assets?.cesium?.terrain?.ionAccessToken || cesiumIonAccessToken, ionAsset: property?.assets?.cesium?.terrain?.ionAsset, ionUrl: property?.assets?.cesium?.terrain?.ionUrl, - customProvider, }); const baseColor = useMemo( @@ -43,17 +39,13 @@ export default function Globe({ providerPromise .then(resolvedProvider => { if (isCancelled) return; - - // Only trigger callback if the resolved provider is actually different if (lastResolvedProviderRef.current !== resolvedProvider) { lastResolvedProviderRef.current = resolvedProvider; onTerrainProviderChange?.(); } }) .catch(error => { - if (!isCancelled) { - console.warn("Terrain provider failed to load:", error); - } + if (!isCancelled) console.warn("Terrain provider failed to load:", error); }); return () => { diff --git a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts index 0c2e39e..96415e4 100644 --- a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts +++ b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts @@ -8,33 +8,26 @@ import { import { useMemo, useRef } from "react"; import { TerrainProperty } from "../../.."; -import { AssetsCesiumProperty, CustomProviderConfig } from "../../../../Map"; -import { resolveTerrainUrl } from "../customProviderResolver"; +import { AssetsCesiumProperty } from "../../../../Map"; -// Extended terrain types to include 'reearth' type TerrainType = NonNullable | "reearth"; +const REEARTH_TERRAIN_URL = "https://terrain.reearth.land/cesium-mesh/ellipsoid"; + type ProviderOpts = Pick & AssetsCesiumProperty["terrain"] & { terrain?: boolean; terrainType?: TerrainType | null | undefined; ionAccessToken?: string | undefined; - /** Reearth terrain URL (for type='reearth') */ - reearthTerrainUrl?: string | undefined; - /** CustomProviderConfig for provider-agnostic configuration */ - customProvider?: CustomProviderConfig | undefined; }; export default function useTerrainProviderPromise(opts: ProviderOpts) { - // Cache promises so we don't recreate providers on every toggle const cacheRef = useRef(new Map>()); const ellipsoidRef = useRef(undefined); return useMemo>(() => { if (!opts.terrain) { - // single shared ellipsoid provider - if (!ellipsoidRef.current) - ellipsoidRef.current = new EllipsoidTerrainProvider(); + if (!ellipsoidRef.current) ellipsoidRef.current = new EllipsoidTerrainProvider(); return Promise.resolve(ellipsoidRef.current); } @@ -52,45 +45,27 @@ export default function useTerrainProviderPromise(opts: ProviderOpts) { function makeKey(type: TerrainType, opts: ProviderOpts) { const asset = opts.ionAsset ?? ""; const url = opts.ionUrl ?? ""; - const reearthTerrainUrl = opts.reearthTerrainUrl ?? resolveTerrainUrl(opts.customProvider) ?? ""; const ionToken = opts.ionAccessToken ?? ""; const normal = String(!!opts.normal); - return `${type}|asset:${asset}|url:${url}|reearth:${reearthTerrainUrl}|ion:${ionToken}|normal:${normal}`; + return `${type}|asset:${asset}|url:${url}|reearth:${REEARTH_TERRAIN_URL}|ion:${ionToken}|normal:${normal}`; } function createProvider(type: TerrainType, opts: ProviderOpts): Promise { - // First, try to use CustomProviderConfig if available - const tileProviderUrl = resolveTerrainUrl(opts.customProvider); - switch (type) { - case "reearth": { - // Explicit Reearth terrain — must have a URL configured. - const terrainUrl = opts.reearthTerrainUrl ?? tileProviderUrl; - if (terrainUrl) { - return CesiumTerrainProvider.fromUrl(terrainUrl, { - requestVertexNormals: !!opts.normal, - requestWaterMask: false, - }) as Promise; - } - console.warn( - "[Terrain] type='reearth' requires customProvider.terrain.providers to be configured. " + - "Falling back to ellipsoid.", - ); - return Promise.resolve(new EllipsoidTerrainProvider()); - } + case "reearth": + return CesiumTerrainProvider.fromUrl(REEARTH_TERRAIN_URL, { + requestVertexNormals: !!opts.normal, + requestWaterMask: false, + }) as Promise; case "cesium": { - // "cesium" preserves its original meaning: CesiumWorld Terrain (Ion Asset 1). - // If a tileProvider URL is configured it takes precedence — this allows reearth terrain - // deployments to upgrade without requiring a scene-data migration from "cesium" → "reearth". - const terrainUrl = opts.reearthTerrainUrl ?? tileProviderUrl; - if (terrainUrl) { - return CesiumTerrainProvider.fromUrl(terrainUrl, { + // "cesium" without Ion-specific config falls through to Re:Earth terrain (migration compat) + if (!opts.ionAsset && !opts.ionUrl) { + return CesiumTerrainProvider.fromUrl(REEARTH_TERRAIN_URL, { requestVertexNormals: !!opts.normal, requestWaterMask: false, }) as Promise; } - // Legacy: fall back to CesiumWorld Terrain via Ion. return CesiumTerrainProvider.fromUrl( IonResource.fromAssetId(1, { accessToken: opts.ionAccessToken }), { requestVertexNormals: !!opts.normal, requestWaterMask: false }, @@ -103,9 +78,7 @@ function createProvider(type: TerrainType, opts: ProviderOpts): Promise; case "cesiumion": { - if (!opts.ionAsset && !opts.ionUrl) { - return Promise.resolve(new EllipsoidTerrainProvider()); - } + if (!opts.ionAsset && !opts.ionUrl) return Promise.resolve(new EllipsoidTerrainProvider()); return CesiumTerrainProvider.fromUrl( opts.ionUrl ?? IonResource.fromAssetId(parseInt(String(opts.ionAsset), 10), { @@ -116,7 +89,6 @@ function createProvider(type: TerrainType, opts: ProviderOpts): Promise p.id === "default") ?? providers[0]; -} - -export function resolveTerrainUrl(config: CustomProviderConfig | undefined): string | null { - return resolveTerrainEntry(config)?.url ?? null; -} - -export function resolveImageryUrl(config: CustomProviderConfig | undefined, presetName: string): string | null { +export function resolveImageryUrl( + config: CustomProviderConfig | undefined, + presetName: string, +): string | null { return config?.imagery?.providers?.find(p => p.id === presetName)?.url ?? null; } -export function resolveImageryCredit(config: CustomProviderConfig | undefined, presetName: string): string | undefined { +export function resolveImageryCredit( + config: CustomProviderConfig | undefined, + presetName: string, +): string | undefined { return config?.imagery?.providers?.find(p => p.id === presetName)?.credit; } -export function resolveImageryMaxZoom(config: CustomProviderConfig | undefined, presetName: string): number | undefined { +export function resolveImageryMaxZoom( + config: CustomProviderConfig | undefined, + presetName: string, +): number | undefined { return config?.imagery?.providers?.find(p => p.id === presetName)?.maximumLevel; } - -export function resolveTilesetUrl(config: CustomProviderConfig | undefined, tilesetId: string): string | null { - return config?.layers?.providers?.find(p => p.id === tilesetId)?.url ?? null; -} diff --git a/src/engines/Cesium/index.tsx b/src/engines/Cesium/index.tsx index a718eda..b92b7ce 100644 --- a/src/engines/Cesium/index.tsx +++ b/src/engines/Cesium/index.tsx @@ -267,7 +267,6 @@ const Cesium: React.ForwardRefRenderFunction = ( {ready ? children : null} From 8faf325dd420bf174e2c8bfcda437060f82796a2 Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Thu, 21 May 2026 10:44:48 +0900 Subject: [PATCH 06/19] refactor: fix lint error --- .env.example | 2 +- example/hooks.ts | 30 +- src/Map/Sketch/sketchMachine.ts | 868 ++++++++++-------- src/engines/Cesium/core/Imagery.test.ts | 21 +- src/engines/Cesium/core/presets.ts | 9 +- src/engines/Cesium/hooks.ts | 130 +-- .../useOverrideGlobeShader.ts | 34 +- src/engines/Cesium/index.tsx | 10 +- 8 files changed, 565 insertions(+), 539 deletions(-) diff --git a/.env.example b/.env.example index 269a9b3..e3fc344 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,4 @@ EXAMPLE_IMAGERY_PROVIDERS= # Layer (3D tileset) providers — JSON array of LayerProviderEntry objects (URLs without token) # Each entry: { "id": string, "url": string } # The token from EXAMPLE_TILE_SERVER_TOKEN is appended automatically. -EXAMPLE_LAYER_PROVIDERS= \ No newline at end of file +EXAMPLE_LAYER_PROVIDERS= diff --git a/example/hooks.ts b/example/hooks.ts index 9de8758..c06c049 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -35,9 +35,7 @@ export default () => { // TODO: use onLayerSelect props (core should export a type for selection). const [selectedLayer, setSelectedLayer] = useState(); - const [selectedFeature, setSelectedFeature] = useState< - ComputedFeature | undefined - >(); + const [selectedFeature, setSelectedFeature] = useState(); const handleSelect: ( layerId: string | undefined, layer: (() => Promise) | undefined, @@ -51,9 +49,12 @@ export default () => { const customProvider = useMemo(() => buildExampleTileProvider(), []); - const meta = useMemo(() => ({ - cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, - }), []); + const meta = useMemo( + () => ({ + cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, + }), + [], + ); const customTileIds = customProvider?.imagery?.providers?.map(p => p.id) ?? []; @@ -67,8 +68,7 @@ export default () => { const [currentCamera, setCurrentCamera] = useState(DEFAULT_CAMERA); const [terrainEnabled, setTerrainEnabled] = useState(true); const [hideUnderground, setHideUnderground] = useState(false); - const [activeLayerIds, setActiveLayerIds] = - useState(DEFAULT_LAYERS); + const [activeLayerIds, setActiveLayerIds] = useState(DEFAULT_LAYERS); const viewerProperty: ViewerProperty = useMemo( () => ({ @@ -96,13 +96,11 @@ export default () => { ); const layers = useMemo( - () => TEST_LAYERS.filter((layer) => activeLayerIds.includes(layer.id)), + () => TEST_LAYERS.filter(layer => activeLayerIds.includes(layer.id)), [activeLayerIds], ); - const [sketchTool, setSketchTool] = useState( - undefined, - ); + const [sketchTool, setSketchTool] = useState(undefined); useEffect(() => { ref.current?.sketch?.setType(sketchTool); }, [ref, sketchTool]); @@ -123,9 +121,7 @@ export default () => { const handleEditSketchFeature = useCallback(() => { if ( - !( - selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer - ) || + !(selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer) || !selectedLayer.id || !selectedFeature?.id ) @@ -146,9 +142,7 @@ export default () => { const handleDeleteSketchFeature = useCallback(() => { if ( - !( - selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer - ) || + !(selectedLayer?.type === "simple" && selectedLayer.data?.isSketchLayer) || !selectedLayer.id || !selectedFeature?.id ) diff --git a/src/Map/Sketch/sketchMachine.ts b/src/Map/Sketch/sketchMachine.ts index 21f7d50..d8d5209 100644 --- a/src/Map/Sketch/sketchMachine.ts +++ b/src/Map/Sketch/sketchMachine.ts @@ -58,548 +58,620 @@ export interface Context { } export function createSketchMachine() { - return createMachine( - { - id: "sketch", - initial: "idle", - context: {} as Context, - states: { - idle: { - on: { - MARKER: { - target: "drawing.marker", - actions: ["createMarker"], - }, - POLYLINE: { - target: "drawing.polyline", - actions: ["createPolyline"], - }, - CIRCLE: { - target: "drawing.circle", - actions: ["createCircle"], - }, - RECTANGLE: { - target: "drawing.rectangle", - actions: ["createRectangle"], - }, - POLYGON: { - target: "drawing.polygon", - actions: ["createPolygon"], - }, - EXTRUDED_CIRCLE: { - target: "drawing.circle", - actions: ["createExtrudedCircle"], - }, - EXTRUDED_RECTANGLE: { - target: "drawing.extrudedRectangle", - actions: ["createExtrudedRectangle"], - }, - EXTRUDED_POLYGON: { - target: "drawing.extrudedPolygon", - actions: ["createExtrudedPolygon"], - }, - EDIT_MARKER: { - target: "editing.marker", - actions: ["editMarker"], - }, - EDIT_POLYLINE: { - target: "editing.polyline", - actions: ["editPolyline"], - }, - EDIT_CIRCLE: { - target: "editing.circle", - actions: ["editCircle"], - }, - EDIT_RECTANGLE: { - target: "editing.rectangle", - actions: ["editRectangle"], - }, - EDIT_POLYGON: { - target: "editing.polygon", - actions: ["editPolygon"], - }, - EDIT_EXTRUDED_CIRCLE: { - target: "editing.circle", - actions: ["editExtrudedCircle"], - }, - EDIT_EXTRUDED_RECTANGLE: { - target: "editing.extrudedRectangle", - actions: ["editExtrudedRectangle"], - }, - EDIT_EXTRUDED_POLYGON: { - target: "editing.extrudedPolygon", - actions: ["editExtrudedPolygon"], - }, + return createMachine({ + id: "sketch", + initial: "idle", + context: {} as Context, + states: { + idle: { + on: { + MARKER: { + target: "drawing.marker", + actions: ["createMarker"], + }, + POLYLINE: { + target: "drawing.polyline", + actions: ["createPolyline"], + }, + CIRCLE: { + target: "drawing.circle", + actions: ["createCircle"], + }, + RECTANGLE: { + target: "drawing.rectangle", + actions: ["createRectangle"], + }, + POLYGON: { + target: "drawing.polygon", + actions: ["createPolygon"], + }, + EXTRUDED_CIRCLE: { + target: "drawing.circle", + actions: ["createExtrudedCircle"], + }, + EXTRUDED_RECTANGLE: { + target: "drawing.extrudedRectangle", + actions: ["createExtrudedRectangle"], + }, + EXTRUDED_POLYGON: { + target: "drawing.extrudedPolygon", + actions: ["createExtrudedPolygon"], + }, + EDIT_MARKER: { + target: "editing.marker", + actions: ["editMarker"], + }, + EDIT_POLYLINE: { + target: "editing.polyline", + actions: ["editPolyline"], + }, + EDIT_CIRCLE: { + target: "editing.circle", + actions: ["editCircle"], + }, + EDIT_RECTANGLE: { + target: "editing.rectangle", + actions: ["editRectangle"], + }, + EDIT_POLYGON: { + target: "editing.polygon", + actions: ["editPolygon"], + }, + EDIT_EXTRUDED_CIRCLE: { + target: "editing.circle", + actions: ["editExtrudedCircle"], + }, + EDIT_EXTRUDED_RECTANGLE: { + target: "editing.extrudedRectangle", + actions: ["editExtrudedRectangle"], + }, + EDIT_EXTRUDED_POLYGON: { + target: "editing.extrudedPolygon", + actions: ["editExtrudedPolygon"], }, }, - drawing: { - initial: "marker", - states: { - marker: { - initial: "vertex", - states: { - vertex: {}, - }, - }, - polyline: { - initial: "vertex", - states: { - vertex: { - on: { - NEXT: { - target: "vertex", - reenter: false, - actions: ["pushPosition"], - }, - }, - }, - }, - }, - circle: { - initial: "vertex", - states: { - vertex: { - on: { - NEXT: { - target: "#sketch.extruding", - actions: ["pushPosition"], - }, - }, - }, - }, + }, + drawing: { + initial: "marker", + states: { + marker: { + initial: "vertex", + states: { + vertex: {}, }, - rectangle: { - initial: "vertex", - states: { - vertex: { - on: { - NEXT: [ - { - target: "vertex", - reenter: false, - actions: ["pushPosition"], - }, - ], + }, + polyline: { + initial: "vertex", + states: { + vertex: { + on: { + NEXT: { + target: "vertex", + reenter: false, + actions: ["pushPosition"], }, }, }, }, - extrudedRectangle: { - initial: "vertex", - states: { - vertex: { - on: { - NEXT: [ - { - target: "#sketch.extruding", - guard: "willRectangleComplete", - actions: ["pushPosition", "recordOriginalControlPoint"], - }, - { - target: "vertex", - reenter: false, - actions: ["pushPosition"], - }, - ], + }, + circle: { + initial: "vertex", + states: { + vertex: { + on: { + NEXT: { + target: "#sketch.extruding", + actions: ["pushPosition"], }, }, }, }, - polygon: { - initial: "vertex", - states: { - vertex: { - on: { - NEXT: { + }, + rectangle: { + initial: "vertex", + states: { + vertex: { + on: { + NEXT: [ + { target: "vertex", reenter: false, actions: ["pushPosition"], }, - }, + ], }, }, }, - extrudedPolygon: { - initial: "vertex", - states: { - vertex: { - on: { - NEXT: { + }, + extrudedRectangle: { + initial: "vertex", + states: { + vertex: { + on: { + NEXT: [ + { + target: "#sketch.extruding", + guard: "willRectangleComplete", + actions: ["pushPosition", "recordOriginalControlPoint"], + }, + { target: "vertex", reenter: false, actions: ["pushPosition"], }, - EXTRUDE: { - target: "#sketch.extruding", - actions: ["pushPosition"], - }, - }, + ], }, }, }, - history: { - type: "history", - }, }, - on: { - CANCEL: [ - { - target: ".history", - guard: "canPopPosition", - actions: ["popPosition"], - }, - { - target: "idle", - actions: ["clearDrawing"], + polygon: { + initial: "vertex", + states: { + vertex: { + on: { + NEXT: { + target: "vertex", + reenter: false, + actions: ["pushPosition"], + }, + }, }, - ], - ABORT: { - target: "idle", - actions: ["clearDrawing"], }, - CREATE: { - target: "idle", - actions: ["clearDrawing"], + }, + extrudedPolygon: { + initial: "vertex", + states: { + vertex: { + on: { + NEXT: { + target: "vertex", + reenter: false, + actions: ["pushPosition"], + }, + EXTRUDE: { + target: "#sketch.extruding", + actions: ["pushPosition"], + }, + }, + }, }, }, + history: { + type: "history", + }, }, - extruding: { - on: { - CREATE: { - target: "idle", - actions: ["clearDrawing"], - }, - CANCEL: { - target: "drawing.history", + on: { + CANCEL: [ + { + target: ".history", + guard: "canPopPosition", actions: ["popPosition"], }, - ABORT: { + { target: "idle", actions: ["clearDrawing"], }, + ], + ABORT: { + target: "idle", + actions: ["clearDrawing"], + }, + CREATE: { + target: "idle", + actions: ["clearDrawing"], }, }, - editing: { - initial: "marker", - states: { - marker: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, + }, + extruding: { + on: { + CREATE: { + target: "idle", + actions: ["clearDrawing"], + }, + CANCEL: { + target: "drawing.history", + actions: ["popPosition"], + }, + ABORT: { + target: "idle", + actions: ["clearDrawing"], + }, + }, + }, + editing: { + initial: "marker", + states: { + marker: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - polyline: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, - UPDATE: { - target: "waiting", - reenter: false, - actions: ["updateControlPoints"], - }, + }, + polyline: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], + }, + UPDATE: { + target: "waiting", + reenter: false, + actions: ["updateControlPoints"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - circle: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, + }, + circle: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - rectangle: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, + }, + rectangle: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - polygon: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, - UPDATE: { - target: "waiting", - reenter: false, - actions: ["updateControlPoints"], - }, + }, + polygon: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], + }, + UPDATE: { + target: "waiting", + reenter: false, + actions: ["updateControlPoints"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - extrudedCircle: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, + }, + extrudedCircle: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - extrudedRectangle: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, + }, + extrudedRectangle: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, - extrudedPolygon: { - initial: "waiting", - states: { - waiting: { - on: { - CATCH: { - target: "moving", - reenter: false, - actions: ["catchControlPoint"], - }, - UPDATE: { - target: "waiting", - reenter: false, - actions: ["updateControlPoints"], - }, + }, + extrudedPolygon: { + initial: "waiting", + states: { + waiting: { + on: { + CATCH: { + target: "moving", + reenter: false, + actions: ["catchControlPoint"], + }, + UPDATE: { + target: "waiting", + reenter: false, + actions: ["updateControlPoints"], }, }, - moving: { - on: { - MOVE: { - target: "moving", - reenter: false, - actions: ["moveControlPoint"], - }, - RELEASE: { - target: "waiting", - actions: ["releaseControlPoint"], - }, + }, + moving: { + on: { + MOVE: { + target: "moving", + reenter: false, + actions: ["moveControlPoint"], + }, + RELEASE: { + target: "waiting", + actions: ["releaseControlPoint"], }, }, }, }, }, - on: { - EXIT_EDIT: { - target: "idle", - actions: ["clearDrawing"], - }, + }, + on: { + EXIT_EDIT: { + target: "idle", + actions: ["clearDrawing"], }, }, }, }, - ).provide({ + }).provide({ guards: { - canPopPosition: ({ context }) => context.controlPoints != null && context.controlPoints.length > 1, - willRectangleComplete: ({ context }) => context.controlPoints != null && context.controlPoints.length === 2, + canPopPosition: ({ context }) => + context.controlPoints != null && context.controlPoints.length > 1, + willRectangleComplete: ({ context }) => + context.controlPoints != null && context.controlPoints.length === 2, }, actions: { createMarker: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "marker" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "marker" as SketchType, + controlPoints: [controlPoint], + }; }), editMarker: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "marker" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "marker" as SketchType, + controlPoints: e.controlPoints, + }; }), createPolyline: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "polyline" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "polyline" as SketchType, + controlPoints: [controlPoint], + }; }), editPolyline: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "polyline" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "polyline" as SketchType, + controlPoints: e.controlPoints, + }; }), createCircle: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "circle" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "circle" as SketchType, + controlPoints: [controlPoint], + }; }), editCircle: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "circle" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "circle" as SketchType, + controlPoints: e.controlPoints, + }; }), createRectangle: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "rectangle" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "rectangle" as SketchType, + controlPoints: [controlPoint], + }; }), editRectangle: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "rectangle" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "rectangle" as SketchType, + controlPoints: e.controlPoints, + }; }), createPolygon: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "polygon" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "polygon" as SketchType, + controlPoints: [controlPoint], + }; }), editPolygon: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "polygon" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "polygon" as SketchType, + controlPoints: e.controlPoints, + }; }), createExtrudedCircle: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "extrudedCircle" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "extrudedCircle" as SketchType, + controlPoints: [controlPoint], + }; }), editExtrudedCircle: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "extrudedCircle" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "extrudedCircle" as SketchType, + controlPoints: e.controlPoints, + }; }), createExtrudedRectangle: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "extrudedRectangle" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "extrudedRectangle" as SketchType, + controlPoints: [controlPoint], + }; }), editExtrudedRectangle: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "extrudedRectangle" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "extrudedRectangle" as SketchType, + controlPoints: e.controlPoints, + }; }), createExtrudedPolygon: assign(({ event }) => { const e = event as Extract; const controlPoint = [...e.controlPoint] as Position3d; - return { lastPointerPosition: [...e.pointerPosition] as Position2d, lastControlPoint: controlPoint, type: "extrudedPolygon" as SketchType, controlPoints: [controlPoint] }; + return { + lastPointerPosition: [...e.pointerPosition] as Position2d, + lastControlPoint: controlPoint, + type: "extrudedPolygon" as SketchType, + controlPoints: [controlPoint], + }; }), editExtrudedPolygon: assign(({ event }) => { const e = event as Extract; - return { lastControlPoint: undefined, type: "extrudedPolygon" as SketchType, controlPoints: e.controlPoints }; + return { + lastControlPoint: undefined, + type: "extrudedPolygon" as SketchType, + controlPoints: e.controlPoints, + }; }), pushPosition: assign(({ context, event }) => { const e = event as Extract; @@ -625,13 +697,19 @@ export function createSketchMachine() { })), catchControlPoint: assign(({ event }) => { const e = event as Extract; - return { catchedControlPointIndex: e.catchedControlPointIndex, catchedExtrudedPoint: e.catchedExtrudedPoint }; + return { + catchedControlPointIndex: e.catchedControlPointIndex, + catchedExtrudedPoint: e.catchedExtrudedPoint, + }; }), moveControlPoint: assign(({ event }) => { const e = event as Extract; return { controlPoints: e.controlPoints }; }), - releaseControlPoint: assign(() => ({ catchedControlPointIndex: undefined, catchedExtrudedPoint: undefined })), + releaseControlPoint: assign(() => ({ + catchedControlPointIndex: undefined, + catchedExtrudedPoint: undefined, + })), updateControlPoints: assign(({ event }) => { const e = event as Extract; return { controlPoints: e.controlPoints }; diff --git a/src/engines/Cesium/core/Imagery.test.ts b/src/engines/Cesium/core/Imagery.test.ts index a003a0d..943631a 100644 --- a/src/engines/Cesium/core/Imagery.test.ts +++ b/src/engines/Cesium/core/Imagery.test.ts @@ -1,8 +1,9 @@ -import { UrlTemplateImageryProvider } from "cesium"; import { renderHook } from "@testing-library/react"; +import { UrlTemplateImageryProvider } from "cesium"; import { expect, test, vi } from "vitest"; import type { CustomProviderConfig } from "../../../Map/types/customProvider"; + import { type Tile, useImageryProviders } from "./Imagery"; test("useImageryProviders", () => { @@ -45,7 +46,9 @@ test("useImageryProviders", () => { customProvider?: CustomProviderConfig; }) => void; - expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }] }); + expect(result.current.providers).toEqual({ + "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }], + }); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(1); const prevImageryProvider = result.current.providers["1"][3]; @@ -53,14 +56,18 @@ test("useImageryProviders", () => { // re-render with same tiles typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default" }] }); - expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }] }); + expect(result.current.providers).toEqual({ + "1": ["cesium_ion_default", undefined, undefined, { hoge: undefined }], + }); expect(result.current.providers["1"][3]).toBe(prevImageryProvider); // 1's provider should be reused expect(provider).toBeCalledTimes(1); // update a tile URL typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default", url: "a" }] }); - expect(result.current.providers).toEqual({ "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }] }); + expect(result.current.providers).toEqual({ + "1": ["cesium_ion_default", "a", undefined, { hoge: "a" }], + }); expect(result.current.providers["1"][3]).not.toBe(prevImageryProvider); expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(2); @@ -128,7 +135,11 @@ test("useImageryProviders", () => { customProvider: { imagery: { providers: [ - { id: "my_custom_satellite", url: "https://example.com/{z}/{x}/{y}.png", credit: "© Example" }, + { + id: "my_custom_satellite", + url: "https://example.com/{z}/{x}/{y}.png", + credit: "© Example", + }, ], }, }, diff --git a/src/engines/Cesium/core/presets.ts b/src/engines/Cesium/core/presets.ts index 04ca0b3..20f73b2 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -28,10 +28,10 @@ const PRESET_TILE_TYPES = [ "cesium_ion_earth_at_night", // Legacy aliases — kept for backward compatibility with existing apps - "default", // → cesium_ion_default - "default_road", // → cesium_ion_road - "default_label", // → cesium_ion_labelled - "black_marble", // → cesium_ion_earth_at_night + "default", // → cesium_ion_default + "default_road", // → cesium_ion_road + "default_label", // → cesium_ion_labelled + "black_marble", // → cesium_ion_earth_at_night ]; export type PresetTileType = (typeof PRESET_TILE_TYPES)[number]; @@ -133,7 +133,6 @@ export const tiles = { return undefined as unknown as ImageryProvider; }), // --- Legacy aliases --- - // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as { [K in PresetTileType]: (opts?: TileOptions) => Promise | ImageryProvider | null; }; diff --git a/src/engines/Cesium/hooks.ts b/src/engines/Cesium/hooks.ts index 2c2c2b5..1b0dc62 100644 --- a/src/engines/Cesium/hooks.ts +++ b/src/engines/Cesium/hooks.ts @@ -12,19 +12,8 @@ import { ShadowMap, ImageryLayer, } from "cesium"; -import { - MutableRefObject, - RefObject, - useCallback, - useEffect, - useMemo, - useRef, -} from "react"; -import type { - CesiumComponentRef, - CesiumMovementEvent, - RootEventTarget, -} from "resium"; +import { MutableRefObject, RefObject, useCallback, useEffect, useMemo, useRef } from "react"; +import type { CesiumComponentRef, CesiumMovementEvent, RootEventTarget } from "resium"; import type { LayerSelectionReason, @@ -35,13 +24,7 @@ import type { LayerVisibilityEvent, } from ".."; import { e2eAccessToken, setE2ECesiumViewer } from "../../e2eConfig"; -import { - ComputedFeature, - DataType, - SelectedFeatureInfo, - LatLng, - Camera, -} from "../../mantle"; +import { ComputedFeature, DataType, SelectedFeatureInfo, LatLng, Camera } from "../../mantle"; import { Credits, LayerLoadEvent, @@ -76,10 +59,7 @@ interface CustomGlobeSurface { }; } -type CesiumMouseEvent = ( - movement: CesiumMovementEvent, - target: RootEventTarget, -) => void; +type CesiumMouseEvent = (movement: CesiumMovementEvent, target: RootEventTarget) => void; type CesiumMouseWheelEvent = (delta: number) => void; export default ({ @@ -135,11 +115,7 @@ export default ({ options?: LayerSelectionReason, info?: SelectedFeatureInfo, ) => void; - onLayerDrag?: ( - layerId: string, - featureId: string | undefined, - position: LatLng, - ) => void; + onLayerDrag?: (layerId: string, featureId: string | undefined, position: LatLng) => void; onLayerDrop?: ( layerId: string, featureId: string | undefined, @@ -193,12 +169,10 @@ export default ({ }) | undefined; if (!shadowMap) return; - shadowMap.softShadows = - property?.scene?.shadow?.shadowMap?.softShadows ?? false; + shadowMap.softShadows = property?.scene?.shadow?.shadowMap?.softShadows ?? false; shadowMap.darkness = property?.scene?.shadow?.shadowMap?.darkness ?? 0.3; shadowMap.size = property?.scene?.shadow?.shadowMap?.size ?? 2048; - shadowMap.maximumDistance = - property?.scene?.shadow?.shadowMap?.maximumDistance ?? 5000; + shadowMap.maximumDistance = property?.scene?.shadow?.shadowMap?.maximumDistance ?? 5000; shadowMap.fadingEnabled = true; shadowMap.normalOffset = true; @@ -311,7 +285,7 @@ export default ({ // Find ImageryLayerFeature const ImageryLayerDataTypes: DataType[] = []; const layers = layersRef?.current?.findAll( - (layer) => + layer => layer.type === "simple" && !!layer.data?.type && ImageryLayerDataTypes.includes(layer.data?.type), @@ -323,7 +297,7 @@ export default ({ ((): [ComputedFeature, string] | void => { for (const layer of layers) { const f = layer.computed?.features.find( - (feature) => feature.id !== selectedLayerId?.featureId, + feature => feature.id !== selectedLayerId?.featureId, ); if (f) { return [f, layer.id]; @@ -362,9 +336,7 @@ export default ({ if (entity) { const layer = tag?.layerId - ? (layersRef?.current - ?.overriddenLayers() - .find((l) => l.id === tag.layerId) ?? + ? (layersRef?.current?.overriddenLayers().find(l => l.id === tag.layerId) ?? layersRef?.current?.findById(tag.layerId)) : undefined; // Sometimes only featureId is specified, so we need to sync entity tag. @@ -377,11 +349,8 @@ export default ({ title: entity.name, content: getEntityContent( entity, - cesium.current?.cesiumElement?.clock.currentTime ?? - new JulianDate(), - tag?.layerId - ? layer?.infobox?.property?.defaultContent - : undefined, + cesium.current?.cesiumElement?.clock.currentTime ?? new JulianDate(), + tag?.layerId ? layer?.infobox?.property?.defaultContent : undefined, ), }, } @@ -415,11 +384,7 @@ export default ({ }); const handleMouseEvent = useCallback( - ( - type: keyof MouseEvents, - e: CesiumMovementEvent, - target: RootEventTarget, - ) => { + (type: keyof MouseEvents, e: CesiumMovementEvent, target: RootEventTarget) => { if (engineAPI.mouseEventCallbacks[type]?.length > 0) { const viewer = cesium.current?.cesiumElement; if (!viewer || viewer.isDestroyed()) return; @@ -427,7 +392,7 @@ export default ({ if (!props) return; const layerId = getLayerId(target); if (layerId) props.layerId = layerId; - engineAPI.mouseEventCallbacks[type].forEach((cb) => cb(props)); + engineAPI.mouseEventCallbacks[type].forEach(cb => cb(props)); } }, [engineAPI], @@ -436,7 +401,7 @@ export default ({ const handleMouseWheel = useCallback( (delta: number) => { if (engineAPI.mouseEventCallbacks.wheel.length > 0) { - engineAPI.mouseEventCallbacks.wheel.forEach((cb) => cb({ delta })); + engineAPI.mouseEventCallbacks.wheel.forEach(cb => cb({ delta })); } }, [engineAPI], @@ -465,12 +430,9 @@ export default ({ handleMouseWheel(delta); }, }; - (Object.keys(mouseEvents) as (keyof MouseEvents)[]).forEach((type) => { + (Object.keys(mouseEvents) as (keyof MouseEvents)[]).forEach(type => { if (type !== "wheel") - mouseEvents[type] = ( - e: CesiumMovementEvent, - target: RootEventTarget, - ) => { + mouseEvents[type] = (e: CesiumMovementEvent, target: RootEventTarget) => { handleMouseEvent(type as keyof MouseEvents, e, target); }; }); @@ -499,17 +461,10 @@ export default ({ viewer.selectedEntity = undefined; } - if ( - target && - "id" in target && - target.id instanceof Entity && - isSelectable(target.id) - ) { + if (target && "id" in target && target.id instanceof Entity && isSelectable(target.id)) { const tag = getTag(target.id); const layer = tag?.layerId - ? (layersRef?.current - ?.overriddenLayers() - .find((l) => l.id === tag.layerId) ?? + ? (layersRef?.current?.overriddenLayers().find(l => l.id === tag.layerId) ?? layersRef?.current?.findById(tag.layerId)) : undefined; onLayerSelect?.( @@ -522,9 +477,7 @@ export default ({ content: getEntityContent( target.id, viewer.clock.currentTime ?? new JulianDate(), - tag?.layerId - ? layer?.infobox?.property?.defaultContent - : undefined, + tag?.layerId ? layer?.infobox?.property?.defaultContent : undefined, ), }, } @@ -541,8 +494,7 @@ export default ({ if ( target && - (target instanceof Cesium3DTileFeature || - target instanceof Cesium3DTilePointFeature) + (target instanceof Cesium3DTileFeature || target instanceof Cesium3DTilePointFeature) ) { const tag = getTag(target); if (tag) { @@ -587,8 +539,7 @@ export default ({ if ( target?.primitive && - (target.primitive instanceof Primitive || - target.primitive instanceof GroundPrimitive) + (target.primitive instanceof Primitive || target.primitive instanceof GroundPrimitive) ) { const primitive = target.primitive; const tag = getTag(primitive); @@ -606,26 +557,17 @@ export default ({ const pickRay = scene.camera.getPickRay(e.position); if (pickRay) { - const l = await scene.imageryLayers.pickImageryLayerFeatures( - pickRay, - scene, - ); + const l = await scene.imageryLayers.pickImageryLayerFeatures(pickRay, scene); // Find the topmost overlaid feature. - const f = l?.findLast((f) => { + const f = l?.findLast(f => { const appearanceType = f?.data?.appearanceType; - return ( - appearanceType && - f?.data?.feature?.[appearanceType]?.show !== false - ); + return appearanceType && f?.data?.feature?.[appearanceType]?.show !== false; }); const appearanceType = f?.data?.appearanceType; - if ( - appearanceType && - f?.data?.feature?.[appearanceType]?.show !== false - ) { + if (appearanceType && f?.data?.feature?.[appearanceType]?.show !== false) { const tag = getTag(f.imageryLayer); const pos = f.position; @@ -644,17 +586,13 @@ export default ({ } const layer = tag?.layerId - ? (layersRef?.current - ?.overriddenLayers() - .find((l) => l.id === tag.layerId) ?? + ? (layersRef?.current?.overriddenLayers().find(l => l.id === tag.layerId) ?? layersRef?.current?.findById(tag.layerId)) : undefined; const content = getEntityContent( f.data.feature ?? f, viewer.clock.currentTime ?? new JulianDate(), - tag?.layerId - ? layer?.infobox?.property?.defaultContent - : undefined, + tag?.layerId ? layer?.infobox?.property?.defaultContent : undefined, ); prevSelectedImageryFeatureId.current = f.data.featureId; onLayerSelect?.( @@ -763,8 +701,7 @@ export default ({ if (globe) { const surface = (globe as any)._surface as CustomGlobeSurface; if (surface) { - surface.tileProvider._debug.wireframe = - property?.debug?.showGlobeWireframe ?? false; + surface.tileProvider._debug.wireframe = property?.debug?.showGlobeWireframe ?? false; } } }, [property?.debug?.showGlobeWireframe]); @@ -785,11 +722,10 @@ export default ({ }); }, [time, timelineManagerRef]); - const { sceneLight, sceneBackgroundColor, sceneMsaaSamples, sceneMode } = - useViewerProperty({ - property, - cesium, - }); + const { sceneLight, sceneBackgroundColor, sceneMsaaSamples, sceneMode } = useViewerProperty({ + property, + cesium, + }); useLayerDragDrop({ cesium, onLayerDrag, onLayerDrop, isLayerDraggable }); diff --git a/src/engines/Cesium/hooks/useOverrideGlobeShader/useOverrideGlobeShader.ts b/src/engines/Cesium/hooks/useOverrideGlobeShader/useOverrideGlobeShader.ts index 8870f2c..0f8808b 100644 --- a/src/engines/Cesium/hooks/useOverrideGlobeShader/useOverrideGlobeShader.ts +++ b/src/engines/Cesium/hooks/useOverrideGlobeShader/useOverrideGlobeShader.ts @@ -42,23 +42,23 @@ const defaultMatcher = new StringMatcher() #endif vec4 color = computeDayColor(initialColor`, ); - // Note: Commented out erase() call as it's not needed for Cesium 1.118.x - // The replace() above already handles the material code modification - // .erase([ - // "#ifdef APPLY_MATERIAL", - // "czm_materialInput materialInput;", - // "materialInput.st = v_textureCoordinates.st;", - // "materialInput.normalEC = normalize(v_normalEC);", - // "materialInput.positionToEyeEC = -v_positionEC;", - // "materialInput.tangentToEyeMatrix = czm_eastNorthUpToEyeCoordinates(v_positionMC, normalize(v_normalEC));", - // "materialInput.slope = v_slope;", - // "materialInput.height = v_height;", - // "materialInput.aspect = v_aspect;", - // "czm_material material = czm_getMaterial(materialInput);", - // "vec4 materialColor = vec4(material.diffuse, material.alpha);", - // "color = alphaBlend(materialColor, color);", - // "#endif", - // ]); +// Note: Commented out erase() call as it's not needed for Cesium 1.118.x +// The replace() above already handles the material code modification +// .erase([ +// "#ifdef APPLY_MATERIAL", +// "czm_materialInput materialInput;", +// "materialInput.st = v_textureCoordinates.st;", +// "materialInput.normalEC = normalize(v_normalEC);", +// "materialInput.positionToEyeEC = -v_positionEC;", +// "materialInput.tangentToEyeMatrix = czm_eastNorthUpToEyeCoordinates(v_positionMC, normalize(v_normalEC));", +// "materialInput.slope = v_slope;", +// "materialInput.height = v_height;", +// "materialInput.aspect = v_aspect;", +// "czm_material material = czm_getMaterial(materialInput);", +// "vec4 materialColor = vec4(material.diffuse, material.alpha);", +// "color = alphaBlend(materialColor, color);", +// "#endif", +// ]); function makeGlobeShadersDirty(globe: Globe): void { // Invoke the internal makeShadersDirty() by setting a material to globe to diff --git a/src/engines/Cesium/index.tsx b/src/engines/Cesium/index.tsx index b92b7ce..ca70318 100644 --- a/src/engines/Cesium/index.tsx +++ b/src/engines/Cesium/index.tsx @@ -295,5 +295,13 @@ export const engine: Engine = { featureComponent: Feature, clusterComponent: Cluster, sketchComponent: Sketch, - delegatedDataTypes: ["czml", "wms", "mvt", "3dtiles", "osm-buildings", "google-photorealistic", "kml"], + delegatedDataTypes: [ + "czml", + "wms", + "mvt", + "3dtiles", + "osm-buildings", + "google-photorealistic", + "kml", + ], }; From 17416fc1d3941f14ea71613480a9ceb98bf2c9bb Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 10:33:07 +0800 Subject: [PATCH 07/19] fix: types & out-dated-definitons & remove unsupported terrain type --- src/Map/types/customProvider.ts | 2 -- src/Map/types/viewerProperty.ts | 2 +- .../core/Globe/useTerrainProviderPromise.ts | 19 +++---------------- src/engines/Cesium/index.tsx | 1 + src/mantle/types/index.ts | 1 + 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/Map/types/customProvider.ts b/src/Map/types/customProvider.ts index 06a924a..99ef335 100644 --- a/src/Map/types/customProvider.ts +++ b/src/Map/types/customProvider.ts @@ -8,8 +8,6 @@ export interface ImageryProviderEntry { id: string; - name?: string; - nameJa?: string; url: string; credit?: string; maximumLevel?: number; diff --git a/src/Map/types/viewerProperty.ts b/src/Map/types/viewerProperty.ts index b821c2f..91e6bbe 100644 --- a/src/Map/types/viewerProperty.ts +++ b/src/Map/types/viewerProperty.ts @@ -74,7 +74,7 @@ export type GlobeAtmosphereProperty = { export type TerrainProperty = { enabled?: boolean; - type?: "cesium" | "arcgis" | "cesiumion" | "reearth"; + type?: "cesium" | "cesiumion" | "reearth_terrain"; url?: string; normal?: boolean; elevationHeatMap?: ElevationHeatMapProperty; diff --git a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts index 96415e4..9ea3a84 100644 --- a/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts +++ b/src/engines/Cesium/core/Globe/useTerrainProviderPromise.ts @@ -1,5 +1,4 @@ import { - ArcGISTiledElevationTerrainProvider, CesiumTerrainProvider, EllipsoidTerrainProvider, IonResource, @@ -10,7 +9,7 @@ import { useMemo, useRef } from "react"; import { TerrainProperty } from "../../.."; import { AssetsCesiumProperty } from "../../../../Map"; -type TerrainType = NonNullable | "reearth"; +type TerrainType = NonNullable; const REEARTH_TERRAIN_URL = "https://terrain.reearth.land/cesium-mesh/ellipsoid"; @@ -47,36 +46,24 @@ function makeKey(type: TerrainType, opts: ProviderOpts) { const url = opts.ionUrl ?? ""; const ionToken = opts.ionAccessToken ?? ""; const normal = String(!!opts.normal); - return `${type}|asset:${asset}|url:${url}|reearth:${REEARTH_TERRAIN_URL}|ion:${ionToken}|normal:${normal}`; + return `${type}|asset:${asset}|url:${url}|ion:${ionToken}|normal:${normal}`; } function createProvider(type: TerrainType, opts: ProviderOpts): Promise { switch (type) { - case "reearth": + case "reearth_terrain": return CesiumTerrainProvider.fromUrl(REEARTH_TERRAIN_URL, { requestVertexNormals: !!opts.normal, requestWaterMask: false, }) as Promise; case "cesium": { - // "cesium" without Ion-specific config falls through to Re:Earth terrain (migration compat) - if (!opts.ionAsset && !opts.ionUrl) { - return CesiumTerrainProvider.fromUrl(REEARTH_TERRAIN_URL, { - requestVertexNormals: !!opts.normal, - requestWaterMask: false, - }) as Promise; - } return CesiumTerrainProvider.fromUrl( IonResource.fromAssetId(1, { accessToken: opts.ionAccessToken }), { requestVertexNormals: !!opts.normal, requestWaterMask: false }, ) as Promise; } - case "arcgis": - return ArcGISTiledElevationTerrainProvider.fromUrl( - "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer", - ) as Promise; - case "cesiumion": { if (!opts.ionAsset && !opts.ionUrl) return Promise.resolve(new EllipsoidTerrainProvider()); return CesiumTerrainProvider.fromUrl( diff --git a/src/engines/Cesium/index.tsx b/src/engines/Cesium/index.tsx index ca70318..3ab3570 100644 --- a/src/engines/Cesium/index.tsx +++ b/src/engines/Cesium/index.tsx @@ -301,6 +301,7 @@ export const engine: Engine = { "mvt", "3dtiles", "osm-buildings", + "reearth-buildings", "google-photorealistic", "kml", ], diff --git a/src/mantle/types/index.ts b/src/mantle/types/index.ts index e67fc47..b4a5597 100644 --- a/src/mantle/types/index.ts +++ b/src/mantle/types/index.ts @@ -76,6 +76,7 @@ export type Data = { updateInterval?: number; // milliseconds parameters?: Record; idProperty?: string; + provider?: string; serviceTokens?: { googleMapApiKey?: string; }; From 5be88ef66083f036a6e17ea60aebaaaf35b0b448 Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 10:33:21 +0800 Subject: [PATCH 08/19] fix: set reearth terrain as the default --- example/scene.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/example/scene.ts b/example/scene.ts index 5abe23a..05a331d 100644 --- a/example/scene.ts +++ b/example/scene.ts @@ -62,6 +62,7 @@ export const DEFAULT_VIEWER_PROPERTY: ViewerProperty = { terrain: { enabled: true, normal: true, + type: "reearth_terrain", }, geoid: { server: { From 5afa68fa234f3b6281fa15cb9ab53912ac120c8f Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 10:53:57 +0800 Subject: [PATCH 09/19] refactor: align the property name for cesium ion asset id --- example/hooks.ts | 2 +- src/Map/types/viewerProperty.ts | 2 +- src/engines/Cesium/core/Imagery.tsx | 10 +++++----- src/engines/Cesium/core/presets.ts | 15 ++++++++++----- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/example/hooks.ts b/example/hooks.ts index c06c049..6670fb6 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -78,7 +78,7 @@ export default () => { { id: "default", type: currentTile, - ionAssetId: currentTile === "cesium_ion" ? cesiumIonAssetId : undefined, + cesiumIonAssetId: currentTile === "cesium_ion" ? cesiumIonAssetId : undefined, opacity: 1, }, ] diff --git a/src/Map/types/viewerProperty.ts b/src/Map/types/viewerProperty.ts index 91e6bbe..fef3d4c 100644 --- a/src/Map/types/viewerProperty.ts +++ b/src/Map/types/viewerProperty.ts @@ -130,7 +130,7 @@ export type TileProperty = { id: string; type?: string; url?: string; - ionAssetId?: number; + cesiumIonAssetId?: number; opacity?: number; zoomLevel?: number[]; zoomLevelForURL?: number[]; diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index f12fc8f..c3428c5 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -26,7 +26,7 @@ export type Tile = { id: string; url?: string; type?: string; - ionAssetId?: number; + cesiumIonAssetId?: number; opacity?: number; zoomLevel?: number[]; zoomLevelForURL?: number[]; @@ -136,7 +136,7 @@ export function useImageryProviders({ const opts = { url: t.url, cesiumIonAccessToken: ciat, - ionAssetId: t.ionAssetId, + cesiumIonAssetId: t.cesiumIonAssetId, heatmap: t.heatmap, zoomLevel: t.zoomLevelForURL, }; @@ -222,7 +222,7 @@ export function useImageryProviders({ added || prevType !== tile.type || prevUrl !== tile.url || - prevIonAssetId !== tile.ionAssetId || + prevIonAssetId !== tile.cesiumIonAssetId || isTileProviderUpdated || (isCesiumAccessTokenUpdated && (tile.type?.startsWith("cesium_ion") || @@ -233,7 +233,7 @@ export function useImageryProviders({ ? [ tile.type, tile.url, - tile.ionAssetId, + tile.cesiumIonAssetId, newTile(tile, cesiumIonAccessToken, customProvider), ] : [prevType, prevUrl, prevIonAssetId, prevProvider], @@ -260,7 +260,7 @@ export function useImageryProviders({ p.tile && (p.prevType !== p.tile.type || p.prevUrl !== p.tile.url || - p.prevIonAssetId !== p.tile.ionAssetId), + p.prevIonAssetId !== p.tile.cesiumIonAssetId), ); prevTileKeys.current = tileKeys; diff --git a/src/engines/Cesium/core/presets.ts b/src/engines/Cesium/core/presets.ts index 20f73b2..f6a9c52 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -11,7 +11,7 @@ import { import { JapanGSIOptimalBVmapLabelImageryProvider } from "./labels/JapanGSIOptimalBVmapVectorMapLabel/JapanGSIOptimalBVmapLabelImageryProvider"; const PRESET_TILE_TYPES = [ - // Dynamic Cesium Ion asset — asset ID supplied per-tile via ionAssetId field + // Dynamic Cesium Ion asset — asset ID supplied per-tile via cesiumIonAssetId field "cesium_ion", // Public presets — always available, no auth required @@ -43,17 +43,22 @@ export const isValidPresetTileType = (type: string | undefined): type is PresetT export type TileOptions = { url?: string; cesiumIonAccessToken?: string; - ionAssetId?: number; + cesiumIonAssetId?: number | string; heatmap?: boolean; tile_zoomLevel?: number[]; }; export const tiles = { // --- Dynamic Cesium Ion asset --- - // Uses per-tile ionAssetId; requires cesiumIonAccessToken. + // Uses per-tile cesiumIonAssetId; requires cesiumIonAccessToken. cesium_ion: (opts?: TileOptions) => { - if (!opts?.ionAssetId) return null; - return IonImageryProvider.fromAssetId(opts.ionAssetId, { + if (!opts?.cesiumIonAssetId) return null; + const NumberAssetId = parseInt(String(opts.cesiumIonAssetId), 10); + if (isNaN(NumberAssetId)) { + console.warn(`Invalid cesiumIonAssetId: ${opts.cesiumIonAssetId}`); + return null; + } + return IonImageryProvider.fromAssetId(NumberAssetId, { accessToken: opts?.cesiumIonAccessToken, }).catch(err => { console.error(err); From 8b5577f4436d6a85adcb8d63253bc1951a79ba60 Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 11:11:06 +0800 Subject: [PATCH 10/19] refactor: append get provider --- src/engines/Cesium/Feature/Tileset/hooks.ts | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index edf8ea1..51cc38a 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -76,6 +76,7 @@ const useData = (layer: ComputedLayer | undefined) => { : data?.layers : undefined, googleMapApiKey: data?.serviceTokens?.googleMapApiKey, + provider: data?.provider, }; }, [layer]); }; @@ -488,7 +489,7 @@ export const useHooks = ({ } = useClippingBox({ clipping: experimental_clipping, boxId }); const [style, setStyle] = useState(); - const { url, type, idProperty, googleMapApiKey } = useData(layer); + const { url, type, idProperty, googleMapApiKey, provider } = useData(layer); const shouldUseFeatureIndex = !disableIndexingFeature && !!idProperty; const [isTilesetReady, setIsTilesetReady] = useState(false); @@ -756,20 +757,24 @@ export const useHooks = ({ const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - // If the layer has an explicit URL (e.g. a self-hosted tile server), use it directly. - if (url) return url; + console.log("provider", provider); // Otherwise load via Google API key or Cesium Ion. const loadTileset = async (): Promise => { try { - if (googleMapApiKey) { - const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey }); - return tileset.resource; + if (provider === "reearth") { + const resource = await IonResource.fromAssetId(2275207, { + accessToken: meta?.cesiumIonAccessToken as string | undefined, + }); + return resource; + } else if (provider === "cesium-ion") { + const resource = await IonResource.fromAssetId(2275207, { + accessToken: meta?.cesiumIonAccessToken as string | undefined, + }); + return resource; } - const resource = await IonResource.fromAssetId(2275207, { - accessToken: meta?.cesiumIonAccessToken as string | undefined, - }); - return resource; + const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey }); + return tileset.resource; } catch (error) { console.error(`Error loading Photorealistic 3D Tiles tileset: ${error}`); throw error; @@ -777,7 +782,7 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, url, googleMapApiKey, meta?.cesiumIonAccessToken]); + }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, provider]); const tilesetUrl = useMemo((): string | Resource | Promise | null => { if (!isVisible) return null; From 353d25cb6054bbe9e6b63c48ae8d5bde2863de20 Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 11:35:21 +0800 Subject: [PATCH 11/19] feat: support custom provider for google realistic 3d tiles --- src/Map/types/customProvider.ts | 17 +++++++++++++---- src/engines/Cesium/Feature/Tileset/hooks.ts | 21 +++++++++++++-------- src/engines/Cesium/Feature/context.ts | 2 ++ src/engines/Cesium/hooks.ts | 7 ++++++- src/engines/Cesium/index.tsx | 1 + 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Map/types/customProvider.ts b/src/Map/types/customProvider.ts index 99ef335..d46c73e 100644 --- a/src/Map/types/customProvider.ts +++ b/src/Map/types/customProvider.ts @@ -6,16 +6,25 @@ * the engine (hardcoded in useTerrainProviderPromise) or via the layer's own url field. */ -export interface ImageryProviderEntry { +export type ImageryProviderEntry = { id: string; url: string; credit?: string; maximumLevel?: number; minimumLevel?: number; -} +}; -export interface CustomProviderConfig { +export type LayerProviderEntry = { + id: string; + url: string; + options?: Record; +}; + +export type CustomProviderConfig = { imagery?: { providers?: ImageryProviderEntry[]; }; -} + layers?: { + providers?: LayerProviderEntry[]; + }; +}; diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index 51cc38a..d72edb9 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -459,7 +459,7 @@ export const useHooks = ({ }) => { const { viewer } = useCesium(); const tilesetRef = useRef(undefined); - const { onLayerLoad, updateCredits } = useContext(); + const { onLayerLoad, updateCredits, customProvider } = useContext(); const layerIdRef = useRef(layer?.id); layerIdRef.current = layer?.id; @@ -759,15 +759,20 @@ export const useHooks = ({ console.log("provider", provider); + // For Re:Earth provider, use the custom URL from customProvider or layer data + if (provider === "reearth") { + // Try to get URL from customProvider first, then fall back to layer URL + const customUrl = customProvider?.layers?.providers?.find( + p => p.id === "reearth_google_photorealistic_3d_tiles", + )?.url; + + return customUrl || url || null; + } + // Otherwise load via Google API key or Cesium Ion. const loadTileset = async (): Promise => { try { - if (provider === "reearth") { - const resource = await IonResource.fromAssetId(2275207, { - accessToken: meta?.cesiumIonAccessToken as string | undefined, - }); - return resource; - } else if (provider === "cesium-ion") { + if (provider === "cesium-ion") { const resource = await IonResource.fromAssetId(2275207, { accessToken: meta?.cesiumIonAccessToken as string | undefined, }); @@ -782,7 +787,7 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, provider]); + }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, provider, customProvider, url]); const tilesetUrl = useMemo((): string | Resource | Promise | null => { if (!isVisible) return null; diff --git a/src/engines/Cesium/Feature/context.ts b/src/engines/Cesium/Feature/context.ts index 4568ed4..8bc2fb2 100644 --- a/src/engines/Cesium/Feature/context.ts +++ b/src/engines/Cesium/Feature/context.ts @@ -3,12 +3,14 @@ import { createContext, useContext as useReactContext } from "react"; import type { Camera, LayerSelectionReason } from "../.."; import { LayerEditEvent, LayerLoadEvent, LayerVisibilityEvent } from "../../../Map"; +import type { CustomProviderConfig } from "../../../Map/types/customProvider"; import { TimelineManagerRef } from "../../../Map/useTimelineManager"; import type { FlyTo } from "../../../types"; export type Context = { selectionReason?: LayerSelectionReason; timelineManagerRef?: TimelineManagerRef; + customProvider?: CustomProviderConfig; getCamera?: () => Camera | undefined; flyTo?: FlyTo; onLayerEdit?: (e: LayerEditEvent) => void; diff --git a/src/engines/Cesium/hooks.ts b/src/engines/Cesium/hooks.ts index 1b0dc62..1820148 100644 --- a/src/engines/Cesium/hooks.ts +++ b/src/engines/Cesium/hooks.ts @@ -27,6 +27,7 @@ import { e2eAccessToken, setE2ECesiumViewer } from "../../e2eConfig"; import { ComputedFeature, DataType, SelectedFeatureInfo, LatLng, Camera } from "../../mantle"; import { Credits, + CustomProviderConfig, LayerLoadEvent, LayerSelectWithRectEnd, LayerSelectWithRectMove, @@ -69,6 +70,7 @@ export default ({ selectedLayerId, selectionReason, meta, + customProvider, layersRef, featureFlags, timelineManagerRef, @@ -101,6 +103,7 @@ export default ({ layersRef?: RefObject; selectionReason?: LayerSelectionReason; meta?: Record; + customProvider?: CustomProviderConfig; featureFlags: number; timelineManagerRef?: TimelineManagerRef; isLayerDraggable?: boolean; @@ -673,6 +676,7 @@ export default ({ () => ({ selectionReason, timelineManagerRef, + customProvider, flyTo: engineAPI.flyTo, getCamera: engineAPI.getCamera, onLayerEdit, @@ -687,11 +691,12 @@ export default ({ }), [ selectionReason, + timelineManagerRef, + customProvider, engineAPI, onLayerEdit, onLayerVisibility, onLayerLoad, - timelineManagerRef, updateCredits, ], ); diff --git a/src/engines/Cesium/index.tsx b/src/engines/Cesium/index.tsx index 3ab3570..9d2e30d 100644 --- a/src/engines/Cesium/index.tsx +++ b/src/engines/Cesium/index.tsx @@ -97,6 +97,7 @@ const Cesium: React.ForwardRefRenderFunction = ( selectedLayerId, selectionReason: layerSelectionReason, meta, + customProvider, layersRef, featureFlags, timelineManagerRef, From 04b033cfc5d7e933d6acd8b1d0f41f474499380c Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 11:57:41 +0800 Subject: [PATCH 12/19] refactor: revert unnecessary changes --- src/Map/types/viewerProperty.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Map/types/viewerProperty.ts b/src/Map/types/viewerProperty.ts index fef3d4c..9a1bffd 100644 --- a/src/Map/types/viewerProperty.ts +++ b/src/Map/types/viewerProperty.ts @@ -210,7 +210,6 @@ export type IndicatorProperty = { export type AssetsProperty = { cesium?: AssetsCesiumProperty; - reearthTerrain?: AssetsReearthTerrainProperty; }; export type AssetsCesiumProperty = { @@ -221,13 +220,6 @@ export type AssetsCesiumProperty = { }; }; -export type AssetsReearthTerrainProperty = { - terrain?: { - /** Reearth terrain base URL — do NOT include '/layer.json', Cesium appends it automatically. e.g. 'https://terrain.reearth.land/' */ - url?: string; - }; -}; - export type DebugProperty = { showGlobeWireframe?: boolean; showFramesPerSecond?: boolean; From 2f6f6b068abef97a584d3b5d0098e4ad5e298ee2 Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 17:26:56 +0800 Subject: [PATCH 13/19] fix: tiles loader with index --- src/engines/Cesium/core/Imagery.tsx | 34 +++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index c3428c5..9d7ea1d 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -65,6 +65,31 @@ export default function ImageryLayers({ let cancelled = false; const addedLayers: CesiumImageryLayer[] = []; + // Track layers by their intended index to maintain order with async loading + const layersByIndex: (CesiumImageryLayer | null)[] = new Array(tiles?.length || 0).fill(null); + + const reorderLayers = () => { + if (cancelled || scene.isDestroyed()) return; + + // Move each layer to its correct position based on layersByIndex + layersByIndex.forEach((layer, targetIndex) => { + if (!layer) return; + + const currentIndex = imageryLayerCollection.indexOf(layer); + if (currentIndex === -1) return; // Layer not in collection + + // Calculate where this layer should be: count non-null layers before it + const desiredIndex = layersByIndex.slice(0, targetIndex).filter(l => l !== null).length; + + if (currentIndex !== desiredIndex) { + // Move layer to correct position + imageryLayerCollection.remove(layer, false); // Don't destroy + imageryLayerCollection.add(layer, desiredIndex); + } + }); + + scene.requestRender(); + }; tiles?.forEach(({ id, zoomLevel, opacity, heatmap }, i) => { const providerOrPromise = providers[id]?.[3]; @@ -81,9 +106,14 @@ export default function ImageryLayers({ magnificationFilter: heatmap ? TextureMagnificationFilter.LINEAR : undefined, minificationFilter: heatmap ? TextureMinificationFilter.NEAREST : undefined, }); - imageryLayerCollection.add(layer, i); + + // Always append to avoid index out of bounds + imageryLayerCollection.add(layer); + layersByIndex[i] = layer; addedLayers.push(layer); - scene.requestRender(); + + // Reorder all layers after each addition + reorderLayers(); }; if (providerOrPromise instanceof Promise) { From c5b999c3a4e0d07b1e24d875be7169ff76ca56d2 Mon Sep 17 00:00:00 2001 From: airslice Date: Fri, 22 May 2026 17:27:04 +0800 Subject: [PATCH 14/19] chore: clean up --- src/engines/Cesium/Feature/Tileset/hooks.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index d72edb9..7952a85 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -757,8 +757,6 @@ export const useHooks = ({ const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - console.log("provider", provider); - // For Re:Earth provider, use the custom URL from customProvider or layer data if (provider === "reearth") { // Try to get URL from customProvider first, then fall back to layer URL From d1a5871411ee99129ecdebeb13238c7ec1f1a23a Mon Sep 17 00:00:00 2001 From: airslice Date: Mon, 25 May 2026 10:02:11 +0800 Subject: [PATCH 15/19] refactor: simplify example --- .env.example | 22 ++++++----------- example/buildExampleMeta.ts | 47 ------------------------------------- example/hooks.ts | 15 +++++++++--- vite.config.example.ts | 1 + 4 files changed, 20 insertions(+), 65 deletions(-) delete mode 100644 example/buildExampleMeta.ts diff --git a/.env.example b/.env.example index e3fc344..4be227c 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,12 @@ -# Used by the google-photorealistic layer +# Used by the google-photorealistic layer as a fallback when no custom Google 3D Tiles URL is configured EXAMPLE_GOOGLE_MAP_API_KEY= # Cesium Ion access token — required to test cesium_ion_* imagery tiles and osm-buildings layer EXAMPLE_CESIUM_ION_ACCESS_TOKEN= -# Tile server auth token — appended as ?token= to imagery and layer provider URLs in code. -# Leave empty to use the URLs as-is (e.g. for public tile servers). -EXAMPLE_TILE_SERVER_TOKEN= - -# Imagery providers — JSON array of ImageryProviderEntry objects (URLs without token) -# Each entry: { "id": string, "url": string, "credit"?: string, "maximumLevel"?: number, "minimumLevel"?: number } -# The token from EXAMPLE_TILE_SERVER_TOKEN is appended automatically. -# Leave empty to use only the built-in preset tiles (OSM, Cesium Ion, etc.) -EXAMPLE_IMAGERY_PROVIDERS= - -# Layer (3D tileset) providers — JSON array of LayerProviderEntry objects (URLs without token) -# Each entry: { "id": string, "url": string } -# The token from EXAMPLE_TILE_SERVER_TOKEN is appended automatically. -EXAMPLE_LAYER_PROVIDERS= +# Custom provider configuration — JSON object matching CustomProviderConfig type +# Structure: { "imagery": { "providers": [...] } } +# Each imagery provider: { "id": string, "url": string, "credit"?: string, "maximumLevel"?: number, "minimumLevel"?: number } +# Note: Include authentication tokens directly in URLs if needed (e.g., ?token=xxx or ?key=yyy) +# Example: {"imagery":{"providers":[{"id":"my_satellite","url":"https://tiles.example.com/{z}/{x}/{y}.png?token=abc123","credit":"© Example","maximumLevel":19}]}} +EXAMPLE_CUSTOM_PROVIDER= diff --git a/example/buildExampleMeta.ts b/example/buildExampleMeta.ts deleted file mode 100644 index bc4c806..0000000 --- a/example/buildExampleMeta.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CustomProviderConfig, ImageryProviderEntry } from "@reearth/core"; - -function parseEnvJson(envVar: string | undefined): T[] { - if (!envVar) return []; - try { - const parsed = JSON.parse(envVar); - return Array.isArray(parsed) ? parsed : []; - } catch { - console.warn(`[buildExampleTileProvider] Failed to parse env JSON: ${envVar}`); - return []; - } -} - -function appendToken(url: string, token: string): string { - const sep = url.includes("?") ? "&" : "?"; - return `${url}${sep}token=${token}`; -} - -/** - * Build CustomProviderConfig from env vars. - * - * Env vars: - * EXAMPLE_TILE_SERVER_TOKEN — auth token appended as ?token= to imagery URLs - * EXAMPLE_IMAGERY_PROVIDERS — JSON array of ImageryProviderEntry (URLs without token) - * - * Terrain URL is hardcoded in core (useTerrainProviderPromise). - * Layer URLs are set directly on individual layer data.url fields. - * Returns undefined when no imagery providers are configured. - */ -export function buildExampleTileProvider(): CustomProviderConfig | undefined { - const token: string | undefined = import.meta.env.EXAMPLE_TILE_SERVER_TOKEN || undefined; - const rawImageryProviders = parseEnvJson( - import.meta.env.EXAMPLE_IMAGERY_PROVIDERS, - ); - - if (rawImageryProviders.length === 0) { - return undefined; - } - - const imageryProviders = token - ? rawImageryProviders.map(p => ({ ...p, url: appendToken(p.url, token) })) - : rawImageryProviders; - - return { - imagery: { providers: imageryProviders }, - }; -} diff --git a/example/hooks.ts b/example/hooks.ts index 6670fb6..3109a60 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -4,6 +4,7 @@ import { ComputedFeature, ComputedLayer, Credits, + CustomProviderConfig, LayerSelectionReason, LazyLayer, MapRef, @@ -12,7 +13,6 @@ import { ViewerProperty, } from "@reearth/core"; -import { buildExampleTileProvider } from "./buildExampleMeta"; import { DEFAULT_CAMERA, DEFAULT_LAYERS, TILES } from "./constants"; import { DEFAULT_VIEWER_PROPERTY } from "./scene"; import { TEST_LAYERS } from "./testLayers"; @@ -47,7 +47,16 @@ export default () => { setSelectedFeature(ref.current?.layers.selectedFeature() ?? feature); }, []); - const customProvider = useMemo(() => buildExampleTileProvider(), []); + const customProvider = useMemo(() => { + const envValue = import.meta.env.EXAMPLE_CUSTOM_PROVIDER; + if (!envValue) return undefined; + try { + return JSON.parse(envValue) as CustomProviderConfig; + } catch (error) { + console.warn("[example] Failed to parse EXAMPLE_CUSTOM_PROVIDER:", error); + return undefined; + } + }, []); const meta = useMemo( () => ({ @@ -56,7 +65,7 @@ export default () => { [], ); - const customTileIds = customProvider?.imagery?.providers?.map(p => p.id) ?? []; + const customTileIds = customProvider?.imagery?.providers?.map((p: { id: string }) => p.id) ?? []; const allTileIds = [...TILES, ...customTileIds]; const [currentTile, setCurrentTile] = useState( diff --git a/vite.config.example.ts b/vite.config.example.ts index d61359f..3e2a8ac 100644 --- a/vite.config.example.ts +++ b/vite.config.example.ts @@ -24,6 +24,7 @@ export default defineConfig({ }, envPrefix: "EXAMPLE_", server: { + port: 3000, proxy: { // Proxy terrain requests to avoid CORS in dev — terrain.reearth.land // does not send Access-Control-Allow-Origin headers. From 56df131047ac34cce81e32cedac14432f9ff596c Mon Sep 17 00:00:00 2001 From: "Tongci.Z" <116621270+ZTongci@users.noreply.github.com> Date: Mon, 25 May 2026 12:40:23 +0900 Subject: [PATCH 16/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- example/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/constants.ts b/example/constants.ts index 072c3dc..3c75d84 100644 --- a/example/constants.ts +++ b/example/constants.ts @@ -13,7 +13,7 @@ export const DEFAULT_CAMERA = { }; // Static preset tile types — always available regardless of CustomProviderConfig. -// Dynamic provider IDs (from EXAMPLE_IMAGERY_PROVIDERS) are appended at runtime in hooks.ts. +// Dynamic provider IDs (from EXAMPLE_CUSTOM_PROVIDER) are appended at runtime in hooks.ts. export const TILES = [ // Public (always available) "open_street_map", From 528a706e1002a5d2ecf64ecaca99dd4c7bf8b0a8 Mon Sep 17 00:00:00 2001 From: "Tongci.Z" <116621270+ZTongci@users.noreply.github.com> Date: Mon, 25 May 2026 12:40:40 +0900 Subject: [PATCH 17/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/engines/Cesium/core/Imagery.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index 9d7ea1d..75149b4 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -140,7 +140,12 @@ export default function ImageryLayers({ } type Providers = { - [id: string]: [string | undefined, string | undefined, number | undefined, ImageryProvider]; + [id: string]: [ + string | undefined, + string | undefined, + number | undefined, + Promise | ImageryProvider, + ]; }; export function useImageryProviders({ From 0e08ad2e38e402fdbf82d21afdbdb202851a6d1e Mon Sep 17 00:00:00 2001 From: "Tongci.Z" <116621270+ZTongci@users.noreply.github.com> Date: Mon, 25 May 2026 12:41:50 +0900 Subject: [PATCH 18/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Map/types/customProvider.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Map/types/customProvider.ts b/src/Map/types/customProvider.ts index d46c73e..4784452 100644 --- a/src/Map/types/customProvider.ts +++ b/src/Map/types/customProvider.ts @@ -1,9 +1,11 @@ /** - * Generic provider configuration — imagery overrides only. + * Generic provider configuration for overriding external data sources. * - * Allows configuring custom imagery tile providers in a vendor-agnostic way. - * Terrain and layer (3D tileset) URLs are configured directly on the engine - * the engine (hardcoded in useTerrainProviderPromise) or via the layer's own url field. + * - imagery.providers: imagery tile providers keyed by id + * - layers.providers: 3D tileset providers keyed by id + * + * Terrain is configured via viewer/engine terrain settings (see useTerrainProviderPromise) + * or via each layer's own `url` field. */ export type ImageryProviderEntry = { From 785e1f330eb8927aed9cc4446e58edc03f0d883f Mon Sep 17 00:00:00 2001 From: tongcizhang Date: Mon, 25 May 2026 12:50:22 +0900 Subject: [PATCH 19/19] refactor: refactor by copilot suggestion --- src/engines/Cesium/Feature/Tileset/hooks.ts | 2 +- src/engines/Cesium/core/Imagery.tsx | 15 ++++++++----- .../Cesium/core/customProviderResolver.ts | 22 ------------------- 3 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 src/engines/Cesium/core/customProviderResolver.ts diff --git a/src/engines/Cesium/Feature/Tileset/hooks.ts b/src/engines/Cesium/Feature/Tileset/hooks.ts index 7952a85..30d1eba 100644 --- a/src/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/engines/Cesium/Feature/Tileset/hooks.ts @@ -770,7 +770,7 @@ export const useHooks = ({ // Otherwise load via Google API key or Cesium Ion. const loadTileset = async (): Promise => { try { - if (provider === "cesium-ion") { + if (provider === "cesium-ion" || !googleMapApiKey) { const resource = await IonResource.fromAssetId(2275207, { accessToken: meta?.cesiumIonAccessToken as string | undefined, }); diff --git a/src/engines/Cesium/core/Imagery.tsx b/src/engines/Cesium/core/Imagery.tsx index 75149b4..b306ca8 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -96,7 +96,7 @@ export default function ImageryLayers({ if (!providerOrPromise) return; const doAdd = (provider: ImageryProvider) => { - if (cancelled || scene.isDestroyed()) return; + if (!provider || cancelled || scene.isDestroyed()) return; const layer = new CesiumImageryLayer(provider, { minimumTerrainLevel: zoomLevel?.[0], maximumTerrainLevel: zoomLevel?.[1], @@ -117,9 +117,9 @@ export default function ImageryLayers({ }; if (providerOrPromise instanceof Promise) { - providerOrPromise.then(doAdd); + providerOrPromise.then(doAdd).catch(err => console.error("Failed to load imagery provider:", err)); } else { - doAdd(providerOrPromise as ImageryProvider); + doAdd(providerOrPromise); } }); @@ -173,7 +173,7 @@ export function useImageryProviders({ cesiumIonAccessToken: ciat, cesiumIonAssetId: t.cesiumIonAssetId, heatmap: t.heatmap, - zoomLevel: t.zoomLevelForURL, + tile_zoomLevel: t.zoomLevelForURL, }; if (isValidPresetTileType(t.type)) { return presets[t.type](opts); @@ -279,7 +279,12 @@ export function useImageryProviders({ e, ): e is [ string, - [string | undefined, string | undefined, number | undefined, ImageryProvider], + [ + string | undefined, + string | undefined, + number | undefined, + Promise | ImageryProvider, + ], ] => !!e?.[1][3], ), ); diff --git a/src/engines/Cesium/core/customProviderResolver.ts b/src/engines/Cesium/core/customProviderResolver.ts deleted file mode 100644 index 756f826..0000000 --- a/src/engines/Cesium/core/customProviderResolver.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { CustomProviderConfig } from "../../../Map/types/customProvider"; - -export function resolveImageryUrl( - config: CustomProviderConfig | undefined, - presetName: string, -): string | null { - return config?.imagery?.providers?.find(p => p.id === presetName)?.url ?? null; -} - -export function resolveImageryCredit( - config: CustomProviderConfig | undefined, - presetName: string, -): string | undefined { - return config?.imagery?.providers?.find(p => p.id === presetName)?.credit; -} - -export function resolveImageryMaxZoom( - config: CustomProviderConfig | undefined, - presetName: string, -): number | undefined { - return config?.imagery?.providers?.find(p => p.id === presetName)?.maximumLevel; -}