531 lines
16 KiB
TypeScript
531 lines
16 KiB
TypeScript
|
|
import Feature from 'ol/Feature';
|
|||
|
|
import Point from 'ol/geom/Point';
|
|||
|
|
import OlMap from 'ol/Map';
|
|||
|
|
import Overlay from 'ol/Overlay';
|
|||
|
|
import VectorLayer from 'ol/layer/Vector';
|
|||
|
|
import VectorSource from 'ol/source/Vector';
|
|||
|
|
import {
|
|||
|
|
generatePopupHtml,
|
|||
|
|
shouldPreferEng2Popup
|
|||
|
|
} from '@/utils/popupHtmlGenerator';
|
|||
|
|
|
|||
|
|
type PopupManagerOptions = {
|
|||
|
|
map: OlMap | null;
|
|||
|
|
getPointLayers: () => VectorLayer<VectorSource>[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type PopupHitResult = {
|
|||
|
|
detectedFeature: Feature | undefined;
|
|||
|
|
isHitIcon: boolean;
|
|||
|
|
coordinate?: number[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type PointerMoveChangePayload = {
|
|||
|
|
hoveredId: string | number | null;
|
|||
|
|
detectedFeature: Feature | undefined;
|
|||
|
|
coordinate?: number[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export class PopupManager {
|
|||
|
|
private map: OlMap | null;
|
|||
|
|
private popupOverlay: Overlay | null = null;
|
|||
|
|
private popupElement: HTMLElement | null = null;
|
|||
|
|
private batchPopupContainer: HTMLDivElement | null = null;
|
|||
|
|
private batchPopupItems: Array<{
|
|||
|
|
feature: Feature;
|
|||
|
|
element: HTMLDivElement;
|
|||
|
|
}> = [];
|
|||
|
|
private lastHoveredId: string | number | null = null;
|
|||
|
|
private animationFrameId: number | null = null;
|
|||
|
|
private getPointLayers: () => VectorLayer<VectorSource>[];
|
|||
|
|
private popupMouseEnterHandler: (() => void) | null = null;
|
|||
|
|
private popupMouseMoveHandler: (() => void) | null = null;
|
|||
|
|
private hoverChangeHandler:
|
|||
|
|
| ((payload: PointerMoveChangePayload) => void)
|
|||
|
|
| undefined;
|
|||
|
|
|
|||
|
|
constructor(options: PopupManagerOptions) {
|
|||
|
|
this.map = options.map;
|
|||
|
|
this.getPointLayers = options.getPointLayers;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:同步地图实例,供地图初始化和销毁后更新 Popup 管理器上下文。
|
|||
|
|
setMap(map: OlMap | null) {
|
|||
|
|
this.map = map;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:初始化 Popup Overlay 并挂载到地图实例。
|
|||
|
|
initPopupOverlay(container: HTMLElement) {
|
|||
|
|
this.popupElement = container;
|
|||
|
|
this.popupElement.style.display = 'none';
|
|||
|
|
this.popupElement.style.removeProperty('position');
|
|||
|
|
this.popupElement.style.removeProperty('transform');
|
|||
|
|
this.popupElement.style.removeProperty('left');
|
|||
|
|
this.popupElement.style.removeProperty('top');
|
|||
|
|
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({
|
|||
|
|
element: this.popupElement,
|
|||
|
|
positioning: 'bottom-center',
|
|||
|
|
offset: [0, -10],
|
|||
|
|
stopEvent: false,
|
|||
|
|
autoPan: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (this.map) {
|
|||
|
|
this.map.addOverlay(this.popupOverlay);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:统一处理地图点击命中检测,命中图标时通过回调把要素抛给外层。
|
|||
|
|
handleMapClick(
|
|||
|
|
pixel: number[],
|
|||
|
|
onHit?: (feature: Feature, coordinate?: number[]) => void
|
|||
|
|
) {
|
|||
|
|
const { detectedFeature, isHitIcon, coordinate } =
|
|||
|
|
this.detectFeatureAtPixel(pixel);
|
|||
|
|
if (detectedFeature && isHitIcon) {
|
|||
|
|
onHit?.(detectedFeature, coordinate);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:统一处理鼠标悬停命中检测与节流,只有 hover 目标变化时才通知外层刷新。
|
|||
|
|
handlePointerMove(
|
|||
|
|
pixel: number[],
|
|||
|
|
onHoverChange?: (payload: PointerMoveChangePayload) => void
|
|||
|
|
) {
|
|||
|
|
if (!this.map) return;
|
|||
|
|
this.hoverChangeHandler = onHoverChange;
|
|||
|
|
|
|||
|
|
const zoom = this.map.getView().getZoom();
|
|||
|
|
if (zoom !== undefined && zoom < 4.7) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.animationFrameId) {
|
|||
|
|
cancelAnimationFrame(this.animationFrameId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.animationFrameId = requestAnimationFrame(() => {
|
|||
|
|
const { detectedFeature, isHitIcon, coordinate } =
|
|||
|
|
this.detectFeatureAtPixel(pixel);
|
|||
|
|
const hoveredId =
|
|||
|
|
detectedFeature && isHitIcon ? (detectedFeature.getId() as any) : null;
|
|||
|
|
const nextFeature = hoveredId ? detectedFeature : undefined;
|
|||
|
|
const nextCoordinate = hoveredId ? coordinate : undefined;
|
|||
|
|
|
|||
|
|
if (hoveredId !== this.lastHoveredId) {
|
|||
|
|
this.emitHoverChange({
|
|||
|
|
hoveredId,
|
|||
|
|
detectedFeature: nextFeature,
|
|||
|
|
coordinate: nextCoordinate
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:在指定像素位置检测点图层要素,并判断是否命中图标区域。
|
|||
|
|
detectFeatureAtPixel(pixel: number[]): PopupHitResult {
|
|||
|
|
let detectedFeature: Feature | undefined = undefined;
|
|||
|
|
let isHitIcon = false;
|
|||
|
|
let coordinate: number[] | undefined;
|
|||
|
|
const pointLayers = this.getPointLayers();
|
|||
|
|
|
|||
|
|
if (this.isPixelInsidePopup(pixel)) {
|
|||
|
|
return {
|
|||
|
|
detectedFeature: undefined,
|
|||
|
|
isHitIcon: false,
|
|||
|
|
coordinate: undefined
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.map?.forEachFeatureAtPixel(
|
|||
|
|
pixel,
|
|||
|
|
(feature, layer) => {
|
|||
|
|
if (pointLayers.includes(layer as VectorLayer<VectorSource>)) {
|
|||
|
|
detectedFeature = feature as Feature;
|
|||
|
|
const geom = feature.getGeometry();
|
|||
|
|
if (geom && geom.getType() === 'Point') {
|
|||
|
|
coordinate = (geom as Point).getCoordinates();
|
|||
|
|
const iconPixel = this.map?.getPixelFromCoordinate(coordinate);
|
|||
|
|
if (iconPixel) {
|
|||
|
|
const dx = pixel[0] - iconPixel[0];
|
|||
|
|
const dy = pixel[1] - iconPixel[1];
|
|||
|
|
if (this.isPixelInsideIconArea(dx, dy)) {
|
|||
|
|
isHitIcon = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
},
|
|||
|
|
{ hitTolerance: 0 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return { detectedFeature, isHitIcon, coordinate };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:仅把图标本体区域作为 hover 命中范围,避免上方文字标签被误判为图标命中。
|
|||
|
|
private isPixelInsideIconArea(dx: number, dy: number): boolean {
|
|||
|
|
if (!this.map) return false;
|
|||
|
|
|
|||
|
|
const zoom = this.map.getView().getZoom() ?? 4.5;
|
|||
|
|
let dynamicScale = 0.7 + (zoom - 4.5) * 0.08;
|
|||
|
|
dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale));
|
|||
|
|
|
|||
|
|
const halfWidth = 12 * dynamicScale;
|
|||
|
|
const topReach = 8 * dynamicScale;
|
|||
|
|
const bottomReach = 12 * dynamicScale;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
dx >= -halfWidth &&
|
|||
|
|
dx <= halfWidth &&
|
|||
|
|
dy >= -topReach &&
|
|||
|
|
dy <= bottomReach
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:根据当前悬停要素统一显示或隐藏 Popup 内容。
|
|||
|
|
showPopup(feature: Feature | undefined, coordinate: number[] | undefined) {
|
|||
|
|
if (!this.popupOverlay || !this.popupElement) return;
|
|||
|
|
|
|||
|
|
if (feature && coordinate) {
|
|||
|
|
const props = feature.getProperties();
|
|||
|
|
const popupHtml = this.getPopupHtml(props);
|
|||
|
|
|
|||
|
|
if (popupHtml) {
|
|||
|
|
this.popupElement.innerHTML = popupHtml;
|
|||
|
|
this.popupOverlay.setPosition(coordinate);
|
|||
|
|
this.popupElement.style.display = 'block';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.popupOverlay.setPosition(undefined);
|
|||
|
|
this.popupElement.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private getPopupHtml(props: Record<string, any>) {
|
|||
|
|
return props.popupHtml || generatePopupHtml(props);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:批量显示多个要素的 Popup,使用绝对定位的 div 而非 Overlay。
|
|||
|
|
showPopupsForFeatures(
|
|||
|
|
features: Feature[],
|
|||
|
|
styleChecker?: (feature: Feature) => boolean,
|
|||
|
|
options?: {
|
|||
|
|
rebuild?: boolean;
|
|||
|
|
}
|
|||
|
|
) {
|
|||
|
|
if (!this.map || !this.popupElement) return;
|
|||
|
|
const shouldRebuild = options?.rebuild !== false;
|
|||
|
|
|
|||
|
|
// 过滤:只显示图例可见的要素
|
|||
|
|
const visibleFeatures = features.filter(
|
|||
|
|
f => f.get('_legendVisible') !== false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (visibleFeatures.length === 0) {
|
|||
|
|
this.clearBatchPopups();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!shouldRebuild && this.batchPopupItems.length > 0) {
|
|||
|
|
this.updateBatchPopupPositions(styleChecker);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.clearBatchPopups();
|
|||
|
|
|
|||
|
|
const mapElement = this.map.getTargetElement();
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
// 先添加容器到 DOM,以便后续元素能正确测量尺寸
|
|||
|
|
mapElement.appendChild(batchContainer);
|
|||
|
|
|
|||
|
|
// 用于碰撞检测的已放置 popup 列表
|
|||
|
|
const placedPopups: Array<{
|
|||
|
|
left: number;
|
|||
|
|
right: number;
|
|||
|
|
top: number;
|
|||
|
|
bottom: number;
|
|||
|
|
}> = [];
|
|||
|
|
visibleFeatures.forEach(feature => {
|
|||
|
|
const geom = feature.getGeometry();
|
|||
|
|
if (!geom || geom.getType() !== 'Point') return;
|
|||
|
|
|
|||
|
|
const coords = (geom as Point).getCoordinates();
|
|||
|
|
const pixel = this.map?.getPixelFromCoordinate(coords);
|
|||
|
|
if (!pixel) return;
|
|||
|
|
|
|||
|
|
// 检查样式是否隐藏(declutter、距离过滤等)
|
|||
|
|
if (styleChecker && !styleChecker(feature)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = feature.getProperties();
|
|||
|
|
const popupHtml = shouldPreferEng2Popup(props)
|
|||
|
|
? generatePopupHtml(props, { forceEng2: true }) ||
|
|||
|
|
props.popupHtml ||
|
|||
|
|
generatePopupHtml(props)
|
|||
|
|
: this.getPopupHtml(props);
|
|||
|
|
if (!popupHtml) return;
|
|||
|
|
|
|||
|
|
// 创建与原始 popupElement 完全相同的样式
|
|||
|
|
const popupEl = document.createElement('div');
|
|||
|
|
popupEl.className = this.popupElement.className;
|
|||
|
|
popupEl.innerHTML = popupHtml;
|
|||
|
|
// 不设置自定义 cssText,只设置定位(对应原始 popup 的 positioning: 'bottom-center', offset: [0, -10])
|
|||
|
|
popupEl.style.position = 'absolute';
|
|||
|
|
popupEl.style.display = 'block';
|
|||
|
|
popupEl.style.transform = 'translate(-50%, -100%)'; // bottom-center 对齐
|
|||
|
|
popupEl.style.pointerEvents = 'none';
|
|||
|
|
|
|||
|
|
// 临时设置 visibility: hidden 来测量尺寸
|
|||
|
|
popupEl.style.visibility = 'hidden';
|
|||
|
|
popupEl.style.left = `${pixel[0]}px`;
|
|||
|
|
popupEl.style.top = `${pixel[1] - 10}px`;
|
|||
|
|
batchContainer.appendChild(popupEl);
|
|||
|
|
|
|||
|
|
// 获取 popup 尺寸
|
|||
|
|
const rect = popupEl.getBoundingClientRect();
|
|||
|
|
const popupWidth = rect.width;
|
|||
|
|
const popupHeight = rect.height;
|
|||
|
|
|
|||
|
|
// 计算 popup 的位置(对应原始 positioning: 'bottom-center', offset: [0, -10])
|
|||
|
|
const popupX = pixel[0];
|
|||
|
|
const popupY = pixel[1] - 10; // bottom-center 对齐 + 10px 偏移
|
|||
|
|
const popupRect = {
|
|||
|
|
left: popupX - popupWidth / 2,
|
|||
|
|
right: popupX + popupWidth / 2,
|
|||
|
|
top: popupY - popupHeight,
|
|||
|
|
bottom: popupY
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 碰撞检测
|
|||
|
|
let hasCollision = false;
|
|||
|
|
for (const placed of placedPopups) {
|
|||
|
|
if (this.checkCollision(popupRect, placed)) {
|
|||
|
|
hasCollision = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!hasCollision) {
|
|||
|
|
// 无碰撞,显示 popup
|
|||
|
|
popupEl.style.visibility = 'visible';
|
|||
|
|
popupEl.style.left = `${popupX}px`;
|
|||
|
|
popupEl.style.top = `${popupY}px`;
|
|||
|
|
this.batchPopupItems.push({
|
|||
|
|
feature,
|
|||
|
|
element: popupEl
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
placedPopups.push({
|
|||
|
|
left: popupRect.left,
|
|||
|
|
right: popupRect.right,
|
|||
|
|
top: popupRect.top,
|
|||
|
|
bottom: popupRect.bottom
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 有碰撞,移除该 popup
|
|||
|
|
batchContainer.removeChild(popupEl);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateBatchPopupPositions(styleChecker?: (feature: Feature) => boolean) {
|
|||
|
|
if (
|
|||
|
|
!this.map ||
|
|||
|
|
!this.batchPopupContainer ||
|
|||
|
|
this.batchPopupItems.length === 0
|
|||
|
|
) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mapSize = this.map.getSize();
|
|||
|
|
const viewportWidth = mapSize?.[0] ?? 0;
|
|||
|
|
const viewportHeight = mapSize?.[1] ?? 0;
|
|||
|
|
const viewportPadding = 48;
|
|||
|
|
|
|||
|
|
this.batchPopupItems.forEach(({ feature, element }) => {
|
|||
|
|
if (feature.get('_legendVisible') === false) {
|
|||
|
|
element.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (styleChecker && !styleChecker(feature)) {
|
|||
|
|
element.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const geom = feature.getGeometry();
|
|||
|
|
if (!geom || geom.getType() !== 'Point') {
|
|||
|
|
element.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const coords = (geom as Point).getCoordinates();
|
|||
|
|
const pixel = this.map?.getPixelFromCoordinate(coords);
|
|||
|
|
if (!pixel) {
|
|||
|
|
element.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const popupX = pixel[0];
|
|||
|
|
const popupY = pixel[1] - 10;
|
|||
|
|
if (
|
|||
|
|
popupX < -viewportPadding ||
|
|||
|
|
popupY < -viewportPadding ||
|
|||
|
|
popupX > viewportWidth + viewportPadding ||
|
|||
|
|
popupY > viewportHeight + viewportPadding
|
|||
|
|
) {
|
|||
|
|
element.style.display = 'none';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
element.style.left = `${popupX}px`;
|
|||
|
|
element.style.top = `${popupY}px`;
|
|||
|
|
element.style.display = 'block';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:清除批量显示的 popups。
|
|||
|
|
clearBatchPopups() {
|
|||
|
|
this.batchPopupItems = [];
|
|||
|
|
if (this.batchPopupContainer) {
|
|||
|
|
this.batchPopupContainer.remove();
|
|||
|
|
this.batchPopupContainer = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const existing = document.getElementById('batch-popup-container');
|
|||
|
|
if (existing) {
|
|||
|
|
existing.remove();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:检测两个 popup 矩形是否碰撞。
|
|||
|
|
private checkCollision(
|
|||
|
|
rect1: { left: number; right: number; top: number; bottom: number },
|
|||
|
|
rect2: { left: number; right: number; top: number; bottom: number }
|
|||
|
|
): boolean {
|
|||
|
|
const padding = 4;
|
|||
|
|
return !(
|
|||
|
|
rect1.right + padding < rect2.left ||
|
|||
|
|
rect1.left - padding > rect2.right ||
|
|||
|
|
rect1.bottom + padding < rect2.top ||
|
|||
|
|
rect1.top - padding > rect2.bottom
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:统一重置 hover 和 Popup 状态,供地图销毁和切换时复用。
|
|||
|
|
reset() {
|
|||
|
|
if (this.animationFrameId) {
|
|||
|
|
cancelAnimationFrame(this.animationFrameId);
|
|||
|
|
this.animationFrameId = null;
|
|||
|
|
}
|
|||
|
|
this.emitHoverChange({
|
|||
|
|
hoveredId: null,
|
|||
|
|
detectedFeature: undefined,
|
|||
|
|
coordinate: undefined
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:在弹窗层意外接管鼠标时,通过统一 hover 回调链路立即清空当前悬停状态。
|
|||
|
|
private forceHidePopup() {
|
|||
|
|
this.emitHoverChange({
|
|||
|
|
hoveredId: null,
|
|||
|
|
detectedFeature: undefined,
|
|||
|
|
coordinate: undefined
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:统一派发 hover 变化,确保地图侧的 cursor、Popup 和图层刷新走同一条处理链路。
|
|||
|
|
private emitHoverChange(payload: PointerMoveChangePayload) {
|
|||
|
|
this.lastHoveredId = payload.hoveredId;
|
|||
|
|
this.hoverChangeHandler?.(payload);
|
|||
|
|
if (!this.hoverChangeHandler) {
|
|||
|
|
this.showPopup(payload.detectedFeature, payload.coordinate);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:当鼠标进入当前 Popup 可见区域时,禁止再把该区域重新识别为图标 hover。
|
|||
|
|
private isPixelInsidePopup(pixel: number[]): boolean {
|
|||
|
|
if (!this.map || !this.popupElement) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.popupElement.style.display === 'none') {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const popupRect = this.popupElement.getBoundingClientRect();
|
|||
|
|
if (popupRect.width <= 0 || popupRect.height <= 0) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mapRect = this.map.getTargetElement().getBoundingClientRect();
|
|||
|
|
const clientX = mapRect.left + pixel[0];
|
|||
|
|
const clientY = mapRect.top + pixel[1];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
clientX >= popupRect.left &&
|
|||
|
|
clientX <= popupRect.right &&
|
|||
|
|
clientY >= popupRect.top &&
|
|||
|
|
clientY <= popupRect.bottom
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备注:销毁 Popup 管理器内部状态和 Overlay 引用。
|
|||
|
|
destroy() {
|
|||
|
|
this.clearBatchPopups();
|
|||
|
|
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) {
|
|||
|
|
this.map.removeOverlay(this.popupOverlay);
|
|||
|
|
}
|
|||
|
|
this.popupMouseEnterHandler = null;
|
|||
|
|
this.popupMouseMoveHandler = null;
|
|||
|
|
this.hoverChangeHandler = undefined;
|
|||
|
|
this.popupOverlay = null;
|
|||
|
|
this.popupElement = null;
|
|||
|
|
this.map = null;
|
|||
|
|
}
|
|||
|
|
}
|