Skip to content

沿线标注

介绍

  1. 使用Cesium.LabelCollection创建标签集合
  2. 使用Cesium.GeoJsonDataSource加载GeoJSON数据
  3. 通过viewer.scene.postRender监听渲染事件,动态更新标签位置
  4. 使用Cesium.Cartesian3Cesium.Cartesian2进行坐标转换
  5. 通过Cesium.SceneTransforms.worldToWindowCoordinates将世界坐标转换为屏幕坐标
  6. 使用Cesium.DistanceDisplayCondition控制标签的显示距离
  7. 通过Cesium.BoundingSphere计算实体的包围球
  8. 使用Cesium.CullingVolume进行视锥体裁剪,优化标签显示
展开代码
vue
<template>
  <div ref="cesiumContainer" class="container"></div>
</template>

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

const cesiumContainer = ref(null);
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,
};

// 天地图TOKEN
const token = "05be06461004055923091de7f3e51aa6";

onMounted(() => {
  // 初始化Viewer
  viewer = new Cesium.Viewer(cesiumContainer.value, {
    geocoder: false, // 关闭地理编码搜索
    homeButton: false, // 关闭主页按钮
    sceneModePicker: false, // 关闭场景模式选择器
    baseLayerPicker: false, // 关闭底图选择器
    navigationHelpButton: false, // 关闭导航帮助
    animation: false, // 关闭动画控件
    timeline: false, // 关闭时间轴
    fullscreenButton: false, // 关闭全屏按钮
    baseLayer: false, // 关闭默认地图
  });
  // 清空logo
  viewer.cesiumWidget.creditContainer.style.display = "none";

  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();
  });
  initMap();
});

// 初始化实体标签
const 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,
  });
};

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

// 更新可见标签 - 核心逻辑
const 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;
}

// 检查位置是否有效
const 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();
  }
});

// 加载天地图
const initMap = () => {
  // 以下为天地图及天地图标注加载
  const tiandituProvider = new Cesium.WebMapTileServiceImageryProvider({
    url:
      "http://{s}.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=" +
      token,
    layer: "img",
    style: "default",
    format: "tiles",
    tileMatrixSetID: "w", // 天地图使用 Web 墨卡托投影(EPSG:3857),需确保 tileMatrixSetID: "w"
    subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"], // 子域名
    maximumLevel: 18,
    credit: new Cesium.Credit("天地图影像"),
  });

  // 添加地理标注
  const labelProvider = new Cesium.WebMapTileServiceImageryProvider({
    url:
      "http://{s}.tianditu.gov.cn/cia_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=w&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}&style=default&format=tiles&tk=" +
      token,
    layer: "img",
    style: "default",
    format: "tiles",
    tileMatrixSetID: "w",
    subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"], // 子域名轮询
    maximumLevel: 18,
    credit: new Cesium.Credit("天地图影像"),
  });
  // 天地图影像添加到viewer实例的影像图层集合中
  viewer.imageryLayers.addImageryProvider(tiandituProvider);
  // 天地图地理标注(后添加的会覆盖前面的)
  viewer.imageryLayers.addImageryProvider(labelProvider);
};
</script>
<style scoped>
.container {
  width: 100vw;
  height: 100vh;
}
</style>