优化2d和3d的地图展示 优化漫游,锚点添加淡入淡出效果
This commit is contained in:
parent
ae2ff1ced4
commit
2622b9d511
@ -7,10 +7,6 @@ const appStore = useAppStore();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!--
|
|
||||||
1.地图抽吸
|
|
||||||
2.地图文字和点显示
|
|
||||||
-->
|
|
||||||
<el-config-provider :locale="appStore.locale" :size="appStore.size">
|
<el-config-provider :locale="appStore.locale" :size="appStore.size">
|
||||||
<a-config-provider :theme="usetTheme" :locale="locale">
|
<a-config-provider :theme="usetTheme" :locale="locale">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|||||||
@ -160,24 +160,6 @@ onUnmounted(() => {
|
|||||||
cursor: grab;
|
cursor: grab;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
/* 消除瓦片之间的缝隙和默认边框 */
|
|
||||||
.leaflet-tile {
|
|
||||||
border: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
image-rendering: -webkit-optimize-contrast;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保地图容器背景色与瓦片一致,避免透出底色 */
|
|
||||||
.leaflet-container {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 如果使用 Canvas 模式,确保 Canvas 没有边框 */
|
|
||||||
.leaflet-pane canvas {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-popup-container {
|
.map-popup-container {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|||||||
@ -638,7 +638,9 @@ const renderChart = async () => {
|
|||||||
startValue: nextStartValue,
|
startValue: nextStartValue,
|
||||||
endValue: nextEndValue
|
endValue: nextEndValue
|
||||||
});
|
});
|
||||||
} catch (_error: any) {}
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
}, 32);
|
}, 32);
|
||||||
};
|
};
|
||||||
zr.on('mousewheel', handleWheel);
|
zr.on('mousewheel', handleWheel);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
|||||||
import type { layer, MapInterface } from './map.d';
|
import type { layer, MapInterface } from './map.d';
|
||||||
// import { MapLeaflet } from "./map.leaflet";
|
|
||||||
import { MapOl } from './map.ol';
|
import { MapOl } from './map.ol';
|
||||||
import { MapCesium } from './map.cesium';
|
import { MapCesium } from './map.cesium';
|
||||||
|
|
||||||
@ -29,7 +28,6 @@ export class MapClass implements MapClassInterface {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.layers = new Map();
|
this.layers = new Map();
|
||||||
this.view = null;
|
this.view = null;
|
||||||
// this.service = new MapLeaflet();
|
|
||||||
this.service = new MapOl();
|
this.service = new MapOl();
|
||||||
}
|
}
|
||||||
static getInstance(): MapClass {
|
static getInstance(): MapClass {
|
||||||
@ -143,6 +141,10 @@ export class MapClass implements MapClassInterface {
|
|||||||
removePointLayer(layerKey: string): void {
|
removePointLayer(layerKey: string): void {
|
||||||
this.service.removePointLayer?.(layerKey);
|
this.service.removePointLayer?.(layerKey);
|
||||||
}
|
}
|
||||||
|
// 漫游模式锚点深度测试开关(仅 3D 有效,2D 无此能力直接忽略)
|
||||||
|
setRoamDepthTest(enabled: boolean): void {
|
||||||
|
this.service.setRoamDepthTest?.(enabled);
|
||||||
|
}
|
||||||
// 销毁地图
|
// 销毁地图
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.service.destroy();
|
this.service.destroy();
|
||||||
|
|||||||
7
frontend/src/components/gis/map.d.ts
vendored
7
frontend/src/components/gis/map.d.ts
vendored
@ -213,6 +213,13 @@ export interface MapInterface {
|
|||||||
checked: boolean
|
checked: boolean
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 漫游模式锚点深度测试开关(仅 3D 有效)
|
||||||
|
* 漫游时启用后,锚点文字/图标开启深度测试,被漫游模型正确遮挡
|
||||||
|
* @param enabled 是否处于漫游状态
|
||||||
|
*/
|
||||||
|
setRoamDepthTest?(enabled: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 长度量算
|
* 长度量算
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,440 +0,0 @@
|
|||||||
// import { MapInterface } from './map';
|
|
||||||
// import * as L from 'leaflet';
|
|
||||||
// import * as esriLeaflet from 'esri-leaflet';
|
|
||||||
// import '@/utils/leaflet/leaflet-tilelayer-wmts-src.js';
|
|
||||||
// import { mapServerBaseUrl } from './map.class';
|
|
||||||
// import '@/utils/leaflet/leaflet.inflatable-markers-group.js';
|
|
||||||
// import {MDOptions} from './map.class';
|
|
||||||
// import { getIconPath } from "@/utils/index";
|
|
||||||
// // @ts-ignore
|
|
||||||
// import axios from 'axios';
|
|
||||||
// // import "@/components/thematicMap/leaflet/leaflet.inflatable-markers-group.js"
|
|
||||||
|
|
||||||
// const tiledMapGroup = L.layerGroup();
|
|
||||||
// const chartMapGroup = L.layerGroup();
|
|
||||||
// const overlayGroup = L.layerGroup();
|
|
||||||
// const CENTER_positionCN: any = [38, 114.17112499999996]; //中心纬经度 中国
|
|
||||||
// const basinCenter = {
|
|
||||||
// DA_HHGLSX: [103.343357, 35.931812],
|
|
||||||
// FA_CJGLSX: [111.001911, 30.821327]
|
|
||||||
// };
|
|
||||||
// let boundCavansLayer: any = null;
|
|
||||||
// let ganliulist: any = [];
|
|
||||||
// export class MapLeaflet implements MapInterface {
|
|
||||||
// map: any = null;
|
|
||||||
// htmlMakerLayer: any = [];
|
|
||||||
// defaultScale = 10;
|
|
||||||
// minimumZoom = 7;
|
|
||||||
// setDrawPlug: any = null;
|
|
||||||
// layermarkers: any = [];
|
|
||||||
// rainlayerslist: any = [];
|
|
||||||
// private layerRegistry: Map<string, any> = new Map(); // ✅ 新增:存储 key -> layer 实例
|
|
||||||
|
|
||||||
// private currentBaseLayerKey: string | null = null; // ✅ 新增:记录当前激活的底图 Key
|
|
||||||
|
|
||||||
// temperatureMapObj: any = [];
|
|
||||||
// //地图初始化
|
|
||||||
// init(container: HTMLElement, rectangle?: any): Promise<any> {
|
|
||||||
// try {
|
|
||||||
// console.log('init初始化container', container);
|
|
||||||
// var corner1 = L.latLng(55.35715491537772, 140.7821677051657);
|
|
||||||
// var corner2 = L.latLng(0.975580441812298, 67.56008229483018);
|
|
||||||
// var bounds = L.latLngBounds(corner2, corner1);
|
|
||||||
// const map = L.map(container as any, {
|
|
||||||
// preferCanvas: true,
|
|
||||||
// zoom: 4.5,
|
|
||||||
// minZoom: 4.23,
|
|
||||||
// maxZoom: 22, // 【修改点1】增大最大缩放级别,允许用户继续滚轮放大
|
|
||||||
// maxNativeZoom: 12,
|
|
||||||
// zoomSnap: 0.1, // 【修改点2】让缩放更平滑,不强制对齐整数
|
|
||||||
// zoomDelta: 0.5,
|
|
||||||
// center: CENTER_positionCN,
|
|
||||||
// attributionControl: false,
|
|
||||||
// zoomControl: false,
|
|
||||||
// trackResize: true,
|
|
||||||
// maxBounds: bounds,
|
|
||||||
// maxBoundsViscosity: 1.0,
|
|
||||||
// wheelPxPerZoomLevel: 180,
|
|
||||||
// });
|
|
||||||
// // ✅ 新增:可视化显示 maxBounds 范围
|
|
||||||
// // L.rectangle(bounds, {color: "#ff7800", weight: 3, opacity: 0.8, fillOpacity: 0.1})
|
|
||||||
// // .addTo(map)
|
|
||||||
// // .bindPopup("这是 maxBounds 的范围");
|
|
||||||
// this.map = map;
|
|
||||||
// this.map.on('zoomend', (e: any) => {
|
|
||||||
// console.log('当前缩放级别', e.target.getZoom());
|
|
||||||
// });
|
|
||||||
// return Promise.resolve(map);
|
|
||||||
// } catch (e) {
|
|
||||||
// console.log('测试', e);
|
|
||||||
// return Promise.reject({});
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// addBaseDataLayer(layer: any): any {
|
|
||||||
// // The WMTS URL
|
|
||||||
// console.log(layer);
|
|
||||||
// switch (layer.type) {
|
|
||||||
// case 'wmts':
|
|
||||||
// if (layer.url) {
|
|
||||||
// console.log('https://211.99.26.225:18085' + layer.url);
|
|
||||||
// if (layer) {
|
|
||||||
// var ignLayer = L.tileLayer
|
|
||||||
// .wmts('https://211.99.26.225:18085/geoserver/gwc/service/wmts', {
|
|
||||||
// tileMatrixSet: 'EPSG:3857_qgc_qsj_arcgistiles_l13',
|
|
||||||
// tileSize: 256, //切片大小
|
|
||||||
// maxZoom: 13,
|
|
||||||
// noWrap: true,
|
|
||||||
// opacity: 0.99,
|
|
||||||
// minZoom: 4,
|
|
||||||
// styles:{
|
|
||||||
// abc:123
|
|
||||||
// },
|
|
||||||
// layer: 'qgc_qsj_arcgistiles_l13'
|
|
||||||
// })
|
|
||||||
// .addTo(this.map)
|
|
||||||
// .bringToBack();
|
|
||||||
// const registryKey = layer.key || layer.title;
|
|
||||||
// if (registryKey) {
|
|
||||||
// this.layerRegistry.set(registryKey, ignLayer);
|
|
||||||
// }
|
|
||||||
// layer._layer = ignLayer;
|
|
||||||
// layer._layer.layerGroup = overlayGroup;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return layer;
|
|
||||||
|
|
||||||
// case 'markers':
|
|
||||||
// this.getRain(layer);
|
|
||||||
// break;
|
|
||||||
// default: {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// baseLayerSwitcher(key: string) {
|
|
||||||
// if (!this.map) return;
|
|
||||||
|
|
||||||
// console.log('切换底图 key:', key);
|
|
||||||
|
|
||||||
// // 1. 如果当前已有底图且不是同一个,先移除
|
|
||||||
// if (this.currentBaseLayerKey && this.currentBaseLayerKey !== key) {
|
|
||||||
// const oldLayer = this.layerRegistry.get(this.currentBaseLayerKey);
|
|
||||||
// if (oldLayer && this.map.hasLayer(oldLayer)) {
|
|
||||||
// this.map.removeLayer(oldLayer);
|
|
||||||
// console.log(`已移除旧底图: ${this.currentBaseLayerKey}`);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 2. 检查新底图是否已存在 registry 中
|
|
||||||
// let newLayer = this.layerRegistry.get(key);
|
|
||||||
|
|
||||||
// // 3. 如果不存在,则创建并添加到地图和 registry
|
|
||||||
// if (!newLayer) {
|
|
||||||
// newLayer = this.createBaseLayerByKey(key);
|
|
||||||
// if (newLayer) {
|
|
||||||
// this.layerRegistry.set(key, newLayer);
|
|
||||||
// newLayer.addTo(this.map);
|
|
||||||
// // 确保底图在最底层
|
|
||||||
// newLayer.bringToBack();
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// // 4. 如果已存在,直接添加(如果未添加)
|
|
||||||
// if (!this.map.hasLayer(newLayer)) {
|
|
||||||
// newLayer.addTo(this.map);
|
|
||||||
// newLayer.bringToBack();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 5. 更新当前激活的 Key
|
|
||||||
// if (newLayer) {
|
|
||||||
// this.currentBaseLayerKey = key;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * 根据 Key 创建具体的 Leaflet 图层实例
|
|
||||||
// */
|
|
||||||
// createBaseLayerByKey(key: string): L.Layer | null {
|
|
||||||
// console.log(key)
|
|
||||||
// const tdtToken = 'e90d56e5a09d1767899ad45846b0cefd' //企业版密钥,勿换e650f138c4481cca888cd13094bb9026
|
|
||||||
// const mapType = 'w' //c:天地图经纬度底图;w:天地图墨卡托底图
|
|
||||||
// const URL_TerTDT = `https://t0.tianditu.gov.cn/ter_${mapType}/wmts?tk=${tdtToken}` //terMap
|
|
||||||
// switch (key) {
|
|
||||||
// case 's_province_boundaries':
|
|
||||||
// // 假设这是之前的 WMTS 矢量图层
|
|
||||||
// // 注意:原代码中硬编码了 URL 和 layer 名称,这里需要确保与实际服务对应
|
|
||||||
// return L.tileLayer.wmts('https://211.99.26.225:18085/geoserver/gwc/service/wmts', {
|
|
||||||
// tileMatrixSet: 'EPSG:3857_qgc_qsj_arcgistiles_l13',
|
|
||||||
// tileSize: 256,
|
|
||||||
// // maxZoom: 13,
|
|
||||||
// noWrap: true,
|
|
||||||
// opacity: 1,
|
|
||||||
// minZoom: 4,
|
|
||||||
// zIndex: 1,
|
|
||||||
// layer: 'qgc_qsj_arcgistiles_l13', // 请确认此 layer 名称是否对应“矢量”
|
|
||||||
// format: 'image/png'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// case 'BASEMAP-white':
|
|
||||||
// // 地形图示例 (使用 OpenStreetMap 或其他地形服务作为占位,请替换为实际地址)
|
|
||||||
// return L.tileLayer(`${URL_TerTDT}&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=${mapType}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles`, {
|
|
||||||
// // maxZoom: 17,
|
|
||||||
// format: "image/png",
|
|
||||||
// zIndex: 12,
|
|
||||||
// attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// case 'BASEMAP-img':
|
|
||||||
// // 影像图示例 (使用 Esri World Imagery 作为占位,请替换为实际地址)
|
|
||||||
// return L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
||||||
// attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// default:
|
|
||||||
// console.warn(`未知的底图 Key: ${key}`);
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// controlBaseLayerTreeShowAndHidden(layerType: String, key: String, checked: boolean) {
|
|
||||||
// // 优先使用 key 查找,如果没有 key 则尝试用 layerType
|
|
||||||
// // console.log(this.getAllLayers())
|
|
||||||
// console.log(key)
|
|
||||||
// console.log(this.layerRegistry)
|
|
||||||
// const registryKey:any = key || layerType;
|
|
||||||
// const layerInstance = this.layerRegistry.get(registryKey);
|
|
||||||
// if (layerInstance) {
|
|
||||||
// if (checked) {
|
|
||||||
// // 显示:如果不在地图上,则添加
|
|
||||||
// if (!this.map.hasLayer(layerInstance)) {
|
|
||||||
// layerInstance.addTo(this.map);
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// // 隐藏:如果在地图上,则移除
|
|
||||||
// if (this.map.hasLayer(layerInstance)) {
|
|
||||||
// this.map.removeLayer(layerInstance);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// console.warn(`未找到标识为 ${registryKey} 的图层实例`);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * 添加初始化数据图层
|
|
||||||
// * @param pointLayerObj 图层配置对象 (包含 data, key, checked 等属性)
|
|
||||||
// * @param layerType 图层类型/Key (如果 pointLayerObj 中有 key,则优先使用 obj.key)
|
|
||||||
// * @param mdoptions 描点选项配置
|
|
||||||
// * @param legendArray 图例映射数据 (用于根据 anchoPointState 匹配图标)
|
|
||||||
// */
|
|
||||||
// addInitDataLayer = (pointLayerObj: any, layerType?: any, mdoptions?: MDOptions, legendArray?: any) => {
|
|
||||||
// // 1. 参数校验与数据提取
|
|
||||||
// if (!this.map || !pointLayerObj) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let dataArray: any[] = [];
|
|
||||||
// let targetLayerKey: string = layerType;
|
|
||||||
|
|
||||||
// if (Array.isArray(pointLayerObj)) {
|
|
||||||
// dataArray = pointLayerObj;
|
|
||||||
// } else {
|
|
||||||
// dataArray = pointLayerObj.data || [];
|
|
||||||
// targetLayerKey = pointLayerObj.key || layerType;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!targetLayerKey) {
|
|
||||||
// console.warn('缺少图层 Key,无法加载描点');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (dataArray.length === 0) {
|
|
||||||
// const existingGroup = this.layerRegistry.get(targetLayerKey);
|
|
||||||
// if (existingGroup && existingGroup instanceof L.LayerGroup) {
|
|
||||||
// existingGroup.clearLayers();
|
|
||||||
// }
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log(`开始加载图层 [${targetLayerKey}] 的描点,数量: ${dataArray.length}`);
|
|
||||||
|
|
||||||
// // 2. 获取或创建该图层的 LayerGroup
|
|
||||||
// let layerGroup = this.layerRegistry.get(targetLayerKey);
|
|
||||||
|
|
||||||
// const shouldClear = mdoptions?.isRemove !== false;
|
|
||||||
|
|
||||||
// if (!layerGroup || !(layerGroup instanceof L.LayerGroup)) {
|
|
||||||
// if (layerGroup && this.map.hasLayer(layerGroup)) {
|
|
||||||
// this.map.removeLayer(layerGroup);
|
|
||||||
// }
|
|
||||||
// layerGroup = L.layerGroup();
|
|
||||||
// this.layerRegistry.set(targetLayerKey, layerGroup);
|
|
||||||
// layerGroup.addTo(this.map);
|
|
||||||
// } else {
|
|
||||||
// if (shouldClear) {
|
|
||||||
// layerGroup.clearLayers();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 3. 遍历数据生成 Marker
|
|
||||||
// dataArray.forEach((item: any) => {
|
|
||||||
// const { lgtd, lttd, stcd, stnm, iconCode, anchoPointState, _id, titleName, ennm } = item;
|
|
||||||
|
|
||||||
// if (lgtd == null || lttd == null) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 4. 确定图标样式和文字
|
|
||||||
// let iconUrl = '';
|
|
||||||
// let iconSize = [15, 15];
|
|
||||||
|
|
||||||
// // 获取图标 URL
|
|
||||||
// if (iconCode) {
|
|
||||||
// iconUrl = getIconPath(iconCode);
|
|
||||||
// } else if (anchoPointState && legendArray) {
|
|
||||||
// const legendItem = legendArray[anchoPointState];
|
|
||||||
// if (legendItem && legendItem.icon) {
|
|
||||||
// iconUrl = getIconPath(legendItem.icon);
|
|
||||||
// if (legendItem.width && legendItem.height) {
|
|
||||||
// iconSize = [legendItem.width, legendItem.height];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 如果没有找到图标,使用默认或跳过
|
|
||||||
// if (!iconUrl) {
|
|
||||||
// // 可以选择使用默认图标或者继续
|
|
||||||
// iconUrl = getIconPath('default'); // 假设有一个默认图标
|
|
||||||
// if(!iconUrl) return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 5. 创建带文字的 DivIcon
|
|
||||||
// // 显示的文字优先使用 titleName,其次 ennm,最后 stnm
|
|
||||||
// const labelText = titleName || ennm || stnm || '';
|
|
||||||
|
|
||||||
// // 构建 HTML 结构:一个容器包含图片和文字
|
|
||||||
// // 注意:这里使用内联样式简单演示,建议在实际项目中提取到 CSS 类中
|
|
||||||
// const iconHtml = `
|
|
||||||
// <div style="position: relative; display: flex; flex-direction: column; align-items: center; width: max-content;">
|
|
||||||
// <div style="
|
|
||||||
// font: 10px sans-serif;
|
|
||||||
// font-weight: bold;
|
|
||||||
// color: #fff;
|
|
||||||
// white-space: nowrap;
|
|
||||||
// margin-bottom: 2px;
|
|
||||||
// -webkit-text-stroke: 2px #2e2d2d;
|
|
||||||
// paint-order: stroke fill; /* 确保描边在填充外部,防止文字变细 */
|
|
||||||
|
|
||||||
// ">
|
|
||||||
// ${labelText}
|
|
||||||
// </div>
|
|
||||||
// <img src="${iconUrl}" style="width: ${iconSize[0]}px; height: ${iconSize[1]}px; display: block;" />
|
|
||||||
// </div>`;
|
|
||||||
|
|
||||||
// const customIcon = L.divIcon({
|
|
||||||
// html: iconHtml,
|
|
||||||
// className: '', // 重要:设置为空字符串以避免 Leaflet 默认样式干扰
|
|
||||||
// iconSize: [iconSize[0], iconSize[1] + 20], // 高度增加以容纳文字 (假设文字高约20px)
|
|
||||||
// iconAnchor: [iconSize[0] / 2, iconSize[1] + 10], // 锚点设在图片底部中心
|
|
||||||
// popupAnchor: [0, -(iconSize[1] + 20)] // 弹窗锚点设在整体顶部
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // 6. 创建 Marker
|
|
||||||
// const marker = L.marker([lttd, lgtd], {
|
|
||||||
// icon: customIcon,
|
|
||||||
// // @ts-ignore
|
|
||||||
// options: {
|
|
||||||
// ...item,
|
|
||||||
// layerKey: targetLayerKey,
|
|
||||||
// originalEvent: item
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // 7. 绑定点击事件 (如果需要)
|
|
||||||
// marker.on('click', (e: any) => {
|
|
||||||
// // 触发全局事件,例如:
|
|
||||||
// // GlobalEvents.get('map_dataLayer_click').set(item);
|
|
||||||
// console.log('Marker clicked:', item);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // 8. 将 Marker 添加到 LayerGroup
|
|
||||||
// layerGroup.addLayer(marker);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
|
||||||
// }
|
|
||||||
// getRain(data: any) {
|
|
||||||
// if (data?.geojson) {
|
|
||||||
// this.removeRainLayer();
|
|
||||||
// this.removeLayermarkers();
|
|
||||||
// const renderColor = (item: any, colRules: any) => {
|
|
||||||
// let color = 'rgba(0,0,0,0)';
|
|
||||||
// colRules.map((rule: any, index: number) => {
|
|
||||||
// if (
|
|
||||||
// item.properties.hvalue >= rule?.sv &&
|
|
||||||
// item.properties.hvalue < rule?.ev
|
|
||||||
// ) {
|
|
||||||
// color = rule.colors;
|
|
||||||
// }
|
|
||||||
// if (index === colRules.length - 1) {
|
|
||||||
// if (item.properties.hvalue >= rule?.ev) {
|
|
||||||
// color = rule.colors;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// return color;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// data.geojson.features.map((item: any, index: number) => {
|
|
||||||
// let pushlist = L.geoJSON(item, {
|
|
||||||
// style: {
|
|
||||||
// color: renderColor(item, data.colRules),
|
|
||||||
// weight: 2,
|
|
||||||
// opacity: 0.8,
|
|
||||||
// fillOpacity: 0.7
|
|
||||||
// // fillColor: "#1D91C0",
|
|
||||||
// }
|
|
||||||
// }).addTo(this.map);
|
|
||||||
// this.layermarkers.push(pushlist);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// removeLayermarkers = () => {
|
|
||||||
// // console.log('layermarkers',layermarkers);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// if (this.layermarkers.length > 0) {
|
|
||||||
// if (this.map.hasLayer(this.layermarkers)) {
|
|
||||||
// this.map.removeLayer(this.layermarkers);
|
|
||||||
// } else {
|
|
||||||
// console.log('移除图层失败,应已经移除');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // removeAllGeojson(map);
|
|
||||||
// } catch (e) {
|
|
||||||
// // message.info('移除图层失败')
|
|
||||||
// console.log('移除图层失败,是否应移除');
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// removeRainLayer(): void {
|
|
||||||
// console.log('test!删除Layer');
|
|
||||||
// const _this = this;
|
|
||||||
// if (this.rainlayerslist.length > 0) {
|
|
||||||
// this.rainlayerslist.map((item: any) => {
|
|
||||||
// _this.map.removeLayer(item);
|
|
||||||
// });
|
|
||||||
// this.rainlayerslist = [];
|
|
||||||
// }
|
|
||||||
// } // 缩放
|
|
||||||
// zoomToggle(type: 'out' | 'in') {
|
|
||||||
// if (this.map) {
|
|
||||||
// if (type === 'out') {
|
|
||||||
// this.map && this.map.zoomOut();
|
|
||||||
// } else {
|
|
||||||
// this.map && this.map.zoomIn();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -50,6 +50,9 @@ const MAX_ZOOM = 22;
|
|||||||
const INITIAL_ZOOM = 4.5;
|
const INITIAL_ZOOM = 4.5;
|
||||||
const BATCH_POPUP_MODE_ZOOM = 15;
|
const BATCH_POPUP_MODE_ZOOM = 15;
|
||||||
const FULL_DISPLAY_NO_COLLISION_ZOOM = Number.POSITIVE_INFINITY;
|
const FULL_DISPLAY_NO_COLLISION_ZOOM = Number.POSITIVE_INFINITY;
|
||||||
|
const POINT_FADE_DURATION = 300; // 锚点/文字碰撞显隐的淡入淡出时长(ms)
|
||||||
|
// 标签文字上方的避让边距(px):防止下方锚点文字紧贴上方锚点图标底部,看着像重叠
|
||||||
|
const LABEL_ICON_TOP_CLEARANCE = 6;
|
||||||
|
|
||||||
// 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标)
|
// 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标)
|
||||||
const BOUNDS_SW = [26.5, -9.99999999999929];
|
const BOUNDS_SW = [26.5, -9.99999999999929];
|
||||||
@ -61,6 +64,13 @@ export class MapOl implements MapInterface {
|
|||||||
private layerRegistry: Map<string, any> = new Map();
|
private layerRegistry: Map<string, any> = new Map();
|
||||||
private iconLoadState = new Map<string, 'loading' | 'loaded' | 'error'>();
|
private iconLoadState = new Map<string, 'loading' | 'loaded' | 'error'>();
|
||||||
private pointStyleCache = new Map<string, Style[]>();
|
private pointStyleCache = new Map<string, Style[]>();
|
||||||
|
private measureCanvas: HTMLCanvasElement | null = null;
|
||||||
|
private measureTextCache = new Map<
|
||||||
|
string,
|
||||||
|
{ width: number; height: number }
|
||||||
|
>();
|
||||||
|
private fadeRefreshFrameId: number | null = null;
|
||||||
|
private fadingFeatures = new Set<Feature>();
|
||||||
private baseLayerConfig: any | null = null;
|
private baseLayerConfig: any | null = null;
|
||||||
private hydropBaseConfig: any | null = null;
|
private hydropBaseConfig: any | null = null;
|
||||||
private REGISTRY_KEY = 'customBaseLayer';
|
private REGISTRY_KEY = 'customBaseLayer';
|
||||||
@ -75,7 +85,6 @@ export class MapOl implements MapInterface {
|
|||||||
private BASEID = '';
|
private BASEID = '';
|
||||||
private currentClipGeoJson: any | null = null;
|
private currentClipGeoJson: any | null = null;
|
||||||
private clipRequestController: AbortController | null = null;
|
private clipRequestController: AbortController | null = null;
|
||||||
private hoveredFeatureId: string | number | null = null;
|
|
||||||
private wfsHighlightLayer: VectorLayer | null = null;
|
private wfsHighlightLayer: VectorLayer | null = null;
|
||||||
private popupManager: PopupManager;
|
private popupManager: PopupManager;
|
||||||
private regionMaskManager: RegionMaskManager;
|
private regionMaskManager: RegionMaskManager;
|
||||||
@ -256,25 +265,30 @@ export class MapOl implements MapInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 命中河段时直接切换鼠标样式(与 demo 一致,popup 是节流回调不能依赖它)
|
// 命中河段或锚点时直接切换鼠标样式(与 demo 一致,popup 是节流回调不能依赖它)
|
||||||
const targetElement = this.map?.getTargetElement() as HTMLElement;
|
const targetElement = this.map?.getTargetElement() as HTMLElement;
|
||||||
|
let isHitAnchor = false;
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
targetElement.style.cursor = hitFeature ? 'pointer' : '';
|
isHitAnchor = this.detectFeatureAtPixel(evt.pixel).isHitIcon;
|
||||||
|
targetElement.style.cursor =
|
||||||
|
isHitAnchor || hitFeature ? 'pointer' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popupManager.handlePointerMove(evt.pixel, payload => {
|
this.popupManager.handlePointerMove(evt.pixel, payload => {
|
||||||
this.hoveredFeatureId = payload.hoveredId;
|
|
||||||
|
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
// 命中河段或 hover popup 命中锚点时显示 pointer 光标
|
// 命中河段或 hover popup 命中锚点时显示 pointer 光标
|
||||||
targetElement.style.cursor =
|
targetElement.style.cursor =
|
||||||
payload.hoveredId || hitFeature ? 'pointer' : '';
|
isHitAnchor || payload.hoveredId || hitFeature ? 'pointer' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量 popup 模式下不触发 hover popup
|
// 该锚点已有固定展示(批量 popup)的 popup,跳过 hover popup,避免同一内容出现双份;
|
||||||
if (!this.isBatchPopupMode) {
|
// 未被固定展示的锚点(如碰撞被隐藏)仍可通过 hover 查看,且 hover popup 展示时会临时提升层级,不被固定展示遮挡
|
||||||
this.showPopup(payload.detectedFeature, payload.coordinate);
|
const fixedShown =
|
||||||
}
|
payload.detectedFeature?.get('_fixedPopupShown') === true;
|
||||||
|
this.showPopup(
|
||||||
|
fixedShown ? undefined : payload.detectedFeature,
|
||||||
|
fixedShown ? undefined : payload.coordinate
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -475,25 +489,48 @@ export class MapOl implements MapInterface {
|
|||||||
const iconCollisionVisible = feature.get('_iconCollisionVisible') !== false;
|
const iconCollisionVisible = feature.get('_iconCollisionVisible') !== false;
|
||||||
const labelCollisionVisible =
|
const labelCollisionVisible =
|
||||||
feature.get('_labelCollisionVisible') === true;
|
feature.get('_labelCollisionVisible') === true;
|
||||||
const finalIconVisible = iconTargetVisible && iconCollisionVisible;
|
|
||||||
const labelTargetVisible =
|
|
||||||
!this.isBatchPopupMode &&
|
|
||||||
finalIconVisible &&
|
|
||||||
!!formattedLabelText &&
|
|
||||||
labelCollisionVisible;
|
|
||||||
|
|
||||||
if (!finalIconVisible) {
|
// 淡入淡出 alpha(null = 未在淡变中)
|
||||||
|
const iconAlpha = this.getFadeAlpha(feature, 'icon');
|
||||||
|
const labelAlpha = this.getFadeAlpha(feature, 'label');
|
||||||
|
const isIconFading = iconAlpha !== null;
|
||||||
|
const isLabelFading = labelAlpha !== null;
|
||||||
|
|
||||||
|
const finalIconVisible = iconTargetVisible && iconCollisionVisible;
|
||||||
|
// 碰撞隐藏时仍在淡出中则继续渲染,动画结束(alpha 归 0)后才会真正隐藏
|
||||||
|
const iconRendered = finalIconVisible || isIconFading;
|
||||||
|
|
||||||
|
// 图例/区域/密度隐藏:不做淡出,直接隐藏
|
||||||
|
if (!iconTargetVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 碰撞隐藏且不在淡出中:直接隐藏
|
||||||
|
if (!iconRendered) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelRenderVisible =
|
||||||
|
!this.isBatchPopupMode &&
|
||||||
|
iconRendered &&
|
||||||
|
!!formattedLabelText &&
|
||||||
|
(labelCollisionVisible || isLabelFading);
|
||||||
|
|
||||||
|
const iconRenderAlpha = iconAlpha ?? 1;
|
||||||
|
const labelRenderAlpha = labelAlpha ?? 1;
|
||||||
|
// 文字淡变跟随整锚点:取锚点与文字各自 alpha 的最小值
|
||||||
|
const effectiveLabelAlpha = Math.min(iconRenderAlpha, labelRenderAlpha);
|
||||||
|
const fading = isIconFading || isLabelFading;
|
||||||
|
|
||||||
const styleCacheKey = [
|
const styleCacheKey = [
|
||||||
iconUrl,
|
iconUrl,
|
||||||
dynamicScale.toFixed(2),
|
dynamicScale.toFixed(2),
|
||||||
fontSize,
|
fontSize,
|
||||||
engPriority,
|
engPriority,
|
||||||
labelOffsetY,
|
labelOffsetY,
|
||||||
labelTargetVisible ? formattedLabelText : '',
|
labelRenderVisible ? formattedLabelText : '',
|
||||||
labelTargetVisible ? 1 : 0
|
labelRenderVisible ? 1 : 0,
|
||||||
|
fading ? 1 : 0
|
||||||
].join('|');
|
].join('|');
|
||||||
const cachedStyles = this.pointStyleCache.get(styleCacheKey);
|
const cachedStyles = this.pointStyleCache.get(styleCacheKey);
|
||||||
if (cachedStyles) {
|
if (cachedStyles) {
|
||||||
@ -508,12 +545,13 @@ export class MapOl implements MapInterface {
|
|||||||
scale: dynamicScale,
|
scale: dynamicScale,
|
||||||
anchor: [0.5, 0.5],
|
anchor: [0.5, 0.5],
|
||||||
crossOrigin: 'anonymous',
|
crossOrigin: 'anonymous',
|
||||||
declutterMode: 'none'
|
declutterMode: 'none',
|
||||||
|
opacity: iconRenderAlpha
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
if (formattedLabelText && labelTargetVisible) {
|
if (formattedLabelText && labelRenderVisible) {
|
||||||
styles.push(
|
styles.push(
|
||||||
new Style({
|
new Style({
|
||||||
zIndex: engPriority + 1,
|
zIndex: engPriority + 1,
|
||||||
@ -521,9 +559,11 @@ export class MapOl implements MapInterface {
|
|||||||
text: formattedLabelText,
|
text: formattedLabelText,
|
||||||
offsetY: labelOffsetY,
|
offsetY: labelOffsetY,
|
||||||
font: `${fontSize}px sans-serif`,
|
font: `${fontSize}px sans-serif`,
|
||||||
fill: new Fill({ color: '#fff' }),
|
fill: new Fill({
|
||||||
|
color: `rgba(255, 255, 255, ${effectiveLabelAlpha})`
|
||||||
|
}),
|
||||||
stroke: new Stroke({
|
stroke: new Stroke({
|
||||||
color: 'rgba(0, 0, 0, 0.9)',
|
color: `rgba(0, 0, 0, ${0.9 * effectiveLabelAlpha})`,
|
||||||
width: 2
|
width: 2
|
||||||
}),
|
}),
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@ -533,7 +573,9 @@ export class MapOl implements MapInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pointStyleCache.set(styleCacheKey, styles);
|
if (!fading) {
|
||||||
|
this.pointStyleCache.set(styleCacheKey, styles);
|
||||||
|
}
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,7 +585,8 @@ export class MapOl implements MapInterface {
|
|||||||
visible: boolean,
|
visible: boolean,
|
||||||
dirtyLayerKeys?: Set<string>
|
dirtyLayerKeys?: Set<string>
|
||||||
) {
|
) {
|
||||||
if (feature.get(key) === visible) {
|
const prev = feature.get(key);
|
||||||
|
if (prev === visible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,6 +595,21 @@ export class MapOl implements MapInterface {
|
|||||||
if (dirtyLayerKeys && layerKey) {
|
if (dirtyLayerKeys && layerKey) {
|
||||||
dirtyLayerKeys.add(String(layerKey));
|
dirtyLayerKeys.add(String(layerKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 碰撞显隐切换时启动淡入淡出(初始状态 prev 为 undefined 时不做动画)
|
||||||
|
if (key === '_iconCollisionVisible') {
|
||||||
|
if (prev === true && !visible) {
|
||||||
|
this.startFade(feature, 'out', 'icon');
|
||||||
|
} else if (prev === false && visible) {
|
||||||
|
this.startFade(feature, 'in', 'icon');
|
||||||
|
}
|
||||||
|
} else if (key === '_labelCollisionVisible') {
|
||||||
|
if (prev === true && !visible) {
|
||||||
|
this.startFade(feature, 'out', 'label');
|
||||||
|
} else if (prev === false && visible) {
|
||||||
|
this.startFade(feature, 'in', 'label');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private flushDirtyPointLayers(dirtyLayerKeys: Set<string>) {
|
private flushDirtyPointLayers(dirtyLayerKeys: Set<string>) {
|
||||||
@ -561,6 +619,127 @@ export class MapOl implements MapInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动锚点(icon)或文字(label)的淡入/淡出动画。
|
||||||
|
* 对应需求:碰撞隐藏时不直接 show=false,而是打上淡出标记 + 起始时间,
|
||||||
|
* 由 createPointStyle(等价于 Cesium 的 alpha 回调)每帧计算 alpha 驱动淡出。
|
||||||
|
*/
|
||||||
|
private startFade(
|
||||||
|
feature: Feature,
|
||||||
|
dir: 'in' | 'out',
|
||||||
|
target: 'icon' | 'label'
|
||||||
|
) {
|
||||||
|
const outFlag = target === 'icon' ? '_isFadingOut' : '_labelFadingOut';
|
||||||
|
const outStart =
|
||||||
|
target === 'icon' ? '_fadeStartTime' : '_labelFadeStartTime';
|
||||||
|
const inFlag = target === 'icon' ? '_isFadingIn' : '_labelFadingIn';
|
||||||
|
const inStart =
|
||||||
|
target === 'icon' ? '_fadeInStartTime' : '_labelFadeInStartTime';
|
||||||
|
|
||||||
|
if (dir === 'out') {
|
||||||
|
if (feature.get(outFlag)) return;
|
||||||
|
feature.set(outFlag, true, true);
|
||||||
|
feature.set(outStart, performance.now(), true);
|
||||||
|
// 若正在淡入则中断,改向淡出
|
||||||
|
feature.set(inFlag, false, true);
|
||||||
|
feature.set(inStart, 0, true);
|
||||||
|
} else {
|
||||||
|
if (feature.get(inFlag)) return;
|
||||||
|
feature.set(inFlag, true, true);
|
||||||
|
feature.set(inStart, performance.now(), true);
|
||||||
|
// 若正在淡出则中断,改向淡入
|
||||||
|
feature.set(outFlag, false, true);
|
||||||
|
feature.set(outStart, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fadingFeatures.add(feature);
|
||||||
|
this.requestFadeRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算锚点/文字的当前 alpha。
|
||||||
|
* 淡出 alpha 1.0→0.0,淡入 0.0→1.0;未在淡变中返回 null。
|
||||||
|
* 动画结束分支:清除淡变标记(自动清理),并返回 null,
|
||||||
|
* 由调用方按碰撞标志决定最终显隐(等价于"结束时可安全置 show=false")。
|
||||||
|
*/
|
||||||
|
private getFadeAlpha(
|
||||||
|
feature: Feature,
|
||||||
|
target: 'icon' | 'label'
|
||||||
|
): number | null {
|
||||||
|
const outFlag = target === 'icon' ? '_isFadingOut' : '_labelFadingOut';
|
||||||
|
const outStart =
|
||||||
|
target === 'icon' ? '_fadeStartTime' : '_labelFadeStartTime';
|
||||||
|
const inFlag = target === 'icon' ? '_isFadingIn' : '_labelFadingIn';
|
||||||
|
const inStart =
|
||||||
|
target === 'icon' ? '_fadeInStartTime' : '_labelFadeInStartTime';
|
||||||
|
|
||||||
|
if (feature.get(outFlag)) {
|
||||||
|
const alpha = this.computeFadeAlpha(feature.get(outStart), false);
|
||||||
|
if (alpha === null) {
|
||||||
|
// 淡出结束:自动清理标记
|
||||||
|
feature.set(outFlag, false, true);
|
||||||
|
feature.set(outStart, 0, true);
|
||||||
|
}
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
if (feature.get(inFlag)) {
|
||||||
|
const alpha = this.computeFadeAlpha(feature.get(inStart), true);
|
||||||
|
if (alpha === null) {
|
||||||
|
// 淡入结束:自动清理标记
|
||||||
|
feature.set(inFlag, false, true);
|
||||||
|
feature.set(inStart, 0, true);
|
||||||
|
}
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据起始时间与当前时间计算 alpha;动画已结束返回 null */
|
||||||
|
private computeFadeAlpha(
|
||||||
|
startTime: unknown,
|
||||||
|
fadingIn: boolean
|
||||||
|
): number | null {
|
||||||
|
const start = Number(startTime) || 0;
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
if (elapsed >= POINT_FADE_DURATION) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const progress = Math.max(0, elapsed / POINT_FADE_DURATION);
|
||||||
|
return fadingIn ? progress : 1 - progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求一次淡变重绘;已有循环在跑则忽略 */
|
||||||
|
private requestFadeRefresh() {
|
||||||
|
if (this.fadeRefreshFrameId !== null) return;
|
||||||
|
this.fadeRefreshFrameId = requestAnimationFrame(() =>
|
||||||
|
this.tickFadeRefresh()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 淡变驱动循环:每帧让所有点图层重绘一次(createPointStyle 会读取最新 alpha),
|
||||||
|
* 直到没有任何 feature 处于淡变中(此时已由 getFadeAlpha 清理标记,样式函数返回最终显隐)。
|
||||||
|
*/
|
||||||
|
private tickFadeRefresh() {
|
||||||
|
this.fadeRefreshFrameId = null;
|
||||||
|
const stillFading: Feature[] = [];
|
||||||
|
this.fadingFeatures.forEach(feature => {
|
||||||
|
if (
|
||||||
|
this.getFadeAlpha(feature, 'icon') !== null ||
|
||||||
|
this.getFadeAlpha(feature, 'label') !== null
|
||||||
|
) {
|
||||||
|
stillFading.push(feature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.fadingFeatures = new Set(stillFading);
|
||||||
|
this.pointLayerManager.forEachLayer(layer => layer.changed());
|
||||||
|
if (this.fadingFeatures.size > 0) {
|
||||||
|
this.fadeRefreshFrameId = requestAnimationFrame(() =>
|
||||||
|
this.tickFadeRefresh()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private syncFeatureDensityVisible(
|
private syncFeatureDensityVisible(
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
@ -937,15 +1116,18 @@ export class MapOl implements MapInterface {
|
|||||||
const iconBottom = entry.pixelY + iconCollisionSize / 2 + iconPadding;
|
const iconBottom = entry.pixelY + iconCollisionSize / 2 + iconPadding;
|
||||||
const labelText = entry.labelText;
|
const labelText = entry.labelText;
|
||||||
const fontSize = this.getPointFontSize(dynamicScale);
|
const fontSize = this.getPointFontSize(dynamicScale);
|
||||||
const lines = labelText ? labelText.split('\n') : [''];
|
const labelLineCount = labelText ? labelText.split('\n').length : 1;
|
||||||
const maxLineLength = Math.max(...lines.map(line => line.length), 1);
|
// 碰撞偏移与渲染偏移统一,保证碰撞块与实际渲染文字完全重合
|
||||||
const labelLineCount = lines.length;
|
const labelOffsetY = this.getPointLabelRenderOffsetY(
|
||||||
const labelOffsetY = this.getPointLabelCollisionOffsetY(
|
|
||||||
dynamicScale,
|
dynamicScale,
|
||||||
labelLineCount
|
labelLineCount
|
||||||
);
|
);
|
||||||
const estimatedWidth = maxLineLength * fontSize * 0.6 + 16;
|
// 碰撞块大小 = 文字实测像素大小(Canvas 2D measureText 精确测量,与 3D 对齐)
|
||||||
const estimatedHeight = labelLineCount * (fontSize + 4) + 8;
|
const measured = labelText
|
||||||
|
? this.measureLabelPixelSize(labelText, fontSize)
|
||||||
|
: { width: 0, height: 0 };
|
||||||
|
const labelWidth = measured.width;
|
||||||
|
const labelHeight = measured.height;
|
||||||
const centerX = entry.pixelX;
|
const centerX = entry.pixelX;
|
||||||
const centerY = entry.pixelY + labelOffsetY;
|
const centerY = entry.pixelY + labelOffsetY;
|
||||||
const candidateId = String(
|
const candidateId = String(
|
||||||
@ -967,10 +1149,10 @@ export class MapOl implements MapInterface {
|
|||||||
iconTop,
|
iconTop,
|
||||||
iconBottom,
|
iconBottom,
|
||||||
hasLabel: !!labelText,
|
hasLabel: !!labelText,
|
||||||
left: centerX - estimatedWidth / 2,
|
left: centerX - labelWidth / 2,
|
||||||
right: centerX + estimatedWidth / 2,
|
right: centerX + labelWidth / 2,
|
||||||
top: centerY - estimatedHeight / 2,
|
top: centerY - labelHeight / 2,
|
||||||
bottom: centerY + estimatedHeight / 2,
|
bottom: centerY + labelHeight / 2,
|
||||||
priority: Number(entry.feature.get('_nearbyPriority') || 9999),
|
priority: Number(entry.feature.get('_nearbyPriority') || 9999),
|
||||||
engPriority: this.getFeatureEngRenderPriority(entry.feature),
|
engPriority: this.getFeatureEngRenderPriority(entry.feature),
|
||||||
densityPriority: entry.densityPriority,
|
densityPriority: entry.densityPriority,
|
||||||
@ -1307,6 +1489,71 @@ export class MapOl implements MapInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 复用离屏 canvas 获取 2d context */
|
||||||
|
private getMeasureCtx(): CanvasRenderingContext2D | null {
|
||||||
|
if (!this.measureCanvas) {
|
||||||
|
this.measureCanvas = document.createElement('canvas');
|
||||||
|
this.measureCanvas.width = 1;
|
||||||
|
this.measureCanvas.height = 1;
|
||||||
|
}
|
||||||
|
return this.measureCanvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用 Canvas 2D measureText 精确测量文字在屏幕上的像素宽度和高度(与 3D 实现对齐),
|
||||||
|
* 碰撞块的大小 = 文字实际大小。结果按 `text@roundedFontSize` 缓存,fontSize 随 zoom 变化时自动失效。
|
||||||
|
* 宽度优先取墨迹实测(actualBoundingBoxLeft/Right),避免 CJK 字体右留白导致块比文字宽。
|
||||||
|
* @param text 标签文字(支持多行 \n 分隔)
|
||||||
|
* @param fontSize 标签实际使用的像素字号
|
||||||
|
* @returns { width, height } 像素尺寸
|
||||||
|
*/
|
||||||
|
private measureLabelPixelSize(
|
||||||
|
text: string,
|
||||||
|
fontSize: number
|
||||||
|
): { width: number; height: number } {
|
||||||
|
const roundedFs = Math.round(fontSize);
|
||||||
|
const cacheKey = `${text}@${roundedFs}`;
|
||||||
|
const cached = this.measureTextCache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const ctx = this.getMeasureCtx();
|
||||||
|
// fallback:canvas 不可用时用简单估算(极少发生)
|
||||||
|
if (!ctx) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const maxLen = Math.max(...lines.map(l => l.length), 1);
|
||||||
|
const fallback = {
|
||||||
|
width: maxLen * roundedFs + 20,
|
||||||
|
height: lines.length * (roundedFs + 4) + 8
|
||||||
|
};
|
||||||
|
this.measureTextCache.set(cacheKey, fallback);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = `${roundedFs}px sans-serif`;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let maxWidth = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
const metrics = ctx.measureText(line);
|
||||||
|
// 墨迹宽 = 左右墨迹边界之和;浏览器不支持 actualBoundingBox 时退回排版宽
|
||||||
|
const inkWidth =
|
||||||
|
metrics.actualBoundingBoxLeft !== undefined &&
|
||||||
|
metrics.actualBoundingBoxRight !== undefined
|
||||||
|
? metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight
|
||||||
|
: metrics.width;
|
||||||
|
maxWidth = Math.max(maxWidth, inkWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 宽度 = 文字墨迹像素宽度(取多行中最宽的一行,无额外边距)
|
||||||
|
const width = maxWidth;
|
||||||
|
// 高度 = 行数 × 字号(与 3D 对齐,作为文字块的像素高度)
|
||||||
|
const height = lines.length * roundedFs;
|
||||||
|
|
||||||
|
const result = { width, height };
|
||||||
|
this.measureTextCache.set(cacheKey, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private checkLabelCollision(
|
private checkLabelCollision(
|
||||||
left: { left: number; right: number; top: number; bottom: number },
|
left: { left: number; right: number; top: number; bottom: number },
|
||||||
right: { left: number; right: number; top: number; bottom: number }
|
right: { left: number; right: number; top: number; bottom: number }
|
||||||
@ -1334,12 +1581,20 @@ export class MapOl implements MapInterface {
|
|||||||
top: iconRect.top + inset,
|
top: iconRect.top + inset,
|
||||||
bottom: iconRect.bottom - inset
|
bottom: iconRect.bottom - inset
|
||||||
};
|
};
|
||||||
|
// 标签块向上扩展避让边距(等价于给文字加"上边距"),
|
||||||
|
// 让下方锚点文字不再紧贴上方锚点图标底部(含文字描边/图标视觉边缘)看着像重叠
|
||||||
|
const extendedLabelRect = {
|
||||||
|
left: labelRect.left,
|
||||||
|
right: labelRect.right,
|
||||||
|
top: labelRect.top - LABEL_ICON_TOP_CLEARANCE,
|
||||||
|
bottom: labelRect.bottom
|
||||||
|
};
|
||||||
|
|
||||||
return !(
|
return !(
|
||||||
narrowedIconRect.right + padding <= labelRect.left ||
|
narrowedIconRect.right + padding <= extendedLabelRect.left ||
|
||||||
narrowedIconRect.left - padding >= labelRect.right ||
|
narrowedIconRect.left - padding >= extendedLabelRect.right ||
|
||||||
narrowedIconRect.bottom + padding <= labelRect.top ||
|
narrowedIconRect.bottom + padding <= extendedLabelRect.top ||
|
||||||
narrowedIconRect.top - padding >= labelRect.bottom
|
narrowedIconRect.top - padding >= extendedLabelRect.bottom
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1397,17 +1652,6 @@ export class MapOl implements MapInterface {
|
|||||||
return labelLineCount > 1 ? -30 * dynamicScale : -22 * dynamicScale;
|
return labelLineCount > 1 ? -30 * dynamicScale : -22 * dynamicScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一计算标签碰撞时的纵向偏移。
|
|
||||||
* 这里允许比视觉位置略高一点,避免相邻点图标过早压掉主标签。
|
|
||||||
*/
|
|
||||||
private getPointLabelCollisionOffsetY(
|
|
||||||
dynamicScale: number,
|
|
||||||
labelLineCount: number
|
|
||||||
): number {
|
|
||||||
return labelLineCount > 1 ? -36 * dynamicScale : -28 * dynamicScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* distance 字段表示点位密度分档,不是物理距离。
|
* distance 字段表示点位密度分档,不是物理距离。
|
||||||
* 这里仅用它决定点位从哪个缩放级别开始参与显示候选。
|
* 这里仅用它决定点位从哪个缩放级别开始参与显示候选。
|
||||||
@ -2684,6 +2928,13 @@ export class MapOl implements MapInterface {
|
|||||||
this.clearBatchPopupPostRenderSync();
|
this.clearBatchPopupPostRenderSync();
|
||||||
this.removeQueryLayer();
|
this.removeQueryLayer();
|
||||||
this.regionMaskManager.destroy();
|
this.regionMaskManager.destroy();
|
||||||
|
this.measureTextCache.clear();
|
||||||
|
this.measureCanvas = null;
|
||||||
|
if (this.fadeRefreshFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.fadeRefreshFrameId);
|
||||||
|
this.fadeRefreshFrameId = null;
|
||||||
|
}
|
||||||
|
this.fadingFeatures.clear();
|
||||||
|
|
||||||
const clearLayer = (layer: any) => {
|
const clearLayer = (layer: any) => {
|
||||||
if (layer && layer.getSource) {
|
if (layer && layer.getSource) {
|
||||||
|
|||||||
@ -38,8 +38,6 @@ export class PopupManager {
|
|||||||
private lastHoveredId: string | number | null = null;
|
private lastHoveredId: string | number | null = null;
|
||||||
private animationFrameId: number | null = null;
|
private animationFrameId: number | null = null;
|
||||||
private getPointLayers: () => VectorLayer<VectorSource>[];
|
private getPointLayers: () => VectorLayer<VectorSource>[];
|
||||||
private popupMouseEnterHandler: (() => void) | null = null;
|
|
||||||
private popupMouseMoveHandler: (() => void) | null = null;
|
|
||||||
private hoverChangeHandler:
|
private hoverChangeHandler:
|
||||||
| ((payload: PointerMoveChangePayload) => void)
|
| ((payload: PointerMoveChangePayload) => void)
|
||||||
| undefined;
|
| undefined;
|
||||||
@ -63,17 +61,6 @@ export class PopupManager {
|
|||||||
this.popupElement.style.removeProperty('left');
|
this.popupElement.style.removeProperty('left');
|
||||||
this.popupElement.style.removeProperty('top');
|
this.popupElement.style.removeProperty('top');
|
||||||
this.popupElement.style.setProperty('pointer-events', 'none', 'important');
|
this.popupElement.style.setProperty('pointer-events', 'none', 'important');
|
||||||
this.popupMouseEnterHandler = () => {
|
|
||||||
this.forceHidePopup();
|
|
||||||
};
|
|
||||||
this.popupMouseMoveHandler = () => {
|
|
||||||
this.forceHidePopup();
|
|
||||||
};
|
|
||||||
this.popupElement.addEventListener(
|
|
||||||
'mouseenter',
|
|
||||||
this.popupMouseEnterHandler
|
|
||||||
);
|
|
||||||
this.popupElement.addEventListener('mousemove', this.popupMouseMoveHandler);
|
|
||||||
this.popupOverlay = new Overlay({
|
this.popupOverlay = new Overlay({
|
||||||
element: this.popupElement,
|
element: this.popupElement,
|
||||||
positioning: 'bottom-center',
|
positioning: 'bottom-center',
|
||||||
@ -209,12 +196,43 @@ export class PopupManager {
|
|||||||
this.popupElement.innerHTML = popupHtml;
|
this.popupElement.innerHTML = popupHtml;
|
||||||
this.popupOverlay.setPosition(coordinate);
|
this.popupOverlay.setPosition(coordinate);
|
||||||
this.popupElement.style.display = 'block';
|
this.popupElement.style.display = 'block';
|
||||||
|
this.raisePopupLayer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popupOverlay.setPosition(undefined);
|
this.popupOverlay.setPosition(undefined);
|
||||||
this.popupElement.style.display = 'none';
|
this.popupElement.style.display = 'none';
|
||||||
|
this.restorePopupLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备注:hover popup 展示时临时提升层级,避免被批量固定 popup 容器(z-index: 1000)遮挡。
|
||||||
|
// OL Overlay 会把元素包在 .ol-overlay-container 包装层里,该层又挂在 z-index: 0 的
|
||||||
|
// .ol-overlaycontainer 下,单独抬升 popup 元素或其包装层都无法越过顶层容器的层叠上下文,
|
||||||
|
// 因此必须同时抬升 map.getOverlayContainer()(.ol-overlaycontainer)才能盖过批量容器。
|
||||||
|
private raisePopupLayer() {
|
||||||
|
if (!this.popupElement) return;
|
||||||
|
this.popupElement.style.zIndex = '1100';
|
||||||
|
if (this.popupElement.parentElement) {
|
||||||
|
this.popupElement.parentElement.style.zIndex = '1100';
|
||||||
|
}
|
||||||
|
const container = this.map?.getOverlayContainer();
|
||||||
|
if (container) {
|
||||||
|
container.style.zIndex = '1001';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备注:隐藏 hover popup 时恢复层级,避免长期抬高影响其它 Overlay。
|
||||||
|
private restorePopupLayer() {
|
||||||
|
if (!this.popupElement) return;
|
||||||
|
this.popupElement.style.removeProperty('zIndex');
|
||||||
|
if (this.popupElement.parentElement) {
|
||||||
|
this.popupElement.parentElement.style.removeProperty('zIndex');
|
||||||
|
}
|
||||||
|
const container = this.map?.getOverlayContainer();
|
||||||
|
if (container) {
|
||||||
|
container.style.zIndex = '0';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPopupHtml(props: Record<string, any>) {
|
private getPopupHtml(props: Record<string, any>) {
|
||||||
@ -340,6 +358,8 @@ export class PopupManager {
|
|||||||
popupEl.style.visibility = 'visible';
|
popupEl.style.visibility = 'visible';
|
||||||
popupEl.style.left = `${popupX}px`;
|
popupEl.style.left = `${popupX}px`;
|
||||||
popupEl.style.top = `${popupY}px`;
|
popupEl.style.top = `${popupY}px`;
|
||||||
|
// 标记该锚点已有固定展示的 popup,hover 时跳过 hover popup
|
||||||
|
feature.set('_fixedPopupShown', true);
|
||||||
this.batchPopupItems.push({
|
this.batchPopupItems.push({
|
||||||
feature,
|
feature,
|
||||||
element: popupEl
|
element: popupEl
|
||||||
@ -353,6 +373,7 @@ export class PopupManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 有碰撞,移除该 popup
|
// 有碰撞,移除该 popup
|
||||||
|
feature.set('_fixedPopupShown', false);
|
||||||
batchContainer.removeChild(popupEl);
|
batchContainer.removeChild(popupEl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -373,26 +394,32 @@ export class PopupManager {
|
|||||||
const viewportPadding = 48;
|
const viewportPadding = 48;
|
||||||
|
|
||||||
this.batchPopupItems.forEach(({ feature, element }) => {
|
this.batchPopupItems.forEach(({ feature, element }) => {
|
||||||
|
// 统一维护固定展示标记:显示时置 true、隐藏时置 false
|
||||||
|
const setVisible = (visible: boolean) => {
|
||||||
|
element.style.display = visible ? 'block' : 'none';
|
||||||
|
feature.set('_fixedPopupShown', visible);
|
||||||
|
};
|
||||||
|
|
||||||
if (feature.get('_legendVisible') === false) {
|
if (feature.get('_legendVisible') === false) {
|
||||||
element.style.display = 'none';
|
setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (styleChecker && !styleChecker(feature)) {
|
if (styleChecker && !styleChecker(feature)) {
|
||||||
element.style.display = 'none';
|
setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const geom = feature.getGeometry();
|
const geom = feature.getGeometry();
|
||||||
if (!geom || geom.getType() !== 'Point') {
|
if (!geom || geom.getType() !== 'Point') {
|
||||||
element.style.display = 'none';
|
setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const coords = (geom as Point).getCoordinates();
|
const coords = (geom as Point).getCoordinates();
|
||||||
const pixel = this.map?.getPixelFromCoordinate(coords);
|
const pixel = this.map?.getPixelFromCoordinate(coords);
|
||||||
if (!pixel) {
|
if (!pixel) {
|
||||||
element.style.display = 'none';
|
setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,18 +431,22 @@ export class PopupManager {
|
|||||||
popupX > viewportWidth + viewportPadding ||
|
popupX > viewportWidth + viewportPadding ||
|
||||||
popupY > viewportHeight + viewportPadding
|
popupY > viewportHeight + viewportPadding
|
||||||
) {
|
) {
|
||||||
element.style.display = 'none';
|
setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.style.left = `${popupX}px`;
|
element.style.left = `${popupX}px`;
|
||||||
element.style.top = `${popupY}px`;
|
element.style.top = `${popupY}px`;
|
||||||
element.style.display = 'block';
|
setVisible(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 备注:清除批量显示的 popups。
|
// 备注:清除批量显示的 popups。
|
||||||
clearBatchPopups() {
|
clearBatchPopups() {
|
||||||
|
// 清除前复位所有要素的固定展示标记
|
||||||
|
this.batchPopupItems.forEach(({ feature }) => {
|
||||||
|
feature.set('_fixedPopupShown', false);
|
||||||
|
});
|
||||||
this.batchPopupItems = [];
|
this.batchPopupItems = [];
|
||||||
if (this.batchPopupContainer) {
|
if (this.batchPopupContainer) {
|
||||||
this.batchPopupContainer.remove();
|
this.batchPopupContainer.remove();
|
||||||
@ -456,15 +487,6 @@ export class PopupManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 备注:在弹窗层意外接管鼠标时,通过统一 hover 回调链路立即清空当前悬停状态。
|
|
||||||
private forceHidePopup() {
|
|
||||||
this.emitHoverChange({
|
|
||||||
hoveredId: null,
|
|
||||||
detectedFeature: undefined,
|
|
||||||
coordinate: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 备注:统一派发 hover 变化,确保地图侧的 cursor、Popup 和图层刷新走同一条处理链路。
|
// 备注:统一派发 hover 变化,确保地图侧的 cursor、Popup 和图层刷新走同一条处理链路。
|
||||||
private emitHoverChange(payload: PointerMoveChangePayload) {
|
private emitHoverChange(payload: PointerMoveChangePayload) {
|
||||||
this.lastHoveredId = payload.hoveredId;
|
this.lastHoveredId = payload.hoveredId;
|
||||||
@ -505,23 +527,9 @@ export class PopupManager {
|
|||||||
destroy() {
|
destroy() {
|
||||||
this.clearBatchPopups();
|
this.clearBatchPopups();
|
||||||
this.reset();
|
this.reset();
|
||||||
if (this.popupElement && this.popupMouseEnterHandler) {
|
|
||||||
this.popupElement.removeEventListener(
|
|
||||||
'mouseenter',
|
|
||||||
this.popupMouseEnterHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.popupElement && this.popupMouseMoveHandler) {
|
|
||||||
this.popupElement.removeEventListener(
|
|
||||||
'mousemove',
|
|
||||||
this.popupMouseMoveHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.map && this.popupOverlay) {
|
if (this.map && this.popupOverlay) {
|
||||||
this.map.removeOverlay(this.popupOverlay);
|
this.map.removeOverlay(this.popupOverlay);
|
||||||
}
|
}
|
||||||
this.popupMouseEnterHandler = null;
|
|
||||||
this.popupMouseMoveHandler = null;
|
|
||||||
this.hoverChangeHandler = undefined;
|
this.hoverChangeHandler = undefined;
|
||||||
this.popupOverlay = null;
|
this.popupOverlay = null;
|
||||||
this.popupElement = null;
|
this.popupElement = null;
|
||||||
|
|||||||
@ -23,6 +23,9 @@ export class ThreeDRoamManager {
|
|||||||
// 相机平滑跟随插值状态
|
// 相机平滑跟随插值状态
|
||||||
private _smoothCenter: Cesium.Cartesian3 | null = null;
|
private _smoothCenter: Cesium.Cartesian3 | null = null;
|
||||||
private _smoothOrientation: Cesium.Quaternion | null = null;
|
private _smoothOrientation: Cesium.Quaternion | null = null;
|
||||||
|
// 点击跳转后的过渡截止时间(performance.now() 时间戳):
|
||||||
|
// 过渡期内 setCameraLookAt 使用更大的插值系数,让相机平滑“飞”向目标而非瞬移
|
||||||
|
private _transitionEnd = 0;
|
||||||
|
|
||||||
constructor(viewer: any) {
|
constructor(viewer: any) {
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
@ -116,6 +119,8 @@ export class ThreeDRoamManager {
|
|||||||
this.viewer.clock.shouldAnimate = false;
|
this.viewer.clock.shouldAnimate = false;
|
||||||
this.entitiesVisible(true);
|
this.entitiesVisible(true);
|
||||||
this.removeTickCallback();
|
this.removeTickCallback();
|
||||||
|
// 不清除平滑插值状态:暂停后相机仍会平滑收敛到模型位置再停下(保留缓冲,
|
||||||
|
// 避免“立刻暂停”的突兀感);此时时钟已冻结,目标固定,不会产生抖动
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置飞行(回到起点/第0帧) */
|
/** 重置飞行(回到起点/第0帧) */
|
||||||
@ -177,21 +182,17 @@ export class ThreeDRoamManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 视角定位到指定时间点的位置 */
|
/**
|
||||||
|
* 视角定位到指定时间点的位置
|
||||||
|
* 说明:不再使用 camera.flyTo —— 漫游中第一人称跟随(setCameraLookAt)每帧执行
|
||||||
|
* lookAtTransform,camera.flyTo 会与其争抢相机控制,导致相机高度/zoom 振荡,
|
||||||
|
* 锚点文字随之“先缩小后重新放大”。改为开启一段过渡窗口,
|
||||||
|
* setCameraLookAt 在窗口内用更大的插值系数平滑“飞”向目标时间点的跟随视角。
|
||||||
|
* 位置/朝向仍由 setCameraLookAt 每帧从模型实时取值,因此无需在此读取坐标。
|
||||||
|
*/
|
||||||
flyToPositionAtTime(seconds: number): void {
|
flyToPositionAtTime(seconds: number): void {
|
||||||
if (!this.roamingEntity || !this.startTime) return;
|
if (!this.roamingEntity || !this.startTime) return;
|
||||||
const time = Cesium.JulianDate.addSeconds(
|
this._transitionEnd = performance.now() + 500;
|
||||||
this.startTime,
|
|
||||||
seconds,
|
|
||||||
new Cesium.JulianDate()
|
|
||||||
);
|
|
||||||
const position = this.roamingEntity.position?.getValue(time);
|
|
||||||
if (position) {
|
|
||||||
this.viewer.camera.flyTo({
|
|
||||||
destination: position,
|
|
||||||
duration: 0.5
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否已加载漫游实体 */
|
/** 是否已加载漫游实体 */
|
||||||
@ -267,8 +268,14 @@ export class ThreeDRoamManager {
|
|||||||
const targetOrientation = entity.orientation?.getValue(time);
|
const targetOrientation = entity.orientation?.getValue(time);
|
||||||
if (!targetCenter || !targetOrientation) return;
|
if (!targetCenter || !targetOrientation) return;
|
||||||
|
|
||||||
// 插值系数:越小越平滑(跟随延迟越大)
|
// 插值系数:常规跟随 0.1(越小越平滑,跟随延迟越大);
|
||||||
const lerpFactor = 0.06;
|
// 点击跳转后的过渡期内 0.15 → 0.1 线性衰减,让相机快速且平滑地“飞”向目标位置
|
||||||
|
const now = performance.now();
|
||||||
|
let lerpFactor = 0.1;
|
||||||
|
if (now < this._transitionEnd) {
|
||||||
|
const t = 1 - (this._transitionEnd - now) / 500; // 0 → 1
|
||||||
|
lerpFactor = 0.15 + (0.1 - 0.15) * t;
|
||||||
|
}
|
||||||
|
|
||||||
// 对位置做 lerp
|
// 对位置做 lerp
|
||||||
let center: Cesium.Cartesian3;
|
let center: Cesium.Cartesian3;
|
||||||
@ -360,4 +367,4 @@ export class ThreeDRoamManager {
|
|||||||
}
|
}
|
||||||
this.removeTickCallback();
|
this.removeTickCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,6 +105,7 @@
|
|||||||
:max="totalDuration"
|
:max="totalDuration"
|
||||||
:marks="milestoneMarks"
|
:marks="milestoneMarks"
|
||||||
:tipFormatter="tipFormatter"
|
:tipFormatter="tipFormatter"
|
||||||
|
@change="handleProgressChange"
|
||||||
@afterChange="handleProgressChange"
|
@afterChange="handleProgressChange"
|
||||||
/>
|
/>
|
||||||
<div class="value-display">
|
<div class="value-display">
|
||||||
@ -188,6 +189,8 @@ async function initRoamManager(): Promise<void> {
|
|||||||
console.warn('Cesium viewer 未就绪');
|
console.warn('Cesium viewer 未就绪');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 进入漫游:锚点文字/图标开启深度测试,避免文字遮挡漫游模型(glb)
|
||||||
|
MapClass.getInstance().setRoamDepthTest(true);
|
||||||
// 进入漫游:保存当前相机状态用于退出时恢复
|
// 进入漫游:保存当前相机状态用于退出时恢复
|
||||||
savedCamera = {
|
savedCamera = {
|
||||||
position: viewer.camera.position.clone(),
|
position: viewer.camera.position.clone(),
|
||||||
@ -369,6 +372,12 @@ function handleFlightModelChange(value: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进度条拖动/点击时同步 Cesium 时钟并开启相机过渡
|
||||||
|
* - jumpToTime:拖动/点击瞬间即同步时钟,避免播放中 onTick 进度回调按旧时钟位置
|
||||||
|
* 把滑块值覆盖回去(问题1:10→20 先到 20 再回退到 10);
|
||||||
|
* - flyToPositionAtTime:开启过渡窗口,让第一人称跟随平滑“飞”向目标位置(保留缓冲)。
|
||||||
|
*/
|
||||||
function handleProgressChange(value: number) {
|
function handleProgressChange(value: number) {
|
||||||
if (roamManager) {
|
if (roamManager) {
|
||||||
roamManager.jumpToTime(value);
|
roamManager.jumpToTime(value);
|
||||||
@ -382,6 +391,8 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
// 退出漫游:恢复锚点深度测试策略(按俯仰角动态切换)
|
||||||
|
MapClass.getInstance().setRoamDepthTest(false);
|
||||||
// dim 切换时跳过 Cesium 清理(由地图切换自行处理),避免影响 2D 地图初始化
|
// dim 切换时跳过 Cesium 清理(由地图切换自行处理),避免影响 2D 地图初始化
|
||||||
const skipCesiumCleanup = !!uiStore.skipRoamCameraRestore;
|
const skipCesiumCleanup = !!uiStore.skipRoamCameraRestore;
|
||||||
roamManager?.destroy(skipCesiumCleanup);
|
roamManager?.destroy(skipCesiumCleanup);
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="map-controller" class="map-controller" :style="{ right: drawerOpen ? '480px' : '12px' }">
|
<div
|
||||||
|
id="map-controller"
|
||||||
|
class="map-controller"
|
||||||
|
:style="{ right: drawerOpen ? '480px' : '12px' }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="map-controller-group"
|
class="map-controller-group"
|
||||||
v-for="(item, index) in controllers"
|
v-for="(item, index) in controllers"
|
||||||
@ -424,10 +428,10 @@ const handleControllerClick = (item: any) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'threedRoam': // 流域三维漫游
|
case 'threedRoam': // 流域三维漫游
|
||||||
if (is3DTransition.value) break;
|
|
||||||
// 互斥:选中threedRoam时取消其他选中
|
|
||||||
activeKey.value = activeKey.value === 'threedRoam' ? null : 'threedRoam';
|
activeKey.value = activeKey.value === 'threedRoam' ? null : 'threedRoam';
|
||||||
uiStore.isRoaming = !uiStore.isRoaming;
|
uiStore.isRoaming = !uiStore.isRoaming;
|
||||||
|
// 进入/退出漫游立即同步深度测试与强制批量 popup,不依赖漫游面板的数据异步加载链路
|
||||||
|
map.value.setRoamDepthTest?.(uiStore.isRoaming);
|
||||||
break;
|
break;
|
||||||
// 可以在这里添加其他控制器的处理逻辑
|
// 可以在这里添加其他控制器的处理逻辑
|
||||||
case 'screenShot':
|
case 'screenShot':
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user