Skip to content

想法

  • 过去做二维地图的时候,经常会实现沿线标注的效果,类似于地铁线路的标注。
  • 就想着在 Cesium 中也实现一下这个功能,网上查了很久没有找到相关的方案。
  • 做的还有一些瑕疵,标注有时不在正中心。

效果

源码

vue
<template>
  <div id="cesium-container"></div>
</template>

<script setup>
import { onMounted, onBeforeUnmount } from "vue";
import * as Cesium from "cesium";

let viewer = null;
const labelCollection = new Cesium.LabelCollection();
const lineEntities = [];
const labelPool = [];
const viewModel = {
  labelsVisible: true,
  minDistance: 100,
  fontSize: 16,
  fontFamily: "Microsoft YaHei",
  outlineWidth: 2,
  scale: 1.0,
  maxDistance: 300000,
};

onMounted(() => {
  viewer = new Cesium.Viewer("cesium-container", {
    geocoder: false, // 关闭地理编码搜索
    homeButton: false, // 关闭主页按钮
    sceneModePicker: false, // 关闭场景模式选择器
    baseLayerPicker: false, // 关闭底图选择器
    navigationHelpButton: false, // 关闭导航帮助
    animation: false, // 关闭动画控件
    timeline: false, // 关闭时间轴
    fullscreenButton: false, // 关闭全屏按钮
  });

  viewer.scene.primitives.add(labelCollection);

  // 加载GeoJSON数据
  Cesium.GeoJsonDataSource.load("/src/cesium/json/line.geojson", {
    stroke: Cesium.Color.BLUE,
    strokeWidth: 2,
  }).then((dataSource) => {
    viewer.zoomTo(dataSource);
    viewer.dataSources.add(dataSource);

    const entities = dataSource.entities.values;
    for (let i = 0; i < entities.length; i++) {
      const entity = entities[i];
      if (entity.polyline) {
        lineEntities.push(entity);
        initLabelForEntity(entity);
      }
    }
    startLabelUpdater();
  });
});

// 初始化实体标签
function initLabelForEntity(entity) {
  const label = labelCollection.add({
    show: false,
    text: entity.name || "未命名线路",
    font: `${viewModel.fontSize}px ${viewModel.fontFamily}`,
    fillColor: Cesium.Color.RED,
    outlineColor: Cesium.Color.BLACK,
    outlineWidth: viewModel.outlineWidth,
    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
    pixelOffset: new Cesium.Cartesian2(0, -20),
    scale: viewModel.scale,
    distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
      0,
      viewModel.maxDistance
    ),
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
  });

  labelPool.push({
    label,
    entity,
    lastPosition: new Cesium.Cartesian3(),
    lastUpdate: 0,
  });
}

// 启动标签更新器
function startLabelUpdater() {
  viewer.scene.postRender.addEventListener(updateVisibleLabels);
}

// 更新可见标签 - 核心逻辑
function updateVisibleLabels() {
  if (!viewModel.labelsVisible) return;

  // 获取一个时间戳,用于测量事件之间的时间差
  const now = Cesium.getTimestamp();

  const camera = viewer.scene.camera;

  // 重置所有标签状态
  labelPool.forEach((item) => {
    item.label.show = false;
  });

  // 创建视锥体裁剪体
  const cullingVolume = camera.frustum.computeCullingVolume(
    camera.position,
    camera.direction,
    camera.up
  );

  // 可见实体过滤
  const visibleEntities = lineEntities.filter((entity) => {
    const positions = entity.polyline.positions.getValue(
      Cesium.JulianDate.now()
    );
    const boundingSphere = Cesium.BoundingSphere.fromPoints(positions);

    // 使用正确的视锥体裁剪方法
    const visibility = cullingVolume.computeVisibility(boundingSphere);
    return visibility !== Cesium.Intersect.OUTSIDE;
  });

  // 按距离排序(近到远)
  visibleEntities.sort((a, b) => {
    const aCenter = getPolylineCenter(a);
    const bCenter = getPolylineCenter(b);

    const aDist = Cesium.Cartesian3.distance(aCenter, camera.position);
    const bDist = Cesium.Cartesian3.distance(bCenter, camera.position);
    return aDist - bDist;
  });

  // 处理可见标签
  const placedPositions = [];
  visibleEntities.forEach((entity) => {
    const item = labelPool.find((i) => i.entity === entity);
    if (!item) return;

    const positions = entity.polyline.positions.getValue(
      Cesium.JulianDate.now()
    );
    const labelPosition = calculateBestLabelPosition(
      positions,
      placedPositions
    );

    if (labelPosition) {
      Cesium.Cartesian3.clone(labelPosition, item.lastPosition);
      item.label.position = labelPosition;
      item.label.show = true;
      item.lastUpdate = now;

      placedPositions.push({
        position: labelPosition,
        time: now,
      });
    }
  });

  // 清理旧位置记录
  for (let i = placedPositions.length - 1; i >= 0; i--) {
    if (now - placedPositions[i].time > 3000) {
      placedPositions.splice(i, 1);
    }
  }
}

