163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
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<VectorSource> | 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<Point>[] {
|
||
return data.map((item) => {
|
||
const mercatorCoord = fromLonLat([item.lon, item.lat]);
|
||
const point = new Point(mercatorCoord);
|
||
const feature = new Feature<Point>({
|
||
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<Feature<Point>>({ 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<Feature<Point>>;
|
||
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));
|
||
}
|