cesium地图添加画空域的工具类

This commit is contained in:
zhulongchuan 2025-10-11 16:10:41 +08:00
parent e12886ae71
commit 16bd368d47
17 changed files with 4325 additions and 0 deletions

1
auto-imports.d.ts vendored
View File

@ -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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 = [];
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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)
};
}
}

View 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
};
}

View 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
};
}

View 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
};
}

View 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
};
}

View 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
};
}