沿线标注
介绍
- 使用
Cesium.LabelCollection
创建标签集合 - 使用
Cesium.GeoJsonDataSource
加载GeoJSON数据 - 通过
viewer.scene.postRender
监听渲染事件,动态更新标签位置 - 使用
Cesium.Cartesian3
和Cesium.Cartesian2
进行坐标转换 - 通过
Cesium.SceneTransforms.worldToWindowCoordinates
将世界坐标转换为屏幕坐标 - 使用
Cesium.DistanceDisplayCondition
控制标签的显示距离 - 通过
Cesium.BoundingSphere
计算实体的包围球 - 使用
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>