cesium地图添加画空域的工具类
This commit is contained in:
parent
e12886ae71
commit
16bd368d47
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@ -2,6 +2,7 @@
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
|
||||
49
src/views/cesiums/App.vue
Normal file
49
src/views/cesiums/App.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<!-- src/App.vue -->
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- <header class="app-header">
|
||||
<h1>🌉 重庆三维地图系统</h1>
|
||||
<p>基于 Vue 3 + Cesium 的三维地理信息系统</p>
|
||||
</header> -->
|
||||
<main class="app-main">
|
||||
<CesiumViewer />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CesiumViewer from './CesiumViewer.vue';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.app-header p {
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
height: calc(100vh - 80px);
|
||||
}
|
||||
</style>
|
||||
168
src/views/cesiums/CesiumToolPanel.vue
Normal file
168
src/views/cesiums/CesiumToolPanel.vue
Normal file
@ -0,0 +1,168 @@
|
||||
<!-- src/components/CesiumToolPanel.vue -->
|
||||
<template>
|
||||
<div class="tool-panel">
|
||||
<div class="panel-section">
|
||||
<h3>🌍 视角控制</h3>
|
||||
<div class="button-group">
|
||||
<button
|
||||
v-for="option in viewOptions"
|
||||
:key="option.value"
|
||||
:class="['view-btn', { active: currentView === option.value }]"
|
||||
@click="$emit('view-change', option.value)"
|
||||
>
|
||||
<span class="icon">{{ option.icon }}</span>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h3>📍 地标管理</h3>
|
||||
<div class="button-group">
|
||||
<button
|
||||
v-for="option in landmarkOptions"
|
||||
:key="option.value"
|
||||
class="tool-btn"
|
||||
@click="$emit('add-landmarks', option.value)"
|
||||
>
|
||||
<span class="icon">{{ option.icon }}</span>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
<button class="tool-btn clear-btn" @click="$emit('clear-landmarks')">
|
||||
🗑️ 清除地标
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h3>📊 位置信息</h3>
|
||||
<div class="info-display">
|
||||
<div class="info-item">
|
||||
<label>经度:</label>
|
||||
<span>{{ cameraInfo.longitude }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>纬度:</label>
|
||||
<span>{{ cameraInfo.latitude }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>高度:</label>
|
||||
<span>{{ cameraInfo.height }} 米</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
currentView: string;
|
||||
cameraInfo: any;
|
||||
viewOptions: any[];
|
||||
landmarkOptions: any[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
'view-change': [value: string];
|
||||
'add-landmarks': [value: string];
|
||||
'clear-landmarks': [value: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(42, 42, 42, 0.95);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
min-width: 250px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.panel-section h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.view-btn, .tool-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.view-btn:hover, .tool-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: rgba(255, 77, 79, 0.2);
|
||||
border-color: rgba(255, 77, 79, 0.4);
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: rgba(255, 77, 79, 0.3);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-display {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
249
src/views/cesiums/CesiumViewer.vue
Normal file
249
src/views/cesiums/CesiumViewer.vue
Normal file
@ -0,0 +1,249 @@
|
||||
<!-- src/components/CesiumViewer.vue -->
|
||||
<template>
|
||||
<div class="cesium-container">
|
||||
<!-- Cesium 容器 -->
|
||||
<div id="cesiumContainer" ref="cesiumContainer"></div>
|
||||
|
||||
<!-- 工具控制面板 -->
|
||||
<CesiumToolPanel
|
||||
v-if="isInitialized"
|
||||
:current-view="currentView"
|
||||
:camera-info="cameraInfo"
|
||||
:view-options="viewOptions"
|
||||
:landmark-options="landmarkOptions"
|
||||
@view-change="setView"
|
||||
@add-landmarks="addLandmarks"
|
||||
@clear-landmarks="clearLandmarks"
|
||||
/>
|
||||
|
||||
<!-- 模型控制面板 -->
|
||||
<ModelControlPanel
|
||||
v-if="isInitialized"
|
||||
:models="models"
|
||||
:selected-model="selectedModel"
|
||||
:is-model-loading="isModelLoading"
|
||||
:predefined-models="predefinedModels"
|
||||
@load-landmarks="loadChongqingLandmarks"
|
||||
@add-model="addPredefinedModel"
|
||||
@select-model="selectModel"
|
||||
@deselect-model="deselectModel"
|
||||
@update-scale="updateModelScale"
|
||||
@update-position="updateModelPosition"
|
||||
@remove-model="removeModel"
|
||||
@fly-to-model="flyToModel"
|
||||
@clear-models="clearAllModels"
|
||||
/>
|
||||
|
||||
<!-- 坐标拾取面板 -->
|
||||
<CoordinatePickerPanel
|
||||
v-if="isInitialized"
|
||||
:is-picker-enabled="isPickerEnabled"
|
||||
:pick-history="pickHistory"
|
||||
:last-pick-result="lastPickResult"
|
||||
@toggle-picker="togglePicker"
|
||||
@clear-history="clearHistory"
|
||||
@copy-coordinate="copyCoordinate"
|
||||
@fly-to-coordinate="flyToCoordinate"
|
||||
/>
|
||||
|
||||
<!-- 绘图工具面板 -->
|
||||
<DrawingToolPanel
|
||||
v-if="isInitialized"
|
||||
:is-drawing="isDrawing"
|
||||
:current-drawing-type="currentDrawingType"
|
||||
:drawings="drawings"
|
||||
:selected-drawing="selectedDrawing"
|
||||
:drawing-options="drawingOptions"
|
||||
:get-drawing-status="getDrawingStatus"
|
||||
@start-circle-drawing="startCircleDrawing"
|
||||
@start-polygon-drawing="startPolygonDrawing"
|
||||
@cancel-drawing="cancelDrawing"
|
||||
@select-drawing="selectDrawing"
|
||||
@deselect-drawing="deselectDrawing"
|
||||
@remove-drawing="removeDrawing"
|
||||
@clear-drawings="clearAllDrawings"
|
||||
@fly-to-drawing="flyToDrawing"
|
||||
@update-circle-option="updateCircleOption"
|
||||
@update-polygon-option="updatePolygonOption"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useCesium } from './components/useCesium';
|
||||
import { useCesiumTools } from './components/useCesiumTools';
|
||||
import CesiumToolPanel from './CesiumToolPanel.vue';
|
||||
import { useCoordinatePicker } from './components/useCoordinatePicker';
|
||||
import { useModelManager } from './components/useModelManager';
|
||||
import { useDrawingManager } from './components/useDrawingManager';
|
||||
import ModelControlPanel from './ModelControlPanel.vue';
|
||||
import CoordinatePickerPanel from './CoordinatePickerPanel.vue';
|
||||
import DrawingToolPanel from './DrawingToolPanel.vue';
|
||||
|
||||
|
||||
const cesiumContainer = ref<HTMLElement>();
|
||||
|
||||
// 初始化 Cesium
|
||||
// const { viewer, viewTool, markerTool, modelTool, coordinatePicker, isInitialized } = useCesium('cesiumContainer');
|
||||
// 初始化 Cesium
|
||||
const { viewer, viewTool, markerTool, modelTool, coordinatePicker, drawingTool, isInitialized } = useCesium('cesiumContainer');
|
||||
|
||||
// 初始化工具
|
||||
const {
|
||||
currentView,
|
||||
cameraInfo,
|
||||
viewOptions,
|
||||
landmarkOptions,
|
||||
setView,
|
||||
addLandmarks,
|
||||
clearLandmarks,
|
||||
startCameraTracking
|
||||
} = useCesiumTools(viewTool, markerTool);
|
||||
|
||||
// 初始化模型管理
|
||||
const {
|
||||
models,
|
||||
selectedModel,
|
||||
isModelLoading,
|
||||
predefinedModels,
|
||||
loadChongqingLandmarks,
|
||||
loadPredefinedModel,
|
||||
selectModel,
|
||||
deselectModel,
|
||||
updateModelScale,
|
||||
updateModelPosition,
|
||||
removeModel,
|
||||
clearAllModels,
|
||||
flyToModel
|
||||
} = useModelManager(modelTool);
|
||||
|
||||
// 初始化坐标拾取
|
||||
const {
|
||||
isPickerEnabled,
|
||||
pickHistory,
|
||||
lastPickResult,
|
||||
enablePicker,
|
||||
disablePicker,
|
||||
togglePicker,
|
||||
clearHistory,
|
||||
copyToClipboard,
|
||||
formatCoordinate
|
||||
} = useCoordinatePicker(coordinatePicker);
|
||||
|
||||
// 初始化绘图管理
|
||||
const {
|
||||
isDrawing,
|
||||
currentDrawingType,
|
||||
drawings,
|
||||
selectedDrawing,
|
||||
drawingInfo,
|
||||
drawingOptions,
|
||||
startCircleDrawing,
|
||||
startPolygonDrawing,
|
||||
cancelDrawing,
|
||||
selectDrawing,
|
||||
deselectDrawing,
|
||||
removeDrawing,
|
||||
clearAllDrawings,
|
||||
flyToDrawing,
|
||||
updateDrawingOptions,
|
||||
getDrawingStatus,
|
||||
getDrawingInfoText
|
||||
} = useDrawingManager(drawingTool); // 直接传递 drawingTool ref
|
||||
|
||||
|
||||
// 添加预定义模型
|
||||
const addPredefinedModel = async (modelType: string, position: any) => {
|
||||
const modelId = `model_${Date.now()}`;
|
||||
await loadPredefinedModel(modelId, modelType as any, position);
|
||||
};
|
||||
|
||||
// 复制坐标
|
||||
const copyCoordinate = (result: any) => {
|
||||
copyToClipboard(result);
|
||||
// 可以在这里添加复制成功的提示
|
||||
console.log('坐标已复制:', formatCoordinate(result));
|
||||
};
|
||||
|
||||
// 飞向坐标
|
||||
const flyToCoordinate = (result: any) => {
|
||||
if (viewTool.value) {
|
||||
viewTool.value.flyTo(
|
||||
result.longitude,
|
||||
result.latitude,
|
||||
result.height + 100, // 在坐标上方100米查看
|
||||
1.5
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新圆形选项
|
||||
const updateCircleOption = (key: string, value: any) => {
|
||||
updateDrawingOptions('circle', { [key]: value });
|
||||
};
|
||||
|
||||
// 更新多边形选项
|
||||
const updatePolygonOption = (key: string, value: any) => {
|
||||
updateDrawingOptions('polygon', { [key]: value });
|
||||
};
|
||||
|
||||
// 复制绘图信息
|
||||
const copyDrawingInfo = () => {
|
||||
if (drawingInfo.value) {
|
||||
const text = getDrawingInfoText();
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('绘图信息已复制到剪贴板');
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 飞向选中的绘图
|
||||
const flyToSelectedDrawing = () => {
|
||||
if (selectedDrawing.value && drawingTool.value) {
|
||||
drawingTool.value.flyToDrawing(selectedDrawing.value);
|
||||
}
|
||||
};
|
||||
|
||||
// const copyDrawingInfo = () => {
|
||||
// if (drawingInfo.value) {
|
||||
// const text = getDrawingInfoText();
|
||||
// navigator.clipboard.writeText(text).then(() => {
|
||||
// console.log('绘图信息已复制到剪贴板');
|
||||
// }).catch(err => {
|
||||
// console.error('复制失败:', err);
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
// // 飞向选中的绘图
|
||||
// const flyToSelectedDrawing = () => {
|
||||
// if (selectedDrawing.value && drawingTool.value) {
|
||||
// drawingTool.value.flyToDrawing(selectedDrawing.value);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// 开始跟踪相机信息
|
||||
setTimeout(() => {
|
||||
startCameraTracking();
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cesium-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#cesiumContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
437
src/views/cesiums/CoordinatePickerPanel.vue
Normal file
437
src/views/cesiums/CoordinatePickerPanel.vue
Normal file
@ -0,0 +1,437 @@
|
||||
<!-- src/components/CoordinatePickerPanel.vue -->
|
||||
<template>
|
||||
<div class="coordinate-picker-panel">
|
||||
<div class="panel-header">
|
||||
<h3>🎯 坐标拾取工具</h3>
|
||||
<div class="toggle-switch">
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-bind="isPickerEnabled"
|
||||
@change="togglePicker"
|
||||
>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<span class="toggle-label">
|
||||
{{ isPickerEnabled ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section" v-if="lastPickResult">
|
||||
<h4>最新拾取</h4>
|
||||
<div class="coordinate-display">
|
||||
<div class="coordinate-item">
|
||||
<label>经度:</label>
|
||||
<span class="value">{{ lastPickResult.longitude.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="coordinate-item">
|
||||
<label>纬度:</label>
|
||||
<span class="value">{{ lastPickResult.latitude.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="coordinate-item">
|
||||
<label>高程:</label>
|
||||
<span class="value">{{ lastPickResult.height.toFixed(2) }} 米</span>
|
||||
</div>
|
||||
<div class="coordinate-item" v-if="lastPickResult.hasTerrain">
|
||||
<label>地形高程:</label>
|
||||
<span class="value">{{ lastPickResult.terrainHeight?.toFixed(2) }} 米</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button @click="copyLastCoordinate" class="action-btn copy-btn">
|
||||
📋 复制
|
||||
</button>
|
||||
<button @click="flyToLastCoordinate" class="action-btn fly-btn">
|
||||
✈️ 飞向
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section" v-if="pickHistory.length > 0">
|
||||
<div class="history-header">
|
||||
<h4>历史记录 ({{ pickHistory.length }})</h4>
|
||||
<button @click="clearHistory" class="clear-btn" title="清除历史">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
<div class="history-list">
|
||||
<div
|
||||
v-for="(result, index) in pickHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
@click="selectHistoryItem(result)"
|
||||
>
|
||||
<div class="history-coordinate">
|
||||
<div class="coord-line">
|
||||
{{ result.longitude.toFixed(6) }}, {{ result.latitude.toFixed(6) }}
|
||||
</div>
|
||||
<div class="coord-height">
|
||||
高程: {{ result.height.toFixed(2) }}米
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-actions">
|
||||
<button @click.stop="copyCoordinate(result)" title="复制">📋</button>
|
||||
<button @click.stop="flyToCoordinate(result)" title="飞向">✈️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section" v-else>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🎯</div>
|
||||
<p>右键点击地图拾取坐标</p>
|
||||
<small>启用拾取功能后,在地图上右键点击即可获取经纬度和高程信息</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section help-section">
|
||||
<h4>使用说明</h4>
|
||||
<ul class="help-list">
|
||||
<li>✅ 右键点击地图任意位置</li>
|
||||
<li>✅ 自动获取经纬度和高程</li>
|
||||
<li>✅ 支持地形高程计算</li>
|
||||
<li>✅ 点击坐标可快速复制</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue';
|
||||
import type { PickResult } from './components/CoordinatePicker';
|
||||
|
||||
const props = defineProps<{
|
||||
isPickerEnabled: boolean;
|
||||
pickHistory: PickResult[];
|
||||
lastPickResult: PickResult | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'toggle-picker': [];
|
||||
'clear-history': [];
|
||||
'copy-coordinate': [result: PickResult];
|
||||
'fly-to-coordinate': [result: PickResult];
|
||||
}>();
|
||||
|
||||
const togglePicker = () => {
|
||||
emit('toggle-picker');
|
||||
};
|
||||
|
||||
const clearHistory = () => {
|
||||
emit('clear-history');
|
||||
};
|
||||
|
||||
const copyLastCoordinate = () => {
|
||||
if (props.lastPickResult) {
|
||||
emit('copy-coordinate', props.lastPickResult);
|
||||
}
|
||||
};
|
||||
|
||||
const flyToLastCoordinate = () => {
|
||||
if (props.lastPickResult) {
|
||||
emit('fly-to-coordinate', props.lastPickResult);
|
||||
}
|
||||
};
|
||||
|
||||
const selectHistoryItem = (result: PickResult) => {
|
||||
// 可以选择历史项,这里可以扩展功能
|
||||
console.log('Selected history item:', result);
|
||||
};
|
||||
|
||||
const copyCoordinate = (result: PickResult) => {
|
||||
emit('copy-coordinate', result);
|
||||
};
|
||||
|
||||
const flyToCoordinate = (result: PickResult) => {
|
||||
emit('fly-to-coordinate', result);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.coordinate-picker-panel {
|
||||
position: absolute;
|
||||
bottom: 150px;
|
||||
/* right: 20px; */
|
||||
left: 20px;
|
||||
background: rgba(42, 42, 42, 0.95);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
min-width: 280px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.panel-section h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.coordinate-display {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #52c41a;
|
||||
}
|
||||
|
||||
.coordinate-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.coordinate-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.coordinate-item label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.coordinate-item .value {
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(24, 144, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(24, 144, 255, 0.5);
|
||||
}
|
||||
|
||||
.fly-btn {
|
||||
background: rgba(82, 196, 26, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fly-btn:hover {
|
||||
background: rgba(82, 196, 26, 0.5);
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.history-header h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: rgba(255, 77, 79, 0.2);
|
||||
}
|
||||
|
||||
.history-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
margin-bottom: 5px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.history-coordinate {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.coord-line {
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.coord-height {
|
||||
font-size: 10px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.history-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.history-actions button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.history-actions button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty-state small {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.help-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.help-list li {
|
||||
font-size: 11px;
|
||||
margin-bottom: 6px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.help-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
407
src/views/cesiums/DrawingToolPanel.vue
Normal file
407
src/views/cesiums/DrawingToolPanel.vue
Normal file
@ -0,0 +1,407 @@
|
||||
<!-- src/components/DrawingToolPanel.vue -->
|
||||
<template>
|
||||
<div class="drawing-tool-panel">
|
||||
<div class="panel-header">
|
||||
<h3>🛡️ 空域绘制工具</h3>
|
||||
<div class="drawing-status" :class="{ active: isDrawing }">
|
||||
{{ getDrawingStatus() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h4>绘制工具</h4>
|
||||
<div class="drawing-buttons">
|
||||
<button
|
||||
class="drawing-btn circle-btn"
|
||||
:class="{ active: isDrawing && currentDrawingType === 'circle' }"
|
||||
@click="startCircleDrawing"
|
||||
:disabled="isDrawing && currentDrawingType !== 'circle'"
|
||||
>
|
||||
⭕ 绘制圆形
|
||||
</button>
|
||||
<button
|
||||
class="drawing-btn polygon-btn"
|
||||
:class="{ active: isDrawing && currentDrawingType === 'polygon' }"
|
||||
@click="startPolygonDrawing"
|
||||
:disabled="isDrawing && currentDrawingType !== 'polygon'"
|
||||
>
|
||||
🔷 绘制多边形
|
||||
</button>
|
||||
<button
|
||||
class="drawing-btn cancel-btn"
|
||||
@click="cancelDrawing"
|
||||
:disabled="!isDrawing"
|
||||
>
|
||||
❌ 取消绘制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空域信息显示 -->
|
||||
<div class="panel-section info-section" v-if="drawingInfo">
|
||||
<h4>空域详细信息</h4>
|
||||
<div class="info-display">
|
||||
<pre class="info-text">{{ getDrawingInfoText() }}</pre>
|
||||
<div class="info-actions">
|
||||
<button @click="copyDrawingInfo" class="action-btn copy-btn">
|
||||
📋 复制信息
|
||||
</button>
|
||||
<button @click="flyToSelectedDrawing" class="action-btn fly-btn">
|
||||
✈️ 飞向空域
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section" v-if="drawings.size > 0">
|
||||
<div class="drawings-header">
|
||||
<h4>已绘制空域 ({{ drawings.size }})</h4>
|
||||
<button @click="clearAllDrawings" class="clear-btn" title="清除所有">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
<div class="drawings-list">
|
||||
<div
|
||||
v-for="[id, drawing] in drawings"
|
||||
:key="id"
|
||||
:class="['drawing-item', { active: selectedDrawing === id }]"
|
||||
@click="selectDrawing(id)"
|
||||
>
|
||||
<div class="drawing-info">
|
||||
<div class="drawing-type">
|
||||
{{ drawing.type === 'circle' ? '⭕' : '🔷' }}
|
||||
{{ drawing.type === 'circle' ? '圆形空域' : '多边形空域' }}
|
||||
</div>
|
||||
<div class="drawing-props">
|
||||
<span v-if="drawing.type === 'circle'">
|
||||
半径: {{ (getCircleRadius(drawing) / 1000).toFixed(2) }}km
|
||||
</span>
|
||||
<span v-else>
|
||||
面积: {{ (drawing.properties.area / 1000000).toFixed(2) }}km²
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawing-actions">
|
||||
<button @click.stop="flyToDrawing(id)" title="飞向">✈️</button>
|
||||
<button @click.stop="removeDrawing(id)" title="删除">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section options-section">
|
||||
<h4>样式设置</h4>
|
||||
<div class="options-tabs">
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === 'circle' }"
|
||||
@click="activeTab = 'circle'"
|
||||
>
|
||||
圆形样式
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === 'polygon' }"
|
||||
@click="activeTab = 'polygon'"
|
||||
>
|
||||
多边形样式
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="options-content" v-if="activeTab === 'circle'">
|
||||
<div class="option-group">
|
||||
<label>填充颜色:</label>
|
||||
<input
|
||||
type="color"
|
||||
:value="colorToHex(drawingOptions.circle.color)"
|
||||
@input="updateCircleOption('color', hexToColor(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>边框颜色:</label>
|
||||
<input
|
||||
type="color"
|
||||
:value="colorToHex(drawingOptions.circle.outlineColor)"
|
||||
@input="updateCircleOption('outlineColor', hexToColor(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>透明度:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
:value="drawingOptions.circle.color.alpha * 100"
|
||||
@input="updateCircleOption('color', drawingOptions.circle.color.withAlpha(parseInt(($event.target as HTMLInputElement).value) / 100))"
|
||||
>
|
||||
<span>{{ Math.round(drawingOptions.circle.color.alpha * 100) }}%</span>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>边框宽度:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
:value="drawingOptions.circle.outlineWidth"
|
||||
@input="updateCircleOption('outlineWidth', parseInt(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
<span>{{ drawingOptions.circle.outlineWidth }}px</span>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>拉伸高度:</label>
|
||||
<input
|
||||
type="number"
|
||||
:value="drawingOptions.circle.extrudedHeight"
|
||||
@input="updateCircleOption('extrudedHeight', parseInt(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
<span>米</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-content" v-if="activeTab === 'polygon'">
|
||||
<div class="option-group">
|
||||
<label>填充颜色:</label>
|
||||
<input
|
||||
type="color"
|
||||
:value="colorToHex(drawingOptions.polygon.color)"
|
||||
@input="updatePolygonOption('color', hexToColor(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>边框颜色:</label>
|
||||
<input
|
||||
type="color"
|
||||
:value="colorToHex(drawingOptions.polygon.outlineColor)"
|
||||
@input="updatePolygonOption('outlineColor', hexToColor(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>透明度:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
:value="drawingOptions.polygon.color.alpha * 100"
|
||||
@input="updatePolygonOption('color', drawingOptions.polygon.color.withAlpha(parseInt(($event.target as HTMLInputElement).value) / 100))"
|
||||
>
|
||||
<span>{{ Math.round(drawingOptions.polygon.color.alpha * 100) }}%</span>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>边框宽度:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
:value="drawingOptions.polygon.outlineWidth"
|
||||
@input="updatePolygonOption('outlineWidth', parseInt(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
<span>{{ drawingOptions.polygon.outlineWidth }}px</span>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label>拉伸高度:</label>
|
||||
<input
|
||||
type="number"
|
||||
:value="drawingOptions.polygon.extrudedHeight"
|
||||
@input="updatePolygonOption('extrudedHeight', parseInt(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
<span>米</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section help-section">
|
||||
<h4>操作说明</h4>
|
||||
<ul class="help-list">
|
||||
<li>✅ <strong>圆形:</strong> 点击确定圆心,再次点击确定半径</li>
|
||||
<li>✅ <strong>多边形:</strong> 点击添加顶点,右键完成</li>
|
||||
<li>✅ <strong>查看信息:</strong> 点击空域显示详细信息</li>
|
||||
<li>✅ <strong>取消:</strong> 按ESC键或点击取消按钮</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import * as Cesium from 'cesium';
|
||||
import type { DrawingResult } from './components/DrawingTool';
|
||||
|
||||
const props = defineProps<{
|
||||
isDrawing: boolean;
|
||||
currentDrawingType: string | null;
|
||||
drawings: Map<string, DrawingResult>;
|
||||
selectedDrawing: string | null;
|
||||
drawingInfo: any;
|
||||
drawingOptions: any;
|
||||
getDrawingStatus: () => string;
|
||||
getDrawingInfoText: () => string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'start-circle-drawing': [];
|
||||
'start-polygon-drawing': [];
|
||||
'cancel-drawing': [];
|
||||
'select-drawing': [id: string];
|
||||
'deselect-drawing': [];
|
||||
'remove-drawing': [id: string];
|
||||
'clear-drawings': [];
|
||||
'fly-to-drawing': [id: string];
|
||||
'update-circle-option': [key: string, value: any];
|
||||
'update-polygon-option': [key: string, value: any];
|
||||
'copy-drawing-info': [];
|
||||
'fly-to-selected-drawing': [];
|
||||
}>();
|
||||
|
||||
const activeTab = ref<'circle' | 'polygon'>('circle');
|
||||
|
||||
// 方法实现
|
||||
const startCircleDrawing = () => {
|
||||
emit('start-circle-drawing');
|
||||
};
|
||||
|
||||
const startPolygonDrawing = () => {
|
||||
emit('start-polygon-drawing');
|
||||
};
|
||||
|
||||
const cancelDrawing = () => {
|
||||
emit('cancel-drawing');
|
||||
};
|
||||
|
||||
const selectDrawing = (id: string) => {
|
||||
emit('select-drawing', id);
|
||||
};
|
||||
|
||||
const removeDrawing = (id: string) => {
|
||||
emit('remove-drawing', id);
|
||||
};
|
||||
|
||||
const clearAllDrawings = () => {
|
||||
emit('clear-drawings');
|
||||
};
|
||||
|
||||
const flyToDrawing = (id: string) => {
|
||||
emit('fly-to-drawing', id);
|
||||
};
|
||||
|
||||
const updateCircleOption = (key: string, value: any) => {
|
||||
emit('update-circle-option', key, value);
|
||||
};
|
||||
|
||||
const updatePolygonOption = (key: string, value: any) => {
|
||||
emit('update-polygon-option', key, value);
|
||||
};
|
||||
|
||||
const copyDrawingInfo = () => {
|
||||
emit('copy-drawing-info');
|
||||
};
|
||||
|
||||
const flyToSelectedDrawing = () => {
|
||||
emit('fly-to-selected-drawing');
|
||||
};
|
||||
|
||||
// 工具函数
|
||||
const getCircleRadius = (drawing: DrawingResult): number => {
|
||||
if (drawing.type !== 'circle' || drawing.positions.length < 2) return 0;
|
||||
return Cesium.Cartesian3.distance(drawing.positions[0], drawing.positions[1]);
|
||||
};
|
||||
|
||||
const colorToHex = (color: Cesium.Color): string => {
|
||||
try {
|
||||
return color.toCssColorString();
|
||||
} catch (error) {
|
||||
const r = Math.floor(color.red * 255);
|
||||
const g = Math.floor(color.green * 255);
|
||||
const b = Math.floor(color.blue * 255);
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
const hexToColor = (hex: string): Cesium.Color => {
|
||||
try {
|
||||
return Cesium.Color.fromCssColorString(hex).withAlpha(0.3);
|
||||
} catch (error) {
|
||||
hex = hex.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
||||
return new Cesium.Color(r, g, b, 0.3);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
/* 新增信息显示样式 */
|
||||
.info-section {
|
||||
border: 1px solid #1890ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.info-display {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #fff;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0 12px 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(24, 144, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(24, 144, 255, 0.5);
|
||||
}
|
||||
|
||||
.fly-btn {
|
||||
background: rgba(82, 196, 26, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fly-btn:hover {
|
||||
background: rgba(82, 196, 26, 0.5);
|
||||
}
|
||||
|
||||
/* 其他样式保持不变 */
|
||||
.drawing-tool-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 320px;
|
||||
background: rgba(42, 42, 42, 0.95);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
min-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* ... 其他样式代码保持不变 ... */
|
||||
</style>
|
||||
386
src/views/cesiums/ModelControlPanel.vue
Normal file
386
src/views/cesiums/ModelControlPanel.vue
Normal file
@ -0,0 +1,386 @@
|
||||
<!-- src/components/ModelControlPanel.vue -->
|
||||
<template>
|
||||
<div class="model-control-panel">
|
||||
<div class="panel-header">
|
||||
<h3>🏗️ 3D 模型管理</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h4>快速添加</h4>
|
||||
<div class="quick-add-buttons">
|
||||
<button
|
||||
v-for="model in predefinedModels"
|
||||
:key="model.type"
|
||||
class="model-btn"
|
||||
@click="addModelAtCenter(model.type)"
|
||||
:disabled="isModelLoading"
|
||||
>
|
||||
{{ model.name }}
|
||||
</button>
|
||||
<button
|
||||
class="landmark-btn"
|
||||
@click="loadLandmarks"
|
||||
:disabled="isModelLoading"
|
||||
>
|
||||
🏛️ 加载地标
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section" v-if="models.size > 0">
|
||||
<h4>模型列表 ({{ models.size }})</h4>
|
||||
<div class="model-list">
|
||||
<div
|
||||
v-for="[id, model] in models"
|
||||
:key="id"
|
||||
:class="['model-item', { active: selectedModel === id }]"
|
||||
@click="selectModel(id)"
|
||||
>
|
||||
<div class="model-info">
|
||||
<span class="model-name">{{ id }}</span>
|
||||
<span class="model-position">
|
||||
{{ model.position.longitude.toFixed(4) }},
|
||||
{{ model.position.latitude.toFixed(4) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="model-actions">
|
||||
<button @click.stop="flyToModel(id)" title="飞向模型">✈️</button>
|
||||
<button @click.stop="removeModel(id)" title="删除模型">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section" v-if="selectedModel">
|
||||
<h4>模型控制</h4>
|
||||
<div class="model-controls">
|
||||
<div class="control-group">
|
||||
<label>缩放:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="5"
|
||||
step="0.1"
|
||||
:value="getSelectedModelScale()"
|
||||
@input="updateSelectedModelScale(parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
<span>{{ getSelectedModelScale() }}x</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>经度:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
:value="getSelectedModelPosition().longitude"
|
||||
@input="updateSelectedModelPosition('longitude', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>纬度:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
:value="getSelectedModelPosition().latitude"
|
||||
@input="updateSelectedModelPosition('latitude', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>高度:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
:value="getSelectedModelPosition().height"
|
||||
@input="updateSelectedModelPosition('height', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="clear-btn" @click="clearAllModels">
|
||||
🗑️ 清除所有模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isModelLoading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>模型加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
models: Map<string, any>;
|
||||
selectedModel: string | null;
|
||||
isModelLoading: boolean;
|
||||
predefinedModels: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'load-landmarks': [];
|
||||
'add-model': [type: string, position: any];
|
||||
'select-model': [id: string];
|
||||
'deselect-model': [];
|
||||
'update-scale': [id: string, scale: number];
|
||||
'update-position': [id: string, position: any];
|
||||
'remove-model': [id: string];
|
||||
'fly-to-model': [id: string];
|
||||
'clear-models': [];
|
||||
}>();
|
||||
|
||||
const loadLandmarks = () => {
|
||||
emit('load-landmarks');
|
||||
};
|
||||
|
||||
const addModelAtCenter = (modelType: string) => {
|
||||
// 在重庆中心位置添加模型
|
||||
const position = {
|
||||
longitude: 106.5516,
|
||||
latitude: 29.5630,
|
||||
height: 50
|
||||
};
|
||||
emit('add-model', modelType, position);
|
||||
};
|
||||
|
||||
const selectModel = (id: string) => {
|
||||
if (props.selectedModel === id) {
|
||||
emit('deselect-model');
|
||||
} else {
|
||||
emit('select-model', id);
|
||||
}
|
||||
};
|
||||
|
||||
const flyToModel = (id: string) => {
|
||||
emit('fly-to-model', id);
|
||||
};
|
||||
|
||||
const removeModel = (id: string) => {
|
||||
emit('remove-model', id);
|
||||
};
|
||||
|
||||
const clearAllModels = () => {
|
||||
emit('clear-models');
|
||||
};
|
||||
|
||||
const getSelectedModelScale = (): number => {
|
||||
if (!props.selectedModel) return 1;
|
||||
const model = props.models.get(props.selectedModel);
|
||||
return model?.options?.scale || 1;
|
||||
};
|
||||
|
||||
const getSelectedModelPosition = () => {
|
||||
if (!props.selectedModel) return { longitude: 0, latitude: 0, height: 0 };
|
||||
const model = props.models.get(props.selectedModel);
|
||||
return model?.position || { longitude: 0, latitude: 0, height: 0 };
|
||||
};
|
||||
|
||||
const updateSelectedModelScale = (scale: number) => {
|
||||
if (props.selectedModel) {
|
||||
emit('update-scale', props.selectedModel, scale);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSelectedModelPosition = (field: string, value: number) => {
|
||||
if (props.selectedModel) {
|
||||
const currentPosition = getSelectedModelPosition();
|
||||
const newPosition = { ...currentPosition, [field]: value };
|
||||
emit('update-position', props.selectedModel, newPosition);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.model-control-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: rgba(42, 42, 42, 0.95);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
min-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel-section h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.quick-add-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-btn, .landmark-btn {
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.model-btn:hover, .landmark-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.model-btn:disabled, .landmark-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.model-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.model-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
margin-bottom: 5px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.model-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.model-item.active {
|
||||
background: rgba(24, 144, 255, 0.3);
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.model-position {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.model-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.model-actions button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.model-actions button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 12px;
|
||||
min-width: 50px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.control-group input {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.control-group span {
|
||||
font-size: 12px;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: rgba(255, 77, 79, 0.2);
|
||||
border: 1px solid rgba(255, 77, 79, 0.4);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: rgba(255, 77, 79, 0.3);
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
377
src/views/cesiums/components/CoordinatePicker.ts
Normal file
377
src/views/cesiums/components/CoordinatePicker.ts
Normal file
@ -0,0 +1,377 @@
|
||||
// src/utils/cesium/CoordinatePicker.ts
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
export interface PickResult {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
height: number;
|
||||
cartesian: Cesium.Cartesian3;
|
||||
cartographic: Cesium.Cartographic;
|
||||
terrainHeight: number | undefined;
|
||||
hasTerrain: boolean;
|
||||
}
|
||||
|
||||
export class CoordinatePicker {
|
||||
private viewer: Cesium.Viewer;
|
||||
private handler: Cesium.ScreenSpaceEventHandler;
|
||||
private isEnabled: boolean = false;
|
||||
private pickCallbacks: ((result: PickResult) => void)[] = [];
|
||||
|
||||
constructor(viewer: Cesium.Viewer) {
|
||||
this.viewer = viewer;
|
||||
this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用坐标拾取
|
||||
*/
|
||||
enable(): void {
|
||||
if (this.isEnabled) return;
|
||||
|
||||
this.handler.setInputAction((event: any) => {
|
||||
this.handleRightClick(event);
|
||||
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
||||
|
||||
// 添加鼠标移动时的预览效果
|
||||
this.handler.setInputAction((event: any) => {
|
||||
this.handleMouseMove(event);
|
||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
|
||||
this.isEnabled = true;
|
||||
this.updateCursorStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用坐标拾取
|
||||
*/
|
||||
disable(): void {
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
||||
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
|
||||
this.isEnabled = false;
|
||||
this.updateCursorStyle();
|
||||
this.clearPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换拾取状态
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this.isEnabled) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加拾取回调
|
||||
*/
|
||||
onPick(callback: (result: PickResult) => void): void {
|
||||
this.pickCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除拾取回调
|
||||
*/
|
||||
offPick(callback: (result: PickResult) => void): void {
|
||||
const index = this.pickCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.pickCallbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理右键点击
|
||||
*/
|
||||
private async handleRightClick(event: any): Promise<void> {
|
||||
const pickResult = await this.pickCoordinate(event.position);
|
||||
|
||||
if (pickResult) {
|
||||
// 执行所有回调
|
||||
this.pickCallbacks.forEach(callback => {
|
||||
callback(pickResult);
|
||||
});
|
||||
|
||||
// 添加临时标记
|
||||
this.addTemporaryMarker(pickResult);
|
||||
|
||||
// 显示坐标信息
|
||||
this.showCoordinateInfo(pickResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移动
|
||||
*/
|
||||
private handleMouseMove(event: any): void {
|
||||
this.updatePreview(event.endPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拾取坐标
|
||||
*/
|
||||
private async pickCoordinate(position: Cesium.Cartesian2): Promise<PickResult | null> {
|
||||
try {
|
||||
// 尝试从场景中拾取位置(包括地形)
|
||||
const pickedObject = this.viewer.scene.pick(position);
|
||||
|
||||
let cartesian: Cesium.Cartesian3;
|
||||
let terrainHeight: number | undefined;
|
||||
|
||||
if (pickedObject && pickedObject.position) {
|
||||
// 如果拾取到了实体,使用实体的位置
|
||||
cartesian = pickedObject.position;
|
||||
} else {
|
||||
// 否则从相机射线与地形的交点获取位置
|
||||
const ray = this.viewer.camera.getPickRay(position);
|
||||
if (!ray) return null;
|
||||
|
||||
cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);
|
||||
if (!cartesian) {
|
||||
// 如果没有地形,使用椭球体表面
|
||||
cartesian = this.viewer.scene.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cartesian) return null;
|
||||
|
||||
// 转换为地理坐标
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||||
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
|
||||
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
|
||||
let height = cartographic.height;
|
||||
|
||||
// 获取地形高度
|
||||
try {
|
||||
terrainHeight = await this.getTerrainHeight(longitude, latitude);
|
||||
if (terrainHeight !== undefined) {
|
||||
// 调整高度为相对地形的高度
|
||||
height = height - terrainHeight;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to get terrain height:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
longitude,
|
||||
latitude,
|
||||
height,
|
||||
cartesian,
|
||||
cartographic,
|
||||
terrainHeight,
|
||||
hasTerrain: terrainHeight !== undefined
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error picking coordinate:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地形高度
|
||||
*/
|
||||
private async getTerrainHeight(longitude: number, latitude: number): Promise<number | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const cartographic = new Cesium.Cartographic(
|
||||
Cesium.Math.toRadians(longitude),
|
||||
Cesium.Math.toRadians(latitude)
|
||||
);
|
||||
|
||||
try {
|
||||
const terrainProvider = this.viewer.terrainProvider;
|
||||
if (terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
|
||||
resolve(undefined); // 没有地形数据
|
||||
return;
|
||||
}
|
||||
|
||||
terrainProvider.getHeight(cartographic).then((height: number) => {
|
||||
resolve(height);
|
||||
}).catch(() => {
|
||||
resolve(undefined);
|
||||
});
|
||||
} catch (error) {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加临时标记
|
||||
*/
|
||||
private addTemporaryMarker(result: PickResult): void {
|
||||
const entity = this.viewer.entities.add({
|
||||
position: result.cartesian,
|
||||
point: {
|
||||
pixelSize: 10,
|
||||
color: Cesium.Color.YELLOW,
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
outlineWidth: 2,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
},
|
||||
label: {
|
||||
text: `经度: ${result.longitude.toFixed(6)}\n纬度: ${result.latitude.toFixed(6)}\n高程: ${result.height.toFixed(2)}米`,
|
||||
font: '12pt sans-serif',
|
||||
pixelOffset: new Cesium.Cartesian2(0, -40),
|
||||
fillColor: Cesium.Color.WHITE,
|
||||
backgroundColor: Cesium.Color.BLACK.withAlpha(0.7),
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
outlineWidth: 2,
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
}
|
||||
});
|
||||
|
||||
// 5秒后自动移除标记
|
||||
setTimeout(() => {
|
||||
this.viewer.entities.remove(entity);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示坐标信息
|
||||
*/
|
||||
private showCoordinateInfo(result: PickResult): void {
|
||||
const terrainInfo = result.hasTerrain ?
|
||||
`地形高程: ${result.terrainHeight?.toFixed(2)}米` :
|
||||
'无地形数据';
|
||||
|
||||
const message =
|
||||
`📍 坐标拾取成功!\n\n` +
|
||||
`经度: ${result.longitude.toFixed(6)}\n` +
|
||||
`纬度: ${result.latitude.toFixed(6)}\n` +
|
||||
`相对高程: ${result.height.toFixed(2)}米\n` +
|
||||
`${terrainInfo}`;
|
||||
|
||||
console.log('Coordinate picked:', result);
|
||||
|
||||
// 可以在这里集成消息提示组件
|
||||
this.showNotification(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知
|
||||
*/
|
||||
private showNotification(message: string): void {
|
||||
// 创建临时通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #52c41a;
|
||||
max-width: 300px;
|
||||
z-index: 1000;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
white-space: pre-line;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新预览效果
|
||||
*/
|
||||
private updatePreview(position: Cesium.Cartesian2): void {
|
||||
this.clearPreview();
|
||||
|
||||
// 添加预览点
|
||||
const ray = this.viewer.camera.getPickRay(position);
|
||||
if (!ray) return;
|
||||
|
||||
const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene) ||
|
||||
this.viewer.scene.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid);
|
||||
|
||||
if (cartesian) {
|
||||
const previewEntity = this.viewer.entities.add({
|
||||
position: cartesian,
|
||||
point: {
|
||||
pixelSize: 8,
|
||||
color: Cesium.Color.CYAN.withAlpha(0.7),
|
||||
outlineColor: Cesium.Color.BLUE,
|
||||
outlineWidth: 1,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
}
|
||||
});
|
||||
|
||||
// 存储预览实体引用以便清除
|
||||
(this as any).previewEntity = previewEntity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除预览效果
|
||||
*/
|
||||
private clearPreview(): void {
|
||||
const previewEntity = (this as any).previewEntity;
|
||||
if (previewEntity) {
|
||||
this.viewer.entities.remove(previewEntity);
|
||||
(this as any).previewEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新光标样式
|
||||
*/
|
||||
private updateCursorStyle(): void {
|
||||
const canvas = this.viewer.scene.canvas;
|
||||
if (this.isEnabled) {
|
||||
canvas.style.cursor = 'crosshair';
|
||||
} else {
|
||||
canvas.style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动拾取指定屏幕坐标
|
||||
*/
|
||||
async pickAtPosition(x: number, y: number): Promise<PickResult | null> {
|
||||
const position = new Cesium.Cartesian2(x, y);
|
||||
return this.pickCoordinate(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动拾取指定地理坐标
|
||||
*/
|
||||
async pickAtCoordinate(longitude: number, latitude: number): Promise<PickResult | null> {
|
||||
const cartesian = Cesium.Cartesian3.fromDegrees(longitude, latitude);
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||||
|
||||
const terrainHeight = await this.getTerrainHeight(longitude, latitude);
|
||||
const height = cartographic.height;
|
||||
|
||||
return {
|
||||
longitude,
|
||||
latitude,
|
||||
height: terrainHeight !== undefined ? height - terrainHeight : height,
|
||||
cartesian,
|
||||
cartographic,
|
||||
terrainHeight,
|
||||
hasTerrain: terrainHeight !== undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy(): void {
|
||||
this.disable();
|
||||
this.handler.destroy();
|
||||
this.pickCallbacks = [];
|
||||
}
|
||||
}
|
||||
919
src/views/cesiums/components/DrawingTool.ts
Normal file
919
src/views/cesiums/components/DrawingTool.ts
Normal file
@ -0,0 +1,919 @@
|
||||
// src/utils/cesium/DrawingTool.ts
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
export interface DrawingOptions {
|
||||
color?: Cesium.Color;
|
||||
outlineColor?: Cesium.Color;
|
||||
outlineWidth?: number;
|
||||
fill?: boolean;
|
||||
classificationType?: Cesium.ClassificationType;
|
||||
height?: number;
|
||||
extrudedHeight?: number;
|
||||
}
|
||||
|
||||
export interface CircleOptions extends DrawingOptions {
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface PolygonOptions extends DrawingOptions {
|
||||
closePath?: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface DrawingInfo {
|
||||
id: string;
|
||||
type: 'circle' | 'polygon';
|
||||
positions: Cesium.Cartesian3[];
|
||||
center?: Cesium.Cartesian3; // 圆心(仅圆形)
|
||||
radius?: number; // 半径(仅圆形)
|
||||
bounds?: { // 边界信息(仅多边形)
|
||||
north: number;
|
||||
south: number;
|
||||
east: number;
|
||||
west: number;
|
||||
};
|
||||
area: number;
|
||||
properties: any;
|
||||
}
|
||||
|
||||
export interface DrawingResult {
|
||||
id: string;
|
||||
type: 'circle' | 'polygon';
|
||||
positions: Cesium.Cartesian3[];
|
||||
entity: Cesium.Entity;
|
||||
properties: any;
|
||||
info: DrawingInfo; // 新增信息字段
|
||||
}
|
||||
|
||||
export class DrawingTool {
|
||||
private viewer: Cesium.Viewer;
|
||||
private handler: Cesium.ScreenSpaceEventHandler;
|
||||
private entities: Cesium.EntityCollection;
|
||||
|
||||
private isDrawing: boolean = false;
|
||||
private currentType: 'circle' | 'polygon' | null = null;
|
||||
private currentPositions: Cesium.Cartesian3[] = [];
|
||||
private tempEntities: Cesium.Entity[] = [];
|
||||
private drawingEntities: Map<string, DrawingResult> = new Map();
|
||||
|
||||
private defaultOptions: DrawingOptions = {
|
||||
color: Cesium.Color.YELLOW.withAlpha(0.3),
|
||||
outlineColor: Cesium.Color.YELLOW,
|
||||
outlineWidth: 2,
|
||||
fill: true,
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
height: 0,
|
||||
extrudedHeight: 1000
|
||||
};
|
||||
|
||||
private drawingCallbacks: {
|
||||
onStart?: () => void;
|
||||
onPointAdd?: (position: Cesium.Cartesian3) => void;
|
||||
onComplete?: (result: DrawingResult) => void;
|
||||
onCancel?: () => void;
|
||||
onClick?: (result: DrawingResult, info: DrawingInfo) => void; // 新增点击回调
|
||||
} = {};
|
||||
|
||||
constructor(viewer: Cesium.Viewer) {
|
||||
this.viewer = viewer;
|
||||
this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
this.entities = viewer.entities;
|
||||
|
||||
// 初始化点击事件监听
|
||||
this.initClickHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化点击事件处理器
|
||||
*/
|
||||
private initClickHandler(): void {
|
||||
this.handler.setInputAction((event: any) => {
|
||||
this.handleEntityClick(event.position);
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理实体点击事件
|
||||
*/
|
||||
private handleEntityClick(position: Cesium.Cartesian2): void {
|
||||
const pickedObject = this.viewer.scene.pick(position);
|
||||
if (!pickedObject || !pickedObject.id) return;
|
||||
|
||||
const clickedEntity = pickedObject.id;
|
||||
const drawingResult = this.findDrawingByEntity(clickedEntity);
|
||||
|
||||
if (drawingResult) {
|
||||
const drawingInfo = this.generateDrawingInfo(drawingResult);
|
||||
this.drawingCallbacks.onClick?.(drawingResult, drawingInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体查找绘图结果
|
||||
*/
|
||||
private findDrawingByEntity(entity: Cesium.Entity): DrawingResult | undefined {
|
||||
for (const drawing of this.drawingEntities.values()) {
|
||||
if (drawing.entity === entity) {
|
||||
return drawing;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成绘图信息
|
||||
*/
|
||||
private generateDrawingInfo(drawing: DrawingResult): DrawingInfo {
|
||||
if (drawing.type === 'circle') {
|
||||
return this.generateCircleInfo(drawing);
|
||||
} else {
|
||||
return this.generatePolygonInfo(drawing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成圆形信息
|
||||
*/
|
||||
private generateCircleInfo(drawing: DrawingResult): DrawingInfo {
|
||||
const center = drawing.positions[0];
|
||||
const radius = drawing.positions.length > 1 ?
|
||||
Cesium.Cartesian3.distance(center, drawing.positions[1]) :
|
||||
(drawing.properties.options.radius || 1000);
|
||||
|
||||
const centerCartographic = Cesium.Cartographic.fromCartesian(center);
|
||||
const centerLongitude = Cesium.Math.toDegrees(centerCartographic.longitude);
|
||||
const centerLatitude = Cesium.Math.toDegrees(centerCartographic.latitude);
|
||||
const centerHeight = centerCartographic.height;
|
||||
|
||||
return {
|
||||
id: drawing.id,
|
||||
type: 'circle',
|
||||
positions: drawing.positions,
|
||||
center: center,
|
||||
radius: radius,
|
||||
area: drawing.properties.area,
|
||||
properties: {
|
||||
...drawing.properties,
|
||||
center: {
|
||||
longitude: centerLongitude,
|
||||
latitude: centerLatitude,
|
||||
height: centerHeight
|
||||
},
|
||||
radius: radius,
|
||||
circumference: 2 * Math.PI * radius,
|
||||
diameter: 2 * radius
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成多边形信息
|
||||
*/
|
||||
private generatePolygonInfo(drawing: DrawingResult): DrawingInfo {
|
||||
// 计算边界
|
||||
let minLon = Infinity, maxLon = -Infinity;
|
||||
let minLat = Infinity, maxLat = -Infinity;
|
||||
let minHeight = Infinity, maxHeight = -Infinity;
|
||||
|
||||
const boundaryPoints = drawing.positions.map(position => {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(position);
|
||||
const lon = Cesium.Math.toDegrees(cartographic.longitude);
|
||||
const lat = Cesium.Math.toDegrees(cartographic.latitude);
|
||||
const height = cartographic.height;
|
||||
|
||||
// 更新边界
|
||||
minLon = Math.min(minLon, lon);
|
||||
maxLon = Math.max(maxLon, lon);
|
||||
minLat = Math.min(minLat, lat);
|
||||
maxLat = Math.max(maxLat, lat);
|
||||
minHeight = Math.min(minHeight, height);
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
|
||||
return {
|
||||
longitude: lon,
|
||||
latitude: lat,
|
||||
height: height
|
||||
};
|
||||
});
|
||||
|
||||
// 计算中心点
|
||||
const centerLon = (minLon + maxLon) / 2;
|
||||
const centerLat = (minLat + maxLat) / 2;
|
||||
const centerHeight = (minHeight + maxHeight) / 2;
|
||||
|
||||
return {
|
||||
id: drawing.id,
|
||||
type: 'polygon',
|
||||
positions: drawing.positions,
|
||||
bounds: {
|
||||
north: maxLat,
|
||||
south: minLat,
|
||||
east: maxLon,
|
||||
west: minLon
|
||||
},
|
||||
area: drawing.properties.area,
|
||||
properties: {
|
||||
...drawing.properties,
|
||||
boundaryPoints: boundaryPoints,
|
||||
bounds: {
|
||||
north: maxLat,
|
||||
south: minLat,
|
||||
east: maxLon,
|
||||
west: minLon
|
||||
},
|
||||
center: {
|
||||
longitude: centerLon,
|
||||
latitude: centerLat,
|
||||
height: centerHeight
|
||||
},
|
||||
width: this.calculateDistance(minLon, minLat, maxLon, minLat), // 东西宽度
|
||||
height: this.calculateDistance(minLon, minLat, minLon, maxLat), // 南北高度
|
||||
perimeter: this.calculatePerimeter(drawing.positions)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点间距离
|
||||
*/
|
||||
private calculateDistance(lon1: number, lat1: number, lon2: number, lat2: number): number {
|
||||
const cartesian1 = Cesium.Cartesian3.fromDegrees(lon1, lat1);
|
||||
const cartesian2 = Cesium.Cartesian3.fromDegrees(lon2, lat2);
|
||||
return Cesium.Cartesian3.distance(cartesian1, cartesian2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多边形周长
|
||||
*/
|
||||
private calculatePerimeter(positions: Cesium.Cartesian3[]): number {
|
||||
let perimeter = 0;
|
||||
const n = positions.length;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
perimeter += Cesium.Cartesian3.distance(positions[i], positions[j]);
|
||||
}
|
||||
|
||||
return perimeter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 开始绘制圆形
|
||||
*/
|
||||
startDrawingCircle(options: CircleOptions = {}): void {
|
||||
this.startDrawing('circle', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始绘制多边形
|
||||
*/
|
||||
startDrawingPolygon(options: PolygonOptions = {}): void {
|
||||
this.startDrawing('polygon', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始绘制
|
||||
*/
|
||||
private startDrawing(type: 'circle' | 'polygon', options: DrawingOptions = {}): void {
|
||||
if (this.isDrawing) {
|
||||
this.cancelDrawing();
|
||||
}
|
||||
|
||||
this.isDrawing = true;
|
||||
this.currentType = type;
|
||||
this.currentPositions = [];
|
||||
this.tempEntities = [];
|
||||
|
||||
const mergedOptions = { ...this.defaultOptions, ...options };
|
||||
|
||||
// 设置鼠标样式
|
||||
this.viewer.scene.canvas.style.cursor = 'crosshair';
|
||||
|
||||
// 左键点击添加点
|
||||
this.handler.setInputAction((event: any) => {
|
||||
this.handleLeftClick(event.position, mergedOptions);
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
|
||||
// 鼠标移动预览
|
||||
this.handler.setInputAction((event: any) => {
|
||||
this.handleMouseMove(event.endPosition, mergedOptions);
|
||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
|
||||
// 右键完成绘制
|
||||
this.handler.setInputAction(() => {
|
||||
this.completeDrawing(mergedOptions);
|
||||
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
||||
|
||||
// 取消绘制
|
||||
const cancelHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
this.cancelDrawing();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', cancelHandler);
|
||||
(this as any).cancelHandler = cancelHandler;
|
||||
|
||||
this.drawingCallbacks.onStart?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理左键点击
|
||||
*/
|
||||
private handleLeftClick(position: Cesium.Cartesian2, options: DrawingOptions): void {
|
||||
const cartesian = this.pickCoordinate(position);
|
||||
if (!cartesian) return;
|
||||
|
||||
this.currentPositions.push(cartesian);
|
||||
|
||||
// 添加临时点标记
|
||||
const pointEntity = this.entities.add({
|
||||
position: cartesian,
|
||||
point: {
|
||||
pixelSize: 8,
|
||||
color: Cesium.Color.RED,
|
||||
outlineColor: Cesium.Color.WHITE,
|
||||
outlineWidth: 2,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
}
|
||||
});
|
||||
this.tempEntities.push(pointEntity);
|
||||
|
||||
this.drawingCallbacks.onPointAdd?.(cartesian);
|
||||
|
||||
// 如果是圆形,第一个点确定圆心,第二个点确定半径
|
||||
if (this.currentType === 'circle' && this.currentPositions.length === 2) {
|
||||
this.completeDrawing(options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移动
|
||||
*/
|
||||
private handleMouseMove(position: Cesium.Cartesian2, options: DrawingOptions): void {
|
||||
if (this.currentPositions.length === 0) return;
|
||||
|
||||
const cartesian = this.pickCoordinate(position);
|
||||
if (!cartesian) return;
|
||||
|
||||
// 清除之前的临时图形
|
||||
this.clearTempShapes();
|
||||
|
||||
if (this.currentType === 'circle') {
|
||||
this.drawTempCircle(cartesian, options);
|
||||
} else if (this.currentType === 'polygon') {
|
||||
this.drawTempPolygon(cartesian, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制临时圆形
|
||||
*/
|
||||
private drawTempCircle(mousePosition: Cesium.Cartesian3, options: CircleOptions): void {
|
||||
const center = this.currentPositions[0];
|
||||
const radius = Cesium.Cartesian3.distance(center, mousePosition);
|
||||
|
||||
const circleEntity = this.entities.add({
|
||||
position: center,
|
||||
ellipse: {
|
||||
semiMinorAxis: radius,
|
||||
semiMajorAxis: radius,
|
||||
material: options.color || this.defaultOptions.color,
|
||||
outline: true,
|
||||
outlineColor: options.outlineColor || this.defaultOptions.outlineColor,
|
||||
outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth,
|
||||
height: options.height || this.defaultOptions.height,
|
||||
extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight,
|
||||
classificationType: options.classificationType || this.defaultOptions.classificationType
|
||||
}
|
||||
});
|
||||
this.tempEntities.push(circleEntity);
|
||||
|
||||
// 绘制半径线
|
||||
if (this.currentPositions.length === 1) {
|
||||
const radiusLine = this.entities.add({
|
||||
polyline: {
|
||||
positions: [center, mousePosition],
|
||||
width: 2,
|
||||
material: Cesium.Color.WHITE.withAlpha(0.7)
|
||||
}
|
||||
});
|
||||
this.tempEntities.push(radiusLine);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制临时多边形
|
||||
*/
|
||||
private drawTempPolygon(mousePosition: Cesium.Cartesian3, options: PolygonOptions): void {
|
||||
const positions = [...this.currentPositions, mousePosition];
|
||||
|
||||
// 绘制临时多边形
|
||||
const polygonEntity = this.entities.add({
|
||||
polygon: {
|
||||
hierarchy: new Cesium.PolygonHierarchy(positions),
|
||||
material: options.color || this.defaultOptions.color,
|
||||
outline: true,
|
||||
outlineColor: options.outlineColor || this.defaultOptions.outlineColor,
|
||||
outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth,
|
||||
height: options.height || this.defaultOptions.height,
|
||||
extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight,
|
||||
classificationType: options.classificationType || this.defaultOptions.classificationType
|
||||
}
|
||||
});
|
||||
this.tempEntities.push(polygonEntity);
|
||||
|
||||
// 绘制临时边线
|
||||
if (positions.length > 1) {
|
||||
const linePositions = [...positions];
|
||||
if (positions.length > 2) {
|
||||
linePositions.push(positions[0]); // 闭合多边形
|
||||
}
|
||||
|
||||
const lineEntity = this.entities.add({
|
||||
polyline: {
|
||||
positions: linePositions,
|
||||
width: 3,
|
||||
material: options.outlineColor || this.defaultOptions.outlineColor
|
||||
}
|
||||
});
|
||||
this.tempEntities.push(lineEntity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成绘制
|
||||
*/
|
||||
// private completeDrawing(options: DrawingOptions): void {
|
||||
// if (this.currentPositions.length < 2) {
|
||||
// this.cancelDrawing();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const id = `drawing_${Date.now()}`;
|
||||
// let entity: Cesium.Entity;
|
||||
|
||||
// if (this.currentType === 'circle') {
|
||||
// entity = this.createCircleEntity(id, this.currentPositions, options as CircleOptions);
|
||||
// } else {
|
||||
// entity = this.createPolygonEntity(id, this.currentPositions, options as PolygonOptions);
|
||||
// }
|
||||
|
||||
// const result: DrawingResult = {
|
||||
// id,
|
||||
// type: this.currentType!,
|
||||
// positions: this.currentPositions,
|
||||
// entity,
|
||||
// properties: {
|
||||
// area: this.calculateArea(this.currentPositions, this.currentType!),
|
||||
// options
|
||||
// }
|
||||
// };
|
||||
|
||||
// this.drawingEntities.set(id, result);
|
||||
// this.cleanupDrawing();
|
||||
|
||||
// this.drawingCallbacks.onComplete?.(result);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 完成绘制
|
||||
*/
|
||||
private completeDrawing(options: DrawingOptions): void {
|
||||
if (this.currentPositions.length < 2) {
|
||||
this.cancelDrawing();
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `drawing_${Date.now()}`;
|
||||
let entity: Cesium.Entity;
|
||||
|
||||
if (this.currentType === 'circle') {
|
||||
entity = this.createCircleEntity(id, this.currentPositions, options as CircleOptions);
|
||||
} else {
|
||||
entity = this.createPolygonEntity(id, this.currentPositions, options as PolygonOptions);
|
||||
}
|
||||
|
||||
// 生成绘图信息
|
||||
const drawingInfo = this.currentType === 'circle' ?
|
||||
this.generateCircleInfo({ id, type: 'circle', positions: this.currentPositions, entity, properties: { options, area: 0 } } as DrawingResult) :
|
||||
this.generatePolygonInfo({ id, type: 'polygon', positions: this.currentPositions, entity, properties: { options, area: 0 } } as DrawingResult);
|
||||
|
||||
// 计算面积
|
||||
const area = this.calculateArea(this.currentPositions, this.currentType!);
|
||||
|
||||
const result: DrawingResult = {
|
||||
id,
|
||||
type: this.currentType!,
|
||||
positions: this.currentPositions,
|
||||
entity,
|
||||
properties: {
|
||||
area: area,
|
||||
options
|
||||
},
|
||||
info: {
|
||||
...drawingInfo,
|
||||
area: area
|
||||
}
|
||||
};
|
||||
|
||||
this.drawingEntities.set(id, result);
|
||||
this.cleanupDrawing();
|
||||
|
||||
this.drawingCallbacks.onComplete?.(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形实体
|
||||
*/
|
||||
// private createCircleEntity(id: string, positions: Cesium.Cartesian3[], options: CircleOptions): Cesium.Entity {
|
||||
// const center = positions[0];
|
||||
// const radius = positions.length > 1 ? Cesium.Cartesian3.distance(center, positions[1]) : (options.radius || 1000);
|
||||
|
||||
// return this.entities.add({
|
||||
// id,
|
||||
// position: center,
|
||||
// ellipse: {
|
||||
// semiMinorAxis: radius,
|
||||
// semiMajorAxis: radius,
|
||||
// material: options.color || this.defaultOptions.color,
|
||||
// outline: true,
|
||||
// outlineColor: options.outlineColor || this.defaultOptions.outlineColor,
|
||||
// outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth,
|
||||
// height: options.height || this.defaultOptions.height,
|
||||
// extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight,
|
||||
// classificationType: options.classificationType || this.defaultOptions.classificationType
|
||||
// },
|
||||
// label: {
|
||||
// text: `圆形空域\n半径: ${(radius / 1000).toFixed(2)}km`,
|
||||
// font: '14pt sans-serif',
|
||||
// pixelOffset: new Cesium.Cartesian2(0, -50),
|
||||
// fillColor: Cesium.Color.WHITE,
|
||||
// outlineColor: Cesium.Color.BLACK,
|
||||
// outlineWidth: 2,
|
||||
// style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||
// heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* 创建圆形实体
|
||||
*/
|
||||
private createCircleEntity(id: string, positions: Cesium.Cartesian3[], options: CircleOptions): Cesium.Entity {
|
||||
const center = positions[0];
|
||||
const radius = positions.length > 1 ? Cesium.Cartesian3.distance(center, positions[1]) : (options.radius || 1000);
|
||||
const centerCartographic = Cesium.Cartographic.fromCartesian(center);
|
||||
|
||||
return this.entities.add({
|
||||
id,
|
||||
position: center,
|
||||
ellipse: {
|
||||
semiMinorAxis: radius,
|
||||
semiMajorAxis: radius,
|
||||
material: options.color || this.defaultOptions.color,
|
||||
outline: true,
|
||||
outlineColor: options.outlineColor || this.defaultOptions.outlineColor,
|
||||
outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth,
|
||||
height: options.height || this.defaultOptions.height,
|
||||
extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight,
|
||||
classificationType: options.classificationType || this.defaultOptions.classificationType
|
||||
},
|
||||
label: {
|
||||
text: `圆形空域\n半径: ${(radius / 1000).toFixed(2)}km`,
|
||||
font: '14pt sans-serif',
|
||||
pixelOffset: new Cesium.Cartesian2(0, -50),
|
||||
fillColor: Cesium.Color.WHITE,
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
outlineWidth: 2,
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
},
|
||||
// 添加自定义属性便于识别
|
||||
drawingType: 'circle',
|
||||
drawingId: id
|
||||
});
|
||||
console.log("圆形空域:",)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多边形实体
|
||||
*/
|
||||
private createPolygonEntity(id: string, positions: Cesium.Cartesian3[], options: PolygonOptions): Cesium.Entity {
|
||||
const hierarchy = new Cesium.PolygonHierarchy(positions);
|
||||
const area = this.calculateArea(positions, 'polygon');
|
||||
|
||||
return this.entities.add({
|
||||
id,
|
||||
polygon: {
|
||||
hierarchy,
|
||||
material: options.color || this.defaultOptions.color,
|
||||
outline: true,
|
||||
outlineColor: options.outlineColor || this.defaultOptions.outlineColor,
|
||||
outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth,
|
||||
height: options.height || this.defaultOptions.height,
|
||||
extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight,
|
||||
classificationType: options.classificationType || this.defaultOptions.classificationType
|
||||
},
|
||||
label: {
|
||||
text: `多边形空域\n面积: ${(area / 1000000).toFixed(2)}km²`,
|
||||
font: '14pt sans-serif',
|
||||
pixelOffset: new Cesium.Cartesian2(0, -50),
|
||||
fillColor: Cesium.Color.WHITE,
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
outlineWidth: 2,
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
},
|
||||
// 添加自定义属性便于识别
|
||||
drawingType: 'polygon',
|
||||
drawingId: id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多边形实体
|
||||
*/
|
||||
// private createPolygonEntity(id: string, positions: Cesium.Cartesian3[], options: PolygonOptions): Cesium.Entity {
|
||||
// const hierarchy = new Cesium.PolygonHierarchy(positions);
|
||||
// const area = this.calculateArea(positions, 'polygon');
|
||||
|
||||
// return this.entities.add({
|
||||
// id,
|
||||
// polygon: {
|
||||
// hierarchy,
|
||||
// material: options.color || this.defaultOptions.color,
|
||||
// outline: true,
|
||||
// outlineColor: options.outlineColor || this.defaultOptions.outlineColor,
|
||||
// outlineWidth: options.outlineWidth || this.defaultOptions.outlineWidth,
|
||||
// height: options.height || this.defaultOptions.height,
|
||||
// extrudedHeight: options.extrudedHeight || this.defaultOptions.extrudedHeight,
|
||||
// classificationType: options.classificationType || this.defaultOptions.classificationType
|
||||
// },
|
||||
// label: {
|
||||
// text: `多边形空域\n面积: ${(area / 1000000).toFixed(2)}km²`,
|
||||
// font: '14pt sans-serif',
|
||||
// pixelOffset: new Cesium.Cartesian2(0, -50),
|
||||
// fillColor: Cesium.Color.WHITE,
|
||||
// outlineColor: Cesium.Color.BLACK,
|
||||
// outlineWidth: 2,
|
||||
// style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||
// heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* 计算面积
|
||||
*/
|
||||
private calculateArea(positions: Cesium.Cartesian3[], type: 'circle' | 'polygon'): number {
|
||||
if (type === 'circle') {
|
||||
const center = positions[0];
|
||||
console.log("圆形的圆心:",center)
|
||||
const radius = positions.length > 1 ? Cesium.Cartesian3.distance(center, positions[1]) : 0;
|
||||
console.log("圆形的半径:",radius)
|
||||
return Math.PI * radius * radius;
|
||||
} else {
|
||||
// 计算多边形面积
|
||||
// const cartographics = positions.map(pos =>
|
||||
// Cesium.Cartographic.fromCartesian(pos)
|
||||
// );
|
||||
// return Cesium.PolygonGeometry.computeArea(cartographics);
|
||||
|
||||
// 修正:使用正确的多边形面积计算方法
|
||||
const area = this.computePolygonAreaSimple(positions);
|
||||
console.log(`Polygon area: ${area} m², vertices: ${positions.length}`);
|
||||
return area;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化但有效的多边形面积计算
|
||||
*/
|
||||
private computePolygonAreaSimple(positions: Cesium.Cartesian3[]): number {
|
||||
if (positions.length < 3) return 0;
|
||||
|
||||
// 将3D坐标转换为2D平面坐标(使用第一个点作为参考平面)
|
||||
const normal = new Cesium.Cartesian3();
|
||||
Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(positions[0], normal);
|
||||
|
||||
let area = 0;
|
||||
const n = positions.length;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
|
||||
const p1 = positions[i];
|
||||
const p2 = positions[j];
|
||||
|
||||
// 计算两个向量在切平面上的投影
|
||||
const v1 = Cesium.Cartesian3.subtract(p1, positions[0], new Cesium.Cartesian3());
|
||||
const v2 = Cesium.Cartesian3.subtract(p2, positions[0], new Cesium.Cartesian3());
|
||||
|
||||
// 计算叉积的模长(平行四边形面积)
|
||||
const cross = Cesium.Cartesian3.cross(v1, v2, new Cesium.Cartesian3());
|
||||
const parallelogramArea = Cesium.Cartesian3.magnitude(cross);
|
||||
|
||||
area += parallelogramArea;
|
||||
}
|
||||
|
||||
// 三角形面积是平行四边形面积的一半
|
||||
return Math.abs(area) / 2;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 取消绘制
|
||||
*/
|
||||
cancelDrawing(): void {
|
||||
this.cleanupDrawing();
|
||||
this.drawingCallbacks.onCancel?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理绘制状态
|
||||
*/
|
||||
private cleanupDrawing(): void {
|
||||
this.isDrawing = false;
|
||||
this.currentType = null;
|
||||
this.currentPositions = [];
|
||||
|
||||
// 清除临时实体
|
||||
this.tempEntities.forEach(entity => this.entities.remove(entity));
|
||||
this.tempEntities = [];
|
||||
|
||||
// 移除事件监听
|
||||
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
||||
|
||||
// 移除键盘事件
|
||||
if ((this as any).cancelHandler) {
|
||||
document.removeEventListener('keydown', (this as any).cancelHandler);
|
||||
(this as any).cancelHandler = null;
|
||||
}
|
||||
|
||||
// 恢复鼠标样式
|
||||
this.viewer.scene.canvas.style.cursor = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除临时图形
|
||||
*/
|
||||
private clearTempShapes(): void {
|
||||
this.tempEntities.forEach(entity => this.entities.remove(entity));
|
||||
this.tempEntities = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 拾取坐标
|
||||
*/
|
||||
private pickCoordinate(position: Cesium.Cartesian2): Cesium.Cartesian3 | null {
|
||||
const ray = this.viewer.camera.getPickRay(position);
|
||||
if (!ray) return null;
|
||||
|
||||
const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene) ||
|
||||
this.viewer.scene.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid);
|
||||
return cartesian;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置绘图回调
|
||||
*/
|
||||
setCallbacks(callbacks: typeof this.drawingCallbacks): void {
|
||||
this.drawingCallbacks = { ...this.drawingCallbacks, ...callbacks };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有绘制的图形
|
||||
*/
|
||||
getAllDrawings(): DrawingResult[] {
|
||||
return Array.from(this.drawingEntities.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取图形
|
||||
*/
|
||||
getDrawing(id: string): DrawingResult | undefined {
|
||||
return this.drawingEntities.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除图形
|
||||
*/
|
||||
removeDrawing(id: string): boolean {
|
||||
const drawing = this.drawingEntities.get(id);
|
||||
if (drawing) {
|
||||
this.entities.remove(drawing.entity);
|
||||
this.drawingEntities.delete(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有图形
|
||||
*/
|
||||
clearAllDrawings(): void {
|
||||
this.drawingEntities.forEach(drawing => {
|
||||
this.entities.remove(drawing.entity);
|
||||
});
|
||||
this.drawingEntities.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮显示图形
|
||||
*/
|
||||
highlightDrawing(id: string, highlight: boolean = true): void {
|
||||
const drawing = this.drawingEntities.get(id);
|
||||
if (!drawing) return;
|
||||
|
||||
if (drawing.type === 'circle' && drawing.entity.ellipse) {
|
||||
drawing.entity.ellipse.outlineColor = highlight ?
|
||||
Cesium.Color.RED :
|
||||
(drawing.properties.options.outlineColor || this.defaultOptions.outlineColor);
|
||||
drawing.entity.ellipse.outlineWidth = highlight ? 4 : 2;
|
||||
} else if (drawing.entity.polygon) {
|
||||
drawing.entity.polygon.outlineColor = highlight ?
|
||||
Cesium.Color.RED :
|
||||
(drawing.properties.options.outlineColor || this.defaultOptions.outlineColor);
|
||||
drawing.entity.polygon.outlineWidth = highlight ? 4 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞向图形
|
||||
*/
|
||||
flyToDrawing(id: string, duration: number = 2): void {
|
||||
console.log("飞飞飞非法欸")
|
||||
const drawing = this.drawingEntities.get(id);
|
||||
console.log("飞飞飞非111")
|
||||
if (drawing) {
|
||||
console.log("飞飞飞222")
|
||||
this.viewer.flyTo(drawing.entity, {
|
||||
duration: duration,
|
||||
offset: new Cesium.HeadingPitchRange(0, -0.5, 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图形数据
|
||||
*/
|
||||
exportDrawings(): any[] {
|
||||
return this.getAllDrawings().map(drawing => ({
|
||||
id: drawing.id,
|
||||
type: drawing.type,
|
||||
positions: drawing.positions.map(pos => {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(pos);
|
||||
return {
|
||||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||||
height: cartographic.height
|
||||
};
|
||||
}),
|
||||
properties: drawing.properties
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入图形数据
|
||||
*/
|
||||
importDrawings(data: any[]): void {
|
||||
data.forEach(item => {
|
||||
const positions = item.positions.map((pos: any) =>
|
||||
Cesium.Cartesian3.fromDegrees(pos.longitude, pos.latitude, pos.height)
|
||||
);
|
||||
|
||||
let entity: Cesium.Entity;
|
||||
if (item.type === 'circle') {
|
||||
entity = this.createCircleEntity(item.id, positions, item.properties.options);
|
||||
} else {
|
||||
entity = this.createPolygonEntity(item.id, positions, item.properties.options);
|
||||
}
|
||||
|
||||
const drawing: DrawingResult = {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
positions,
|
||||
entity,
|
||||
properties: item.properties
|
||||
};
|
||||
|
||||
this.drawingEntities.set(item.id, drawing);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy(): void {
|
||||
this.cancelDrawing();
|
||||
this.clearAllDrawings();
|
||||
this.handler.destroy();
|
||||
}
|
||||
}
|
||||
93
src/views/cesiums/components/MarkerTool.ts
Normal file
93
src/views/cesiums/components/MarkerTool.ts
Normal file
@ -0,0 +1,93 @@
|
||||
// src/utils/cesium/MarkerTool.ts
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
export interface Landmark {
|
||||
name: string;
|
||||
lon: number;
|
||||
lat: number;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export class MarkerTool {
|
||||
private viewer: Cesium.Viewer;
|
||||
private entities: Cesium.EntityCollection;
|
||||
private markers: Map<string, Cesium.Entity>;
|
||||
|
||||
constructor(viewer: Cesium.Viewer) {
|
||||
this.viewer = viewer;
|
||||
this.entities = viewer.entities;
|
||||
this.markers = new Map();
|
||||
}
|
||||
|
||||
// 添加点标注
|
||||
addPoint(
|
||||
longitude: number,
|
||||
latitude: number,
|
||||
name: string,
|
||||
description: string = '',
|
||||
options: any = {}
|
||||
): Cesium.Entity {
|
||||
const defaultOptions = {
|
||||
position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
|
||||
point: {
|
||||
pixelSize: 10,
|
||||
color: Cesium.Color.YELLOW,
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
outlineWidth: 2,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
},
|
||||
label: {
|
||||
text: name,
|
||||
font: '14pt sans-serif',
|
||||
pixelOffset: new Cesium.Cartesian2(0, -40),
|
||||
fillColor: Cesium.Color.WHITE,
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
outlineWidth: 2,
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
if (description) {
|
||||
mergedOptions.description = description;
|
||||
}
|
||||
|
||||
const entity = this.entities.add(mergedOptions);
|
||||
this.markers.set(name, entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
// 添加重庆地标
|
||||
addChongqingLandmark(): void {
|
||||
const landmarks: Landmark[] = [
|
||||
{ name: '解放碑', lon: 106.574, lat: 29.556, desc: '重庆解放碑' },
|
||||
{ name: '洪崖洞', lon: 106.577, lat: 29.561, desc: '洪崖洞民俗风貌区' },
|
||||
{ name: '朝天门', lon: 106.583, lat: 29.565, desc: '朝天门广场' },
|
||||
{ name: '南山', lon: 106.600, lat: 29.553, desc: '南山一棵树观景台' }
|
||||
];
|
||||
|
||||
landmarks.forEach(landmark => {
|
||||
this.addPoint(landmark.lon, landmark.lat, landmark.name, landmark.desc);
|
||||
});
|
||||
}
|
||||
|
||||
// 移除标注
|
||||
removeMarker(name: string): void {
|
||||
if (this.markers.has(name)) {
|
||||
const entity = this.markers.get(name);
|
||||
if (entity) {
|
||||
this.entities.remove(entity);
|
||||
}
|
||||
this.markers.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除所有标注
|
||||
clearAllMarkers(): void {
|
||||
this.markers.forEach((entity) => {
|
||||
this.entities.remove(entity);
|
||||
});
|
||||
this.markers.clear();
|
||||
}
|
||||
}
|
||||
415
src/views/cesiums/components/ModelTool.ts
Normal file
415
src/views/cesiums/components/ModelTool.ts
Normal file
@ -0,0 +1,415 @@
|
||||
// src/utils/cesium/ModelTool.ts
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
export interface ModelOptions {
|
||||
position: Cesium.Cartesian3;
|
||||
orientation?: Cesium.Quaternion;
|
||||
scale?: number;
|
||||
minimumPixelSize?: number;
|
||||
maximumScale?: number;
|
||||
heightReference?: Cesium.HeightReference;
|
||||
color?: Cesium.Color;
|
||||
colorBlendMode?: Cesium.ColorBlendMode;
|
||||
colorBlendAmount?: number;
|
||||
silhouetteColor?: Cesium.Color;
|
||||
silhouetteSize?: number;
|
||||
debugShowBoundingVolume?: boolean;
|
||||
debugWireframe?: boolean;
|
||||
}
|
||||
|
||||
export interface ModelAnimation {
|
||||
name: string;
|
||||
speed?: number;
|
||||
loop?: boolean;
|
||||
startTime?: Cesium.JulianDate;
|
||||
stopTime?: Cesium.JulianDate;
|
||||
}
|
||||
|
||||
export class ModelTool {
|
||||
private viewer: Cesium.Viewer;
|
||||
private entities: Cesium.EntityCollection;
|
||||
private models: Map<string, Cesium.Entity>;
|
||||
private modelCache: Map<string, any>;
|
||||
|
||||
constructor(viewer: Cesium.Viewer) {
|
||||
this.viewer = viewer;
|
||||
this.entities = viewer.entities;
|
||||
this.models = new Map();
|
||||
this.modelCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 3D 模型
|
||||
*/
|
||||
async loadModel(
|
||||
id: string,
|
||||
url: string,
|
||||
position: { longitude: number; latitude: number; height: number },
|
||||
options: Partial<ModelOptions> = {}
|
||||
): Promise<Cesium.Entity> {
|
||||
try {
|
||||
const defaultOptions: Partial<ModelOptions> = {
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 128,
|
||||
maximumScale: 1000,
|
||||
heightReference: Cesium.HeightReference.NONE,
|
||||
color: Cesium.Color.WHITE,
|
||||
colorBlendMode: Cesium.ColorBlendMode.HIGHLIGHT,
|
||||
colorBlendAmount: 0.5,
|
||||
debugShowBoundingVolume: false,
|
||||
debugWireframe: false
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
const entity = this.entities.add({
|
||||
id: id,
|
||||
position: Cesium.Cartesian3.fromDegrees(
|
||||
position.longitude,
|
||||
position.latitude,
|
||||
position.height
|
||||
),
|
||||
orientation: mergedOptions.orientation,
|
||||
model: {
|
||||
uri: url,
|
||||
scale: mergedOptions.scale,
|
||||
minimumPixelSize: mergedOptions.minimumPixelSize,
|
||||
maximumScale: mergedOptions.maximumScale,
|
||||
heightReference: mergedOptions.heightReference,
|
||||
color: mergedOptions.color,
|
||||
colorBlendMode: mergedOptions.colorBlendMode,
|
||||
colorBlendAmount: mergedOptions.colorBlendAmount,
|
||||
silhouetteColor: mergedOptions.silhouetteColor,
|
||||
silhouetteSize: mergedOptions.silhouetteSize,
|
||||
debugShowBoundingVolume: mergedOptions.debugShowBoundingVolume,
|
||||
debugWireframe: mergedOptions.debugWireframe
|
||||
}
|
||||
});
|
||||
|
||||
this.models.set(id, entity);
|
||||
|
||||
// 等待模型加载完成
|
||||
await new Promise((resolve, reject) => {
|
||||
const removeListener = this.viewer.scene.postRender.addEventListener(() => {
|
||||
const model = entity.model;
|
||||
if (model && model.ready) {
|
||||
removeListener();
|
||||
resolve(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
removeListener();
|
||||
reject(new Error(`Model loading timeout: ${id}`));
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load model ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 GLTF/GLB 模型
|
||||
*/
|
||||
async loadGltfModel(
|
||||
id: string,
|
||||
url: string,
|
||||
position: { longitude: number; latitude: number; height: number },
|
||||
options: Partial<ModelOptions> = {}
|
||||
): Promise<Cesium.Entity> {
|
||||
return this.loadModel(id, url, position, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加重庆地标建筑模型
|
||||
*/
|
||||
async addChongqingLandmarkModels(): Promise<void> {
|
||||
const landmarks = [
|
||||
{
|
||||
id: 'jiefangbei',
|
||||
name: '解放碑',
|
||||
url: '/models/jiefangbei.glb', // 替换为实际模型路径
|
||||
position: { longitude: 106.574, latitude: 29.556, height: 50 },
|
||||
scale: 2.0
|
||||
},
|
||||
{
|
||||
id: 'hongyadong',
|
||||
name: '洪崖洞',
|
||||
url: '/models/hongyadong.glb',
|
||||
position: { longitude: 106.577, latitude: 29.561, height: 20 },
|
||||
scale: 1.5
|
||||
},
|
||||
{
|
||||
id: 'chaotianmen',
|
||||
name: '朝天门',
|
||||
url: '/models/chaotianmen.glb',
|
||||
position: { longitude: 106.583, latitude: 29.565, height: 10 },
|
||||
scale: 1.8
|
||||
}
|
||||
];
|
||||
|
||||
for (const landmark of landmarks) {
|
||||
try {
|
||||
await this.loadGltfModel(
|
||||
landmark.id,
|
||||
landmark.url,
|
||||
landmark.position,
|
||||
{ scale: landmark.scale }
|
||||
);
|
||||
console.log(`Loaded landmark model: ${landmark.name}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load landmark model ${landmark.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加动态车辆模型
|
||||
*/
|
||||
async addVehicleModel(
|
||||
id: string,
|
||||
path: { longitude: number; latitude: number; height: number }[],
|
||||
speed: number = 10
|
||||
): Promise<Cesium.Entity> {
|
||||
const modelUrl = '/models/vehicle.glb'; // 车辆模型路径
|
||||
|
||||
// 创建路径位置
|
||||
const positions = path.map(point =>
|
||||
Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
|
||||
);
|
||||
|
||||
// 创建样条曲线路径
|
||||
const spline = new Cesium.HermiteSpline({
|
||||
times: path.map((_, index) => index),
|
||||
points: positions
|
||||
});
|
||||
|
||||
const entity = this.entities.add({
|
||||
id: id,
|
||||
position: new Cesium.CallbackProperty((time, result) => {
|
||||
const seconds = Cesium.JulianDate.secondsDifference(time, startTime);
|
||||
const position = spline.evaluate(seconds * speed, result);
|
||||
return position;
|
||||
}, false),
|
||||
orientation: new Cesium.VelocityOrientationProperty(
|
||||
new Cesium.CallbackProperty((time, result) => {
|
||||
const seconds = Cesium.JulianDate.secondsDifference(time, startTime);
|
||||
return spline.evaluate(seconds * speed, result);
|
||||
}, false)
|
||||
),
|
||||
model: {
|
||||
uri: modelUrl,
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 64
|
||||
}
|
||||
});
|
||||
|
||||
const startTime = Cesium.JulianDate.now();
|
||||
this.models.set(id, entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制模型动画
|
||||
*/
|
||||
playModelAnimation(
|
||||
modelId: string,
|
||||
animation: ModelAnimation
|
||||
): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (!entity || !entity.model) return;
|
||||
|
||||
const model = entity.model;
|
||||
|
||||
// 获取模型动画
|
||||
const modelAnimation = model.activeAnimations.add({
|
||||
name: animation.name,
|
||||
speedup: animation.speed || 1.0,
|
||||
loop: animation.loop || true,
|
||||
startTime: animation.startTime,
|
||||
stopTime: animation.stopTime
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止模型动画
|
||||
*/
|
||||
stopModelAnimation(modelId: string, animationName?: string): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (!entity || !entity.model) return;
|
||||
|
||||
const model = entity.model;
|
||||
|
||||
if (animationName) {
|
||||
// 停止特定动画
|
||||
const animations = model.activeAnimations;
|
||||
for (let i = 0; i < animations.length; i++) {
|
||||
if (animations[i].name === animationName) {
|
||||
animations.remove(animations[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 停止所有动画
|
||||
model.activeAnimations.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型位置
|
||||
*/
|
||||
setModelPosition(
|
||||
modelId: string,
|
||||
position: { longitude: number; latitude: number; height: number }
|
||||
): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity) {
|
||||
entity.position = Cesium.Cartesian3.fromDegrees(
|
||||
position.longitude,
|
||||
position.latitude,
|
||||
position.height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型缩放
|
||||
*/
|
||||
setModelScale(modelId: string, scale: number): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity && entity.model) {
|
||||
entity.model.scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型颜色
|
||||
*/
|
||||
setModelColor(modelId: string, color: Cesium.Color): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity && entity.model) {
|
||||
entity.model.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示/隐藏模型
|
||||
*/
|
||||
setModelVisibility(modelId: string, visible: boolean): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity) {
|
||||
entity.show = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮显示模型
|
||||
*/
|
||||
highlightModel(modelId: string, highlight: boolean = true): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity && entity.model) {
|
||||
if (highlight) {
|
||||
entity.model.silhouetteColor = Cesium.Color.YELLOW;
|
||||
entity.model.silhouetteSize = 2;
|
||||
} else {
|
||||
entity.model.silhouetteColor = undefined;
|
||||
entity.model.silhouetteSize = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除模型
|
||||
*/
|
||||
removeModel(modelId: string): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity) {
|
||||
this.entities.remove(entity);
|
||||
this.models.delete(modelId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有模型
|
||||
*/
|
||||
clearAllModels(): void {
|
||||
this.models.forEach((entity, modelId) => {
|
||||
this.entities.remove(entity);
|
||||
});
|
||||
this.models.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型信息
|
||||
*/
|
||||
getModelInfo(modelId: string): any {
|
||||
const entity = this.models.get(modelId);
|
||||
if (!entity) return null;
|
||||
|
||||
const position = entity.position?.getValue(Cesium.JulianDate.now());
|
||||
const cartographic = position ?
|
||||
Cesium.Cartographic.fromCartesian(position) : null;
|
||||
|
||||
return {
|
||||
id: modelId,
|
||||
position: cartographic ? {
|
||||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||||
height: cartographic.height
|
||||
} : null,
|
||||
visible: entity.show,
|
||||
model: entity.model ? {
|
||||
scale: entity.model.scale,
|
||||
color: entity.model.color
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞向模型
|
||||
*/
|
||||
flyToModel(modelId: string, duration: number = 2): void {
|
||||
const entity = this.models.get(modelId);
|
||||
if (entity) {
|
||||
this.viewer.flyTo(entity, {
|
||||
duration: duration,
|
||||
offset: new Cesium.HeadingPitchRange(0, -0.5, 100)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模型点击事件
|
||||
*/
|
||||
onModelClick(callback: (modelId: string, entity: Cesium.Entity) => void): void {
|
||||
this.viewer.entities.collectionChanged.addEventListener(() => {
|
||||
this.viewer.screenSpaceEventHandler.setInputAction((clickEvent: any) => {
|
||||
const pickedObject = this.viewer.scene.pick(clickEvent.position);
|
||||
if (pickedObject && pickedObject.id) {
|
||||
const entity = pickedObject.id;
|
||||
const modelId = this.findModelIdByEntity(entity);
|
||||
if (modelId) {
|
||||
callback(modelId, entity);
|
||||
}
|
||||
}
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体查找模型ID
|
||||
*/
|
||||
private findModelIdByEntity(entity: Cesium.Entity): string | null {
|
||||
for (const [modelId, modelEntity] of this.models) {
|
||||
if (modelEntity === entity) {
|
||||
return modelId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
68
src/views/cesiums/components/ViewTool.ts
Normal file
68
src/views/cesiums/components/ViewTool.ts
Normal file
@ -0,0 +1,68 @@
|
||||
// src/utils/cesium/ViewTool.ts
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
export class ViewTool {
|
||||
private viewer: Cesium.Viewer;
|
||||
|
||||
constructor(viewer: Cesium.Viewer) {
|
||||
this.viewer = viewer;
|
||||
}
|
||||
|
||||
// 飞往指定坐标
|
||||
flyTo(longitude: number, latitude: number, height: number = 10000, duration: number = 2): void {
|
||||
this.viewer.camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
|
||||
duration: duration
|
||||
});
|
||||
}
|
||||
|
||||
// 飞往矩形区域
|
||||
flyToRectangle(
|
||||
west: number,
|
||||
south: number,
|
||||
east: number,
|
||||
north: number,
|
||||
height: number | null = null,
|
||||
duration: number = 2
|
||||
): void {
|
||||
const rectangle = Cesium.Rectangle.fromDegrees(west, south, east, north);
|
||||
|
||||
const options: any = {
|
||||
destination: rectangle,
|
||||
duration: duration
|
||||
};
|
||||
|
||||
if (height) {
|
||||
options.offset = new Cesium.HeadingPitchRange(0, -Cesium.Math.PI / 4, height);
|
||||
}
|
||||
|
||||
this.viewer.camera.flyTo(options);
|
||||
}
|
||||
|
||||
// 设置重庆视角
|
||||
setChongqingView(viewType: string = 'default'): void {
|
||||
const views: { [key: string]: any } = {
|
||||
'default': { west: 106.3, south: 29.3, east: 106.8, north: 29.8, height: 50000 },
|
||||
'yuzhong': { west: 106.52, south: 29.52, east: 106.60, north: 29.58, height: 5000 },
|
||||
'overview': { west: 105.5, south: 28.5, east: 108.5, north: 31.5, height: 100000 }
|
||||
};
|
||||
|
||||
const view = views[viewType] || views['default'];
|
||||
this.flyToRectangle(view.west, view.south, view.east, view.north, view.height);
|
||||
}
|
||||
|
||||
// 获取当前相机信息
|
||||
getCameraInfo() {
|
||||
const position = this.viewer.camera.position;
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(position);
|
||||
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
|
||||
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
|
||||
const height = cartographic.height;
|
||||
|
||||
return {
|
||||
longitude: longitude.toFixed(6),
|
||||
latitude: latitude.toFixed(6),
|
||||
height: height.toFixed(2)
|
||||
};
|
||||
}
|
||||
}
|
||||
90
src/views/cesiums/components/useCesium.ts
Normal file
90
src/views/cesiums/components/useCesium.ts
Normal file
@ -0,0 +1,90 @@
|
||||
// src/composables/useCesium.ts
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import * as Cesium from 'cesium';
|
||||
import { ViewTool } from './ViewTool';
|
||||
import { MarkerTool } from './MarkerTool';
|
||||
import { ModelTool } from './ModelTool';
|
||||
import { CoordinatePicker } from './CoordinatePicker';
|
||||
import { DrawingTool } from './DrawingTool';
|
||||
|
||||
export function useCesium(containerId: string) {
|
||||
const viewer = ref<Cesium.Viewer | null>(null);
|
||||
const viewTool = ref<ViewTool | null>(null);
|
||||
const markerTool = ref<MarkerTool | null>(null);
|
||||
const modelTool = ref<ModelTool | null>(null);
|
||||
const coordinatePicker = ref<CoordinatePicker | null>(null);
|
||||
const drawingTool = ref<DrawingTool | null>(null);
|
||||
const isInitialized = ref(false);
|
||||
|
||||
// 初始化 Cesium
|
||||
const initializeCesium = async (): Promise<void> => {
|
||||
if (viewer.value) return;
|
||||
|
||||
try {
|
||||
// Cesium.Ion.defaultAccessToken = 'your-cesium-ion-access-token';
|
||||
|
||||
viewer.value = new Cesium.Viewer(containerId, {
|
||||
terrainProvider: await Cesium.createWorldTerrainAsync(),
|
||||
animation: false,
|
||||
timeline: false,
|
||||
baseLayerPicker: false,
|
||||
geocoder: false,
|
||||
homeButton: false,
|
||||
sceneModePicker: false,
|
||||
navigationHelpButton: false,
|
||||
fullscreenButton: false,
|
||||
infoBox: false
|
||||
});
|
||||
|
||||
// 初始化工具类
|
||||
viewTool.value = new ViewTool(viewer.value);
|
||||
markerTool.value = new MarkerTool(viewer.value);
|
||||
modelTool.value = new ModelTool(viewer.value);
|
||||
coordinatePicker.value = new CoordinatePicker(viewer.value);
|
||||
drawingTool.value = new DrawingTool(viewer.value);
|
||||
|
||||
isInitialized.value = true;
|
||||
|
||||
// 设置初始视角
|
||||
viewTool.value.setChongqingView('overview');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Cesium:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 销毁 Cesium
|
||||
const destroyCesium = (): void => {
|
||||
if (viewer.value) {
|
||||
coordinatePicker.value?.destroy();
|
||||
drawingTool.value?.destroy();
|
||||
viewer.value.destroy();
|
||||
viewer.value = null;
|
||||
viewTool.value = null;
|
||||
markerTool.value = null;
|
||||
modelTool.value = null;
|
||||
coordinatePicker.value = null;
|
||||
drawingTool.value = null;
|
||||
isInitialized.value = false;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
initializeCesium();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyCesium();
|
||||
});
|
||||
|
||||
return {
|
||||
viewer,
|
||||
viewTool,
|
||||
markerTool,
|
||||
modelTool,
|
||||
coordinatePicker,
|
||||
drawingTool,
|
||||
isInitialized,
|
||||
initializeCesium,
|
||||
destroyCesium
|
||||
};
|
||||
}
|
||||
73
src/views/cesiums/components/useCesiumTools.ts
Normal file
73
src/views/cesiums/components/useCesiumTools.ts
Normal file
@ -0,0 +1,73 @@
|
||||
// src/composables/useCesiumTools.ts
|
||||
import { ref, computed } from 'vue';
|
||||
import type { ViewTool } from './ViewTool';
|
||||
import type { MarkerTool } from './MarkerTool';
|
||||
|
||||
export function useCesiumTools(viewTool: ViewTool | null, markerTool: MarkerTool | null) {
|
||||
const currentView = ref('overview');
|
||||
const cameraInfo = ref({ longitude: '0', latitude: '0', height: '0' });
|
||||
|
||||
// 视角选项
|
||||
const viewOptions = [
|
||||
{ value: 'overview', label: '重庆全景', icon: '🌆' },
|
||||
{ value: 'yuzhong', label: '渝中半岛', icon: '🏞️' },
|
||||
{ value: 'default', label: '默认视角', icon: '📍' }
|
||||
];
|
||||
|
||||
// 地标选项
|
||||
const landmarkOptions = [
|
||||
{ value: 'chongqing', label: '重庆地标', icon: '🏛️' }
|
||||
];
|
||||
|
||||
// 设置视角
|
||||
const setView = (viewType: string): void => {
|
||||
if (viewTool.value) {
|
||||
viewTool.value.setChongqingView(viewType);
|
||||
currentView.value = viewType;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加地标
|
||||
const addLandmarks = (type: string): void => {
|
||||
console.log("添加地标")
|
||||
if (markerTool.value) {
|
||||
switch (type) {
|
||||
case 'chongqing':
|
||||
markerTool.value.addChongqingLandmark();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 清除地标
|
||||
const clearLandmarks = (): void => {
|
||||
console.log("清除地标")
|
||||
if (markerTool.value) {
|
||||
markerTool.value.clearAllMarkers();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新相机信息
|
||||
const updateCameraInfo = (): void => {
|
||||
if (viewTool.value) {
|
||||
cameraInfo.value = viewTool.value.getCameraInfo();
|
||||
}
|
||||
};
|
||||
|
||||
// 自动更新相机信息
|
||||
const startCameraTracking = (): void => {
|
||||
setInterval(updateCameraInfo, 1000);
|
||||
};
|
||||
|
||||
return {
|
||||
currentView,
|
||||
cameraInfo,
|
||||
viewOptions,
|
||||
landmarkOptions,
|
||||
setView,
|
||||
addLandmarks,
|
||||
clearLandmarks,
|
||||
updateCameraInfo,
|
||||
startCameraTracking
|
||||
};
|
||||
}
|
||||
107
src/views/cesiums/components/useCoordinatePicker.ts
Normal file
107
src/views/cesiums/components/useCoordinatePicker.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// src/composables/useCoordinatePicker.ts
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { CoordinatePicker, PickResult } from './CoordinatePicker';
|
||||
|
||||
export function useCoordinatePicker(coordinatePicker: CoordinatePicker | null) {
|
||||
const isPickerEnabled = ref(false);
|
||||
const pickHistory = reactive<PickResult[]>([]);
|
||||
const lastPickResult = ref<PickResult | null>(null);
|
||||
const maxHistoryLength = 10;
|
||||
|
||||
// 启用坐标拾取
|
||||
const enablePicker = (): void => {
|
||||
if (coordinatePicker.value) {
|
||||
coordinatePicker.value.enable();
|
||||
isPickerEnabled.value = true;
|
||||
|
||||
// 添加拾取回调
|
||||
coordinatePicker.value.onPick(handlePick);
|
||||
}
|
||||
};
|
||||
|
||||
// 禁用坐标拾取
|
||||
const disablePicker = (): void => {
|
||||
if (coordinatePicker.value) {
|
||||
coordinatePicker.value.disable();
|
||||
isPickerEnabled.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换拾取状态
|
||||
const togglePicker = (): void => {
|
||||
if (isPickerEnabled.value) {
|
||||
disablePicker();
|
||||
} else {
|
||||
enablePicker();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理拾取结果
|
||||
const handlePick = (result: PickResult): void => {
|
||||
lastPickResult.value = result;
|
||||
|
||||
// 添加到历史记录
|
||||
pickHistory.unshift(result);
|
||||
|
||||
// 限制历史记录长度
|
||||
if (pickHistory.length > maxHistoryLength) {
|
||||
pickHistory.pop();
|
||||
}
|
||||
};
|
||||
|
||||
// 清除历史记录
|
||||
const clearHistory = (): void => {
|
||||
pickHistory.splice(0, pickHistory.length);
|
||||
lastPickResult.value = null;
|
||||
};
|
||||
|
||||
// 复制坐标到剪贴板
|
||||
const copyToClipboard = (result: PickResult): void => {
|
||||
const text = `经度: ${result.longitude.toFixed(6)}, 纬度: ${result.latitude.toFixed(6)}, 高程: ${result.height.toFixed(2)}米`;
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('坐标已复制到剪贴板:', text);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化坐标显示
|
||||
const formatCoordinate = (result: PickResult): string => {
|
||||
return `经度: ${result.longitude.toFixed(6)}\n纬度: ${result.latitude.toFixed(6)}\n高程: ${result.height.toFixed(2)}米`;
|
||||
};
|
||||
|
||||
// 获取WKT格式坐标
|
||||
const getWKT = (result: PickResult): string => {
|
||||
return `POINT(${result.longitude.toFixed(6)} ${result.latitude.toFixed(6)})`;
|
||||
};
|
||||
|
||||
// 获取GeoJSON格式坐标
|
||||
const getGeoJSON = (result: PickResult): any => {
|
||||
return {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [result.longitude, result.latitude, result.height]
|
||||
},
|
||||
properties: {
|
||||
height: result.height,
|
||||
terrainHeight: result.terrainHeight
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
isPickerEnabled,
|
||||
pickHistory,
|
||||
lastPickResult,
|
||||
enablePicker,
|
||||
disablePicker,
|
||||
togglePicker,
|
||||
clearHistory,
|
||||
copyToClipboard,
|
||||
formatCoordinate,
|
||||
getWKT,
|
||||
getGeoJSON
|
||||
};
|
||||
}
|
||||
302
src/views/cesiums/components/useDrawingManager.ts
Normal file
302
src/views/cesiums/components/useDrawingManager.ts
Normal file
@ -0,0 +1,302 @@
|
||||
// src/composables/useDrawingManager.ts
|
||||
import { ref, reactive,watch} from 'vue';
|
||||
import type { DrawingTool, DrawingResult, DrawingInfo, DrawingOptions, CircleOptions, PolygonOptions } from './DrawingTool';
|
||||
|
||||
export function useDrawingManager(drawingTool: any) { // 修改参数类型为 any 或 Ref<DrawingTool | null>
|
||||
const isDrawing = ref(false);
|
||||
const currentDrawingType = ref<'circle' | 'polygon' | null>(null);
|
||||
const drawings = reactive(new Map<string, DrawingResult>());
|
||||
const selectedDrawing = ref<string | null>(null);
|
||||
const drawingInfo = ref<DrawingInfo | null>(null);
|
||||
|
||||
// 绘图选项
|
||||
const drawingOptions = reactive<{
|
||||
circle: CircleOptions;
|
||||
polygon: PolygonOptions;
|
||||
}>({
|
||||
circle: {
|
||||
color: Cesium.Color.YELLOW.withAlpha(0.3),
|
||||
outlineColor: Cesium.Color.YELLOW,
|
||||
outlineWidth: 2,
|
||||
fill: true,
|
||||
height: 0,
|
||||
extrudedHeight: 1000
|
||||
},
|
||||
polygon: {
|
||||
color: Cesium.Color.CYAN.withAlpha(0.3),
|
||||
outlineColor: Cesium.Color.CYAN,
|
||||
outlineWidth: 2,
|
||||
fill: true,
|
||||
height: 0,
|
||||
extrudedHeight: 1000,
|
||||
closePath: true
|
||||
}
|
||||
});
|
||||
|
||||
// // 初始化点击回调 - 使用 watch 来确保 drawingTool 已初始化
|
||||
// import { watch } from 'vue';
|
||||
|
||||
watch(() => drawingTool?.value, (newTool) => {
|
||||
if (newTool) {
|
||||
newTool.setCallbacks({
|
||||
onClick: (result: DrawingResult, info: DrawingInfo) => {
|
||||
handleDrawingClick(result, info);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 处理绘图点击
|
||||
const handleDrawingClick = (result: DrawingResult, info: DrawingInfo): void => {
|
||||
selectedDrawing.value = result.id;
|
||||
drawingInfo.value = info;
|
||||
|
||||
// 高亮显示
|
||||
if (drawingTool?.value) {
|
||||
drawingTool.value.highlightDrawing(result.id, true);
|
||||
}
|
||||
|
||||
console.log('Drawing clicked:', info);
|
||||
};
|
||||
|
||||
// 开始绘制圆形
|
||||
const startCircleDrawing = (): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (!tool) return;
|
||||
|
||||
tool.setCallbacks({
|
||||
onStart: () => {
|
||||
isDrawing.value = true;
|
||||
currentDrawingType.value = 'circle';
|
||||
drawingInfo.value = null;
|
||||
},
|
||||
onComplete: (result: DrawingResult) => {
|
||||
isDrawing.value = false;
|
||||
currentDrawingType.value = null;
|
||||
drawings.set(result.id, result);
|
||||
selectedDrawing.value = result.id;
|
||||
drawingInfo.value = result.info;
|
||||
},
|
||||
onCancel: () => {
|
||||
isDrawing.value = false;
|
||||
currentDrawingType.value = null;
|
||||
},
|
||||
onClick: (result: DrawingResult, info: DrawingInfo) => {
|
||||
handleDrawingClick(result, info);
|
||||
}
|
||||
});
|
||||
|
||||
tool.startDrawingCircle(drawingOptions.circle);
|
||||
};
|
||||
|
||||
// 开始绘制多边形
|
||||
const startPolygonDrawing = (): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (!tool) return;
|
||||
|
||||
tool.setCallbacks({
|
||||
onStart: () => {
|
||||
isDrawing.value = true;
|
||||
currentDrawingType.value = 'polygon';
|
||||
drawingInfo.value = null;
|
||||
},
|
||||
onComplete: (result: DrawingResult) => {
|
||||
isDrawing.value = false;
|
||||
currentDrawingType.value = null;
|
||||
drawings.set(result.id, result);
|
||||
selectedDrawing.value = result.id;
|
||||
drawingInfo.value = result.info;
|
||||
},
|
||||
onCancel: () => {
|
||||
isDrawing.value = false;
|
||||
currentDrawingType.value = null;
|
||||
},
|
||||
onClick: (result: DrawingResult, info: DrawingInfo) => {
|
||||
handleDrawingClick(result, info);
|
||||
}
|
||||
});
|
||||
|
||||
tool.startDrawingPolygon(drawingOptions.polygon);
|
||||
};
|
||||
|
||||
// 取消绘制
|
||||
const cancelDrawing = (): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (tool) {
|
||||
tool.cancelDrawing();
|
||||
isDrawing.value = false;
|
||||
currentDrawingType.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 选择图形
|
||||
const selectDrawing = (id: string): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (selectedDrawing.value === id) {
|
||||
deselectDrawing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDrawing.value && tool) {
|
||||
tool.highlightDrawing(selectedDrawing.value, false);
|
||||
}
|
||||
|
||||
selectedDrawing.value = id;
|
||||
const drawing = drawings.get(id);
|
||||
if (drawing && tool) {
|
||||
tool.highlightDrawing(id, true);
|
||||
drawingInfo.value = drawing.info;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消选择
|
||||
const deselectDrawing = (): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (selectedDrawing.value && tool) {
|
||||
tool.highlightDrawing(selectedDrawing.value, false);
|
||||
}
|
||||
selectedDrawing.value = null;
|
||||
drawingInfo.value = null;
|
||||
};
|
||||
|
||||
// 移除图形
|
||||
const removeDrawing = (id: string): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (tool && tool.removeDrawing(id)) {
|
||||
drawings.delete(id);
|
||||
if (selectedDrawing.value === id) {
|
||||
deselectDrawing();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 清除所有图形
|
||||
const clearAllDrawings = (): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (tool) {
|
||||
tool.clearAllDrawings();
|
||||
drawings.clear();
|
||||
deselectDrawing();
|
||||
}
|
||||
};
|
||||
|
||||
// 飞向图形
|
||||
const flyToDrawing = (id: string): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (tool) {
|
||||
tool.flyToDrawing(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新绘图选项
|
||||
const updateDrawingOptions = (type: 'circle' | 'polygon', options: Partial<DrawingOptions>): void => {
|
||||
drawingOptions[type] = { ...drawingOptions[type], ...options };
|
||||
};
|
||||
|
||||
// 导出图形数据
|
||||
const exportDrawings = (): any[] => {
|
||||
const tool = drawingTool?.value;
|
||||
if (tool) {
|
||||
return tool.exportDrawings();
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 导入图形数据
|
||||
const importDrawings = (data: any[]): void => {
|
||||
const tool = drawingTool?.value;
|
||||
if (tool) {
|
||||
tool.importDrawings(data);
|
||||
data.forEach(item => {
|
||||
drawings.set(item.id, item as DrawingResult);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取绘图状态文本
|
||||
const getDrawingStatus = (): string => {
|
||||
if (!isDrawing.value) return '准备就绪';
|
||||
|
||||
if (currentDrawingType.value === 'circle') {
|
||||
return '绘制圆形: 点击确定圆心,再次点击确定半径,右键完成';
|
||||
} else if (currentDrawingType.value === 'polygon') {
|
||||
return '绘制多边形: 点击添加顶点,右键完成绘制,ESC取消';
|
||||
}
|
||||
|
||||
return '绘制中...';
|
||||
};
|
||||
|
||||
// 格式化坐标显示
|
||||
const formatCoordinate = (coord: { longitude: number; latitude: number; height: number }): string => {
|
||||
return `经度: ${coord.longitude.toFixed(6)}\n纬度: ${coord.latitude.toFixed(6)}\n高程: ${coord.height.toFixed(2)}米`;
|
||||
};
|
||||
|
||||
// 获取绘图信息显示文本
|
||||
const getDrawingInfoText = (): string => {
|
||||
if (!drawingInfo.value) return '';
|
||||
|
||||
const info = drawingInfo.value;
|
||||
|
||||
if (info.type === 'circle') {
|
||||
const center = info.properties.center;
|
||||
return `⭕ 圆形空域信息\n\n` +
|
||||
`📍 圆心坐标:\n${formatCoordinate(center)}\n\n` +
|
||||
`📏 半径: ${(info.radius! / 1000).toFixed(3)} km\n` +
|
||||
`📐 直径: ${(info.radius! * 2 / 1000).toFixed(3)} km\n` +
|
||||
`🔄 周长: ${(info.properties.circumference / 1000).toFixed(3)} km\n` +
|
||||
`📊 面积: ${(info.area / 1000000).toFixed(3)} km²`;
|
||||
} else {
|
||||
const bounds = info.properties.bounds;
|
||||
const center = info.properties.center;
|
||||
const boundaryPoints = info.properties.boundaryPoints;
|
||||
|
||||
let text = `🔷 多边形空域信息\n\n` +
|
||||
`📍 中心点:\n${formatCoordinate(center)}\n\n` +
|
||||
`🗺️ 边界范围:\n` +
|
||||
`北: ${bounds.north.toFixed(6)}\n` +
|
||||
`南: ${bounds.south.toFixed(6)}\n` +
|
||||
`东: ${bounds.east.toFixed(6)}\n` +
|
||||
`西: ${bounds.west.toFixed(6)}\n\n` +
|
||||
`📏 尺寸:\n` +
|
||||
`宽度: ${(info.properties.width / 1000).toFixed(3)} km\n` +
|
||||
`高度: ${(info.properties.height / 1000).toFixed(3)} km\n` +
|
||||
`周长: ${(info.properties.perimeter / 1000).toFixed(3)} km\n` +
|
||||
`面积: ${(info.area / 1000000).toFixed(3)} km²\n\n` +
|
||||
`📍 边界顶点 (${boundaryPoints.length}个):\n`;
|
||||
|
||||
// 添加前几个顶点信息,避免信息过长
|
||||
boundaryPoints.slice(0, 6).forEach((point, index) => {
|
||||
text += `顶点${index + 1}: ${point.longitude.toFixed(6)}, ${point.latitude.toFixed(6)}\n`;
|
||||
});
|
||||
|
||||
if (boundaryPoints.length > 6) {
|
||||
text += `... 还有 ${boundaryPoints.length - 6} 个顶点`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isDrawing,
|
||||
currentDrawingType,
|
||||
drawings,
|
||||
selectedDrawing,
|
||||
drawingInfo,
|
||||
drawingOptions,
|
||||
startCircleDrawing,
|
||||
startPolygonDrawing,
|
||||
cancelDrawing,
|
||||
selectDrawing,
|
||||
deselectDrawing,
|
||||
removeDrawing,
|
||||
clearAllDrawings,
|
||||
flyToDrawing,
|
||||
updateDrawingOptions,
|
||||
exportDrawings,
|
||||
importDrawings,
|
||||
getDrawingStatus,
|
||||
getDrawingInfoText,
|
||||
formatCoordinate
|
||||
};
|
||||
}
|
||||
184
src/views/cesiums/components/useModelManager.ts
Normal file
184
src/views/cesiums/components/useModelManager.ts
Normal file
@ -0,0 +1,184 @@
|
||||
// src/composables/useModelManager.ts
|
||||
import { ref, reactive } from 'vue';
|
||||
// import type { ModelTool } from './ModelTool';
|
||||
import type { ModelOptions, ModelAnimation,ModelTool } from './ModelTool';
|
||||
|
||||
export function useModelManager(modelTool: ModelTool | null) {
|
||||
const models = reactive(new Map());
|
||||
const selectedModel = ref<string | null>(null);
|
||||
const isModelLoading = ref(false);
|
||||
|
||||
// 预定义模型配置
|
||||
const predefinedModels = {
|
||||
building: {
|
||||
name: '建筑模型',
|
||||
url: '/models/building.glb',
|
||||
scale: 1.0
|
||||
},
|
||||
vehicle: {
|
||||
name: '车辆模型',
|
||||
url: '/models/vehicle.glb',
|
||||
scale: 0.5
|
||||
},
|
||||
tree: {
|
||||
name: '树木模型',
|
||||
url: '/models/tree.glb',
|
||||
scale: 2.0
|
||||
}
|
||||
};
|
||||
|
||||
// 重庆地标配置
|
||||
const chongqingLandmarks = [
|
||||
{
|
||||
id: 'landmark_1',
|
||||
name: '重庆大剧院',
|
||||
type: 'building',
|
||||
position: { longitude: 106.582, latitude: 29.568, height: 10 }
|
||||
},
|
||||
{
|
||||
id: 'landmark_2',
|
||||
name: '来福士广场',
|
||||
type: 'building',
|
||||
position: { longitude: 106.584, latitude: 29.566, height: 15 }
|
||||
}
|
||||
];
|
||||
|
||||
// 加载模型
|
||||
const loadModel = async (
|
||||
id: string,
|
||||
url: string,
|
||||
position: { longitude: number; latitude: number; height: number },
|
||||
options: Partial<ModelOptions> = {}
|
||||
): Promise<void> => {
|
||||
if (!modelTool.value) return;
|
||||
|
||||
isModelLoading.value = true;
|
||||
try {
|
||||
const entity = await modelTool.value.loadModel(id, url, position, options);
|
||||
models.set(id, {
|
||||
id,
|
||||
entity,
|
||||
position,
|
||||
options
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load model:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
isModelLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载预定义模型
|
||||
const loadPredefinedModel = async (
|
||||
id: string,
|
||||
modelType: keyof typeof predefinedModels,
|
||||
position: { longitude: number; latitude: number; height: number }
|
||||
): Promise<void> => {
|
||||
const modelConfig = predefinedModels[modelType];
|
||||
return loadModel(id, modelConfig.url, position, { scale: modelConfig.scale });
|
||||
};
|
||||
|
||||
// 加载重庆地标
|
||||
const loadChongqingLandmarks = async (): Promise<void> => {
|
||||
if (!modelTool.value) return;
|
||||
|
||||
for (const landmark of chongqingLandmarks) {
|
||||
try {
|
||||
await loadPredefinedModel(
|
||||
landmark.id,
|
||||
landmark.type as keyof typeof predefinedModels,
|
||||
landmark.position
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load landmark ${landmark.name}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 选择模型
|
||||
const selectModel = (modelId: string): void => {
|
||||
selectedModel.value = modelId;
|
||||
if (modelTool.value) {
|
||||
modelTool.value.highlightModel(modelId, true);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消选择模型
|
||||
const deselectModel = (): void => {
|
||||
if (selectedModel.value && modelTool.value) {
|
||||
modelTool.value.highlightModel(selectedModel.value, false);
|
||||
}
|
||||
selectedModel.value = null;
|
||||
};
|
||||
|
||||
// 更新模型位置
|
||||
const updateModelPosition = (
|
||||
modelId: string,
|
||||
position: { longitude: number; latitude: number; height: number }
|
||||
): void => {
|
||||
if (modelTool.value) {
|
||||
modelTool.value.setModelPosition(modelId, position);
|
||||
const model = models.get(modelId);
|
||||
if (model) {
|
||||
model.position = position;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新模型缩放
|
||||
const updateModelScale = (modelId: string, scale: number): void => {
|
||||
if (modelTool.value) {
|
||||
modelTool.value.setModelScale(modelId, scale);
|
||||
const model = models.get(modelId);
|
||||
if (model) {
|
||||
model.options.scale = scale;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 移除模型
|
||||
const removeModel = (modelId: string): void => {
|
||||
if (modelTool.value) {
|
||||
modelTool.value.removeModel(modelId);
|
||||
models.delete(modelId);
|
||||
if (selectedModel.value === modelId) {
|
||||
deselectModel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 清除所有模型
|
||||
const clearAllModels = (): void => {
|
||||
if (modelTool.value) {
|
||||
modelTool.value.clearAllModels();
|
||||
models.clear();
|
||||
deselectModel();
|
||||
}
|
||||
};
|
||||
|
||||
// 飞向模型
|
||||
const flyToModel = (modelId: string): void => {
|
||||
if (modelTool.value) {
|
||||
modelTool.value.flyToModel(modelId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
models,
|
||||
selectedModel,
|
||||
isModelLoading,
|
||||
predefinedModels,
|
||||
chongqingLandmarks,
|
||||
loadModel,
|
||||
loadPredefinedModel,
|
||||
loadChongqingLandmarks,
|
||||
selectModel,
|
||||
deselectModel,
|
||||
updateModelPosition,
|
||||
updateModelScale,
|
||||
removeModel,
|
||||
clearAllModels,
|
||||
flyToModel
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user