想法
- 过去做二维地图的时候,经常会实现沿线标注的效果,类似于地铁线路的标注。
- 就想着在 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>