diff --git a/.env.example b/.env.example index c4643c0..4be227c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,12 @@ -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 +# 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= + +# 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/App.tsx b/example/App.tsx index 0aad9e1..1595aae 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -14,8 +14,12 @@ function App() { handleAPIReady, handleSelect, meta, + customProvider, + customTileIds, currentTile, setCurrentTile, + cesiumIonAssetId, + setCesiumIonAssetId, currentCamera, setCurrentCamera, terrainEnabled, @@ -46,8 +50,11 @@ function App() { return (
void; + cesiumIonAssetId?: number; + setCesiumIonAssetId: (v: number | undefined) => void; terrainEnabled: boolean; setTerrainEnabled: (v: boolean) => void; hideUnderground: boolean; @@ -46,8 +49,11 @@ type OptionsPanelProps = { }; const OptionsPanel: FC = ({ + customTileIds, currentTile, setCurrentTile, + cesiumIonAssetId, + setCesiumIonAssetId, terrainEnabled, setTerrainEnabled, hideUnderground, @@ -92,6 +98,11 @@ const OptionsPanel: FC = ({ + {customTileIds?.map(tile => ( + + {tile} + + ))} {TILES.map(tile => ( {tile} @@ -99,6 +110,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 36ae0ee..3c75d84 100644 --- a/example/constants.ts +++ b/example/constants.ts @@ -12,15 +12,21 @@ export const DEFAULT_CAMERA = { roll: 6.2830631767616465, }; +// Static preset tile types — always available regardless of CustomProviderConfig. +// Dynamic provider IDs (from EXAMPLE_CUSTOM_PROVIDER) are appended at runtime in hooks.ts. export const TILES = [ - "default", - "default_label", - "default_road", + // Public (always available) "open_street_map", - "esri_world_topo", - "black_marble", "japan_gsi_standard", - "test_unexpected_type", + "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) + "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 1650ab1..3109a60 100644 --- a/example/hooks.ts +++ b/example/hooks.ts @@ -4,6 +4,7 @@ import { ComputedFeature, ComputedLayer, Credits, + CustomProviderConfig, LayerSelectionReason, LazyLayer, MapRef, @@ -46,6 +47,17 @@ export default () => { setSelectedFeature(ref.current?.layers.selectedFeature() ?? feature); }, []); + 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( () => ({ cesiumIonAccessToken: import.meta.env.EXAMPLE_CESIUM_ION_ACCESS_TOKEN || undefined, @@ -53,11 +65,15 @@ export default () => { [], ); + const customTileIds = customProvider?.imagery?.providers?.map((p: { id: string }) => 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, ); + const [cesiumIonAssetId, setCesiumIonAssetId] = useState(undefined); const [currentCamera, setCurrentCamera] = useState(DEFAULT_CAMERA); const [terrainEnabled, setTerrainEnabled] = useState(true); const [hideUnderground, setHideUnderground] = useState(false); @@ -71,6 +87,7 @@ export default () => { { id: "default", type: currentTile, + cesiumIonAssetId: currentTile === "cesium_ion" ? cesiumIonAssetId : undefined, opacity: 1, }, ] @@ -84,7 +101,7 @@ export default () => { depthTestAgainstTerrain: hideUnderground, }, }), - [currentTile, terrainEnabled, hideUnderground], + [currentTile, cesiumIonAssetId, terrainEnabled, hideUnderground], ); const layers = useMemo( @@ -118,7 +135,10 @@ export default () => { !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(() => { @@ -165,8 +185,12 @@ export default () => { handleAPIReady, handleSelect, meta, + customProvider, + customTileIds, currentTile, setCurrentTile, + cesiumIonAssetId, + setCesiumIonAssetId, currentCamera, setCurrentCamera, terrainEnabled, 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: { 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/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/Map/types/customProvider.ts b/src/Map/types/customProvider.ts new file mode 100644 index 0000000..4784452 --- /dev/null +++ b/src/Map/types/customProvider.ts @@ -0,0 +1,32 @@ +/** + * Generic provider configuration for overriding external data sources. + * + * - 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 = { + id: string; + url: string; + credit?: string; + maximumLevel?: number; + minimumLevel?: number; +}; + +export type LayerProviderEntry = { + id: string; + url: string; + options?: Record; +}; + +export type CustomProviderConfig = { + imagery?: { + providers?: ImageryProviderEntry[]; + }; + layers?: { + providers?: LayerProviderEntry[]; + }; +}; diff --git a/src/Map/types/index.ts b/src/Map/types/index.ts index 4c453d9..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,6 +80,7 @@ export type { } from "../../mantle"; export * from "./event"; export * from "./viewerProperty"; +export * from "./customProvider"; export type EngineRef = { name: string; @@ -237,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/viewerProperty.ts b/src/Map/types/viewerProperty.ts index 0b1b62e..9a1bffd 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" | "cesiumion" | "reearth_terrain"; url?: string; normal?: boolean; elevationHeatMap?: ElevationHeatMapProperty; @@ -130,6 +130,7 @@ export type TileProperty = { id: string; type?: string; url?: string; + cesiumIonAssetId?: number; opacity?: number; zoomLevel?: number[]; zoomLevelForURL?: number[]; 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 80228ce..30d1eba 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, @@ -75,6 +76,7 @@ const useData = (layer: ComputedLayer | undefined) => { : data?.layers : undefined, googleMapApiKey: data?.serviceTokens?.googleMapApiKey, + provider: data?.provider, }; }, [layer]); }; @@ -457,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; @@ -487,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); @@ -752,20 +754,30 @@ export const useHooks = ({ } }, [style, isTilesetReady]); - const googleMapPhotorealisticResource = useMemo(() => { + const googleMapPhotorealisticResource = useMemo((): string | Promise | null => { if (type !== "google-photorealistic" || !isVisible) return null; - const loadTileset = async () => { + // 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 (googleMapApiKey) { - const tileset = await createGooglePhotorealistic3DTileset({ key: googleMapApiKey }); - return tileset.resource; - } else { - const resource = IonResource.fromAssetId(2275207, { + if (provider === "cesium-ion" || !googleMapApiKey) { + 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; @@ -773,19 +785,35 @@ export const useHooks = ({ }; return loadTileset(); - }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken]); - - 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]); + }, [type, isVisible, googleMapApiKey, meta?.cesiumIonAccessToken, provider, customProvider, url]); + + const tilesetUrl = useMemo((): string | Resource | Promise | null => { + if (!isVisible) return null; + + // Google Photorealistic 3D Tiles + if (googleMapPhotorealisticResource) { + return googleMapPhotorealisticResource; + } + + // Re:Earth Buildings — use layer's own url if provided, otherwise fall back to public service + if (type === "reearth-buildings") { + return url ?? "https://buildings.reearth.land/tileset.json"; + } + + // OSM Buildings — only available via Cesium Ion. + 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/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/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/Globe/index.tsx b/src/engines/Cesium/core/Globe/index.tsx index 667d545..a445e76 100644 --- a/src/engines/Cesium/core/Globe/index.tsx +++ b/src/engines/Cesium/core/Globe/index.tsx @@ -39,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 002f142..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, @@ -12,6 +11,8 @@ import { AssetsCesiumProperty } from "../../../../Map"; type TerrainType = NonNullable; +const REEARTH_TERRAIN_URL = "https://terrain.reearth.land/cesium-mesh/ellipsoid"; + type ProviderOpts = Pick & AssetsCesiumProperty["terrain"] & { terrain?: boolean; @@ -20,13 +21,11 @@ type ProviderOpts = Pick & }; 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(); return Promise.resolve(ellipsoidRef.current); } @@ -43,31 +42,30 @@ 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 ionToken = opts.ionAccessToken ?? ""; const normal = String(!!opts.normal); - return `${type}|asset:${asset}|url:${url}|normal:${normal}`; + return `${type}|asset:${asset}|url:${url}|ion:${ionToken}|normal:${normal}`; } function createProvider(type: TerrainType, opts: ProviderOpts): Promise { switch (type) { - case "cesium": + case "reearth_terrain": + return CesiumTerrainProvider.fromUrl(REEARTH_TERRAIN_URL, { + requestVertexNormals: !!opts.normal, + requestWaterMask: false, + }) as Promise; + + case "cesium": { 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) { - // Fallback to ellipsoid when misconfigured - return Promise.resolve(new EllipsoidTerrainProvider()); - } + case "cesiumion": { + if (!opts.ionAsset && !opts.ionUrl) return Promise.resolve(new EllipsoidTerrainProvider()); return CesiumTerrainProvider.fromUrl( opts.ionUrl ?? IonResource.fromAssetId(parseInt(String(opts.ionAsset), 10), { @@ -75,5 +73,9 @@ function createProvider(type: TerrainType, opts: ProviderOpts): Promise; + } + + default: + 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..943631a 100644 --- a/src/engines/Cesium/core/Imagery.test.ts +++ b/src/engines/Cesium/core/Imagery.test.ts @@ -1,119 +1,154 @@ 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", () => { 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 osmProvider = vi.fn((): any => ({ osm: true })); + + const presets = { + cesium_ion_default: provider, + 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: "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": ["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: "default" }] }); + typedRerender({ tiles: [{ id: "1", type: "cesium_ion_default" }] }); - expect(result.current.providers).toEqual({ "1": ["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: "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["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({ 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, 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 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", 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); - // 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", 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); - // update a tile type + // unknown type without customProvider: falls back directly to open_street_map typedRerender({ - tiles: [{ id: "1", type: "default_label", url: "u" }], - cesiumIonAccessToken: "a", + tiles: [{ id: "1", type: "unexpected_type", url: "u" }], }); - expect(result.current.providers).toEqual({ - "1": ["default_label", "u", { hoge2: "u" }], - }); + 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); - expect(provider2).toBeCalledTimes(1); + expect(osmProvider).toBeCalledTimes(1); - // update a tile type to unexpected type + // unknown type with a matching customProvider entry: uses UrlTemplateImageryProvider typedRerender({ - tiles: [{ id: "1", type: "unexpected_type", url: "u" }], + 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", + }, + ], + }, + }, }); - expect(result.current.providers).toEqual({ - // unexpected type is treated as "default" - "1": ["unexpected_type", "u", { hoge: "u" }], - }); - expect(result.current.updated).toBe(true); - expect(provider).toBeCalledTimes(5); - expect(provider2).toBeCalledTimes(1); + 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 bdf5acc..b306ca8 100644 --- a/src/engines/Cesium/core/Imagery.tsx +++ b/src/engines/Cesium/core/Imagery.tsx @@ -1,12 +1,16 @@ import { Color, + ImageryLayer as CesiumImageryLayer, ImageryProvider, TextureMagnificationFilter, TextureMinificationFilter, + UrlTemplateImageryProvider, } 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 { CustomProviderConfig } from "../../../Map/types/customProvider"; import { isValidPresetTileType, PresetTileType, tiles as tilePresets } from "./presets"; @@ -22,6 +26,7 @@ export type Tile = { id: string; url?: string; type?: string; + cesiumIonAssetId?: number; opacity?: number; zoomLevel?: number[]; zoomLevelForURL?: number[]; @@ -31,64 +36,127 @@ export type Tile = { export type Props = { tiles?: Tile[]; cesiumIonAccessToken?: string; + customProvider?: CustomProviderConfig; 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, + customProvider, + onTilesChange, +}: Props) { + const { imageryLayerCollection, scene } = useCesium(); + + const { providers } = useImageryProviders({ tiles, cesiumIonAccessToken, + customProvider, 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[] = []; + // 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]; + if (!providerOrPromise) return; + + const doAdd = (provider: ImageryProvider) => { + if (!provider || 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, + }); + + // Always append to avoid index out of bounds + imageryLayerCollection.add(layer); + layersByIndex[i] = layer; + addedLayers.push(layer); + + // Reorder all layers after each addition + reorderLayers(); + }; + + if (providerOrPromise instanceof Promise) { + providerOrPromise.then(doAdd).catch(err => console.error("Failed to load imagery provider:", err)); + } else { + doAdd(providerOrPromise); + } + }); + + 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] }; +type Providers = { + [id: string]: [ + string | undefined, + string | undefined, + number | undefined, + Promise | ImageryProvider, + ]; +}; export function useImageryProviders({ tiles = [], cesiumIonAccessToken, + customProvider, presets, }: { tiles?: Tile[]; cesiumIonAccessToken?: string; + customProvider?: CustomProviderConfig; presets: { [K in PresetTileType]: (opts?: { url?: string; @@ -99,17 +167,34 @@ export function useImageryProviders({ }; }): { providers: Providers; updated: boolean } { const newTile = useCallback( - (t: Tile, ciat?: string) => - presets[isValidPresetTileType(t.type) ? t.type : "default"]({ + (t: Tile, ciat?: string, tp?: CustomProviderConfig) => { + const opts = { url: t.url, cesiumIonAccessToken: ciat, + cesiumIonAssetId: t.cesiumIonAssetId, heatmap: t.heatmap, - zoomLevel: t.zoomLevelForURL, - }), + tile_zoomLevel: t.zoomLevelForURL, + }; + if (isValidPresetTileType(t.type)) { + return presets[t.type](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 prevCustomProvider = useRef(customProvider); const tileKeys = tiles.map(t => t.id).join(","); const prevTileKeys = useRef(tileKeys); const prevProviders = useRef({}); @@ -123,9 +208,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 = prevCustomProvider.current !== customProvider; const prevProvidersKeys = Object.keys(prevProviders.current); const added = tiles.map(t => t.id).filter(t => t && !prevProvidersKeys.includes(t)); @@ -137,7 +223,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), })); @@ -149,6 +236,7 @@ export function useImageryProviders({ added, prevType, prevUrl, + prevIonAssetId, prevProvider, tile, }): @@ -157,6 +245,7 @@ export function useImageryProviders({ [ string | undefined, string | undefined, + number | undefined, Promise | ImageryProvider | null | undefined, ], ] @@ -168,30 +257,59 @@ export function useImageryProviders({ added || prevType !== tile.type || prevUrl !== tile.url || - (isCesiumAccessTokenUpdated && (!tile.type || tile.type === "default")) - ? [tile.type, tile.url, newTile(tile, cesiumIonAccessToken)] - : [prevType, prevUrl, prevProvider], + prevIonAssetId !== tile.cesiumIonAssetId || + isTileProviderUpdated || + (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.cesiumIonAssetId, + newTile(tile, cesiumIonAccessToken, customProvider), + ] + : [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, + Promise | ImageryProvider, + ], + ] => !!e?.[1][3], ), ); 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)); + rawProviders.some( + p => + p.tile && + (p.prevType !== p.tile.type || + p.prevUrl !== p.tile.url || + p.prevIonAssetId !== p.tile.cesiumIonAssetId), + ); prevTileKeys.current = tileKeys; prevZoomLevels.current = zoomLevels; prevCesiumIonAccessToken.current = cesiumIonAccessToken; + prevCustomProvider.current = customProvider; return { providers, updated }; - }, [cesiumIonAccessToken, tiles, tileKeys, newTile, zoomLevels]); + }, [cesiumIonAccessToken, customProvider, 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..f6a9c52 100644 --- a/src/engines/Cesium/core/presets.ts +++ b/src/engines/Cesium/core/presets.ts @@ -11,13 +11,27 @@ import { import { JapanGSIOptimalBVmapLabelImageryProvider } from "./labels/JapanGSIOptimalBVmapVectorMapLabel/JapanGSIOptimalBVmapLabelImageryProvider"; const PRESET_TILE_TYPES = [ - "default", - "default_label", - "default_road", + // Dynamic Cesium Ion asset — asset ID supplied per-tile via cesiumIonAssetId field + "cesium_ion", + + // Public presets — always available, no auth required "open_street_map", - "black_marble", "japan_gsi_standard", + "stamen_watercolor", + "carto_light", "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", + + // 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]; @@ -26,35 +40,61 @@ export const isValidPresetTileType = (type: string | undefined): type is PresetT return PRESET_TILE_TYPES.includes(type as PresetTileType); }; +export type TileOptions = { + url?: string; + cesiumIonAccessToken?: string; + cesiumIonAssetId?: number | string; + heatmap?: boolean; + tile_zoomLevel?: number[]; +}; + export const tiles = { - default: ({ cesiumIonAccessToken } = {}) => - IonImageryProvider.fromAssetId(IonWorldImageryStyle.AERIAL, { - accessToken: cesiumIonAccessToken, - }).catch(console.error), - 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), + // --- Dynamic Cesium Ion asset --- + // Uses per-tile cesiumIonAssetId; requires cesiumIonAccessToken. + cesium_ion: (opts?: TileOptions) => { + 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); + return undefined as unknown as ImageryProvider; + }); + }, + + // --- Public presets --- open_street_map: () => new OpenStreetMapImageryProvider({ url: "https://tile.openstreetmap.org", credit: '© OpenStreetMap contributors', }), - black_marble: ({ cesiumIonAccessToken } = {}) => - IonImageryProvider.fromAssetId(3812, { - accessToken: cesiumIonAccessToken, - }).catch(console.error), + 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 } = {}) => + + 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({ url, @@ -63,15 +103,53 @@ export const tiles = { maximumLevel: tile_zoomLevel?.[1], }) : null, -} as { - [K in PresetTileType]: (opts?: { - url?: string; - cesiumIonAccessToken?: string; - heatmap?: boolean; - tile_zoomLevel?: number[]; - }) => Promise | ImageryProvider | 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; + }), + // --- Legacy aliases --- +} 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/hooks.ts b/src/engines/Cesium/hooks.ts index e0d015c..1820148 100644 --- a/src/engines/Cesium/hooks.ts +++ b/src/engines/Cesium/hooks.ts @@ -1,7 +1,6 @@ import { Entity, Cesium3DTileFeature, - Ion, Cesium3DTileset, JulianDate, Cesium3DTilePointFeature, @@ -28,6 +27,7 @@ import { e2eAccessToken, setE2ECesiumViewer } from "../../e2eConfig"; import { ComputedFeature, DataType, SelectedFeatureInfo, LatLng, Camera } from "../../mantle"; import { Credits, + CustomProviderConfig, LayerLoadEvent, LayerSelectWithRectEnd, LayerSelectWithRectMove, @@ -70,6 +70,7 @@ export default ({ selectedLayerId, selectionReason, meta, + customProvider, layersRef, featureFlags, timelineManagerRef, @@ -102,6 +103,7 @@ export default ({ layersRef?: RefObject; selectionReason?: LayerSelectionReason; meta?: Record; + customProvider?: CustomProviderConfig; featureFlags: number; timelineManagerRef?: TimelineManagerRef; isLayerDraggable?: boolean; @@ -137,7 +139,7 @@ export default ({ const cesiumIonAccessToken = typeof meta?.cesiumIonAccessToken === "string" && meta.cesiumIonAccessToken ? meta.cesiumIonAccessToken - : Ion.defaultAccessToken; + : undefined; // expose ref const engineAPI = useEngineRef(ref, cesium); @@ -674,6 +676,7 @@ export default ({ () => ({ selectionReason, timelineManagerRef, + customProvider, flyTo: engineAPI.flyTo, getCamera: engineAPI.getCamera, onLayerEdit, @@ -688,11 +691,12 @@ export default ({ }), [ selectionReason, + timelineManagerRef, + customProvider, engineAPI, onLayerEdit, onLayerVisibility, onLayerLoad, - timelineManagerRef, updateCredits, ], ); @@ -730,7 +734,13 @@ export default ({ useLayerDragDrop({ cesium, onLayerDrag, onLayerDrop, isLayerDraggable }); - useExplicitRender({ cesium, requestingRenderMode, isLayerDragging, shouldRender, property }); + useExplicitRender({ + cesium, + requestingRenderMode, + isLayerDragging, + shouldRender, + property, + }); const { cameraViewBoundaries, 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 8228650..9d2e30d 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, @@ -96,6 +97,7 @@ const Cesium: React.ForwardRefRenderFunction = ( selectedLayerId, selectionReason: layerSelectionReason, meta, + customProvider, layersRef, featureFlags, timelineManagerRef, @@ -167,6 +169,7 @@ const Cesium: React.ForwardRefRenderFunction = ( @@ -293,5 +296,14 @@ 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", + "reearth-buildings", + "google-photorealistic", + "kml", + ], }; diff --git a/src/mantle/types/index.ts b/src/mantle/types/index.ts index 013ad70..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; }; @@ -107,6 +108,7 @@ export type DataType = | "geojson" | "3dtiles" | "osm-buildings" + | "reearth-buildings" | "google-photorealistic" | "czml" | "csv" diff --git a/vite.config.example.ts b/vite.config.example.ts index fd226a9..3e2a8ac 100644 --- a/vite.config.example.ts +++ b/vite.config.example.ts @@ -23,4 +23,16 @@ 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. + "/terrain-proxy": { + target: "https://terrain.reearth.land", + changeOrigin: true, + rewrite: path => path.replace(/^\/terrain-proxy/, ""), + }, + }, + }, });