| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557 |
- <template>
- <div
- v-if="visible"
- class="overview-map-container"
- >
- <div class="overview-map-header">
- <span class="overview-title">鹰眼</span>
- <span class="overview-close" @click="close">×</span>
- </div>
- <div
- class="overview-map-canvas"
- ref="canvasContainer"
- @click="onCanvasClick"
- >
- <div ref="cesiumContainer" class="cesium-mini-map"></div>
- <!-- 半透明红框 -->
- <div
- class="view-rect"
- :style="viewRectStyle"
- ></div>
- </div>
- </div>
- </template>
- <script>
- import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from "vue";
- export default {
- name: "Sm3dOverviewMap",
- props: {
- visible: {
- type: Boolean,
- default: false
- }
- },
- emits: ['close'],
- setup(props, { emit }) {
- // 获取主场景viewer(优先从window获取)
- const getMainViewer = () => {
- return window.viewer || null;
- };
-
- const canvasContainer = ref(null);
- const cesiumContainer = ref(null);
-
- // 福建行政外接矩形(WGS84/EPSG:4326)
- const FUJIAN_EXTENT = {
- minLng: 115.7,
- maxLng: 120.8,
- minLat: 23.5,
- maxLat: 28.3
- };
-
- // 红框样式
- const viewRectStyle = reactive({
- left: '0px',
- top: '0px',
- width: '40px',
- height: '40px',
- display: 'block'
- });
- let miniViewer = null;
- let updateInterval = null;
- // 将经纬度转换为容器坐标(福建范围映射)
- function latLngToContainer(lat, lng, width, height) {
- // 福建范围:经度115.7~120.8,纬度23.5~28.3
- const lngRange = FUJIAN_EXTENT.maxLng - FUJIAN_EXTENT.minLng;
- const latRange = FUJIAN_EXTENT.maxLat - FUJIAN_EXTENT.minLat;
-
- const x = ((lng - FUJIAN_EXTENT.minLng) / lngRange) * width;
- const y = ((FUJIAN_EXTENT.maxLat - lat) / latRange) * height;
-
- return { x, y };
- }
- // 将容器坐标转换为经纬度(福建范围映射)
- function containerToLatLng(x, y, width, height) {
- const lngRange = FUJIAN_EXTENT.maxLng - FUJIAN_EXTENT.minLng;
- const latRange = FUJIAN_EXTENT.maxLat - FUJIAN_EXTENT.minLat;
-
- const lng = (x / width) * lngRange + FUJIAN_EXTENT.minLng;
- const lat = FUJIAN_EXTENT.maxLat - (y / height) * latRange;
-
- return { lng, lat };
- }
- // 获取当前相机视线指向的地面位置(视口中心点对应的地面位置)
- function getCameraPosition() {
- const mainViewer = getMainViewer();
- if (!mainViewer || !mainViewer.scene || !mainViewer.camera) return null;
-
- try {
- const scene = mainViewer.scene;
- const camera = mainViewer.camera;
- const canvas = scene.canvas;
-
- // 创建从视口中心发射的射线(这是获取视口中心点对应地面位置的正确方式)
- const centerScreenPos = new Cesium.Cartesian2(canvas.width / 2, canvas.height / 2);
- const ray = camera.getPickRay(centerScreenPos);
-
- // 获取射线与地面的交点
- const intersection = scene.globe.pick(ray, scene);
-
- if (intersection) {
- // 将交点转换为经纬度
- const cartographic = Cesium.Cartographic.fromCartesian(intersection);
- // 计算相机到目标点的实际距离(这是真正的观察高度,不受相机倾斜影响)
- const distance = Cesium.Cartesian3.distance(camera.position, intersection);
- return {
- lat: Cesium.Math.toDegrees(cartographic.latitude),
- lng: Cesium.Math.toDegrees(cartographic.longitude),
- height: distance
- };
- }
-
- // 如果无法获取交点,使用相机位置的经纬度(正下方地面)
- const cameraCartographic = Cesium.Cartographic.fromCartesian(camera.position);
- if (cameraCartographic) {
- return {
- lat: Cesium.Math.toDegrees(cameraCartographic.latitude),
- lng: Cesium.Math.toDegrees(cameraCartographic.longitude),
- height: cameraCartographic.height
- };
- }
-
- return null;
- } catch (e) {
- console.warn('获取相机位置失败:', e);
- return null;
- }
- }
-
- // 获取当前视口范围
- function getViewportBounds() {
- const mainViewer = getMainViewer();
- if (!mainViewer || !mainViewer.scene) return null;
-
- try {
- const scene = mainViewer.scene;
- const camera = mainViewer.camera;
-
- // 获取视口四角的地面交点
- const canvas = scene.canvas;
- const width = canvas.width;
- const height = canvas.height;
-
- const corners = [
- new Cesium.Cartesian2(0, 0),
- new Cesium.Cartesian2(width, 0),
- new Cesium.Cartesian2(width, height),
- new Cesium.Cartesian2(0, height)
- ];
-
- let minLat = 90, maxLat = -90, minLng = 180, maxLng = -180;
-
- for (const corner of corners) {
- const ray = camera.getPickRay(corner);
- const intersection = scene.globe.pick(ray, scene);
-
- if (intersection) {
- const cartographic = Cesium.Cartographic.fromCartesian(intersection);
- const lat = Cesium.Math.toDegrees(cartographic.latitude);
- const lng = Cesium.Math.toDegrees(cartographic.longitude);
-
- minLat = Math.min(minLat, lat);
- maxLat = Math.max(maxLat, lat);
- minLng = Math.min(minLng, lng);
- maxLng = Math.max(maxLng, lng);
- }
- }
-
- // 如果有有效范围,返回边界
- if (minLat <= maxLat && minLng <= maxLng) {
- return { minLat, maxLat, minLng, maxLng };
- }
-
- return null;
- } catch (e) {
- console.warn('获取视口范围失败:', e);
- return null;
- }
- }
- // 更新红框位置
- function updateViewRect() {
- if (!canvasContainer.value || !miniViewer) {
- return;
- }
-
- const containerRect = canvasContainer.value.getBoundingClientRect();
- const width = containerRect.width;
- const height = containerRect.height;
-
- // 如果没有主场景viewer,隐藏红框
- const mainViewer = getMainViewer();
- if (!mainViewer || !mainViewer.scene) {
- viewRectStyle.display = 'none';
- return;
- }
-
- const cameraPos = getCameraPosition();
- if (!cameraPos) {
- viewRectStyle.display = 'none';
- return;
- }
-
- // 使用Cesium的精确坐标转换获取红框中心位置
- try {
- const cartesian = Cesium.Cartesian3.fromDegrees(cameraPos.lng, cameraPos.lat);
- const screenPos = miniViewer.scene.cartesianToCanvasCoordinates(cartesian);
-
- if (screenPos && !isNaN(screenPos.x) && !isNaN(screenPos.y)) {
- // 使用Cesium转换后的屏幕坐标作为红框中心
- const centerX = screenPos.x;
- const centerY = screenPos.y;
-
- // 计算红框尺寸(基于相机高度估算视口范围)
- // 相机高度越高(越远)→ 视口范围越大 → 红框越大
- // 相机高度越低(越近)→ 视口范围越小 → 红框越小
- const maxRectSize = Math.min(width, height) * 0.35;
- const minRectSize = 15;
- // 使用高度比例:高度越高,红框越大
- const rectSize = Math.max(minRectSize, Math.min(maxRectSize,
- (cameraPos.height / 50000) * Math.min(width, height) * 0.2));
-
- // 设置红框位置(以转换后的坐标为中心)
- viewRectStyle.left = (centerX - rectSize / 2) + 'px';
- viewRectStyle.top = (centerY - rectSize / 2) + 'px';
- viewRectStyle.width = rectSize + 'px';
- viewRectStyle.height = rectSize + 'px';
- viewRectStyle.display = 'block';
- return;
- }
- } catch (e) {
- console.warn('Cesium坐标转换失败:', e);
- }
-
- // 回退方案:使用手动计算
- const center = latLngToContainer(cameraPos.lat, cameraPos.lng, width, height);
- const maxRectSize = Math.min(width, height) * 0.35;
- const minRectSize = 15;
- // 使用高度比例:高度越高,红框越大
- const rectSize = Math.max(minRectSize, Math.min(maxRectSize,
- (cameraPos.height / 50000) * Math.min(width, height) * 0.2));
-
- viewRectStyle.left = (center.x - rectSize / 2) + 'px';
- viewRectStyle.top = (center.y - rectSize / 2) + 'px';
- viewRectStyle.width = rectSize + 'px';
- viewRectStyle.height = rectSize + 'px';
- viewRectStyle.display = 'block';
- }
- // 更新鹰眼地图视图(固定到福建范围)
- function updateMiniViewerView() {
- if (!miniViewer) return;
-
- // 固定缩放到福建范围
- miniViewer.camera.setView({
- destination: Cesium.Rectangle.fromDegrees(
- FUJIAN_EXTENT.minLng,
- FUJIAN_EXTENT.minLat,
- FUJIAN_EXTENT.maxLng,
- FUJIAN_EXTENT.maxLat
- )
- });
- }
- // 飞转到指定位置
- function flyToPosition(lng, lat) {
- const mainViewer = getMainViewer();
- if (!mainViewer) return;
-
- const height = mainViewer.camera.positionCartographic ?
- mainViewer.camera.positionCartographic.height : 10000;
-
- mainViewer.camera.flyTo({
- destination: Cesium.Cartesian3.fromDegrees(lng, lat, height),
- orientation: {
- heading: mainViewer.camera.heading,
- pitch: mainViewer.camera.pitch,
- roll: 0
- },
- duration: 0.5
- });
- }
- // 点击鹰眼地图任意位置,飞转到对应经纬度
- const onCanvasClick = (event) => {
- if (!canvasContainer.value) return;
-
- const rect = canvasContainer.value.getBoundingClientRect();
- const clickX = event.clientX - rect.left;
- const clickY = event.clientY - rect.top;
-
- // 使用线性映射获取经纬度
- const { lng, lat } = containerToLatLng(clickX, clickY, rect.width, rect.height);
-
- // 立即更新红框位置到点击位置(居中显示)
- const maxRectSize = Math.min(rect.width, rect.height) * 0.35;
- const minRectSize = 15;
-
- // 获取当前相机高度用于计算红框尺寸
- const mainViewer = getMainViewer();
- const currentHeight = mainViewer && mainViewer.camera.positionCartographic ?
- mainViewer.camera.positionCartographic.height : 10000;
-
- const rectSize = Math.max(minRectSize, Math.min(maxRectSize,
- (currentHeight / 50000) * Math.min(rect.width, rect.height) * 0.2));
-
- // 将红框居中到点击位置(确保红框中心始终在点击位置)
- viewRectStyle.left = (clickX - rectSize / 2) + 'px';
- viewRectStyle.top = (clickY - rectSize / 2) + 'px';
- viewRectStyle.width = rectSize + 'px';
- viewRectStyle.height = rectSize + 'px';
- viewRectStyle.display = 'block';
-
- // 让三维场景飞转到对应位置
- flyToPosition(lng, lat);
- };
- const close = () => {
- emit('close');
- };
- // 创建小型Cesium视图
- function createMiniViewer() {
- if (!cesiumContainer.value) return;
-
- const tiandituKey = '3fb1e9fda20ee995dc815c8243553ce8';
-
- // 使用地理坐标系(EPSG:4326)的瓦片方案
- const geographicTilingScheme = new Cesium.GeographicTilingScheme();
-
- const imageryProvider = new Cesium.WebMapTileServiceImageryProvider({
- url: `https://t0.tianditu.gov.cn/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
- layer: 'img',
- style: 'default',
- format: 'image/jpeg',
- tileMatrixSetID: 'c',
- subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
- tilingScheme: geographicTilingScheme,
- tileMatrixLabels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
- maximumLevel: 15,
- show: true
- });
-
- // 创建自定义的2D视图投影,使用地理坐标系
- const mapProjection = new Cesium.GeographicProjection();
-
- // 创建场景模式配置,强制使用地理坐标系
- const sceneMode = Cesium.SceneMode.SCENE2D;
-
- miniViewer = new Cesium.Viewer(cesiumContainer.value, {
- imageryProvider: imageryProvider,
- baseLayerPicker: false,
- timeline: false,
- animation: false,
- geocoder: false,
- homeButton: false,
- sceneModePicker: false,
- navigationHelpButton: false,
- navigationInstructionsInitiallyVisible: false,
- fullscreenButton: false,
- shadows: false,
- terrainProvider: new Cesium.EllipsoidTerrainProvider(),
- creditContainer: document.createElement('div')
- });
-
- // 设置为2D模式
- miniViewer.scene.morphTo2D(0);
-
- // 完全禁用所有相机控制(锁定视图)
- miniViewer.scene.screenSpaceCameraController.enableRotate = false;
- miniViewer.scene.screenSpaceCameraController.enableZoom = false;
- miniViewer.scene.screenSpaceCameraController.enablePan = false;
- miniViewer.scene.screenSpaceCameraController.enableTilt = false;
- miniViewer.scene.screenSpaceCameraController.enableLook = false;
-
- // 添加注记图层
- const labelProvider = new Cesium.WebMapTileServiceImageryProvider({
- url: `https://t0.tianditu.gov.cn/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
- layer: 'cia',
- style: 'default',
- format: 'image/png',
- tileMatrixSetID: 'c',
- subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
- tilingScheme: geographicTilingScheme,
- tileMatrixLabels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
- maximumLevel: 15,
- show: true
- });
-
- miniViewer.imageryLayers.addImageryProvider(labelProvider);
-
- // 缩放到全球视图(使用地理坐标系)
- miniViewer.camera.setView({
- destination: Cesium.Rectangle.fromDegrees(-180, -90, 180, 90)
- });
-
- // 延迟resize确保尺寸正确
- setTimeout(() => {
- if (miniViewer) {
- miniViewer.resize();
- updateViewRect();
- updateMiniViewerView();
- }
- }, 150);
-
- // 启动定时更新
- startUpdateLoop();
- }
- function startUpdateLoop() {
- stopUpdateLoop();
-
- updateInterval = setInterval(() => {
- updateViewRect();
- updateMiniViewerView();
- }, 50);
- }
- function stopUpdateLoop() {
- if (updateInterval) {
- clearInterval(updateInterval);
- updateInterval = null;
- }
- }
- onMounted(() => {
- nextTick(() => {
- createMiniViewer();
- });
- });
- onUnmounted(() => {
- stopUpdateLoop();
- if (miniViewer) {
- miniViewer.destroy();
- miniViewer = null;
- }
- });
- watch(() => props.visible, (newVal) => {
- if (newVal) {
- nextTick(() => {
- if (!miniViewer) {
- createMiniViewer();
- } else {
- miniViewer.resize();
- updateViewRect();
- updateMiniViewerView();
- }
- });
- } else {
- stopUpdateLoop();
- if (miniViewer) {
- miniViewer.destroy();
- miniViewer = null;
- }
- }
- });
- return {
- canvasContainer,
- cesiumContainer,
- viewRectStyle,
- onCanvasClick,
- close
- };
- }
- };
- </script>
- <style scoped>
- .overview-map-container {
- position: fixed;
- right: 20px;
- bottom: 120px;
- width: 350px;
- height: 280px;
- background: rgba(30, 41, 59, 0.95);
- border-radius: 10px;
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
- z-index: 1000;
- overflow: hidden;
- border: 1px solid rgba(148, 163, 184, 0.1);
- }
- .overview-map-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 14px;
- background: rgba(51, 65, 85, 0.85);
- border-bottom: 1px solid rgba(148, 163, 184, 0.15);
- flex-shrink: 0;
- }
- .overview-title {
- color: #e2e8f0;
- font-size: 13px;
- font-weight: 500;
- }
- .overview-close {
- color: #94a3b8;
- font-size: 20px;
- cursor: pointer;
- line-height: 1;
- transition: color 0.2s;
- font-weight: 300;
- }
- .overview-close:hover {
- color: #f1f5f9;
- }
- .overview-map-canvas {
- position: relative;
- width: 100%;
- height: calc(100% - 42px);
- cursor: crosshair;
- overflow: hidden;
- }
- .cesium-mini-map {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- }
- .view-rect {
- position: absolute;
- border: 2px solid #ef4444;
- background: rgba(239, 68, 68, 0.25);
- cursor: default;
- box-sizing: border-box;
- transition: all 0.1s ease;
- z-index: 10;
- }
- .view-rect:hover {
- box-shadow: 0 0 15px rgba(239, 68, 68, 0.6);
- border-color: #f87171;
- background: rgba(239, 68, 68, 0.35);
- }
- .view-rect.dragging {
- border-color: #f97316;
- background: rgba(249, 115, 22, 0.4);
- box-shadow: 0 0 20px rgba(249, 115, 22, 0.6);
- }
- </style>
|