地图点击事件给出的是屏幕像素,业务需要的通常是经纬度和高度。中间必须经过场景拾取和坐标转换,不能把鼠标的 x、y 直接当成经纬度。
一个稳定的拾取工具还要区分三种目标:已经渲染的模型或 3D Tiles、真实地形、没有地形时的椭球面。
先分清三套坐标
Cesium 交互中最常见的是三种对象:
| 类型 | 表示内容 | 单位 |
|---|---|---|
Cartesian2 | Canvas 内的屏幕位置 | 像素 |
Cartesian3 | 地心地固坐标系中的世界位置 | 米 |
Cartographic | 经度、纬度和椭球高 | 弧度、弧度、米 |
经纬度转世界坐标可以直接使用 fromDegrees:
function lonLatToCartesian3(lon: number, lat: number, height = 0) {
return Cesium.Cartesian3.fromDegrees(lon, lat, height)
}
世界坐标转经纬度要先得到 Cartographic,再把弧度转成角度:
function cartesian3ToLonLat(cartesian: Cesium.Cartesian3) {
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
return {
lon: Cesium.Math.toDegrees(cartographic.longitude),
lat: Cesium.Math.toDegrees(cartographic.latitude),
height: cartographic.height,
}
}
Cartesian3 的 x、y、z 不是经度、纬度和高度。它们是相对于地球中心的三维直角坐标。
为什么只调用 pickPosition 不够
最短的拾取代码是:
const position = viewer.scene.pickPosition(screenPosition)
pickPosition 根据当前帧的深度缓冲恢复世界坐标,适合拾取已经渲染的地形、模型和 3D Tiles。但它依赖浏览器对深度纹理的支持,也依赖目标已经出现在当前视图的深度缓冲中。
只使用它会遇到几类问题:
- 设备不支持深度拾取。
- 点击天空或场景空白区域。
- 当前底图只有椭球,没有可用地形深度。
- 透明对象没有写入拾取所需的深度。
因此更稳妥的做法是建立降级链。
三级拾取
export function pickWorldPosition(
viewer: Cesium.Viewer,
screenPosition: Cesium.Cartesian2,
): Cesium.Cartesian3 | undefined {
const { scene, camera } = viewer
// 1. 模型、3D Tiles 和当前帧深度
if (scene.pickPositionSupported) {
const depthPosition = scene.pickPosition(screenPosition)
if (Cesium.defined(depthPosition)) return depthPosition
}
// 2. 相机射线和地形求交
const ray = camera.getPickRay(screenPosition)
if (ray) {
const terrainPosition = scene.globe.pick(ray, scene)
if (Cesium.defined(terrainPosition)) return terrainPosition
}
// 3. 回退到 WGS84 椭球
const ellipsoidPosition = camera.pickEllipsoid(
screenPosition,
scene.globe.ellipsoid,
)
return ellipsoidPosition ?? undefined
}
它的执行顺序是:
屏幕坐标
├─ 深度缓冲有结果 → 模型、3D Tiles 或地形表面
├─ 否则发射相机射线 → 与当前地形求交
└─ 仍然失败 → 与 WGS84 椭球求交
三个结果都返回 Cartesian3,后续业务不需要知道拾取发生在哪一层。
高度应该保留还是归零
有些地图标点需求会把负高度改成零:
const cartographic = Cesium.Cartographic.fromCartesian(position)
if (cartographic.height < 0) cartographic.height = 0
这是业务规则,不是通用坐标转换规则。
如果系统需要表达地下管线、隧道、矿井或海底位置,负高度必须保留。更合理的方式是把它做成参数:
function normalizeHeight(
position: Cesium.Cartesian3,
clampNegativeHeight = false,
) {
if (!clampNegativeHeight) return position
const point = Cesium.Cartographic.fromCartesian(position)
point.height = Math.max(point.height, 0)
return Cesium.Cartesian3.fromRadians(
point.longitude,
point.latitude,
point.height,
)
}
把拾取接入鼠标事件
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
handler.setInputAction(
(movement: { position: Cesium.Cartesian2 }) => {
const cartesian = pickWorldPosition(viewer, movement.position)
if (!cartesian) return
const { lon, lat, height } = cartesian3ToLonLat(cartesian)
console.log({ lon, lat, height })
},
Cesium.ScreenSpaceEventType.LEFT_CLICK,
)
movement.position 是点击发生时的 Canvas 坐标。鼠标移动事件使用的是 movement.endPosition,两者不要混用。
在拾取位置添加图钉
function addPin(viewer: Cesium.Viewer, position: Cesium.Cartesian3) {
const { lon, lat } = cartesian3ToLonLat(position)
return viewer.entities.add({
position,
point: {
pixelSize: 14,
color: Cesium.Color.CYAN,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 3,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
label: {
text: `${lon.toFixed(6)}°, ${lat.toFixed(6)}°`,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -20),
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
},
})
}
disableDepthTestDistance: Number.POSITIVE_INFINITY 会让图钉始终显示在地形前面。它适合交互标记,但不适合需要真实遮挡关系的物体。
多个图钉应保存 Entity 引用,清理时逐个移除:
const pins: Cesium.Entity[] = []
pins.push(addPin(viewer, position))
function clearPins() {
pins.forEach(pin => viewer.entities.remove(pin))
pins.length = 0
}
世界坐标转回屏幕坐标
信息气泡如果使用普通 HTML,需要在每帧或相机变化时把世界位置投影回屏幕:
function worldToScreen(
viewer: Cesium.Viewer,
position: Cesium.Cartesian3,
) {
const result = Cesium.SceneTransforms.worldToWindowCoordinates(
viewer.scene,
position,
)
return result ? { x: result.x, y: result.y } : undefined
}
它只负责投影。目标位于地球背面时,还需要结合相机方向或遮挡检测决定是否隐藏 HTML 元素。
销毁事件处理器
ScreenSpaceEventHandler 不会因为局部变量被覆盖就自动解除 Canvas 事件。组件销毁时要显式调用 destroy:
function dispose() {
handler.destroy()
clearPins()
}
坐标拾取的主线可以收成一句话:先从 Canvas 像素获得一个可靠的世界坐标,再把它转换成业务需要的经纬度和高度。深度拾取、地形求交和椭球求交分别解决不同场景,不能互相替代。