2026-06-23 21:25:54 +08:00
|
|
|
import { ref, reactive, onUnmounted, watch, computed } from 'vue';
|
|
|
|
|
import { useStatusStore } from '@/stores/useStatusStore';
|
|
|
|
|
import { useLoadingResourceStore } from '@/stores/useLoadingResourceStore';
|
2026-06-24 17:16:25 +08:00
|
|
|
import { useAroundAnalysisConfig } from './useAroundAnalysisConfig';
|
2026-06-23 21:25:54 +08:00
|
|
|
import type { PointResource, PointResourceCategory, AnalysisButtonConfig, AroundAnalysisState } from '@/types/common/useAroundAnalysisType';
|
|
|
|
|
import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils';
|
|
|
|
|
import { isCategoryVisible, loadAllPointData, calculateDistance } from '@/utils/aroundAnalysisUtils';
|
|
|
|
|
import {
|
|
|
|
|
ScreenSpaceEventHandler,
|
|
|
|
|
ScreenSpaceEventType,
|
|
|
|
|
Cartesian2,
|
|
|
|
|
Cartographic,
|
|
|
|
|
Cartesian3,
|
|
|
|
|
} from 'cesium';
|
|
|
|
|
import { useCircleDrawer } from './useCircleDrawer';
|
|
|
|
|
import { usePulseEffect } from './usePulseEffect';
|
|
|
|
|
import { useMarkerManager } from './useMarkerManager';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 周边分析统一 Hook(合并按钮和搜索逻辑)
|
|
|
|
|
*/
|
|
|
|
|
export const useAroundAnalysis = (): AroundAnalysisState => {
|
|
|
|
|
const statusStore = useStatusStore();
|
2026-06-24 17:16:25 +08:00
|
|
|
const { resourceConfigs, MIN_FLY_HEIGHT, FLY_HEIGHT_MULTIPLIER, FLY_DURATION } = useAroundAnalysisConfig();
|
2026-06-23 21:25:54 +08:00
|
|
|
|
|
|
|
|
// ==================== 响应式状态 ====================
|
|
|
|
|
const selectedButtonIndex = ref<number>(-1);
|
|
|
|
|
const showAreaDialog = ref(false);
|
|
|
|
|
const radius = ref(10);
|
|
|
|
|
const dialogPosition = reactive({ x: 0, y: 0 });
|
|
|
|
|
const pulsePoints = ref<PointResource[]>([]);
|
|
|
|
|
const showPulsePointList = ref(false);
|
|
|
|
|
const searchState = ref('');
|
|
|
|
|
const canSearch = computed(() => {
|
2026-06-24 17:16:25 +08:00
|
|
|
return resourceConfigs.value.some(config => isCategoryVisible(config.category, config.forcedType));
|
2026-06-23 21:25:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let clickHandler: ScreenSpaceEventHandler | null = null;
|
|
|
|
|
let currentCenterPosition: Cartesian3 | null = null;
|
|
|
|
|
|
|
|
|
|
// ==================== 组合子 Hook ====================
|
|
|
|
|
const { drawCircle, clearCircle } = useCircleDrawer();
|
|
|
|
|
const { addPulseEffectToPoints, removePulseEffect } = usePulseEffect();
|
|
|
|
|
const { addMarker, removeMarker } = useMarkerManager();
|
|
|
|
|
|
|
|
|
|
// ==================== 核心功能 ====================
|
|
|
|
|
const getPointsInCircle = (centerPosition: Cartesian3, radiusKm: number): PointResource[] => {
|
|
|
|
|
const cartographic = Cartographic.fromCartesian(centerPosition);
|
|
|
|
|
const centerLon = cartographic.longitude * (180 / Math.PI);
|
|
|
|
|
const centerLat = cartographic.latitude * (180 / Math.PI);
|
|
|
|
|
|
2026-06-24 17:16:25 +08:00
|
|
|
const allPoints = loadAllPointData(resourceConfigs.value);
|
2026-06-23 21:25:54 +08:00
|
|
|
const radiusMeters = radiusKm * 1000;
|
|
|
|
|
|
2026-06-24 11:30:53 +08:00
|
|
|
const filteredPoints = allPoints.filter(point => {
|
2026-06-23 21:25:54 +08:00
|
|
|
if (point.lon === undefined || point.lat === undefined) return false;
|
|
|
|
|
const distance = calculateDistance(centerLon, centerLat, point.lon, point.lat);
|
|
|
|
|
return distance <= radiusMeters && isCategoryVisible(point.category as PointResourceCategory, point.originalType);
|
|
|
|
|
});
|
2026-06-24 11:30:53 +08:00
|
|
|
|
|
|
|
|
// 按坐标去重:相同经纬度的点只保留一个
|
|
|
|
|
const coordMap = new Map<string, PointResource>();
|
|
|
|
|
filteredPoints.forEach(point => {
|
|
|
|
|
const coordKey = `${point.lon},${point.lat}`;
|
|
|
|
|
if (!coordMap.has(coordKey)) {
|
|
|
|
|
coordMap.set(coordKey, point);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Array.from(coordMap.values());
|
2026-06-23 21:25:54 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const refreshPulseEffect = () => {
|
|
|
|
|
if (!currentCenterPosition) return;
|
|
|
|
|
removePulseEffect();
|
|
|
|
|
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
|
|
|
|
addPulseEffectToPoints(pointsInCircle);
|
|
|
|
|
pulsePoints.value = pointsInCircle;
|
|
|
|
|
showPulsePointList.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearAllAnalysisResources = () => {
|
|
|
|
|
removeMarker();
|
|
|
|
|
clearCircle();
|
|
|
|
|
removePulseEffect();
|
|
|
|
|
currentCenterPosition = null;
|
|
|
|
|
pulsePoints.value = [];
|
|
|
|
|
showPulsePointList.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearVisualEffectsOnly = () => {
|
|
|
|
|
clearCircle();
|
|
|
|
|
removePulseEffect();
|
|
|
|
|
pulsePoints.value = [];
|
|
|
|
|
showPulsePointList.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 地图事件 ====================
|
|
|
|
|
const registerMapClickHandler = () => {
|
|
|
|
|
const viewer = CesiumUtilsSingleton.getViewer();
|
|
|
|
|
if (!viewer) return;
|
|
|
|
|
|
|
|
|
|
clickHandler = new ScreenSpaceEventHandler(viewer.scene.canvas);
|
|
|
|
|
clickHandler.setInputAction((clickEvent: { position: Cartesian2 }) => {
|
|
|
|
|
const cartesian = viewer.camera.pickEllipsoid(clickEvent.position, viewer.scene.globe.ellipsoid);
|
|
|
|
|
if (cartesian) {
|
|
|
|
|
currentCenterPosition = cartesian;
|
|
|
|
|
const cartographic = Cartographic.fromCartesian(cartesian);
|
|
|
|
|
console.log('点击位置:', {
|
|
|
|
|
longitude: cartographic.longitude * (180 / Math.PI),
|
|
|
|
|
latitude: cartographic.latitude * (180 / Math.PI)
|
|
|
|
|
});
|
|
|
|
|
addMarker(cartesian);
|
|
|
|
|
showAreaDialog.value = true;
|
|
|
|
|
calculateDialogPosition(clickEvent.position);
|
|
|
|
|
}
|
|
|
|
|
}, ScreenSpaceEventType.LEFT_CLICK);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeMapClickHandler = () => {
|
|
|
|
|
if (clickHandler) {
|
|
|
|
|
clickHandler.destroy();
|
|
|
|
|
clickHandler = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calculateDialogPosition = (clickPosition: Cartesian2) => {
|
|
|
|
|
const { innerWidth: screenWidth, innerHeight: screenHeight } = window;
|
2026-06-24 17:16:25 +08:00
|
|
|
const { DIALOG_WIDTH: dialogWidth, DIALOG_HEIGHT: dialogHeight, DIALOG_PADDING: dialogPadding, DIALOG_OFFSET: dialogOffset } = useAroundAnalysisConfig().getConstants();
|
2026-06-23 21:25:54 +08:00
|
|
|
|
2026-06-24 17:16:25 +08:00
|
|
|
let x = clickPosition.x + dialogOffset;
|
|
|
|
|
let y = clickPosition.y + dialogOffset;
|
2026-06-23 21:25:54 +08:00
|
|
|
|
2026-06-24 17:16:25 +08:00
|
|
|
if (x + dialogWidth > screenWidth - dialogPadding) {
|
|
|
|
|
x = clickPosition.x - dialogWidth - dialogOffset;
|
2026-06-23 21:25:54 +08:00
|
|
|
}
|
2026-06-24 17:16:25 +08:00
|
|
|
if (y + dialogHeight > screenHeight - dialogPadding) {
|
|
|
|
|
y = clickPosition.y - dialogHeight - dialogOffset;
|
2026-06-23 21:25:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:16:25 +08:00
|
|
|
dialogPosition.x = Math.max(dialogPadding, Math.min(x, screenWidth - dialogWidth - dialogPadding));
|
|
|
|
|
dialogPosition.y = Math.max(dialogPadding, Math.min(y, screenHeight - dialogHeight - dialogPadding));
|
2026-06-23 21:25:54 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 事件处理 ====================
|
|
|
|
|
const handleConfirm = () => {
|
|
|
|
|
if (!currentCenterPosition) {
|
|
|
|
|
console.error('中心点位置不存在');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearVisualEffectsOnly();
|
|
|
|
|
drawCircle(currentCenterPosition, radius.value);
|
|
|
|
|
|
|
|
|
|
const pointsInCircle = getPointsInCircle(currentCenterPosition, radius.value);
|
|
|
|
|
addPulseEffectToPoints(pointsInCircle);
|
|
|
|
|
pulsePoints.value = pointsInCircle;
|
|
|
|
|
showPulsePointList.value = true;
|
|
|
|
|
|
|
|
|
|
const cartographic = Cartographic.fromCartesian(currentCenterPosition);
|
|
|
|
|
const longitude = cartographic.longitude * (180 / Math.PI);
|
|
|
|
|
const latitude = cartographic.latitude * (180 / Math.PI);
|
|
|
|
|
|
2026-06-24 17:16:25 +08:00
|
|
|
const flyHeight = Math.max(radius.value * FLY_HEIGHT_MULTIPLIER, MIN_FLY_HEIGHT);
|
|
|
|
|
CesiumUtilsSingleton.flyToTarget([longitude, latitude, flyHeight], FLY_DURATION);
|
2026-06-23 21:25:54 +08:00
|
|
|
|
|
|
|
|
showAreaDialog.value = false;
|
|
|
|
|
removeMapClickHandler();
|
|
|
|
|
|
|
|
|
|
const viewer = CesiumUtilsSingleton.getViewer();
|
|
|
|
|
if (viewer?.canvas) {
|
|
|
|
|
statusStore.cursorStyle = 'default';
|
|
|
|
|
viewer.canvas.style.cursor = 'default';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
showAreaDialog.value = false;
|
|
|
|
|
clearAllAnalysisResources();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleButtonClick = (index: number, callback: (status: boolean) => void) => {
|
|
|
|
|
const isActive = selectedButtonIndex.value === index;
|
|
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
|
selectedButtonIndex.value = -1;
|
|
|
|
|
callback(false);
|
|
|
|
|
} else {
|
|
|
|
|
if (selectedButtonIndex.value !== -1) {
|
|
|
|
|
clearAllAnalysisResources();
|
|
|
|
|
showAreaDialog.value = false;
|
|
|
|
|
}
|
|
|
|
|
selectedButtonIndex.value = index;
|
|
|
|
|
callback(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const startAreaAnalysisFromSearch = (point: PointResource) => {
|
|
|
|
|
if (point.lon == null || point.lat == null) return;
|
|
|
|
|
|
|
|
|
|
clearAllAnalysisResources();
|
|
|
|
|
currentCenterPosition = Cartesian3.fromDegrees(point.lon, point.lat, 0);
|
|
|
|
|
selectedButtonIndex.value = 0;
|
|
|
|
|
|
|
|
|
|
const viewer = CesiumUtilsSingleton.getViewer();
|
|
|
|
|
if (viewer?.canvas) {
|
|
|
|
|
statusStore.cursorStyle = 'crosshair';
|
|
|
|
|
viewer.canvas.style.cursor = 'crosshair';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addMarker(currentCenterPosition);
|
|
|
|
|
showAreaDialog.value = true;
|
|
|
|
|
|
|
|
|
|
const centerX = window.innerWidth / 2;
|
|
|
|
|
const centerY = window.innerHeight / 2;
|
2026-06-24 17:16:25 +08:00
|
|
|
const { DIALOG_WIDTH: dialogWidth, DIALOG_HEIGHT: dialogHeight, DIALOG_PADDING: dialogPadding, DIALOG_OFFSET: dialogOffset } = useAroundAnalysisConfig().getConstants();
|
2026-06-23 21:25:54 +08:00
|
|
|
|
2026-06-24 17:16:25 +08:00
|
|
|
dialogPosition.x = Math.max(dialogPadding, Math.min(centerX + dialogOffset, window.innerWidth - dialogWidth - dialogPadding));
|
|
|
|
|
dialogPosition.y = Math.max(dialogPadding, Math.min(centerY + dialogOffset, window.innerHeight - dialogHeight - dialogPadding));
|
2026-06-23 21:25:54 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 搜索功能 ====================
|
|
|
|
|
const querySearch = (queryString: string, cb: (results: PointResource[]) => void) => {
|
|
|
|
|
if (!canSearch.value) {
|
|
|
|
|
cb([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lowerQuery = queryString.toLowerCase();
|
2026-06-24 17:16:25 +08:00
|
|
|
const allResources = loadAllPointData(resourceConfigs.value);
|
2026-06-23 21:25:54 +08:00
|
|
|
|
|
|
|
|
const filteredResults = allResources.filter(item => {
|
2026-06-24 17:16:25 +08:00
|
|
|
const config = resourceConfigs.value.find(c => c.category === item.category);
|
2026-06-23 21:25:54 +08:00
|
|
|
let isVisible = false;
|
|
|
|
|
|
|
|
|
|
if (config) {
|
|
|
|
|
isVisible = isCategoryVisible(config.category, config.forcedType || item.originalType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isVisible) return false;
|
|
|
|
|
if (!queryString) return true;
|
|
|
|
|
|
|
|
|
|
return (item.value || '').toLowerCase().includes(lowerQuery);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
cb(filteredResults);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelect = async (item: PointResource) => {
|
|
|
|
|
if (item.lon == null || item.lat == null) return;
|
|
|
|
|
await CesiumUtilsSingleton.flyToTarget([item.lon, item.lat, 6000]);
|
|
|
|
|
startAreaAnalysisFromSearch(item);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFocus = () => {
|
|
|
|
|
// 触发数据刷新(如果需要)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 监听器 ====================
|
|
|
|
|
watch(
|
|
|
|
|
() => useLoadingResourceStore().loadingResource,
|
|
|
|
|
() => {
|
|
|
|
|
if (currentCenterPosition && showPulsePointList.value) {
|
|
|
|
|
refreshPulseEffect();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ deep: true }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const poi = computed(() => statusStore.poiLayers);
|
|
|
|
|
const map = computed(() => statusStore.mapLayers);
|
|
|
|
|
const infra = computed(() => statusStore.infrastructureLayers);
|
|
|
|
|
|
|
|
|
|
watch([
|
|
|
|
|
() => poi.value.showSchool.show,
|
|
|
|
|
() => poi.value.showHospital.show,
|
|
|
|
|
() => poi.value.showDangerSource.show,
|
|
|
|
|
() => poi.value.showRefugeeShelter.show,
|
|
|
|
|
() => poi.value.showFireStation.show,
|
|
|
|
|
() => poi.value.showReservePoint.show,
|
|
|
|
|
() => poi.value.showSubwayStation.show,
|
|
|
|
|
() => poi.value.showLandslideHiddenPoint.show,
|
|
|
|
|
() => poi.value.showDebrisFlowHiddenPoint.show,
|
|
|
|
|
() => poi.value.showWaterLoggingHiddenPoint.show,
|
|
|
|
|
() => poi.value.showFlashFloodHiddenPoint.show,
|
|
|
|
|
() => map.value.riskPointShow.show,
|
|
|
|
|
() => infra.value.showBridge.show,
|
|
|
|
|
() => infra.value.showReservoir.show,
|
|
|
|
|
], () => {
|
|
|
|
|
if (currentCenterPosition && showPulsePointList.value) {
|
|
|
|
|
refreshPulseEffect();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
clearAllAnalysisResources();
|
|
|
|
|
removeMapClickHandler();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ==================== 按钮配置 ====================
|
|
|
|
|
const analysisButtons: AnalysisButtonConfig[] = [
|
|
|
|
|
{
|
|
|
|
|
name: '标记区域分析',
|
|
|
|
|
activeName: '取消区域分析',
|
|
|
|
|
callback: (status: boolean) => {
|
|
|
|
|
const viewer = CesiumUtilsSingleton.getViewer();
|
|
|
|
|
if (!viewer?.canvas) return;
|
|
|
|
|
|
|
|
|
|
statusStore.cursorStyle = status ? 'crosshair' : 'default';
|
|
|
|
|
viewer.canvas.style.cursor = status ? 'crosshair' : 'default';
|
|
|
|
|
|
|
|
|
|
if (status) {
|
|
|
|
|
registerMapClickHandler();
|
|
|
|
|
} else {
|
|
|
|
|
removeMapClickHandler();
|
|
|
|
|
clearAllAnalysisResources();
|
|
|
|
|
showAreaDialog.value = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: '隐藏行政区划',
|
|
|
|
|
callback: (status: boolean) => {
|
|
|
|
|
statusStore.mapLayers.showAdministrativeDivision.show = !status;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
selectedButtonIndex,
|
|
|
|
|
showAreaDialog,
|
|
|
|
|
radius,
|
|
|
|
|
dialogPosition,
|
|
|
|
|
analysisButtons,
|
|
|
|
|
searchState,
|
|
|
|
|
canSearch,
|
|
|
|
|
pulsePoints,
|
|
|
|
|
showPulsePointList,
|
|
|
|
|
handleButtonClick,
|
|
|
|
|
handleConfirm,
|
|
|
|
|
handleCancel,
|
|
|
|
|
refreshPulseEffect,
|
|
|
|
|
startAreaAnalysisFromSearch,
|
|
|
|
|
querySearch,
|
|
|
|
|
handleSelect,
|
|
|
|
|
handleFocus,
|
|
|
|
|
};
|
|
|
|
|
};
|