You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

713 lines
27 KiB

<template>
<div>
<div ref="cesiumContainerRef" class="cesium-container"></div>
<div class="btn-group">
<!-- <el-button class="get-view-btn" size="small" type="primary" @click="getCurrentViewInfo"
style="margin-right: 10px;">获取当前视角</el-button> -->
<!-- <el-button class="view-btn" size="small" type="success" @click="switchView('overview')"
style="margin-right: 10px;">概览视角</el-button> -->
<!-- <el-button class="view-btn" size="small" type="warning" @click="switchView('view1')"
style="margin-right: 10px;">视角1</el-button>
<el-button class="view-btn" size="small" type="warning" @click="switchView('view2')"
style="margin-right: 10px;">视角2</el-button> -->
</div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
import coordtransform from 'coordtransform';
import { MapApi } from "@/api/shorepower/map";
import { toRaw } from 'vue'
// ⚠️ 注意:通过 window 访问全局 Cesium 对象
const Cesium = window.Cesium;
let viewer = null;
const cesiumContainerRef = ref(null); // 引用 DOM 元素
const history = ref(null); // 历史数据
const dataWithModels = ref([]); // 存储带有模型实例的数据列表
// 定义预设视角参数
const presetViews = {
overview: {
"longitude": 118.5132903711312,
"latitude": 38.84648563549342,
"height": 12675.768680075478,
"heading": 353.6789759103708,
"pitch": -36.657258995301596,
"roll": 0.0343469697692715
}
};
// 岸电箱点击回调函数
let onElectricalBoxClick = null;
// 设置岸电箱点击回调函数的方法
const setElectricalBoxClickCallback = (callback) => {
onElectricalBoxClick = callback;
};
const createView = (obj) => {
return {
destination: Cesium.Cartesian3.fromDegrees(obj.longitude, obj.latitude, obj.height),
orientation: {
heading: Cesium.Math.toRadians(obj.heading),
pitch: Cesium.Math.toRadians(obj.pitch),
roll: Cesium.Math.toRadians(obj.roll)
}
}
}
onMounted(async () => {
if (!Cesium) {
console.error("Cesium 对象未找到,请检查 index.html 文件!");
return;
}
// 步骤 1: 初始化 Viewer 并配置为使用高德地图影像和平坦地形
try {
viewer = new Cesium.Viewer(cesiumContainerRef.value, {
// **核心设置:禁用所有默认影像,我们将手动添加高德地图**
imageryProvider: false,
// infoBox: true,
// **使用平坦地形,避免依赖 Cesium Ion**
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
// sceneMode: Cesium.SceneMode.COLUMBUS_VIEW,
// 禁用所有不必要的UI控件
timeline: false,
animation: false,
baseLayerPicker: false,
geocoder: false,
sceneModePicker: false,
navigationHelpButton: false,
infoBox: false,
fullscreenButton: false,
homeButton: false,
// 启用抗锯齿和其他渲染优化
contextOptions: {
webgl: {
antialias: true
}
}
});
// 禁用所有鼠标和键盘的相机控制操作
/* viewer.scene.screenSpaceCameraController.enableRotate = false;
viewer.scene.screenSpaceCameraController.enableTranslate = false;
viewer.scene.screenSpaceCameraController.enableZoom = false;
viewer.scene.screenSpaceCameraController.enableTilt = false;
viewer.scene.screenSpaceCameraController.enableLook = false; */
// 渲染优化设置
// 启用抗锯齿
viewer.scene.postProcessStages.fxaa.enabled = true;
// 启用深度测试,确保模型能够被地形正确遮挡
viewer.scene.globe.depthTestAgainstTerrain = true;
// 启用时钟动画,确保模型能够根据时间更新
viewer.clock.shouldAnimate = true;
// 相机控制设置
const controller = viewer.scene.screenSpaceCameraController;
// 设置最大缩放距离(相机到地球中心的最大距离)
// 500,000.0 米 = 500 公里
const MAX_DISTANCE = 500000.0;
// 启用缩放,然后设置其最大限制
controller.enableZoom = true;
controller.maximumZoomDistance = MAX_DISTANCE;
// (可选)同时限制最小缩放距离,防止滚轮进入模型内部
// 例如,限制最小距离为 1000.0 米
const MIN_HEIGHT = 1200;
controller.minimumZoomDistance = MIN_HEIGHT - 100;
const canvas = viewer.scene.canvas;
canvas.addEventListener(
"wheel",
(e) => {
const height = viewer.camera.positionCartographic.height;
// 只拦截“继续向内”
if (height <= MIN_HEIGHT && e.deltaY < 0) {
e.preventDefault();
e.stopPropagation();
}
},
{
passive: false,
capture: true
}
);
// viewer.scene.screenSpaceCameraController.minimumZoomDistance = 200;
// 步骤 2: 添加高德地图卫星影像底图
const gaodeImage = new Cesium.UrlTemplateImageryProvider({
url: "https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
subdomains: ['1', '2', '3', '4'],
layer: 'gaodeImgLayer',
style: 'default',
format: 'image/png',
maximumLevel: 16
});
const dataInfo = history.value ? history.value : await MapApi.getAllData()
const shipData = await MapApi.getShipInfo({
harborDistrictId: 1
})
console.log('shipData', shipData)
// const unitedData =
// 通用添加函数,遍历dataInfo并创建标记
const addMarkersFromDataInfo = (dataInfo) => {
// 更严格的数组检查
if (!dataInfo || !Array.isArray(dataInfo) || dataInfo.length === 0) {
console.log('No valid dataInfo array provided');
return [];
}
// 创建新数组存储带有模型实例的数据
const dataWithModelsArray = [];
console.log(`Processing ${dataInfo.length} items`);
dataInfo.forEach((item, index) => {
// 检查基本数据结构
if (!item) {
console.warn(`Item at index ${index} is null or undefined`);
return;
}
try {
// 创建包含模型实例的数据项副本
let itemWithModel = { ...item };
// 解析data字段为JSON对象
let dataObj;
if (typeof item.data === 'string') {
try {
dataObj = JSON.parse(item.data);
} catch (parseError) {
console.warn(`Failed to parse data for item ${item.id || index}:`, parseError);
dataWithModelsArray.push(itemWithModel); // 添加未解析成功的数据项
return;
}
} else {
dataObj = item.data;
}
// 检查dataObj是否有效
if (!dataObj) {
console.warn(`No data object found for item ${item.id || index}`);
dataWithModelsArray.push(itemWithModel); // 添加无数据对象的数据项
return;
}
// 获取坐标信息 - 更严格的验证
let longitude, latitude;
let wgsLon, wgsLat;
if (dataObj.xy && Array.isArray(dataObj.xy) && dataObj.xy.length >= 2) {
const wgsCoords = coordtransform.wgs84togcj02(dataObj.xy[1], dataObj.xy[0]);
wgsLon = wgsCoords[0];
wgsLat = wgsCoords[1];
// 确保坐标是有效数字
latitude = wgsLat;
longitude = wgsLon;
// 检查坐标是否为有效数字
if (isNaN(latitude) || isNaN(longitude)) {
console.warn(`Invalid coordinates for item ${item.id || index}:`, dataObj.xy);
dataWithModelsArray.push(itemWithModel); // 添加坐标无效的数据项
return;
}
// 检查坐标范围(简单验证)
if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
console.warn(`Coordinates out of range for item ${item.id || index}:`, dataObj.xy[1], dataObj.xy[0]);
dataWithModelsArray.push(itemWithModel); // 添加坐标超出范围的数据项
return;
}
} else {
console.warn('无效的坐标信息:', item);
dataWithModelsArray.push(itemWithModel); // 添加无有效坐标的数据项
return;
}
console.log('dataObj.icon', dataObj.icon)
if (dataObj.icon === 'ship_green' || dataObj.icon === 'ship_red') {
const itemShipInfo = shipData.find(shipItem => (shipItem.shorePower.id === item.parentId) && item.type === 5)
if (!itemShipInfo) {
return;
}
const position = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 15);
const statusPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 1);
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 35);
// 投放船模型
const shipModel = viewer.entities.add({
name: 'Cargo Ship Model',
position: position,
orientation: Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(dataObj.rotationAngle), // 航向角 (绕Z轴旋转)
Cesium.Math.toRadians(0), // 俯仰角 (绕Y轴旋转)
Cesium.Math.toRadians(0) // 翻滚角 (绕X轴旋转)
)
),
model: {
uri: '/model/cargo_ship_07.glb',
scale: 1.5, // 调整模型大小
// minimumPixelSize: 100, // 确保模型至少显示为50像素大小
// 启用深度测试,使模型能够被地形遮挡
enableDepthTest: true,
// 启用背光剔除,提高渲染性能
backFaceCulling: true
}
});
// 保存模型实例到数据项
itemWithModel.modelInstance = shipModel;
itemWithModel.modelType = 'ship';
itemWithModel = { ...itemWithModel, ...itemShipInfo };
viewer.entities.add({
position: labelPosition, // 船上方约10米
label: {
text: itemShipInfo.shipBasicInfo.name || `Marker-${item.id || index}`,
font: '20px sans-serif',
fillColor: Cesium.Color.LIME,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -30), // 偏移量,避免与模型重叠
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见
}
});
// 根据概率分布决定是否显示状态图标
// 50%概率不显示,25%显示故障,25%显示离线
const rand = Math.random();
let statusImage = null;
if (rand < 0.8) {
// 50%概率:不显示状态图标
statusImage = null;
} else if (rand < 0.9) {
// 25%概率:显示故障图标
statusImage = '/img/故障.png';
} else {
// 25%概率:显示离线图标
statusImage = '/img/离线.png';
}
// 只有当需要显示状态图标时才添加实体
if (statusImage) {
const overlayBillboard = viewer.entities.add({
position: statusPosition,
billboard: {
image: statusImage,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
scale: new Cesium.CallbackProperty(function (time, result) {
// t 会随着时间不断变化
const t = Cesium.JulianDate.toDate(time).getTime() / 1000;
const pulse = 0.4 + Math.sin(t * 4) * 0.1; // (基准0.6, 幅度±0.2)
return pulse;
}, false)
}
});
}
}
if (dataObj.type === 'text') {
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 35);
viewer.entities.add({
position: labelPosition, // 船上方约10米
label: {
text: dataObj.name || `Marker-${item.id || index}`,
font: '20px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -30), // 偏移量,避免与模型重叠
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见
}
});
}
if (dataObj.type === 'icon' && (dataObj.icon === 'interface_blue' || dataObj.icon === 'interface_red')) {
const itemShipInfo = shipData.find(shipItem => (shipItem.shorePower.id === item.parentId) && item.type === 5)
const position = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 1);
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 5);
// 投放岸电箱模型
const electricalBoxModel = viewer.entities.add({
name: '岸电箱',
position: position,
// 添加唯一标识用于识别点击事件
properties: {
modelType: 'electrical_box',
data: itemWithModel
},
orientation: Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(dataObj.rotationAngle), // 航向角 (绕Z轴旋转)
Cesium.Math.toRadians(0), // 俯仰角 (绕Y轴旋转)
Cesium.Math.toRadians(0) // 翻滚角 (绕X轴旋转)
)
),
model: {
uri: '/model/electrical_box.glb',
scale: 1.5, // 调整模型大小
// minimumPixelSize: 10, // 确保模型至少显示为50像素大小
// 启用深度测试,使模型能够被地形遮挡
enableDepthTest: true,
// 启用背光剔除,提高渲染性能
backFaceCulling: true
},
});
// 保存模型实例到数据项
itemWithModel.modelInstance = electricalBoxModel;
itemWithModel.modelType = 'electrical_box';
itemWithModel = { ...itemWithModel, ...itemShipInfo };
viewer.entities.add({
// pickable: false,
position: labelPosition, // 船上方约10米
properties: {
modelType: 'electrical_box',
data: itemWithModel
},
label: {
text: '岸电箱' || `Marker-${item.id || index}`,
font: '16px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -30), // 偏移量,避免与模型重叠
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见
}
});
// 根据概率分布决定是否显示状态图标
// 50%概率不显示,25%显示故障,25%显示离线
const rand = Math.random();
let statusImage = null;
if (rand < 0.5) {
// 50%概率:不显示状态图标
statusImage = null;
} else if (rand < 0.75) {
// 25%概率:显示故障图标
statusImage = '/img/故障.png';
} else {
// 25%概率:显示离线图标
statusImage = '/img/离线.png';
}
// 只有当需要显示状态图标时才添加实体
if (statusImage) {
const overlayBillboard = viewer.entities.add({
position: position,
// pickable: false,
properties: {
modelType: 'electrical_box',
data: itemWithModel
},
billboard: {
image: statusImage,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
scale: new Cesium.CallbackProperty(function (time, result) {
// t 会随着时间不断变化
const t = Cesium.JulianDate.toDate(time).getTime() / 1000;
const pulse = 0.2 + Math.sin(t * 4) * 0.1; // (基准0.6, 幅度±0.2)
return pulse;
}, false)
}
});
}
}
// 创建红点标记
/* const entity = viewer.entities.add({
name: item.name || `Marker-${item.id || index}`,
position: Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 1),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
// 可以添加标签显示名称
label: {
text: item.name || `Marker-${item.id || index}`,
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -15)
}
});
// 保存点标记实例到数据项
itemWithModel.modelInstance = entity;
itemWithModel.modelType = 'point';
*/
// 将处理后的数据项添加到新数组
dataWithModelsArray.push(itemWithModel);
} catch (error) {
console.error('Error processing item:', item, error);
// 创建一个基本的数据项副本并添加到数组
dataWithModelsArray.push({ ...item });
}
});
return dataWithModelsArray;
};
// 步骤 3: 将图层添加到 Viewer 中
viewer.imageryLayers.addImageryProvider(gaodeImage);
// 调用通用函数创建标记并获取带有模型实例的数据
dataWithModels.value = addMarkersFromDataInfo(dataInfo);
// 设置初始视角
viewer.camera.flyTo(createView(presetViews.overview));
// 设置显示高度阈值
const SHOW_HEIGHT = 30000; // 高度 > 5000m 才显示模型等信息
// const targets = []; // 把需要控制的 entities push 进来
const targets = dataWithModels.value
.filter(item => item.modelInstance)
.map(item => toRaw(item.modelInstance));
console.log('targets', targets)
viewer.camera.changed.addEventListener(() => {
// 这个函数会在相机的任何属性发生变化后立即被调用。
const height = viewer.camera.positionCartographic.height;
// console.log("相机高度(即缩放级别)已变化:", height.toFixed(2), "米");
// 控制模型显示/隐藏
const visible = height <= SHOW_HEIGHT; // 1000m内才显示模型等信息
targets.forEach(entity => {
if (entity.show !== visible) {
entity.show = visible;
}
});
// 其他缩放级别逻辑
if (height < 100000) {
// 放大到街景级别
console.log("当前处于近距离缩放级别。");
} else if (height > 5000000) {
// 缩小到全球级别
console.log("当前处于远距离缩放级别。");
}
});
// 添加地图点击事件
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function (movement) {
console.log('地图点击事件触发');
// 首先检查是否点击到了模型
const pickedObject = viewer.scene.pick(movement.position);
if (pickedObject && pickedObject.id && pickedObject.id.properties) {
// 检查是否是岸电箱模型
const properties = pickedObject.id.properties;
if (properties.modelType && properties.modelType.getValue() === 'electrical_box') {
// 触发岸电箱点击回调
const electricalBoxData = properties.data.getValue();
console.log('岸电箱模型被点击:', electricalBoxData);
// 如果定义了回调函数,则调用它
if (typeof onElectricalBoxClick === 'function') {
onElectricalBoxClick(electricalBoxData);
}
// 阻止进一步处理
return;
}
}
// 如果不是点击模型,则处理地面点击
// 尝试多种方式获取点击位置
const ray = viewer.camera.getPickRay(movement.position);
// 方法1: 从地球表面拾取
let cartesian = viewer.scene.globe.pick(ray, viewer.scene);
// 如果从地球表面拾取失败,尝试从场景中拾取
if (!cartesian) {
cartesian = viewer.scene.pickPosition(movement.position);
console.log('使用场景拾取方法');
}
// 如果仍然失败,尝试使用相机计算位置
if (!cartesian) {
// 获取屏幕中心点作为替代方案(仅用于调试)
console.log('无法从点击位置获取精确坐标');
return;
}
if (cartesian) {
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
if (cartographic) {
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
const gcj02 = coordtransform.gcj02towgs84(longitude, latitude);
console.log('点击位置经纬度:', {
longitude: gcj02[0],
latitude: gcj02[1]
});
} else {
console.log('无法从笛卡尔坐标转换为大地坐标');
}
} else {
console.log('未能获取到地球表面的点击位置');
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 确保启用场景的鼠标事件
viewer.scene.screenSpaceCameraController.enableInputs = true;
// 启用拾取透明物体
viewer.scene.useDepthPicking = true;
console.log('Cesium 高德地图 Viewer 初始化成功');
} catch (error) {
console.error('Cesium Viewer 初始化失败:', error);
}
});
// 视角切换方法
const switchView = (viewName) => {
const viewParams = presetViews[viewName];
if (viewParams && viewer) {
viewer.camera.flyTo({
...createView(viewParams),
duration: 2.0 // 动画持续时间(秒)
});
}
};
/**
* 切换到指定模型视图,并允许设置自定义的飞离距离。
* @param {Cesium.Entity} view - 要飞向的 Entity 实例。
* @param {number} [extraDistance=15000.0] - 额外的飞离距离 (米)。
* @param {number} [estimatedRadius=500.0] - 用于计算视角范围的估计模型半径 (米)。
*/
const switchModelView = (view, extraDistance = 1000, estimatedRadius = 500.0) => {
const entityInstance = toRaw(view);
console.log('目标模型实体:', entityInstance);
if (viewer && entityInstance && entityInstance.position) {
// 1. 获取模型当前的世界坐标
// 必须使用 getValue(viewer.clock.currentTime) 来获取当前准确的位置
const position = entityInstance.position.getValue(viewer.clock.currentTime);
if (!position) {
console.warn("无法获取模型位置,执行默认 flyTo。");
viewer.flyTo(entityInstance, { duration: 3.0 });
return;
}
// 2. 手动创建边界球 (因为 Entity 实例没有 readyPromise 或 boundingSphere 属性)
// 使用一个估计的半径来计算视角范围
const manualBoundingSphere = new Cesium.BoundingSphere(
position,
estimatedRadius // 使用估计半径
);
// 3. 定义相机相对于边界球的偏移量 (Heading, Pitch, Range)
const cameraOffset = new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(0.0), // Heading (航向), 0度朝北
Cesium.Math.toRadians(-45.0), // Pitch (俯仰), -45度斜向下看
manualBoundingSphere.radius + extraDistance // Range (距离) = 估计半径 + 额外距离
);
// 4. 执行飞行动作
viewer.camera.flyToBoundingSphere(
manualBoundingSphere,
{
offset: cameraOffset,
duration: 3.0 // 飞行时间
}
);
console.log(`飞往模型中心,距离模型中心 ${(manualBoundingSphere.radius + extraDistance) / 1000} 公里。`);
} else if (viewer && entityInstance) {
// 作为一个简单的 fallback 选项
viewer.flyTo(entityInstance, { duration: 3.0 });
console.warn("模型位置属性无效,执行默认 flyTo。");
} else {
console.error("Cesium Viewer 或模型实例无效。");
}
};
// 获取当前视角信息的方法
const getCurrentViewInfo = () => {
if (!viewer) {
console.warn('Viewer 尚未初始化');
return null;
}
const camera = viewer.camera;
const position = camera.position; // 世界笛卡尔坐标系 x,y,z
const cartographic = Cesium.Cartographic.fromCartesian(position);
console.log(
{
longitude: Cesium.Math.toDegrees(cartographic.longitude),
latitude: Cesium.Math.toDegrees(cartographic.latitude),
height: cartographic.height,
heading: Cesium.Math.toDegrees(camera.heading),
pitch: Cesium.Math.toDegrees(camera.pitch),
roll: Cesium.Math.toDegrees(camera.roll)
}
)
};
// 暴露方法和数据给父组件使用
defineExpose({
switchView,
dataWithModels,
switchModelView,
setElectricalBoxClickCallback
});
onBeforeUnmount(() => {
// 销毁 Viewer,释放 WebGL 资源
if (viewer) {
viewer.destroy();
viewer = null;
console.log('Cesium Viewer 已销毁');
}
});
</script>
<style scoped>
.cesium-container {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
.btn-group {
position: absolute;
top: 10px;
z-index: 1000;
display: flex;
gap: 12rpx;
}
</style>