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/, ""),
+ },
+ },
+ },
});