站内搜索

输入关键词开始搜索

Cesium 通用绘制与测量工具:动态预览、距离面积与顶点编辑

文章目录

Cesium 绘制工具不是“点击一次就添加一个 Entity”。完整流程还包含动态预览、结束判定、结果冻结、测量计算、顶点编辑和资源清理。

如果把这些逻辑全部写进一个鼠标回调,线、多边形、圆和矩形很快会互相影响。更稳定的结构是把它分成三层:

交互层:处理鼠标点击、移动、右键和拖拽
状态层:保存确认点、预览点、绘制状态和编辑状态
图形层:不同模式负责创建、更新和冻结自己的 Entity

绘制状态不只有一个 positions

动态绘制至少需要区分两种点:

  • 确认点:用户已经点击确定的位置。
  • 预览点:跟随鼠标移动、尚未确认的位置。

可以用一个数组保存它们,再记录确认点数量:

private positions: Cesium.Cartesian3[] = []
private confirmedCount = 0

private getDisplayPositions() {
  return this.positions.slice()
}

第一次点击时创建主 Entity。后续点击把预览点变成确认点,再追加一个新的预览点:

function confirmPosition(position: Cesium.Cartesian3) {
  if (positions.length === 0) {
    positions.push(position)
    confirmedCount = 1
    positions.push(position.clone())
    mainEntity = createDynamicEntity(() => positions)
    return
  }

  positions[positions.length - 1] = position
  confirmedCount += 1
  positions.push(position.clone())
}

鼠标移动只替换最后一个预览点:

handler.setInputAction(
  (movement: { endPosition: Cesium.Cartesian2 }) => {
    const picked = pickWorldPosition(viewer, movement.endPosition)
    if (!picked || confirmedCount === 0) return

    positions[positions.length - 1] = picked
    onUpdate?.(positions)
  },
  Cesium.ScreenSpaceEventType.MOUSE_MOVE,
)

这个区分可以避免两个常见问题:把预览点算进最终测量结果,或者结束绘制时多出一个重复顶点。

CallbackProperty 只负责读取当前状态

折线的动态预览可以使用 CallbackProperty:

const entity = dataSource.entities.add({
  polyline: {
    positions: new Cesium.CallbackProperty(
      () => positions,
      false,
    ),
    width: 3,
    material: Cesium.Color.CYAN,
    clampToGround: true,
  },
})

多边形则返回 PolygonHierarchy:

const entity = dataSource.entities.add({
  polygon: {
    hierarchy: new Cesium.CallbackProperty(
      () => new Cesium.PolygonHierarchy(positions),
      false,
    ),
    material: Cesium.Color.CYAN.withAlpha(0.25),
  },
})

CallbackProperty 没有主动修改 Entity。Cesium 在更新场景时调用回调,读取当时的 positions。因此数组是当前状态,回调只是它的动态视图。

结束时把动态属性冻结

绘制结束后继续保留 CallbackProperty 会让每一帧都执行不再需要的回调。结束时先丢掉预览点,再把属性改成固定值:

function stop() {
  handler?.destroy()
  handler = null

  const confirmed = positions.slice(0, confirmedCount)

  if (mainEntity?.polyline) {
    mainEntity.polyline.positions = new Cesium.ConstantProperty(confirmed)
  }

  onFinish?.(confirmed, mainEntity)

  positions = []
  confirmedCount = 0
  mainEntity = null
}

多边形冻结时需要保存 PolygonHierarchy,圆和矩形则保存最终半径或 Rectangle。

“安排动态更新”和“保存最终结果”是两个阶段。CallbackProperty 用于前者,固定 Property 用于后者。

用策略隔离不同绘制模式

线、多边形、圆和矩形对点数量和 Entity 属性的要求不同,但鼠标流程相同。可以定义模式策略:

interface DrawModeStrategy {
  minPoints: number
  autoStopPoints?: number
  createEntity(
    dataSource: Cesium.CustomDataSource,
    getPositions: () => Cesium.Cartesian3[],
    style: DrawStyleOptions,
  ): Cesium.Entity
  freezeEntity(
    entity: Cesium.Entity,
    positions: Cesium.Cartesian3[],
  ): void
  extractPositions(
    entity: Cesium.Entity,
    time: Cesium.JulianDate,
  ): Cesium.Cartesian3[]
}

例如折线至少需要两个点,多边形至少需要三个点;圆和矩形通常在第二次点击后自动结束:

const strategies: Record<DrawMode, DrawModeStrategy> = {
  polyline: polylineStrategy,
  polygon: polygonStrategy,
  circle: { ...circleStrategy, minPoints: 2, autoStopPoints: 2 },
  rectangle: { ...rectangleStrategy, minPoints: 2, autoStopPoints: 2 },
}

