SourceTermAnalysisSystem_vue/src/gis/ol/olHeatmap.ts
2026-05-15 10:13:06 +08:00

163 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));
}