<template>
|
<div class="relative w-full h-screen font-inter bg-dark overflow-hidden">
|
<!-- Cesium container -->
|
<!-- <div ref="cesiumContainer" class="w-full h-full" /> -->
|
<CesiumMap></CesiumMap>
|
<!-- Floating help button -->
|
|
<!-- Help modal -->
|
<!-- <div
|
id="helpModal"
|
class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 opacity-0 pointer-events-none transition-opacity duration-300"
|
@click.self="closeHelpModal"
|
>
|
<div
|
class="bg-dark border border-primary/30 rounded-lg shadow-2xl shadow-primary/20 p-6 max-w-md w-full mx-4 transform scale-95 transition-transform duration-300"
|
>
|
<div class="flex justify-between items-center mb-4">
|
<h2 class="text-xl font-bold text-white">键盘控制帮助</h2>
|
<button
|
id="closeHelpBtn"
|
class="text-gray-400 hover:text-white transition-colors"
|
@click="closeHelpModal"
|
>
|
<i class="fa fa-times text-xl" />
|
</button>
|
</div>
|
|
<div class="text-gray-300 space-y-4">
|
<p>使用以下键控制相机视角:</p>
|
<div class="grid grid-cols-2 gap-4">
|
<div>
|
<div class="font-semibold text-white">移动</div>
|
<ul class="list-disc list-inside mt-2 text-sm space-y-1">
|
<li>W - 向前</li>
|
<li>S - 向后</li>
|
<li>A - 向左</li>
|
<li>D - 向右</li>
|
<li>Q - 上升</li>
|
<li>E - 下降</li>
|
</ul>
|
</div>
|
<div>
|
<div class="font-semibold text-white">旋转</div>
|
<ul class="list-disc list-inside mt-2 text-sm space-y-1">
|
<li>↑ - 向上看</li>
|
<li>↓ - 向下看</li>
|
<li>← - 向左转</li>
|
<li>→ - 向右转</li>
|
<li>R - 仰视</li>
|
<li>F - 平视</li>
|
<li>V - 俯视</li>
|
</ul>
|
</div>
|
</div>
|
<div>
|
<div class="font-semibold text-white">其他</div>
|
<ul class="list-disc list-inside mt-2 text-sm">
|
<li>空格 - 重置视图到初始位置</li>
|
<li>1 - 切换到3D视图</li>
|
<li>2 - 切换到2D视图</li>
|
<li>3 - 切换到哥伦布视图</li>
|
</ul>
|
</div>
|
<p class="text-sm text-gray-400">
|
提示:按住Shift键可以加速移动,按住Ctrl键可以减速移动。
|
</p>
|
</div>
|
|
<div class="mt-6">
|
<button
|
id="gotItBtn"
|
class="w-full bg-primary hover:bg-primary/90 text-white py-2 rounded-md transition-colors"
|
@click="closeHelpModal"
|
>
|
明白了
|
</button>
|
</div>
|
</div>
|
</div> -->
|
</div>
|
</template>
|
|
<script>
|
import CesiumMap from "../../utils/components/cesium-map";
|
import { pointArr } from './aaa.js'
|
|
export default {
|
name: "CesiumCameraControl",
|
components:{
|
CesiumMap
|
},
|
data() {
|
return {
|
viewer: null,
|
// 相机移动速率控制
|
moveSpeed: 1,
|
speedMultiplier: 5,
|
slowMultiplier: 0.2,
|
// 键盘状态
|
keyState: {
|
w: false,
|
s: false,
|
a: false,
|
d: false,
|
q: false,
|
e: false,
|
up: false,
|
down: false,
|
left: false,
|
right: false,
|
shift: false,
|
ctrl: false,
|
space: false,
|
},
|
// 相机初始状态
|
initialCameraPosition: null,
|
initialCameraOrientation: null,
|
animationFrameId: null,
|
};
|
},
|
mounted() {
|
this.initCesium();
|
this.addKeyboardListeners();
|
this.updateCameraMovement();
|
this.addModel()
|
},
|
beforeDestroy() {
|
// 清理 RAF 和事件
|
cancelAnimationFrame(this.animationFrameId);
|
document.removeEventListener("keydown", this.onKeyDown);
|
document.removeEventListener("keyup", this.onKeyUp);
|
},
|
methods: {
|
addModel(){
|
pointArr.forEach(entity => {
|
viewer.entities.add({
|
position:Cesium.Cartesian3.fromDegrees(entity.longitude, entity.latitude, entity.altitude),
|
point: {
|
pixelSize: 10,
|
color: Cesium.Color.YELLOW,
|
outlineColor: Cesium.Color.WHITE,
|
outlineWidth: 2,
|
}
|
})
|
})
|
viewer.entities.add({
|
position: Cesium.Cartesian3.fromDegrees(124.99140002766548, 46.5903479189505, 0),
|
model: {
|
uri: "/Model/tower.glb",
|
scale: 1000,
|
minimumPixelSize: 50,
|
},
|
});
|
},
|
/** 初始化 Cesium Viewer */
|
initCesium() {
|
console.log('11111111111111')
|
viewer.camera.setView({
|
destination: Cesium.Cartesian3.fromDegrees(124.9914000, 46.58566, 500),
|
orientation: {
|
heading: Cesium.Math.toRadians(0.0),
|
pitch: Cesium.Math.toRadians(-30),
|
roll: Cesium.Math.toRadians(0.0),
|
},
|
});
|
this.initialCameraPosition = viewer.camera.position.clone();
|
this.initialCameraOrientation = { ...viewer.camera.orientation };
|
},
|
|
/** 监听键盘事件 */
|
addKeyboardListeners() {
|
document.addEventListener("keydown", this.onKeyDown);
|
document.addEventListener("keyup", this.onKeyUp);
|
},
|
onKeyDown(event) {
|
const key = event.key.toLowerCase();
|
switch (key) {
|
case "w":
|
this.keyState.w = true;
|
break;
|
case "s":
|
this.keyState.s = true;
|
break;
|
case "a":
|
this.keyState.a = true;
|
break;
|
case "d":
|
this.keyState.d = true;
|
break;
|
case "q":
|
this.keyState.q = true;
|
break;
|
case "e":
|
this.keyState.e = true;
|
break;
|
case " ":
|
this.keyState.space = true;
|
event.preventDefault();
|
break;
|
case "shift":
|
this.keyState.shift = true;
|
break;
|
case "control":
|
this.keyState.ctrl = true;
|
event.preventDefault();
|
break;
|
}
|
|
// 箭头键 & 视图模式切换
|
switch (event.key) {
|
case "ArrowUp":
|
this.keyState.up = true;
|
break;
|
case "ArrowDown":
|
this.keyState.down = true;
|
break;
|
case "ArrowLeft":
|
this.keyState.left = true;
|
break;
|
case "ArrowRight":
|
this.keyState.right = true;
|
break;
|
case "1":
|
viewer.scene.mode = Cesium.SceneMode.SCENE3D;
|
break;
|
case "2":
|
viewer.scene.mode = Cesium.SceneMode.SCENE2D;
|
break;
|
case "3":
|
viewer.scene.mode = Cesium.SceneMode.COLUMBUS_VIEW;
|
break;
|
}
|
},
|
onKeyUp(event) {
|
const key = event.key.toLowerCase();
|
switch (key) {
|
case "w":
|
this.keyState.w = false;
|
break;
|
case "s":
|
this.keyState.s = false;
|
break;
|
case "a":
|
this.keyState.a = false;
|
break;
|
case "d":
|
this.keyState.d = false;
|
break;
|
case "q":
|
this.keyState.q = false;
|
break;
|
case "e":
|
this.keyState.e = false;
|
break;
|
case " ":
|
this.keyState.space = false;
|
this.resetView();
|
break;
|
case "shift":
|
this.keyState.shift = false;
|
break;
|
case "control":
|
this.keyState.ctrl = false;
|
break;
|
}
|
|
switch (event.key) {
|
case "ArrowUp":
|
this.keyState.up = false;
|
break;
|
case "ArrowDown":
|
this.keyState.down = false;
|
break;
|
case "ArrowLeft":
|
this.keyState.left = false;
|
break;
|
case "ArrowRight":
|
this.keyState.right = false;
|
break;
|
case "r":
|
this.resetPitch(30);
|
break;
|
case "f":
|
this.resetPitch();
|
break;
|
case "v":
|
this.resetPitch(-30);
|
break;
|
}
|
},
|
|
/** 循环更新相机移动 */
|
updateCameraMovement() {
|
const camera = viewer?.camera;
|
if (!camera) return;
|
|
let currentSpeed = this.moveSpeed;
|
if (this.keyState.shift) currentSpeed *= this.speedMultiplier;
|
if (this.keyState.ctrl) currentSpeed *= this.slowMultiplier;
|
|
const deltaTime = 1.0; // 简化时间增量
|
|
// 平移
|
if (this.keyState.w) this.uavMoveForward(1);
|
if (this.keyState.s) this.uavMoveForward(-1);
|
if (this.keyState.a) camera.moveLeft(currentSpeed * deltaTime);
|
if (this.keyState.d) camera.moveRight(currentSpeed * deltaTime);
|
if (this.keyState.q) camera.moveUp(currentSpeed * deltaTime);
|
if (this.keyState.e) camera.moveDown(currentSpeed * deltaTime);
|
|
// 旋转
|
let rotSpeed = 1;
|
if (this.keyState.shift) rotSpeed *= this.speedMultiplier;
|
if (this.keyState.ctrl) rotSpeed *= this.slowMultiplier;
|
|
const rotateAmount = Cesium.Math.toRadians(0.5 * rotSpeed);
|
if (this.keyState.up) camera.lookUp(rotateAmount);
|
if (this.keyState.down) camera.lookDown(rotateAmount);
|
if (this.keyState.left) camera.lookLeft(rotateAmount);
|
if (this.keyState.right) camera.lookRight(rotateAmount);
|
|
// 始终保持 roll 为 0
|
this.resetRoll();
|
|
// 下一帧
|
this.animationFrameId = requestAnimationFrame(this.updateCameraMovement);
|
},
|
|
/** 根据当前相机方位向前/后移动(球面坐标) */
|
uavMoveForward(speed) {
|
const camera = viewer.camera;
|
const cartographic = Cesium.Cartographic.fromCartesian(camera.position);
|
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
|
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
|
const height = cartographic.height;
|
|
const newLonLat = this.calculateDestinationPoint(
|
longitude,
|
latitude,
|
camera.heading,
|
speed
|
);
|
const newPosition = Cesium.Cartesian3.fromDegrees(
|
newLonLat.lon,
|
newLonLat.lat,
|
height
|
);
|
|
camera.setView({
|
destination: newPosition,
|
orientation: {
|
heading: camera.heading,
|
pitch: camera.pitch,
|
roll: 0.0,
|
},
|
});
|
},
|
|
/** 球面坐标正解 */
|
calculateDestinationPoint(lon1, lat1, bearing, distance, radius = 6371000) {
|
const toRadians = (deg) => (deg * Math.PI) / 180;
|
const toDegrees = (rad) => (rad * 180) / Math.PI;
|
|
const lat1Rad = toRadians(lat1);
|
const lon1Rad = toRadians(lon1);
|
const bearingRad = bearing;
|
const angularDistance = distance / radius;
|
|
const lat2Rad = Math.asin(
|
Math.sin(lat1Rad) * Math.cos(angularDistance) +
|
Math.cos(lat1Rad) * Math.sin(angularDistance) * Math.cos(bearingRad)
|
);
|
|
const lon2Rad =
|
lon1Rad +
|
Math.atan2(
|
Math.sin(bearingRad) * Math.sin(angularDistance) * Math.cos(lat1Rad),
|
Math.cos(angularDistance) - Math.sin(lat1Rad) * Math.sin(lat2Rad)
|
);
|
|
const lon2RadNormalized = ((lon2Rad + 3 * Math.PI) % (2 * Math.PI)) - Math.PI;
|
|
return { lon: toDegrees(lon2RadNormalized), lat: toDegrees(lat2Rad) };
|
},
|
|
resetRoll() {
|
const camera = viewer.camera;
|
camera.setView({
|
destination: camera.destination,
|
orientation: {
|
heading: camera.heading,
|
pitch: camera.pitch,
|
roll: 0.0,
|
},
|
});
|
},
|
|
resetPitch(amount = 0.1) {
|
const camera = viewer.camera;
|
camera.setView({
|
destination: camera.destination,
|
orientation: {
|
heading: camera.heading,
|
pitch: Cesium.Math.toRadians(amount),
|
roll: 0,
|
},
|
});
|
},
|
|
resetView() {
|
viewer.camera.setView({
|
destination: this.initialCameraPosition,
|
orientation: this.initialCameraOrientation,
|
});
|
},
|
|
/** Help modal 控制 */
|
openHelpModal() {
|
const modal = document.getElementById("helpModal");
|
modal.classList.remove("opacity-0", "pointer-events-none");
|
modal.querySelector("div").classList.remove("scale-95");
|
modal.querySelector("div").classList.add("scale-100");
|
},
|
closeHelpModal() {
|
const modal = document.getElementById("helpModal");
|
modal.classList.add("opacity-0", "pointer-events-none");
|
modal.querySelector("div").classList.remove("scale-100");
|
modal.querySelector("div").classList.add("scale-95");
|
},
|
},
|
};
|
</script>
|
|
<style scoped>
|
/* 覆盖 Cesium 默认样式 */
|
.cesium-widget-credits {
|
display: none !important;
|
}
|
|
/* 自定义样式类,用 Tailwind 工具类拼接 */
|
.keyboard-key {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
width: 2.5rem; /* w-10 */
|
height: 2.5rem; /* h-10 */
|
background-color: rgba(31, 41, 55, 0.8); /* bg-dark/80 */
|
color: white;
|
border-radius: 0.375rem;
|
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.1);
|
transition: all 0.2s;
|
border: 2px solid rgba(59, 130, 246, 0.3);
|
}
|
|
.keyboard-key:hover {
|
background-color: #3b82f6;
|
}
|
|
.keyboard-key:active {
|
transform: scale(0.95);
|
}
|
|
.keyboard-key-large {
|
width: 4rem; /* w-16 */
|
}
|
|
.control-panel {
|
position: absolute;
|
bottom: 1rem;
|
left: 1rem;
|
background-color: rgba(31, 41, 55, 0.8);
|
backdrop-filter: blur(6px);
|
color: white;
|
padding: 1rem;
|
border-radius: 0.5rem;
|
box-shadow: 0 20px 25px -5px rgba(59, 130, 246, 0.1);
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
max-width: 300px;
|
transition: all 0.3s;
|
}
|
|
.control-panel:hover {
|
box-shadow: 0 20px 25px -5px rgba(59, 130, 246, 0.2);
|
}
|
|
.control-item {
|
display: flex;
|
align-items: center;
|
margin-bottom: 0.75rem;
|
opacity: 0.8;
|
transition: opacity 0.2s;
|
}
|
|
.control-item:hover {
|
opacity: 1;
|
}
|
|
.control-label {
|
margin-left: 0.75rem;
|
font-size: 0.875rem;
|
}
|
|
.floating-btn {
|
position: absolute;
|
top: 1rem;
|
right: 1rem;
|
background-color: #3b82f6; /* bg-primary/90 */
|
color: white;
|
width: 3rem;
|
height: 3rem;
|
border-radius: 9999px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.4);
|
transition: all 0.3s;
|
}
|
|
.floating-btn:hover {
|
background-color: #2563eb;
|
transform: scale(1.1);
|
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.5);
|
}
|
</style>
|
|
<style>
|
.cesium-viewer-bottom {
|
display: none;
|
}
|
</style>
|