新增模式时只补充图形行为,不需要复制整套鼠标事件。

距离:地表距离加高差

直接计算两个 Cartesian3 的直线距离,得到的是空间弦长。地图测距通常更关心椭球表面的大地线距离。

function getGeodesicDistance(
  start: Cesium.Cartesian3,
  end: Cesium.Cartesian3,
) {
  const startCartographic = Cesium.Cartographic.fromCartesian(start)
  const endCartographic = Cesium.Cartographic.fromCartesian(end)
  const geodesic = new Cesium.EllipsoidGeodesic(
    startCartographic,
    endCartographic,
  )

  const surfaceDistance = geodesic.surfaceDistance
  const heightDifference = endCartographic.height - startCartographic.height

  return Math.hypot(surfaceDistance, heightDifference)
}

这里先计算椭球表面距离,再用勾股关系加入两点高差。它不是沿真实起伏地形逐米积分,因此不能当成山路长度。如果业务需要贴地路径长度,必须沿路径加密采样地形,再累计相邻采样点距离。

折线总长就是每段距离的累加:

function calculateLineDistance(positions: Cesium.Cartesian3[]) {
  const segments: number[] = []
  let total = 0

  for (let i = 0; i < positions.length - 1; i += 1) {
    const distance = getGeodesicDistance(positions[i], positions[i + 1])
    segments.push(distance)
    total += distance
  }

  return { total, segments }
}

面积:球面近似及适用边界

局部区域可以采用经纬度球面近似:

function calculatePolygonArea(positions: Cesium.Cartesian3[]) {
  if (positions.length < 3) return 0

  const ellipsoid = Cesium.Ellipsoid.WGS84
  let sum = 0

  for (let i = 0; i < positions.length; i += 1) {
    const current = ellipsoid.cartesianToCartographic(positions[i])
    const next = ellipsoid.cartesianToCartographic(
      positions[(i + 1) % positions.length],
    )

    sum +=
      (next.longitude - current.longitude) *
      (2 + Math.sin(current.latitude) + Math.sin(next.latitude))
  }

  return Math.abs(
    sum * ellipsoid.maximumRadius * ellipsoid.minimumRadius / 2,
  )
}

这个算法适合范围不大、不跨越日期变更线的普通测量。大范围多边形、跨经度 ±180° 的区域、包含洞的多边形和测绘级计算,需要先处理经度展开、环方向和投影误差,不能直接套用这段代码。

顶点编辑本质上是重新进入动态状态

编辑已有图形时,先读取固定位置,再把 Entity 属性改回 CallbackProperty:

function startEdit(entity: Cesium.Entity) {
  editPositions = strategy.extractPositions(
    entity,
    viewer.clock.currentTime,
  )

  strategy.makeDynamic(entity, () => editPositions)
  createEditHandles()
  setupEditHandlers()
}

每个编辑手柄通过索引读取当前顶点:

const handle = dataSource.entities.add({
  position: new Cesium.CallbackPositionProperty(
    () => editPositions[index] ?? Cesium.Cartesian3.ZERO,
    false,
  ),
  point: {
    pixelSize: 14,
    color: Cesium.Color.YELLOW,
    outlineColor: Cesium.Color.WHITE,
    outlineWidth: 3,
  },
})

拖拽期间暂时关闭相机旋转,否则一次鼠标移动会同时修改顶点和相机:

viewer.scene.screenSpaceCameraController.enableRotate = false

const picked = pickWorldPosition(viewer, movement.endPosition)
if (picked) editPositions[dragIndex] = picked

// LEFT_UP 时恢复
viewer.scene.screenSpaceCameraController.enableRotate = true

结束编辑后再次冻结 Entity,并移除编辑手柄。

清理顺序

绘制工具通常同时持有两个事件处理器:绘制处理器和编辑处理器。销毁时依次停止编辑、取消未完成绘制、清空 Entity、移除 DataSource:

function destroy() {
  stopEdit()
  cancel()
  dataSource.entities.removeAll()
  viewer.dataSources.remove(dataSource)
}

通用绘制工具的核心不是支持多少图形,而是把确认点、预览点、动态属性和最终结果分开管理。测量只是读取最终位置数组的一个业务层。

参考资料

相关文章

Cesium 三维风场:用 WebGL 2 GPU 粒子描绘全球气流

拆解案例中的风场数据、GPU 粒子更新、轨迹渲染、颜色映射和 Vue 参数控制

Cesium 北斗三维网格:在城市 3D Tiles 上做 GeoSOT-3D 剖分

介绍区域体素生成、建筑表面采样、结构碰撞检测、Worker 几何构建和网格编码查询