import type { Extent } from 'ol/extent'; import Feature from 'ol/Feature'; import Point from 'ol/geom/Point'; import { Heatmap as HeatmapLayer } from 'ol/layer'; import { fromLonLat } from 'ol/proj'; import { Vector as VectorSource } from 'ol/source'; import { obtainViewer2D } from '@/gis/ol/map'; // 全局变量(仅用于存储当前图层和监听,方便清理) let heatmapLayer: HeatmapLayer | null = null; const targetRadiusKm = 3; // 固定3公里地理半径 const targetBlurKm = 2; // 固定模糊度 let resolutionListener: import('ol/events').EventsKey | null = null; export interface HeatmapItem { id: string | number; lon: number; lat: number; value: number; } // 创建热力图要素(经纬度转墨卡托) function createHeatmapFeatures(data: HeatmapItem[]): Feature[] { return data.map((item) => { const mercatorCoord = fromLonLat([item.lon, item.lat]); const point = new Point(mercatorCoord); const feature = new Feature({ geometry: point, value: item.value, id: item.id, }); return feature; }); } // 计算地理距离对应的像素值 function calculatePixelValue(map: import('ol/Map').default, km: number): number { const resolution = map.getView().getResolution(); const metersPerUnit = map.getView().getProjection().getMetersPerUnit(); const factor = resolution * metersPerUnit; return (km * 1000) / factor; } // 注册分辨率监听(每次创建图层时重新注册) function registerResolutionListener(map: import('ol/Map').default): void { // 先解绑旧监听,避免重复 if (resolutionListener) { map.getView().un('change:resolution', resolutionListener); resolutionListener = null; } // 新监听:窗口缩放时更新半径和模糊度 resolutionListener = map.getView().on('change:resolution', () => { if (heatmapLayer) { const newRadius = calculatePixelValue(map, targetRadiusKm); const newBlur = calculatePixelValue(map, targetBlurKm); heatmapLayer.setRadius(newRadius); heatmapLayer.setBlur(newBlur); } }); } // 新增:完整清理函数(清除图层+解绑监听,复用逻辑) function fullClearHeatmap(): void { const map = obtainViewer2D('olContainer'); if (map && heatmapLayer) { map.removeLayer(heatmapLayer); // 移除旧图层 heatmapLayer = null; } // 解绑分辨率监听,避免内存泄漏 if (resolutionListener) { const map = obtainViewer2D('olContainer'); map?.getView().un('change:resolution', resolutionListener); resolutionListener = null; } } export function addHeatmapLayer( heatmapData: HeatmapItem[], maxValue?: number, ): void { const map = obtainViewer2D('olContainer'); if (!map) { console.error('获取地图实例失败'); return; } // 第一步:先彻底清理旧图层和监听(关键!避免任何残留) fullClearHeatmap(); // 第二步:处理空数据/无效数据(清理后直接返回,不创建新图层) if (!Array.isArray(heatmapData) || heatmapData.length === 0) { console.warn('热力图数据为空,未创建新图层'); return; } // 第三步:计算当前数据的实际最大值(处理极小值,避免除以0) const dataValues = heatmapData.map(item => item.value).filter(v => v > 0); const actualMaxValue = maxValue ?? (dataValues.length ? Math.max(...dataValues) : 0.0001); if (actualMaxValue <= 0) { console.warn('maxValue必须大于0,未创建新图层'); return; } // 第四步:计算初始参数和新要素 const initialRadius = calculatePixelValue(map, targetRadiusKm); const initialBlur = calculatePixelValue(map, targetBlurKm); const features = createHeatmapFeatures(heatmapData); // 第五步:创建全新的数据源和图层(无任何旧配置残留) const vectorSource = new VectorSource>({ features }); heatmapLayer = new HeatmapLayer({ source: vectorSource, blur: initialBlur, radius: initialRadius, gradient: ['#0000CC', '#5EB3FF', '#97EAFD', '#C6FCFF', '#FFEA00', '#FFA200', '#FF3700', '#DC0000', '#850000'], zIndex: 1001, // 权重函数基于当前数据的最大值,纯净无残留 weight: (feature: Feature) => { const value = feature.get('value') || 0; return Math.min(value / actualMaxValue, 1) || 0.0001; // 极小值兜底 }, }); // 第六步:添加新图层并注册监听 map.addLayer(heatmapLayer); registerResolutionListener(map); } // 原有清理函数保持兼容(调用完整清理逻辑) export function clearHeatmapLayer(): void { fullClearHeatmap(); } export function clearHeatmapFeatures(): void { // 清空要素时也彻底清理图层(避免空要素图层残留) fullClearHeatmap(); } export function updateHeatmapPointValue(id: string | number, newValue: number): void { // 单个点更新:由于图层是全新创建的,直接清理旧图层+重新加载全量数据(简单可靠) // 若需要优化性能,可保留图层但重新计算最大值,这里优先保证正确性 if (heatmapLayer) { const source = heatmapLayer.getSource() as VectorSource>; const feature = source.getFeatures().find(f => f.get('id') === id); if (feature) { feature.set('value', newValue); // 重新计算最大值并更新权重 const allFeatures = source.getFeatures(); const currentMax = Math.max(...allFeatures.map(f => f.get('value') || 0), 0.0001); heatmapLayer.set('weight', (f: Feature) => { const val = f.get('value') || 0; return Math.min(val / currentMax, 1) || 0.0001; }); source.changed(); heatmapLayer.changed(); } } } export function batchUpdateHeatmapValues(updates: { id: string | number; newValue: number }[]): void { updates.forEach(update => updateHeatmapPointValue(update.id, update.newValue)); }