Hi everyone,
I’m working on a CesiumJS + React-based application where I load multiple KMLs using a custom hook (usePlotAssets). These KMLs include a variety of geometries like
- Centerlines (solid and dashed polylines)
- RoW boundaries (polygons with solid or dashed outlines)
- Station markers
These are loaded from Cesium Ion and are rendered alongside a 3D Tileset (e.g., SLAM or photogrammetry data). Terrain is disabled—the goal is to clamp everything directly onto the 3D Tileset.
Problem
KMLs do clamp to the 3D Tileset when using Cesium.HeightReference.CLAMP_TO_3D_TILE or ClassificationPrimitive, but the issue is:
The original KML geometries still remain visible — often floating above or rendering through the 3D tileset — causing double rendering or z-fighting.
Even when clamping works for the new primitives, the original Entity-based KML representations are still being rendered in the scene.
What I’ve Tried
-
Used
viewer.entities.add()with:clampToGround: true, heightReference: Cesium.HeightReference.CLAMP_TO_3D_TILE, classificationType: Cesium.ClassificationType.CESIUM_3D_TILE -
Also tried replacing Entity rendering with
ClassificationPrimitive:scene.primitives.add(new Cesium.ClassificationPrimitive({ ... })) -
Confirmed that primitives clamp correctly.
-
Tried removing original
entityafter creating primitive — not always effective since Ion KML loading still auto-adds entities.
Goal
- Ensure that only the ClassificationPrimitive version is rendered.
- Prevent or remove the original entity-based geometries loaded via KML that float or pierce through the 3D tiles.
- Clamp dashed lines, solid lines, and polygon outlines correctly.
- Manage cleanup and re-rendering cleanly in a React setup.
Questions
- How can I prevent or hide the original KML entities that are auto-rendered by Cesium Ion after loading?
- Is there a best practice to replace KML entities with primitives while preserving styles (like dashed lines)?
- Are there known techniques to fully clamp dashed polylines and polygons to 3D Tilesets using primitives?
- Is this a limitation of the
KmlDataSourceand should I parse and plot geometries manually instead?
Tech Stack
- CesiumJS v1.116
- React + Redux
- Cesium Ion for KML loading
- No terrain — only 3D Tileset
scene.primitives.add(...)for manual plots
Code Snippet (Custom Hook)
Here’s the relevant code from my usePlotAssets hook. I’m using KmlDataSource to load the KML and then trying to reprocess or overwrite entities:
Click to expand full code
export const usePlotAssets = (viewerRef) => {
const { kmlLayers, visibleKmlLayers, KMLHeightRefrence: Cesium.HeightReference.CLAMP_TO_3D_TILE } = useSelector(
(state) => state.gisHome3d
);
const viewer = viewerRef.current;
const dispatch = useDispatch();
const heightRefCache = useRef(KMLHeightRefrence);
useEffect(() => {
heightRefCache.current = KMLHeightRefrence;
}, [KMLHeightRefrence]);
const kmlDataSourcesRef = useRef(new Map());
const manualEntitiesRef = useRef(new Map());
const loadingKmlsRef = useRef(new Set());
const plotCenterline = useCallback(
async (
viewer,
assetId,
color,
heightOffset = 0,
isDashed = false,
heightReference
) => {
if (loadingKmlsRef.current.has(assetId)) return;
loadingKmlsRef.current.add(assetId);
dispatch(gisHome3dActions.setLoadingState({
assetId: 'cesiumKmlAssets',
type: 'plot',
value: true,
}));
try {
const resource = await Cesium.IonResource.fromAssetId(assetId);
const dataSource = await Cesium.KmlDataSource.load(resource, {
camera: viewer?.scene.camera,
canvas: viewer?.scene.canvas,
clampToGround: true,
});
const addedEntities = [];
const addEntity = (e) => {
const added = viewer?.entities.add(e);
if (added) addedEntities.push(added);
};
dataSource.entities.values.forEach((entity) => {
if (entity.polyline) {
const originalPositions = entity.polyline.positions.getValue();
const adjustedPositions = originalPositions.map((position) => {
const carto = Cesium.Cartographic.fromCartesian(position);
return Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
carto.height + heightOffset
);
});
const material = isDashed
? new Cesium.PolylineDashMaterialProperty({
color,
dashLength: 16,
gapColor: Cesium.Color.TRANSPARENT,
})
: color;
addEntity({
polyline: {
positions: adjustedPositions,
material,
classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
width: 2,
show: true,
clampToGround:
heightReference === Cesium.HeightReference.CLAMP_TO_3D_TILE,
heightReference: heightReference,
},
});
} else if (entity.polygon) {
entity.polygon.outline = true;
entity.polygon.outlineColor = color;
entity.polygon.height = 0.5;
entity.polygon.closeBottom = true;
entity.polygon.heightReference = heightReference;
entity.polygon.clampToGround =
heightReference === Cesium.HeightReference.CLAMP_TO_3D_TILE;
addEntity(entity);
const hierarchy = Cesium.Property.getValueOrDefault(
entity.polygon.hierarchy,
Cesium.JulianDate.now(),
{}
);
if (hierarchy?.positions) {
addEntity({
polyline: {
positions: hierarchy.positions,
clampToGround: true,
heightReference: heightReference,
width: 2,
material: isDashed
? new Cesium.PolylineDashMaterialProperty({
color,
dashLength: 14,
gapColor: Cesium.Color.TRANSPARENT,
dashPattern: 255.0,
})
: color,
},
});
}
} else {
// Add other types (points, billboards, labels, etc.)
addEntity(entity);
}
});
await viewer.dataSources.add(dataSource);
kmlDataSourcesRef.current.set(assetId, dataSource);
manualEntitiesRef.current.set(assetId, addedEntities);
} catch (err) {
console.error(`❌ Error plotting KML ${assetId}:`, err);
} finally {
loadingKmlsRef.current.delete(assetId);
dispatch(gisHome3dActions.setLoadingState({
assetId: 'cesiumKmlAssets',
type: 'plot',
value: false,
}));
}
},
[dispatch]
);
const flattenVisibleLayers = useCallback((layersGroup) => {
const allLayers = [];
layersGroup.forEach((group) => {
group.Layers.forEach((layer) => {
if (visibleKmlLayers[layer.id]?.visible) allLayers.push(layer);
});
group.districts?.forEach((district) => {
district.Layers.forEach((layer) => {
if (visibleKmlLayers[layer.id]?.visible) allLayers.push(layer);
});
});
});
return allLayers;
}, [visibleKmlLayers]);
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
useEffect(() => {
const viewer = viewerRef.current;
if (!viewer) return;
Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_TOEKN;
Cesium.Ion.defaultServer = 'https://api.ion.cesium.com';
const layersToPlot = flattenVisibleLayers(kmlLayers);
// Remove non-visible KMLs
kmlDataSourcesRef.current.forEach((dataSource, assetId) => {
if (!layersToPlot.find((l) => l.id === assetId)) {
dispatch(gisHome3dActions.setLoadingState({
assetId: 'cesiumKmlAssetsRemove',
type: 'remove',
value: true,
}));
const entities = manualEntitiesRef.current.get(assetId);
if (entities) {
entities.forEach((ent) => viewer.entities.remove(ent));
manualEntitiesRef.current.delete(assetId);
}
viewer.dataSources.remove(dataSource, true);
kmlDataSourcesRef.current.delete(assetId);
delay(300);
dispatch(gisHome3dActions.setLoadingState({
assetId: 'cesiumKmlAssetsRemove',
type: 'remove',
value: false,
}));
}
});
// Add new visible KMLs
layersToPlot.forEach((layer) => {
if (!kmlDataSourcesRef.current.has(layer.id)) {
const alpha = visibleKmlLayers[layer.id]?.opacity ?? 1;
const color = getColorForLayerCesium(layer.name, alpha);
const dashed = isDashedLine(layer.name);
plotCenterline(
viewer,
layer.id,
color,
0,
dashed,
heightRefCache.current
);
}
});
}, [dispatch, flattenVisibleLayers, kmlLayers, plotCenterline, viewerRef, visibleKmlLayers, heightRefCache]);
};
Any help or insights would be greatly appreciated — especially if someone has handled full KML-to-primitive conversion cleanly inside a React + Cesium app.
Happy to share actual code if needed!
Thanks in advance ![]()