// 获取折线中心点
function getPolylineCenter(entity) {
  const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
  const boundingSphere = Cesium.BoundingSphere.fromPoints(positions);
  return boundingSphere.center;
}

// 计算最佳标签位置
function calculateBestLabelPosition(positions, placedPositions) {
  const scratchCartesian = new Cesium.Cartesian3();
  const scratchWindowPosition = new Cesium.Cartesian2();

  // 策略1:尝试线段中点
  const midpointIndex = Math.floor(positions.length / 2);
  const midpoint = positions[midpointIndex];

  if (isPositionValid(midpoint, placedPositions)) {
    return midpoint;
  }

  // 策略2:遍历寻找合适位置
  for (let i = 1; i < positions.length; i++) {
    const segmentCenter = Cesium.Cartesian3.lerp(
      positions[i - 1],
      positions[i],
      0.5,
      scratchCartesian
    );

    if (isPositionValid(segmentCenter, placedPositions)) {
      return segmentCenter;
    }
  }

  // 策略3:尝试屏幕中心附近
  const screenCenter = new Cesium.Cartesian2(
    viewer.canvas.width / 2,
    viewer.canvas.height / 2
  );

  let closestDistance = Number.MAX_VALUE;
  let closestPosition = null;

  positions.forEach((pos) => {
    const windowPos = Cesium.SceneTransforms.worldToWindowCoordinates(
      viewer.scene,
      pos,
      scratchWindowPosition
    );

    if (!windowPos) return;

    const distance = Cesium.Cartesian2.distance(windowPos, screenCenter);
    if (distance < closestDistance) {
      closestDistance = distance;
      closestPosition = pos;
    }
  });

  if (closestPosition && isPositionValid(closestPosition, placedPositions)) {
    return closestPosition;
  }

  return null;
}

// 检查位置是否有效
function isPositionValid(position, placedPositions) {
  const camera = viewer.scene.camera;

  // 使用正确的视锥体裁剪方法
  const cullingVolume = camera.frustum.computeCullingVolume(
    camera.position,
    camera.direction,
    camera.up
  );

  // 创建包围球(半径为1米)
  const boundingSphere = new Cesium.BoundingSphere(position, 1);

  // 检查是否在视锥体内
  if (
    cullingVolume.computeVisibility(boundingSphere) === Cesium.Intersect.OUTSIDE
  ) {
    return false;
  }

  // 转换为屏幕坐标
  const windowPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
    viewer.scene,
    position
  );

  if (!windowPosition) return false;

  // 检查是否在屏幕范围内
  if (
    windowPosition.x < 0 ||
    windowPosition.x > viewer.canvas.width ||
    windowPosition.y < 0 ||
    windowPosition.y > viewer.canvas.height
  ) {
    return false;
  }

  // 检查与其他标签的距离
  for (const placed of placedPositions) {
    const placedWindowPos = Cesium.SceneTransforms.worldToWindowCoordinates(
      viewer.scene,
      placed.position
    );

    if (
      placedWindowPos &&
      Cesium.Cartesian2.distance(windowPosition, placedWindowPos) <
        viewModel.minDistance
    ) {
      return false;
    }
  }

  return true;
}

onBeforeUnmount(() => {
  if (viewer) {
    viewer.scene.postRender.removeEventListener(updateVisibleLabels);
    viewer.destroy();
  }
});
</script>

<style>
#cesium-container {
  width: 100%;
  height: 100vh;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
</style>