添加3dpopup显示
This commit is contained in:
parent
3dd9270911
commit
72d91355bb
@ -2,12 +2,25 @@
|
||||
import type { MDOptions } from './map.class';
|
||||
import type { MapInterface, layer } from './map.d';
|
||||
import { getIconPath } from '@/utils/index';
|
||||
import {
|
||||
generatePopupHtml,
|
||||
shouldPreferEng2Popup
|
||||
} from '@/utils/popupHtmlGenerator';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
|
||||
export class MapCesium implements MapInterface {
|
||||
private viewer: Cesium.Viewer | null = null;
|
||||
private clickEventHandler: Cesium.ScreenSpaceEventHandler | null = null;
|
||||
private popupElement: HTMLElement | null = null;
|
||||
private hoveredEntityId: string | null = null;
|
||||
private removePostRenderListener: (() => void) | null = null;
|
||||
private isBatchPopupMode = false;
|
||||
private batchPopupContainer: HTMLDivElement | null = null;
|
||||
private batchPopupItems: Array<{
|
||||
entity: Cesium.Entity;
|
||||
element: HTMLDivElement;
|
||||
}> = [];
|
||||
private batchPostRenderRemover: (() => void) | null = null;
|
||||
private containerId = '';
|
||||
private containerElement: HTMLElement | null = null;
|
||||
private loadingOverlayElement: HTMLDivElement | null = null;
|
||||
@ -48,6 +61,8 @@ export class MapCesium implements MapInterface {
|
||||
private readonly CESIUM_ZOOM_FORMULA_B = 0.00007096758;
|
||||
private readonly CESIUM_ZOOM_FORMULA_C = 91610.74;
|
||||
private readonly CESIUM_ZOOM_FORMULA_D = -40467.74;
|
||||
private readonly HOVER_POPUP_MAX_HEIGHT = 9_000_000; // ~zoom 4.5,中国视角以上不触发 hover
|
||||
private readonly BATCH_POPUP_MODE_ZOOM = 15; // zoom >= 15 进入批量固定模式
|
||||
|
||||
// 图标缩放可调参数(只影响 3D Cesium,不参与 2D 计算)
|
||||
// scale = base + (zoom - 4.5) * rate,钳位到 [min, max]
|
||||
@ -115,6 +130,7 @@ export class MapCesium implements MapInterface {
|
||||
await this.flyToChina();
|
||||
|
||||
this.setupClickHandler();
|
||||
this.setupCameraPopupReset();
|
||||
|
||||
return this.viewer;
|
||||
} catch (error) {
|
||||
@ -250,23 +266,58 @@ export class MapCesium implements MapInterface {
|
||||
// 默认箭头光标,与 2D 一致(不显示拖拽小手)
|
||||
canvas.style.cursor = 'default';
|
||||
|
||||
// 悬停锚点时显示 pointer
|
||||
// 鼠标移动:光标样式 + hover popup(批量模式下只改光标)
|
||||
this.clickEventHandler.setInputAction(
|
||||
(move: { endPosition: Cesium.Cartesian2 }) => {
|
||||
const scene = this.viewer?.scene;
|
||||
if (!scene) return;
|
||||
|
||||
const cameraHeight = this.getCurrentCameraHeight();
|
||||
// 缩放门槛:中国视角以上不触发 hover
|
||||
if (cameraHeight > this.HOVER_POPUP_MAX_HEIGHT) {
|
||||
canvas.style.cursor = 'default';
|
||||
this.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const picked = scene.pick(move.endPosition);
|
||||
canvas.style.cursor =
|
||||
Cesium.defined(picked) &&
|
||||
picked.id &&
|
||||
(picked.id as any).properties?._layerKey
|
||||
? 'pointer'
|
||||
: 'default';
|
||||
const entity =
|
||||
Cesium.defined(picked) && picked.id instanceof Cesium.Entity
|
||||
? (picked.id as Cesium.Entity)
|
||||
: null;
|
||||
|
||||
// 检测是否命中可交互的锚点
|
||||
if (entity && this.isEntityInteractive(entity)) {
|
||||
canvas.style.cursor = 'pointer';
|
||||
|
||||
// 批量模式下单点 hover 不触发 popup
|
||||
if (this.isBatchPopupMode) return;
|
||||
|
||||
// 同一个实体不重复刷新
|
||||
if (this.hoveredEntityId === entity.id) {
|
||||
const position = entity.position?.getValue(
|
||||
this.viewer!.clock.currentTime
|
||||
);
|
||||
if (position) {
|
||||
this.updatePopupPosition(position);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPopupForEntity(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.style.cursor = 'default';
|
||||
// 批量模式下不隐藏批量 popup
|
||||
if (!this.isBatchPopupMode) {
|
||||
this.hidePopup();
|
||||
}
|
||||
},
|
||||
Cesium.ScreenSpaceEventType.MOUSE_MOVE
|
||||
);
|
||||
|
||||
// 点击:先强制渲染一帧确保 scene.pick 不阻塞,然后打开详情弹框
|
||||
// 点击:打开详情弹框
|
||||
this.clickEventHandler.setInputAction(
|
||||
(click: { position: Cesium.Cartesian2 }) => {
|
||||
const scene = this.viewer?.scene;
|
||||
@ -276,16 +327,10 @@ export class MapCesium implements MapInterface {
|
||||
if (!Cesium.defined(picked) || !picked.id) return;
|
||||
|
||||
const entity = picked.id as Cesium.Entity;
|
||||
const bag = (entity as any).properties;
|
||||
if (!bag?._layerKey) return;
|
||||
if (!this.isEntityInteractive(entity)) return;
|
||||
|
||||
// 直接读取原始数据引用,避免 Cesium Property getValue 遍历开销
|
||||
const rawData: Record<string, any> = (entity as any)._rawData || {};
|
||||
|
||||
if (this.popupElement) {
|
||||
this.popupElement.style.display = 'none';
|
||||
}
|
||||
|
||||
if (rawData.sttpMap === 'ylfb') {
|
||||
modelStore.ylfbModalVisible = true;
|
||||
modelStore.params = rawData;
|
||||
@ -299,9 +344,420 @@ export class MapCesium implements MapInterface {
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Step 5: hover popup + 批量 popup ====================
|
||||
|
||||
/** 监听相机移动结束:检测缩放阈值切换批量模式 */
|
||||
private setupCameraPopupReset() {
|
||||
if (!this.viewer) return;
|
||||
|
||||
if (this.removePostRenderListener) {
|
||||
this.removePostRenderListener();
|
||||
this.removePostRenderListener = null;
|
||||
}
|
||||
|
||||
this.removePostRenderListener = this.viewer.camera.moveEnd.addEventListener(
|
||||
() => {
|
||||
const zoom = this.getZoomLevelFromCameraHeight(
|
||||
this.getCurrentCameraHeight()
|
||||
);
|
||||
|
||||
if (zoom >= this.BATCH_POPUP_MODE_ZOOM) {
|
||||
if (!this.isBatchPopupMode) {
|
||||
this.enableBatchPopupMode();
|
||||
}
|
||||
// 已处于批量模式时不做重建(postRender 已持续更新位置),
|
||||
// 避免 clear+rebuild 产生空白帧闪烁
|
||||
} else {
|
||||
if (this.isBatchPopupMode) {
|
||||
this.disableBatchPopupMode();
|
||||
}
|
||||
// hover popup 重定位而非隐藏,避免缩放结束后闪烁
|
||||
if (this.hoveredEntityId) {
|
||||
this.repositionHoverPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 根据 hoveredEntityId 重定位 popup(相机移动后调用,不重建内容) */
|
||||
private repositionHoverPopup(): void {
|
||||
if (!this.viewer || !this.popupElement || !this.hoveredEntityId) return;
|
||||
const entity = this.viewer.entities.getById(this.hoveredEntityId);
|
||||
if (!entity || !entity.position) {
|
||||
this.hidePopup();
|
||||
return;
|
||||
}
|
||||
const position = entity.position.getValue(this.viewer.clock.currentTime);
|
||||
if (!position) {
|
||||
this.hidePopup();
|
||||
return;
|
||||
}
|
||||
this.updatePopupPosition(position);
|
||||
}
|
||||
|
||||
/** 批量模式下每帧更新 popup 位置,实现流畅跟随拖拽 */
|
||||
private setupBatchPopupPostRenderSync() {
|
||||
if (!this.viewer || this.batchPostRenderRemover) return;
|
||||
this.batchPostRenderRemover = this.viewer.scene.postRender.addEventListener(
|
||||
() => {
|
||||
if (!this.isBatchPopupMode) return;
|
||||
this.updateBatchPopupPositions();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 停止每帧同步 */
|
||||
private clearBatchPopupPostRenderSync() {
|
||||
if (this.batchPostRenderRemover) {
|
||||
this.batchPostRenderRemover();
|
||||
this.batchPostRenderRemover = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前相机高度(米) */
|
||||
private getCurrentCameraHeight(): number {
|
||||
if (!this.viewer) return Number.POSITIVE_INFINITY;
|
||||
const height = this.viewer.camera.positionCartographic.height;
|
||||
if (!height || !isFinite(height) || height <= 0) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
/** 将相机高度映射为近似 2D zoom 级别(与 getCurrentCesiumZoom 同公式) */
|
||||
private getZoomLevelFromCameraHeight(height: number): number {
|
||||
const zoom =
|
||||
this.CESIUM_ZOOM_FORMULA_D +
|
||||
(this.CESIUM_ZOOM_FORMULA_A - this.CESIUM_ZOOM_FORMULA_D) /
|
||||
(1 +
|
||||
Math.pow(
|
||||
height / this.CESIUM_ZOOM_FORMULA_C,
|
||||
this.CESIUM_ZOOM_FORMULA_B
|
||||
)) +
|
||||
1;
|
||||
return zoom > -1 ? zoom : 0;
|
||||
}
|
||||
|
||||
/** 判断实体当前是否可交互(图层可见 + 图例可见 + 区域可见) */
|
||||
private isEntityInteractive(entity: Cesium.Entity): boolean {
|
||||
const e = entity as any;
|
||||
if (e._layerVisible === false) return false;
|
||||
if (e._legendVisible === false) return false;
|
||||
if (e._regionVisible === false) return false;
|
||||
if (entity.show === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 根据实体属性生成 popup HTML,复用 generatePopupHtml */
|
||||
private getPopupHtml(rawData: Record<string, any>): string {
|
||||
return rawData.popupHtml || generatePopupHtml(rawData) || '';
|
||||
}
|
||||
|
||||
/** 关闭单个 hover popup */
|
||||
private hidePopup(): void {
|
||||
this.hoveredEntityId = null;
|
||||
if (!this.popupElement) return;
|
||||
this.popupElement.style.display = 'none';
|
||||
this.popupElement.innerHTML = '';
|
||||
}
|
||||
|
||||
/** 根据实体世界坐标把 popup 定位到屏幕坐标 */
|
||||
private updatePopupPosition(position: Cesium.Cartesian3): void {
|
||||
if (!this.viewer || !this.popupElement) return;
|
||||
|
||||
const canvasPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
this.viewer.scene,
|
||||
position
|
||||
);
|
||||
|
||||
if (!canvasPosition) {
|
||||
this.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
this.popupElement.style.left = `${canvasPosition.x}px`;
|
||||
this.popupElement.style.top = `${canvasPosition.y}px`;
|
||||
this.popupElement.style.display = 'block';
|
||||
}
|
||||
|
||||
/** 读取实体原始数据并渲染 hover popup */
|
||||
private showPopupForEntity(entity: Cesium.Entity): void {
|
||||
if (!this.viewer || !this.popupElement || !entity.position) return;
|
||||
|
||||
const rawData: Record<string, any> = {
|
||||
...((entity as any)._rawData || {})
|
||||
};
|
||||
if (!rawData.sttpMap) {
|
||||
rawData.sttpMap = rawData._sttpMap || rawData.popupSttpMap || '';
|
||||
}
|
||||
const popupHtml = this.getPopupHtml(rawData);
|
||||
const position = entity.position.getValue(this.viewer.clock.currentTime);
|
||||
|
||||
if (!popupHtml || !position) {
|
||||
this.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
this.popupElement.innerHTML = popupHtml;
|
||||
this.hoveredEntityId = entity.id;
|
||||
this.updatePopupPosition(position);
|
||||
}
|
||||
|
||||
// ==================== 批量 popup ====================
|
||||
|
||||
/** 启用批量 popup:所有可见锚点固定显示 popup */
|
||||
private enableBatchPopupMode() {
|
||||
this.isBatchPopupMode = true;
|
||||
this.clearBatchPopups();
|
||||
this.showBatchPopups();
|
||||
this.setupBatchPopupPostRenderSync();
|
||||
}
|
||||
|
||||
/** 退出批量 popup */
|
||||
private disableBatchPopupMode() {
|
||||
this.isBatchPopupMode = false;
|
||||
this.clearBatchPopupPostRenderSync();
|
||||
this.clearBatchPopups();
|
||||
this.hidePopup();
|
||||
}
|
||||
|
||||
/** 遍历所有图层中交互可见且视口内的实体,分帧创建批量 popup(避免卡顿) */
|
||||
private showBatchPopups() {
|
||||
if (!this.viewer || !this.containerElement) return;
|
||||
|
||||
const batchContainer = document.createElement('div');
|
||||
batchContainer.className = 'batch-popup-container';
|
||||
batchContainer.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
`;
|
||||
batchContainer.id = 'batch-popup-container';
|
||||
this.batchPopupContainer = batchContainer;
|
||||
this.containerElement.appendChild(batchContainer);
|
||||
|
||||
const canvas = this.viewer.scene.canvas;
|
||||
const viewW = canvas.clientWidth;
|
||||
const viewH = canvas.clientHeight;
|
||||
const padding = 60;
|
||||
|
||||
// 收集所有需要渲染的实体信息(含视口裁剪)
|
||||
const candidates: Array<{
|
||||
entity: Cesium.Entity;
|
||||
x: number;
|
||||
y: number;
|
||||
popupHtml: string;
|
||||
}> = [];
|
||||
|
||||
this.pointLayerRegistry.forEach(entities => {
|
||||
entities.forEach(entity => {
|
||||
if (!this.isEntityInteractive(entity)) return;
|
||||
const position = entity.position?.getValue(
|
||||
this.viewer!.clock.currentTime
|
||||
);
|
||||
if (!position) return;
|
||||
|
||||
const cp = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
this.viewer!.scene,
|
||||
position
|
||||
);
|
||||
if (!cp) return;
|
||||
|
||||
// 视口裁剪:超出屏幕的直接跳过
|
||||
if (
|
||||
cp.x < -padding ||
|
||||
cp.y < -padding ||
|
||||
cp.x > viewW + padding ||
|
||||
cp.y > viewH + padding
|
||||
)
|
||||
return;
|
||||
|
||||
const rawData: Record<string, any> = {
|
||||
...((entity as any)._rawData || {})
|
||||
};
|
||||
if (!rawData.sttpMap) {
|
||||
rawData.sttpMap = rawData._sttpMap || rawData.popupSttpMap || '';
|
||||
}
|
||||
|
||||
// 批量模式:eng 锚点优先用 eng2(详细数据),与 2D 对齐
|
||||
const popupHtml = shouldPreferEng2Popup(rawData)
|
||||
? generatePopupHtml(rawData, { forceEng2: true }) ||
|
||||
rawData.popupHtml ||
|
||||
generatePopupHtml(rawData)
|
||||
: this.getPopupHtml(rawData);
|
||||
if (!popupHtml) return;
|
||||
|
||||
candidates.push({ entity, x: cp.x, y: cp.y, popupHtml });
|
||||
});
|
||||
});
|
||||
|
||||
// 分帧渲染,每帧最多 80 个,避免一次性 DOM 操作卡顿
|
||||
let index = 0;
|
||||
const CHUNK_SIZE = 80;
|
||||
const placedPopups: Array<{
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}> = [];
|
||||
|
||||
const processChunk = () => {
|
||||
if (!this.isBatchPopupMode || !this.batchPopupContainer) return;
|
||||
|
||||
const end = Math.min(index + CHUNK_SIZE, candidates.length);
|
||||
for (let i = index; i < end; i++) {
|
||||
const { entity, x, y, popupHtml } = candidates[i];
|
||||
|
||||
const popupEl = document.createElement('div');
|
||||
popupEl.className = 'map-popup-container';
|
||||
popupEl.innerHTML = popupHtml;
|
||||
popupEl.style.position = 'absolute';
|
||||
popupEl.style.display = 'block';
|
||||
popupEl.style.transform = 'translate(-50%, calc(-100% - 10px))';
|
||||
popupEl.style.pointerEvents = 'none';
|
||||
|
||||
// 先隐藏测量尺寸
|
||||
popupEl.style.visibility = 'hidden';
|
||||
popupEl.style.left = `${x}px`;
|
||||
popupEl.style.top = `${y}px`;
|
||||
this.batchPopupContainer!.appendChild(popupEl);
|
||||
|
||||
const rect = popupEl.getBoundingClientRect();
|
||||
const pw = rect.width || 120;
|
||||
const ph = rect.height || 40;
|
||||
|
||||
const popupRect = {
|
||||
left: x - pw / 2,
|
||||
right: x + pw / 2,
|
||||
top: y - 10 - ph,
|
||||
bottom: y - 10
|
||||
};
|
||||
|
||||
let hasCollision = false;
|
||||
for (const placed of placedPopups) {
|
||||
if (this.checkPopupCollision(popupRect, placed)) {
|
||||
hasCollision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCollision) {
|
||||
popupEl.style.visibility = 'visible';
|
||||
popupEl.style.left = `${x}px`;
|
||||
popupEl.style.top = `${y}px`;
|
||||
this.batchPopupItems.push({ entity, element: popupEl });
|
||||
placedPopups.push(popupRect);
|
||||
} else {
|
||||
this.batchPopupContainer!.removeChild(popupEl);
|
||||
}
|
||||
}
|
||||
|
||||
index = end;
|
||||
if (index < candidates.length && this.isBatchPopupMode) {
|
||||
requestAnimationFrame(processChunk);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(processChunk);
|
||||
}
|
||||
|
||||
/** 每帧更新批量 popup 屏幕位置(不重建 DOM,拖拽时流畅跟随) */
|
||||
private updateBatchPopupPositions() {
|
||||
if (!this.viewer || !this.batchPopupContainer) return;
|
||||
if (this.batchPopupItems.length === 0) return;
|
||||
|
||||
const canvas = this.viewer.scene.canvas;
|
||||
const viewW = canvas.clientWidth;
|
||||
const viewH = canvas.clientHeight;
|
||||
const padding = 48;
|
||||
|
||||
for (let i = 0; i < this.batchPopupItems.length; i++) {
|
||||
const { entity, element } = this.batchPopupItems[i];
|
||||
|
||||
if (!this.isEntityInteractive(entity)) {
|
||||
element.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
|
||||
const position = entity.position?.getValue(
|
||||
this.viewer!.clock.currentTime
|
||||
);
|
||||
if (!position) {
|
||||
element.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
|
||||
const cp = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
this.viewer!.scene,
|
||||
position
|
||||
);
|
||||
if (!cp) {
|
||||
element.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
cp.x < -padding ||
|
||||
cp.y < -padding ||
|
||||
cp.x > viewW + padding ||
|
||||
cp.y > viewH + padding
|
||||
) {
|
||||
element.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
|
||||
element.style.left = `${cp.x}px`;
|
||||
element.style.top = `${cp.y}px`;
|
||||
element.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除所有批量 popup */
|
||||
private clearBatchPopups() {
|
||||
this.batchPopupItems = [];
|
||||
if (this.batchPopupContainer) {
|
||||
this.batchPopupContainer.remove();
|
||||
this.batchPopupContainer = null;
|
||||
return;
|
||||
}
|
||||
const existing = document.getElementById('batch-popup-container');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/** 简单碰撞检测(AABB + padding) */
|
||||
private checkPopupCollision(
|
||||
a: { left: number; right: number; top: number; bottom: number },
|
||||
b: { left: number; right: number; top: number; bottom: number }
|
||||
): boolean {
|
||||
const padding = 4;
|
||||
return !(
|
||||
a.right + padding < b.left ||
|
||||
a.left - padding > b.right ||
|
||||
a.bottom + padding < b.top ||
|
||||
a.top - padding > b.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== destroy ====================
|
||||
|
||||
destroy(): void {
|
||||
this.clearFlightFallbackTimer();
|
||||
this.hideLoadingOverlay();
|
||||
this.isBatchPopupMode = false;
|
||||
this.clearBatchPopupPostRenderSync();
|
||||
this.clearBatchPopups();
|
||||
this.hidePopup();
|
||||
if (this.removePostRenderListener) {
|
||||
this.removePostRenderListener();
|
||||
this.removePostRenderListener = null;
|
||||
}
|
||||
this.destroyAllPointLayers();
|
||||
this.pointLayerRegistry.clear();
|
||||
this.baseLayerRegistry.clear();
|
||||
@ -620,6 +1076,12 @@ export class MapCesium implements MapInterface {
|
||||
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void {
|
||||
this.popupElement = popupContainer;
|
||||
if (this.popupElement) {
|
||||
this.popupElement.style.display = 'none';
|
||||
this.popupElement.style.position = 'absolute';
|
||||
this.popupElement.style.transform = 'translate(-50%, calc(-100% - 10px))';
|
||||
this.popupElement.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Step 3: 锚点展示 ====================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user