文章目录
cesium 热力图 核心封装,开封即用
typescript
/**
* 多尺度热力图核心模块
* 整合空间索引、KDE处理器、GPU渲染器及主管理器
* @module MultiScaleHeatmap
*/
import * as Cesium from "cesium";
// ==================== 类型定义 (types.ts) ====================
/** 原始数据点 */
export interface HeatPoint {
/** 经度 */
longitude: number;
/** 纬度 */
latitude: number;
/** 权重值 (可选,默认为1) */
weight?: number;
}
/** 屏幕空间点 */
export interface ScreenPoint {
x: number;
y: number;
weight: number;
}
/** 聚合后的数据点 */
export interface ClusteredPoint {
/** 聚合中心经度 */
longitude: number;
/** 聚合中心纬度 */
latitude: number;
/** 聚合点数量 */
count: number;
/** 累积权重 */
totalWeight: number;
/** LOD 级别 */
lodLevel: number;
/** 所属网格键 */
gridKey: string;
}
/** LOD 级别配置 */
export interface LODConfig {
/** 级别编号 (0-n, 0为最粗) */
level: number;
/** 相机高度阈值 (米) */
heightThreshold: number;
/** 网格大小 (度) */
gridSize: number;
/** 核半径 (像素) */
kernelRadius: number;
/** 最大显示点数 */
maxPoints: number;
}
/** 热力图配置 */
export interface HeatmapConfig {
/** 画布宽度 */
canvasWidth: number;
/** 画布高度 */
canvasHeight: number;
/** 最大不透明度 */
maxOpacity: number;
/** 最小不透明度 */
minOpacity: number;
/** 模糊系数 */
blur: number;
/** 颜色梯度 */
gradient: GradientConfig;
/** LOD 配置数组 */
lodConfigs: LODConfig[];
/** 是否使用 GPU 加速 */
useGPU: boolean;
/** 强度缩放因子 */
intensityScale: number;
/** 动画过渡时间 (ms) */
transitionDuration: number;
}
/** 颜色梯度配置 */
export interface GradientConfig {
/** 位置到颜色的映射 (位置为 0-1) */
[position: string]: string;
}
/** 渲染状态 */
export interface RenderState {
/** 当前 LOD 级别 */
currentLOD: number;
/** 当前显示的聚合点 */
visibleClusters: ClusteredPoint[];
/** 屏幕空间点 */
screenPoints: ScreenPoint[];
/** 是否正在渲染 */
isRendering: boolean;
/** 上次渲染时间 */
lastRenderTime: number;
/** 帧率 */
fps: number;
}
/** 空间索引网格单元 */
export interface GridCell {
/** 网格键 (格式: "level_x_y") */
key: string;
/** 网格内点数 */
count: number;
/** 累积权重 */
totalWeight: number;
/** 边界框 */
bounds: {
west: number;
east: number;
south: number;
north: number;
};
/** 质心经度 */
centroidLon: number;
/** 质心纬度 */
centroidLat: number;
/** 包含的原始点索引 */
pointIndices: number[];
}
/** 视口信息 */
export interface ViewportInfo {
/** 西边界 (经度) */
west: number;
/** 东边界 (经度) */
east: number;
/** 南边界 (纬度) */
south: number;
/** 北边界 (纬度) */
north: number;
/** 相机高度 (米) */
cameraHeight: number;
/** 画布宽度 */
canvasWidth: number;
/** 画布高度 */
canvasHeight: number;
}
/** 性能统计 */
export interface PerformanceStats {
/** 数据处理时间 (ms) */
dataProcessTime: number;
/** 空间索引查询时间 (ms) */
spatialQueryTime: number;
/** 聚合计算时间 (ms) */
clusteringTime: number;
/** KDE 计算时间 (ms) */
kdeTime: number;
/** 渲染时间 (ms) */
renderTime: number;
/** 总帧时间 (ms) */
totalFrameTime: number;
/** 当前可见点数 */
visiblePointCount: number;
/** 当前聚合后点数 */
clusteredPointCount: number;
}
/** WebGL Uniform 参数 */
export interface HeatmapUniforms {
u_resolution: [number, number];
u_kernelRadius: number;
u_intensity: number;
u_pointCount: number;
}
/** 颜色 RGBA */
export interface RGBA {
r: number;
g: number;
b: number;
a: number;
}
// ==================== KDE处理器 (KDEProcessor.ts) ====================
/**
* 核函数类型
*/
export type KernelType = "gaussian" | "epanechnikov" | "quartic" | "triangular";
/**
* KDE 处理器类
* 实现 CPU 端的核密度估计计算
*/
export class KDEProcessor {
/** 核半径 */
private radius: number = 50;
/** 核函数类型 */
private kernelType: KernelType = "gaussian";
/** 预计算的核函数查找表 */
private kernelLUT: Float32Array;
/** 查找表分辨率 */
private lutResolution: number = 256;
/** 颜色梯度查找表 */
private gradientLUT: Uint8ClampedArray;
constructor(radius: number = 50, kernelType: KernelType = "gaussian") {
this.radius = radius;
this.kernelType = kernelType;
this.kernelLUT = new Float32Array(this.lutResolution);
this.gradientLUT = new Uint8ClampedArray(256 * 4);
this.buildKernelLUT();
}
/**
* 构建核函数查找表
* 预计算加速 KDE 评估
*/
private buildKernelLUT(): void {
for (let i = 0; i < this.lutResolution; i++) {
const u = i / (this.lutResolution - 1); // 归一化距离 [0, 1]
this.kernelLUT[i] = this.evaluateKernel(u);
}
}
/**
* 评估核函数值
*/
private evaluateKernel(u: number): number {
if (u > 1) return 0;
switch (this.kernelType) {
case "gaussian":
// 高斯核: K(u) = exp(-3u²) (截断高斯)
return Math.exp(-3 * u * u);
case "epanechnikov":
// Epanechnikov 核: K(u) = 0.75 * (1 - u²)
return 0.75 * (1 - u * u);
case "quartic":
// 四次核: K(u) = (15/16) * (1 - u²)²
const t = 1 - u * u;
return (15 / 16) * t * t;
case "triangular":
// 三角核: K(u) = 1 - u
return 1 - u;
default:
return Math.exp(-3 * u * u);
}
}
/**
* 从查找表获取核函数值
*/
private getKernelValue(normalizedDistance: number): number {
if (normalizedDistance >= 1) return 0;
const index = Math.floor(normalizedDistance * (this.lutResolution - 1));
return this.kernelLUT[index];
}
/**
* 设置核半径
*/
setRadius(radius: number): void {
this.radius = radius;
}
/**
* 获取当前核半径
*/
getRadius(): number {
return this.radius;
}
/**
* 构建颜色梯度查找表
*/
buildGradientLUT(gradient: GradientConfig): void {
// 解析梯度配置
const stops: Array<{ pos: number; color: RGBA }> = [];
for (const [posStr, colorStr] of Object.entries(gradient)) {
const pos = parseFloat(posStr);
const color = this.parseColor(colorStr);
stops.push({ pos, color });
}
stops.sort((a, b) => a.pos - b.pos);
// 插值生成完整梯度
for (let i = 0; i < 256; i++) {
const t = i / 255;
const color = this.interpolateGradient(stops, t);
this.gradientLUT[i * 4] = color.r;
this.gradientLUT[i * 4 + 1] = color.g;
this.gradientLUT[i * 4 + 2] = color.b;
this.gradientLUT[i * 4 + 3] = color.a;
}
}
/**
* 解析 CSS 颜色字符串
*/
private parseColor(colorStr: string): RGBA {
// 处理常见颜色名称
const colorNames: Record<string, string> = {
blue: "#0000FF",
green: "#00FF00",
yellow: "#FFFF00",
orange: "#FFA500",
red: "#FF0000",
white: "#FFFFFF",
black: "#000000",
purple: "#800080",
cyan: "#00FFFF",
};
const color = colorNames[colorStr.toLowerCase()] || colorStr;
// 处理 rgba 格式
if (color.startsWith("rgba")) {
const match = color.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
);
if (match) {
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: match[4] ? Math.round(parseFloat(match[4]) * 255) : 255,
};
}
}
// 处理 rgb 格式
if (color.startsWith("rgb")) {
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (match) {
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: 255,
};
}
}
// 处理十六进制格式
if (color.startsWith("#")) {
let hex = color.slice(1);
if (hex.length === 3) {
hex = hex
.split("")
.map((c) => c + c)
.join("");
}
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
a: 255,
};
}
return { r: 0, g: 0, b: 0, a: 255 };
}
/**
* 颜色梯度插值
*/
private interpolateGradient(
stops: Array<{ pos: number; color: RGBA }>,
t: number,
): RGBA {
if (stops.length === 0) return { r: 0, g: 0, b: 0, a: 0 };
if (t <= stops[0].pos) return stops[0].color;
if (t >= stops[stops.length - 1].pos) return stops[stops.length - 1].color;
// 找到插值区间
let low = 0;
let high = stops.length - 1;
while (high - low > 1) {
const mid = Math.floor((low + high) / 2);
if (stops[mid].pos <= t) {
low = mid;
} else {
high = mid;
}
}
const t0 = stops[low].pos;
const t1 = stops[high].pos;
const ratio = (t - t0) / (t1 - t0);
const c0 = stops[low].color;
const c1 = stops[high].color;
return {
r: Math.round(c0.r + (c1.r - c0.r) * ratio),
g: Math.round(c0.g + (c1.g - c0.g) * ratio),
b: Math.round(c0.b + (c1.b - c0.b) * ratio),
a: Math.round(c0.a + (c1.a - c0.a) * ratio),
};
}
/**
* CPU 端 KDE 计算
* 用于 GPU 不可用时的回退方案
*/
computeKDE(
points: ScreenPoint[],
width: number,
height: number,
intensityScale: number = 1.0,
): Float32Array {
const density = new Float32Array(width * height);
const radiusSq = this.radius * this.radius;
// 对每个点计算其对周围像素的贡献
for (const point of points) {
const px = Math.round(point.x);
const py = Math.round(point.y);
const weight = point.weight;
// 只计算核半径范围内的像素
const minX = Math.max(0, px - this.radius);
const maxX = Math.min(width - 1, px + this.radius);
const minY = Math.max(0, py - this.radius);
const maxY = Math.min(height - 1, py + this.radius);
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
const dx = x - px;
const dy = y - py;
const distSq = dx * dx + dy * dy;
if (distSq <= radiusSq) {
const normalizedDist = Math.sqrt(distSq) / this.radius;
const kernelValue = this.getKernelValue(normalizedDist);
density[y * width + x] += kernelValue * weight;
}
}
}
}
// 应用强度缩放
if (intensityScale !== 1.0) {
for (let i = 0; i < density.length; i++) {
density[i] *= intensityScale;
}
}
return density;
}
/**
* 将密度数据转换为 RGBA 图像数据
*/
densityToImageData(
density: Float32Array,
width: number,
height: number,
minOpacity: number,
maxOpacity: number,
): ImageData {
const imageData = new ImageData(width, height);
const data = imageData.data;
// 找到最大密度用于归一化
let maxDensity = 0;
for (let i = 0; i < density.length; i++) {
if (density[i] > maxDensity) maxDensity = density[i];
}
if (maxDensity === 0) return imageData;
// 将密度映射到颜色
for (let i = 0; i < density.length; i++) {
const normalizedDensity = density[i] / maxDensity;
const colorIndex = Math.min(255, Math.floor(normalizedDensity * 255));
const alpha = minOpacity + normalizedDensity * (maxOpacity - minOpacity);
data[i * 4] = this.gradientLUT[colorIndex * 4];
data[i * 4 + 1] = this.gradientLUT[colorIndex * 4 + 1];
data[i * 4 + 2] = this.gradientLUT[colorIndex * 4 + 2];
data[i * 4 + 3] = Math.round(alpha * 255);
}
return imageData;
}
/**
* 优化的 KDE 计算 - 使用格子加速
* 将点按照格子组织,只计算相邻格子的贡献
*/
computeKDEOptimized(
points: ScreenPoint[],
width: number,
height: number,
intensityScale: number = 1.0,
): Float32Array {
const density = new Float32Array(width * height);
const cellSize = this.radius;
const gridWidth = Math.ceil(width / cellSize);
const gridHeight = Math.ceil(height / cellSize);
// 将点分配到网格
const grid: ScreenPoint[][] = new Array(gridWidth * gridHeight);
for (let i = 0; i < grid.length; i++) {
grid[i] = [];
}
for (const point of points) {
const gx = Math.floor(point.x / cellSize);
const gy = Math.floor(point.y / cellSize);
if (gx >= 0 && gx < gridWidth && gy >= 0 && gy < gridHeight) {
grid[gy * gridWidth + gx].push(point);
}
}
// 对每个像素,只检查相邻网格中的点
for (let y = 0; y < height; y++) {
const gyCenter = Math.floor(y / cellSize);
for (let x = 0; x < width; x++) {
const gxCenter = Math.floor(x / cellSize);
let sum = 0;
// 检查 3x3 邻域网格
for (let dgy = -1; dgy <= 1; dgy++) {
const gy = gyCenter + dgy;
if (gy < 0 || gy >= gridHeight) continue;
for (let dgx = -1; dgx <= 1; dgx++) {
const gx = gxCenter + dgx;
if (gx < 0 || gx >= gridWidth) continue;
const cell = grid[gy * gridWidth + gx];
for (const point of cell) {
const dx = x - point.x;
const dy = y - point.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < this.radius) {
sum += this.getKernelValue(dist / this.radius) * point.weight;
}
}
}
}
density[y * width + x] = sum * intensityScale;
}
}
return density;
}
/**
* 获取颜色梯度查找表
*/
getGradientLUT(): Uint8ClampedArray {
return this.gradientLUT;
}
}
/**
* 默认高德风格颜色梯度
*/
export const AMAP_GRADIENT: GradientConfig = {
"0.0": "rgba(0, 0, 255, 0)",
"0.1": "rgba(0, 0, 255, 0.2)",
"0.25": "rgba(0, 255, 255, 0.5)",
"0.5": "rgba(0, 255, 0, 0.7)",
"0.75": "rgba(255, 255, 0, 0.9)",
"0.9": "rgba(255, 128, 0, 1.0)",
"1.0": "rgba(255, 0, 0, 1.0)",
};
/**
* 经典热力图颜色梯度
*/
export const CLASSIC_GRADIENT: GradientConfig = {
"0.0": "rgba(0, 0, 0, 0)",
"0.25": "blue",
"0.55": "green",
"0.85": "yellow",
"1.0": "red",
};
// ==================== GPU渲染器 (GPURenderer.ts) ====================
/** 顶点着色器 - 点精灵绘制 */
const VERTEX_SHADER_SOURCE = `
attribute vec2 a_position;
attribute float a_weight;
uniform vec2 u_resolution;
uniform float u_pointSize;
varying float v_weight;
void main() {
// 将像素坐标转换为裁剪空间坐标 [-1, 1]
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
clipSpace.y = -clipSpace.y; // Y 轴翻转
gl_Position = vec4(clipSpace, 0.0, 1.0);
gl_PointSize = u_pointSize * 2.0;
v_weight = a_weight;
}
`;
/** 片段着色器 - 高斯核密度 */
const FRAGMENT_SHADER_DENSITY = `
precision highp float;
varying float v_weight;
uniform float u_intensity;
void main() {
// 计算点精灵中心到片段的距离
vec2 cxy = 2.0 * gl_PointCoord - 1.0;
float r = dot(cxy, cxy);
// 高斯核函数: exp(-3r²)
// 当 r > 1 时丢弃
if (r > 1.0) {
discard;
}
float kernel = exp(-3.0 * r);
float value = kernel * v_weight * u_intensity;
// 使用加法混合累积密度
gl_FragColor = vec4(value, value, value, 1.0);
}
`;
/** 颜色映射着色器 - 将密度转换为热力图颜色 */
const VERTEX_SHADER_QUAD = `
attribute vec2 a_position;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = (a_position + 1.0) / 2.0;
}
`;
const FRAGMENT_SHADER_COLORMAP = `
precision highp float;
varying vec2 v_texCoord;
uniform sampler2D u_densityTexture;
uniform sampler2D u_gradientTexture;
uniform float u_maxDensity;
uniform float u_minOpacity;
uniform float u_maxOpacity;
void main() {
float density = texture2D(u_densityTexture, v_texCoord).r;
float normalizedDensity = clamp(density / u_maxDensity, 0.0, 1.0);
// 从梯度纹理采样颜色
vec4 color = texture2D(u_gradientTexture, vec2(normalizedDensity, 0.5));
// 应用透明度
float alpha = mix(u_minOpacity, u_maxOpacity, normalizedDensity);
color.a *= alpha;
// 预乘 alpha
color.rgb *= color.a;
gl_FragColor = color;
}
`;
/**
* WebGL 着色器程序封装
*/
interface ShaderProgram {
program: WebGLProgram;
attributes: Record<string, number>;
uniforms: Record<string, WebGLUniformLocation | null>;
}
/**
* GPU 热力图渲染器
*/
export class GPUHeatmapRenderer {
/** WebGL 上下文 */
private gl: WebGLRenderingContext | null = null;
/** 离屏 Canvas */
private canvas: HTMLCanvasElement;
/** 密度计算着色器 */
private densityProgram: ShaderProgram | null = null;
/** 颜色映射着色器 */
private colormapProgram: ShaderProgram | null = null;
/** 点数据缓冲区 */
private pointBuffer: WebGLBuffer | null = null;
/** 权重数据缓冲区 */
private weightBuffer: WebGLBuffer | null = null;
/** 全屏四边形顶点缓冲 */
private quadBuffer: WebGLBuffer | null = null;
/** 密度帧缓冲 */
private densityFramebuffer: WebGLFramebuffer | null = null;
/** 密度纹理 */
private densityTexture: WebGLTexture | null = null;
/** 颜色梯度纹理 */
private gradientTexture: WebGLTexture | null = null;
/** 当前画布尺寸 */
private width: number = 512;
private height: number = 512;
/** 当前点数量 */
private pointCount: number = 0;
/** GPU 是否可用 */
private gpuAvailable: boolean = false;
/** 最大密度值 (用于归一化) */
private maxDensity: number = 1.0;
constructor(width: number = 512, height: number = 512) {
this.width = width;
this.height = height;
this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;
this.initWebGL();
}
/**
* 初始化 WebGL 上下文
*/
private initWebGL(): void {
try {
const contextOptions: WebGLContextAttributes = {
alpha: true,
premultipliedAlpha: true,
antialias: false,
preserveDrawingBuffer: true,
};
this.gl =
this.canvas.getContext("webgl", contextOptions) ||
(this.canvas.getContext(
"experimental-webgl",
contextOptions,
) as WebGLRenderingContext);
if (!this.gl) {
console.warn("WebGL not available, falling back to CPU rendering");
this.gpuAvailable = false;
return;
}
// 检查必要扩展
const floatTexExt = this.gl.getExtension("OES_texture_float");
if (!floatTexExt) {
console.warn("OES_texture_float not available");
}
this.gpuAvailable = true;
this.setupShaders();
this.setupBuffers();
this.setupFramebuffer();
} catch (e) {
console.error("WebGL initialization failed:", e);
this.gpuAvailable = false;
}
}
/**
* 编译着色器
*/
private compileShader(source: string, type: number): WebGLShader | null {
if (!this.gl) return null;
const shader = this.gl.createShader(type);
if (!shader) return null;
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error("Shader compile error:", this.gl.getShaderInfoLog(shader));
this.gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* 创建着色器程序
*/
private createProgram(
vertexSource: string,
fragmentSource: string,
attributes: string[],
uniforms: string[],
): ShaderProgram | null {
if (!this.gl) return null;
const vertexShader = this.compileShader(
vertexSource,
this.gl.VERTEX_SHADER,
);
const fragmentShader = this.compileShader(
fragmentSource,
this.gl.FRAGMENT_SHADER,
);
if (!vertexShader || !fragmentShader) return null;
const program = this.gl.createProgram();
if (!program) return null;
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.error("Program link error:", this.gl.getProgramInfoLog(program));
return null;
}
// 获取属性和 uniform 位置
const result: ShaderProgram = {
program,
attributes: {},
uniforms: {},
};
for (const attr of attributes) {
result.attributes[attr] = this.gl.getAttribLocation(program, attr);
}
for (const uniform of uniforms) {
result.uniforms[uniform] = this.gl.getUniformLocation(program, uniform);
}
return result;
}
/**
* 设置着色器
*/
private setupShaders(): void {
// 密度计算着色器
this.densityProgram = this.createProgram(
VERTEX_SHADER_SOURCE,
FRAGMENT_SHADER_DENSITY,
["a_position", "a_weight"],
["u_resolution", "u_pointSize", "u_intensity"],
);
// 颜色映射着色器
this.colormapProgram = this.createProgram(
VERTEX_SHADER_QUAD,
FRAGMENT_SHADER_COLORMAP,
["a_position"],
[
"u_densityTexture",
"u_gradientTexture",
"u_maxDensity",
"u_minOpacity",
"u_maxOpacity",
],
);
}
/**
* 设置缓冲区
*/
private setupBuffers(): void {
if (!this.gl) return;
// 点位置缓冲
this.pointBuffer = this.gl.createBuffer();
// 权重缓冲
this.weightBuffer = this.gl.createBuffer();
// 全屏四边形
this.quadBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
this.gl.STATIC_DRAW,
);
}
/**
* 设置帧缓冲和纹理
*/
private setupFramebuffer(): void {
if (!this.gl) return;
// 创建密度纹理
this.densityTexture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, this.densityTexture);
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RGBA,
this.width,
this.height,
0,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
null,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MIN_FILTER,
this.gl.LINEAR,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MAG_FILTER,
this.gl.LINEAR,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_S,
this.gl.CLAMP_TO_EDGE,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_T,
this.gl.CLAMP_TO_EDGE,
);
// 创建帧缓冲
this.densityFramebuffer = this.gl.createFramebuffer();
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.densityFramebuffer);
this.gl.framebufferTexture2D(
this.gl.FRAMEBUFFER,
this.gl.COLOR_ATTACHMENT0,
this.gl.TEXTURE_2D,
this.densityTexture,
0,
);
// 检查完整性
if (
this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER) !==
this.gl.FRAMEBUFFER_COMPLETE
) {
console.error("Framebuffer not complete");
}
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
}
/**
* 设置颜色梯度
*/
setGradient(gradient: GradientConfig): void {
if (!this.gl) return;
// 生成梯度纹理数据
const gradientData = new Uint8Array(256 * 4);
const stops: Array<{ pos: number; color: RGBA }> = [];
for (const [posStr, colorStr] of Object.entries(gradient)) {
const pos = parseFloat(posStr);
const color = this.parseColor(colorStr);
stops.push({ pos, color });
}
stops.sort((a, b) => a.pos - b.pos);
for (let i = 0; i < 256; i++) {
const t = i / 255;
const color = this.interpolateGradient(stops, t);
gradientData[i * 4] = color.r;
gradientData[i * 4 + 1] = color.g;
gradientData[i * 4 + 2] = color.b;
gradientData[i * 4 + 3] = color.a;
}
// 创建/更新梯度纹理
if (!this.gradientTexture) {
this.gradientTexture = this.gl.createTexture();
}
this.gl.bindTexture(this.gl.TEXTURE_2D, this.gradientTexture);
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RGBA,
256,
1,
0,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
gradientData,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MIN_FILTER,
this.gl.LINEAR,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MAG_FILTER,
this.gl.LINEAR,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_S,
this.gl.CLAMP_TO_EDGE,
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_T,
this.gl.CLAMP_TO_EDGE,
);
}
private parseColor(colorStr: string): RGBA {
const colorNames: Record<string, string> = {
blue: "#0000FF",
green: "#00FF00",
yellow: "#FFFF00",
orange: "#FFA500",
red: "#FF0000",
white: "#FFFFFF",
black: "#000000",
cyan: "#00FFFF",
};
const color = colorNames[colorStr.toLowerCase()] || colorStr;
if (color.startsWith("rgba")) {
const match = color.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
);
if (match) {
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: match[4] ? Math.round(parseFloat(match[4]) * 255) : 255,
};
}
}
if (color.startsWith("#")) {
let hex = color.slice(1);
if (hex.length === 3)
hex = hex
.split("")
.map((c) => c + c)
.join("");
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
a: 255,
};
}
return { r: 0, g: 0, b: 0, a: 255 };
}
private interpolateGradient(
stops: Array<{ pos: number; color: RGBA }>,
t: number,
): RGBA {
if (stops.length === 0) return { r: 0, g: 0, b: 0, a: 0 };
if (t <= stops[0].pos) return stops[0].color;
if (t >= stops[stops.length - 1].pos) return stops[stops.length - 1].color;
let low = 0,
high = stops.length - 1;
while (high - low > 1) {
const mid = Math.floor((low + high) / 2);
if (stops[mid].pos <= t) low = mid;
else high = mid;
}
const ratio = (t - stops[low].pos) / (stops[high].pos - stops[low].pos);
const c0 = stops[low].color,
c1 = stops[high].color;
return {
r: Math.round(c0.r + (c1.r - c0.r) * ratio),
g: Math.round(c0.g + (c1.g - c0.g) * ratio),
b: Math.round(c0.b + (c1.b - c0.b) * ratio),
a: Math.round(c0.a + (c1.a - c0.a) * ratio),
};
}
/**
* 更新点数据
*/
updatePoints(points: ScreenPoint[]): void {
if (!this.gl || !this.pointBuffer || !this.weightBuffer) return;
this.pointCount = points.length;
if (this.pointCount === 0) return;
// 提取位置和权重数据
const positions = new Float32Array(this.pointCount * 2);
const weights = new Float32Array(this.pointCount);
for (let i = 0; i < this.pointCount; i++) {
positions[i * 2] = points[i].x;
positions[i * 2 + 1] = points[i].y;
weights[i] = points[i].weight;
}
// 上传到 GPU
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.pointBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.DYNAMIC_DRAW);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.weightBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, weights, this.gl.DYNAMIC_DRAW);
}
/**
* 调整画布大小
*/
resize(width: number, height: number): void {
if (width === this.width && height === this.height) return;
this.width = width;
this.height = height;
this.canvas.width = width;
this.canvas.height = height;
if (this.gl) {
// 重新创建帧缓冲纹理
this.gl.bindTexture(this.gl.TEXTURE_2D, this.densityTexture);
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RGBA,
width,
height,
0,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
null,
);
}
}
/**
* 渲染热力图
*/
render(
kernelRadius: number,
intensity: number,
minOpacity: number,
maxOpacity: number,
): HTMLCanvasElement {
if (!this.gl || !this.gpuAvailable || this.pointCount === 0) {
return this.canvas;
}
const gl = this.gl;
// === 第一遍: 渲染密度到帧缓冲 ===
gl.bindFramebuffer(gl.FRAMEBUFFER, this.densityFramebuffer);
gl.viewport(0, 0, this.width, this.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 启用加法混合
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
if (this.densityProgram) {
gl.useProgram(this.densityProgram.program);
// 设置 uniforms
gl.uniform2f(
this.densityProgram.uniforms["u_resolution"]!,
this.width,
this.height,
);
gl.uniform1f(this.densityProgram.uniforms["u_pointSize"]!, kernelRadius);
gl.uniform1f(this.densityProgram.uniforms["u_intensity"]!, intensity);
// 绑定位置属性
gl.bindBuffer(gl.ARRAY_BUFFER, this.pointBuffer);
gl.enableVertexAttribArray(this.densityProgram.attributes["a_position"]);
gl.vertexAttribPointer(
this.densityProgram.attributes["a_position"],
2,
gl.FLOAT,
false,
0,
0,
);
// 绑定权重属性
gl.bindBuffer(gl.ARRAY_BUFFER, this.weightBuffer);
gl.enableVertexAttribArray(this.densityProgram.attributes["a_weight"]);
gl.vertexAttribPointer(
this.densityProgram.attributes["a_weight"],
1,
gl.FLOAT,
false,
0,
0,
);
// 绘制点
gl.drawArrays(gl.POINTS, 0, this.pointCount);
}
// === 读取最大密度值 (用于归一化) ===
// 注: 这是一个简化实现,实际可通过 mipmap 或 reduce shader 优化
this.maxDensity = this.estimateMaxDensity();
// === 第二遍: 颜色映射到屏幕 ===
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, this.width, this.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 使用预乘 alpha 混合
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
if (this.colormapProgram) {
gl.useProgram(this.colormapProgram.program);
// 绑定密度纹理
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.densityTexture);
gl.uniform1i(this.colormapProgram.uniforms["u_densityTexture"]!, 0);
// 绑定梯度纹理
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.gradientTexture);
gl.uniform1i(this.colormapProgram.uniforms["u_gradientTexture"]!, 1);
// 设置 uniforms
gl.uniform1f(
this.colormapProgram.uniforms["u_maxDensity"]!,
this.maxDensity,
);
gl.uniform1f(this.colormapProgram.uniforms["u_minOpacity"]!, minOpacity);
gl.uniform1f(this.colormapProgram.uniforms["u_maxOpacity"]!, maxOpacity);
// 绘制全屏四边形
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
gl.enableVertexAttribArray(this.colormapProgram.attributes["a_position"]);
gl.vertexAttribPointer(
this.colormapProgram.attributes["a_position"],
2,
gl.FLOAT,
false,
0,
0,
);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
gl.disable(gl.BLEND);
return this.canvas;
}
/**
* 估计最大密度值
* 通过采样帧缓冲像素来估计
*/
private estimateMaxDensity(): number {
if (!this.gl) return 1.0;
// 采样一些点来估计最大值
const sampleSize = 64;
const pixels = new Uint8Array(sampleSize * sampleSize * 4);
const sampleX = Math.floor((this.width - sampleSize) / 2);
const sampleY = Math.floor((this.height - sampleSize) / 2);
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.densityFramebuffer);
this.gl.readPixels(
sampleX,
sampleY,
sampleSize,
sampleSize,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
pixels,
);
let max = 0;
for (let i = 0; i < pixels.length; i += 4) {
const value = pixels[i] / 255;
if (value > max) max = value;
}
// 添加一些容差
return Math.max(0.1, max * 1.2);
}
/**
* 获取渲染结果的 ImageData
*/
getImageData(): ImageData {
if (!this.gl) {
return new ImageData(this.width, this.height);
}
const pixels = new Uint8Array(this.width * this.height * 4);
this.gl.readPixels(
0,
0,
this.width,
this.height,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
pixels,
);
// WebGL 的 Y 轴是反的,需要翻转
const flipped = new Uint8ClampedArray(this.width * this.height * 4);
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const srcIdx = ((this.height - 1 - y) * this.width + x) * 4;
const dstIdx = (y * this.width + x) * 4;
flipped[dstIdx] = pixels[srcIdx];
flipped[dstIdx + 1] = pixels[srcIdx + 1];
flipped[dstIdx + 2] = pixels[srcIdx + 2];
flipped[dstIdx + 3] = pixels[srcIdx + 3];
}
}
return new ImageData(flipped, this.width, this.height);
}
/**
* GPU 是否可用
*/
isGPUAvailable(): boolean {
return this.gpuAvailable;
}
/**
* 获取画布
*/
getCanvas(): HTMLCanvasElement {
return this.canvas;
}
/**
* 销毁资源
*/
destroy(): void {
if (this.gl) {
if (this.pointBuffer) this.gl.deleteBuffer(this.pointBuffer);
if (this.weightBuffer) this.gl.deleteBuffer(this.weightBuffer);
if (this.quadBuffer) this.gl.deleteBuffer(this.quadBuffer);
if (this.densityTexture) this.gl.deleteTexture(this.densityTexture);
if (this.gradientTexture) this.gl.deleteTexture(this.gradientTexture);
if (this.densityFramebuffer)
this.gl.deleteFramebuffer(this.densityFramebuffer);
if (this.densityProgram)
this.gl.deleteProgram(this.densityProgram.program);
if (this.colormapProgram)
this.gl.deleteProgram(this.colormapProgram.program);
}
this.gl = null;
this.gpuAvailable = false;
}
}
// ==================== 空间索引 (SpatialIndex.ts) ====================
/**
* 多级空间索引类
* 支持高效的空间查询和动态 LOD 聚合
*/
export class MultiLevelSpatialIndex {
/** 原始数据点 */
private points: HeatPoint[] = [];
/** 各级别的网格索引 */
private gridLevels: Map<number, Map<string, GridCell>> = new Map();
/** LOD 配置 */
private lodConfigs: LODConfig[];
/** 数据脏标记 */
private isDirty = true;
/** 上次构建的数据哈希 */
private lastDataHash = "";
constructor(lodConfigs: LODConfig[]) {
this.lodConfigs = lodConfigs.sort(
(a, b) => b.heightThreshold - a.heightThreshold,
);
// 初始化各级网格
for (const config of this.lodConfigs) {
this.gridLevels.set(config.level, new Map());
}
}
/**
* 设置数据点
*/
setData(points: HeatPoint[]): void {
const dataHash = this.computeDataHash(points);
if (dataHash === this.lastDataHash) {
return; // 数据未变化
}
this.points = points;
this.lastDataHash = dataHash;
this.isDirty = true;
this.rebuildIndex();
}
/**
* 增量添加数据点
*/
addPoints(newPoints: HeatPoint[]): void {
const startIdx = this.points.length;
this.points.push(...newPoints);
// 增量更新索引
this.updateIndexIncremental(newPoints, startIdx);
}
/**
* 清空数据
*/
clear(): void {
this.points = [];
for (const grid of this.gridLevels.values()) {
grid.clear();
}
this.isDirty = true;
this.lastDataHash = "";
}
/**
* 计算数据哈希(用于检测数据变化)
*/
private computeDataHash(points: HeatPoint[]): string {
if (points.length === 0) return "";
// 采样计算哈希,避免大数据量时性能问题
const sampleSize = Math.min(100, points.length);
const step = Math.floor(points.length / sampleSize);
let hash = `${points.length}:`;
for (let i = 0; i < points.length; i += step) {
const p = points[i];
hash += `${p.longitude.toFixed(4)},${p.latitude.toFixed(4)};`;
}
return hash;
}
/**
* 重建完整索引
*/
private rebuildIndex(): void {
// 清空现有网格
for (const grid of this.gridLevels.values()) {
grid.clear();
}
// 为每个点计算所属网格
for (let i = 0; i < this.points.length; i++) {
const point = this.points[i];
this.indexPoint(point, i);
}
this.isDirty = false;
}
/**
* 增量更新索引
*/
private updateIndexIncremental(
newPoints: HeatPoint[],
startIdx: number,
): void {
for (let i = 0; i < newPoints.length; i++) {
this.indexPoint(newPoints[i], startIdx + i);
}
}
/**
* 将单个点加入索引
*/
private indexPoint(point: HeatPoint, index: number): void {
for (const config of this.lodConfigs) {
const grid = this.gridLevels.get(config.level)!;
const gridKey = this.getGridKey(
point.longitude,
point.latitude,
config.gridSize,
config.level,
);
let cell = grid.get(gridKey);
if (!cell) {
const gridX = Math.floor(point.longitude / config.gridSize);
const gridY = Math.floor(point.latitude / config.gridSize);
cell = {
key: gridKey,
count: 0,
totalWeight: 0,
bounds: {
west: gridX * config.gridSize,
east: (gridX + 1) * config.gridSize,
south: gridY * config.gridSize,
north: (gridY + 1) * config.gridSize,
},
centroidLon: 0,
centroidLat: 0,
pointIndices: [],
};
grid.set(gridKey, cell);
}
const weight = point.weight ?? 1;
// 增量更新质心
const newCount = cell.count + 1;
cell.centroidLon =
(cell.centroidLon * cell.count + point.longitude) / newCount;
cell.centroidLat =
(cell.centroidLat * cell.count + point.latitude) / newCount;
cell.count = newCount;
cell.totalWeight += weight;
cell.pointIndices.push(index);
}
}
/**
* 生成网格键
*/
private getGridKey(
lon: number,
lat: number,
gridSize: number,
level: number,
): string {
const gridX = Math.floor(lon / gridSize);
const gridY = Math.floor(lat / gridSize);
return `${level}_${gridX}_${gridY}`;
}
/**
* 根据相机高度获取当前 LOD 级别
*/
getCurrentLODLevel(cameraHeight: number): number {
for (const config of this.lodConfigs) {
if (cameraHeight >= config.heightThreshold) {
return config.level;
}
}
return this.lodConfigs[this.lodConfigs.length - 1].level;
}
/**
* 获取当前 LOD 配置
*/
getLODConfig(level: number): LODConfig | undefined {
return this.lodConfigs.find((c) => c.level === level);
}
/**
* 查询视口内的聚合点
*/
queryViewport(viewport: ViewportInfo): ClusteredPoint[] {
const lodLevel = this.getCurrentLODLevel(viewport.cameraHeight);
const config = this.getLODConfig(lodLevel);
if (!config) return [];
const grid = this.gridLevels.get(lodLevel);
if (!grid) return [];
const clusters: ClusteredPoint[] = [];
// 遍历视口内的网格单元
for (const cell of grid.values()) {
// 检查是否在视口内
if (this.cellIntersectsViewport(cell, viewport)) {
clusters.push({
longitude: cell.centroidLon,
latitude: cell.centroidLat,
count: cell.count,
totalWeight: cell.totalWeight,
lodLevel,
gridKey: cell.key,
});
}
}
// 如果点数超过限制,进行采样
if (clusters.length > config.maxPoints) {
return this.sampleClusters(clusters, config.maxPoints);
}
return clusters;
}
/**
* 检查网格单元是否与视口相交
*/
private cellIntersectsViewport(
cell: GridCell,
viewport: ViewportInfo,
): boolean {
return !(
cell.bounds.east < viewport.west ||
cell.bounds.west > viewport.east ||
cell.bounds.north < viewport.south ||
cell.bounds.south > viewport.north
);
}
/**
* 基于权重的重要性采样
*/
private sampleClusters(
clusters: ClusteredPoint[],
maxCount: number,
): ClusteredPoint[] {
// 按权重排序
const sorted = [...clusters].sort((a, b) => b.totalWeight - a.totalWeight);
// 分层采样:保证高权重点全部保留,低权重点随机采样
const highWeightCount = Math.floor(maxCount * 0.7);
const randomCount = maxCount - highWeightCount;
const result = sorted.slice(0, highWeightCount);
if (sorted.length > highWeightCount) {
const remaining = sorted.slice(highWeightCount);
const step = Math.ceil(remaining.length / randomCount);
for (
let i = 0;
i < remaining.length && result.length < maxCount;
i += step
) {
result.push(remaining[i]);
}
}
return result;
}
/**
* 获取原始点数量
*/
getPointCount(): number {
return this.points.length;
}
/**
* 获取指定级别的网格单元数
*/
getGridCellCount(level: number): number {
return this.gridLevels.get(level)?.size ?? 0;
}
/**
* 获取所有原始点(用于低高度精细渲染)
*/
getAllPoints(): HeatPoint[] {
return this.points;
}
/**
* 查询视口内的原始点(用于最高精度渲染)
*/
queryRawPointsInViewport(
viewport: ViewportInfo,
maxCount: number,
): HeatPoint[] {
const result: HeatPoint[] = [];
for (const point of this.points) {
if (
point.longitude >= viewport.west &&
point.longitude <= viewport.east &&
point.latitude >= viewport.south &&
point.latitude <= viewport.north
) {
result.push(point);
if (result.length >= maxCount) break;
}
}
return result;
}
}
/**
* 默认 LOD 配置
* 参考高德地图热力图的缩放行为设计
*/
export const DEFAULT_LOD_CONFIGS: LODConfig[] = [
{
level: 0,
heightThreshold: 5000000, // 5000km+: 全球视图
gridSize: 5.0, // 5度网格
kernelRadius: 20,
maxPoints: 500,
},
{
level: 1,
heightThreshold: 1000000, // 1000km: 大洲视图
gridSize: 1.0, // 1度网格
kernelRadius: 25,
maxPoints: 1000,
},
{
level: 2,
heightThreshold: 500000, // 500km: 国家视图
gridSize: 0.5, // 0.5度网格
kernelRadius: 30,
maxPoints: 2000,
},
{
level: 3,
heightThreshold: 100000, // 100km: 省级视图
gridSize: 0.1, // 0.1度网格
kernelRadius: 35,
maxPoints: 3000,
},
{
level: 4,
heightThreshold: 50000, // 50km: 城市视图
gridSize: 0.05, // 0.05度网格
kernelRadius: 40,
maxPoints: 5000,
},
{
level: 5,
heightThreshold: 10000, // 10km: 区县视图
gridSize: 0.01, // 0.01度网格
kernelRadius: 50,
maxPoints: 8000,
},
{
level: 6,
heightThreshold: 5000, // 5km: 街道视图
gridSize: 0.005, // 0.005度网格
kernelRadius: 60,
maxPoints: 10000,
},
{
level: 7,
heightThreshold: 1000, // 1km: 建筑视图
gridSize: 0.001, // 0.001度网格
kernelRadius: 80,
maxPoints: 15000,
},
{
level: 8,
heightThreshold: 0, // <1km: 最高精度
gridSize: 0.0005, // 0.0005度网格
kernelRadius: 100,
maxPoints: 20000,
},
];
// ==================== 热力图管理器 (HeatmapManager.ts) ====================
/**
* 默认配置
*/
const DEFAULT_CONFIG: HeatmapConfig = {
canvasWidth: 1024,
canvasHeight: 1024,
maxOpacity: 0.8,
minOpacity: 0.0,
blur: 0.85,
gradient: AMAP_GRADIENT,
lodConfigs: DEFAULT_LOD_CONFIGS,
useGPU: true,
intensityScale: 1.0,
transitionDuration: 300,
};
/**
* 多尺度热力图管理器
*/
export class MultiScaleHeatmapManager {
/** Cesium Viewer 实例 */
private viewer: Cesium.Viewer;
/** 配置 */
private config: HeatmapConfig;
/** 空间索引 */
private spatialIndex: MultiLevelSpatialIndex;
/** KDE 处理器 (CPU 回退) */
private kdeProcessor: KDEProcessor;
/** GPU 渲染器 */
private gpuRenderer: GPUHeatmapRenderer;
/** Cesium Entity 图层 */
private heatmapEntity: Cesium.Entity | null = null;
/** 渲染用 Canvas */
private renderCanvas: HTMLCanvasElement;
/** 渲染用 2D Context */
private renderCtx: CanvasRenderingContext2D;
/** 当前 LOD 级别 */
private currentLOD: number = 0;
/** 上一帧的 LOD 级别 */
private previousLOD: number = 0;
/** 是否正在渲染 */
private isRendering: boolean = false;
/** 动画请求 ID */
private animationFrameId: number | null = null;
/** 相机变化监听器 */
private cameraChangeListener: Cesium.Event.RemoveCallback | null = null;
/** 是否启用 */
private enabled: boolean = false;
/** 性能统计 */
private stats: PerformanceStats = {
dataProcessTime: 0,
spatialQueryTime: 0,
clusteringTime: 0,
kdeTime: 0,
renderTime: 0,
totalFrameTime: 0,
visiblePointCount: 0,
clusteredPointCount: 0,
};
/** 渲染节流计时器 */
private throttleTimer: number | null = null;
/** 节流间隔 (ms) */
private throttleInterval: number = 50;
/** 当前视口边界 */
private currentBounds: Cesium.Rectangle | null = null;
/** LOD 过渡动画状态 */
private transitionState: {
isTransitioning: boolean;
startTime: number;
fromLOD: number;
toLOD: number;
fromOpacity: number;
toOpacity: number;
} = {
isTransitioning: false,
startTime: 0,
fromLOD: 0,
toLOD: 0,
fromOpacity: 1,
toOpacity: 1,
};
constructor(viewer: Cesium.Viewer, config: Partial<HeatmapConfig> = {}) {
this.viewer = viewer;
this.config = { ...DEFAULT_CONFIG, ...config };
// 默认使用 CPU 渲染,更稳定
if (this.config.useGPU === undefined) {
this.config.useGPU = false;
}
// 初始化组件
this.spatialIndex = new MultiLevelSpatialIndex(this.config.lodConfigs);
this.kdeProcessor = new KDEProcessor(50, "gaussian");
this.kdeProcessor.buildGradientLUT(this.config.gradient);
this.gpuRenderer = new GPUHeatmapRenderer(
this.config.canvasWidth,
this.config.canvasHeight
);
this.gpuRenderer.setGradient(this.config.gradient);
// 创建渲染画布
this.renderCanvas = document.createElement("canvas");
this.renderCanvas.width = this.config.canvasWidth;
this.renderCanvas.height = this.config.canvasHeight;
this.renderCtx = this.renderCanvas.getContext("2d")!;
console.log("[MultiScaleHeatmap] Initialized with config:", {
canvasSize: `${this.config.canvasWidth}x${this.config.canvasHeight}`,
useGPU: this.config.useGPU,
lodLevels: this.config.lodConfigs.length,
});
}
/**
* 设置数据
*/
setData(points: HeatPoint[]): void {
const startTime = performance.now();
this.spatialIndex.setData(points);
this.stats.dataProcessTime = performance.now() - startTime;
console.log("[MultiScaleHeatmap] Data set:", points.length, "points");
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 增量添加数据
*/
addData(points: HeatPoint[]): void {
this.spatialIndex.addPoints(points);
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 清空数据
*/
clearData(): void {
this.spatialIndex.clear();
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 启用热力图
*/
enable(): void {
if (this.enabled) return;
this.enabled = true;
// 监听相机变化
this.cameraChangeListener = this.viewer.camera.changed.addEventListener(
() => {
this.scheduleRender();
}
);
// 初始渲染
this.scheduleRender();
}
/**
* 禁用热力图
*/
disable(): void {
if (!this.enabled) return;
this.enabled = false;
// 移除监听器
if (this.cameraChangeListener) {
this.cameraChangeListener();
this.cameraChangeListener = null;
}
// 取消动画
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// 移除图层
this.removeHeatmapLayer();
}
/**
* 切换显示状态
*/
toggle(show?: boolean): void {
const shouldShow = show ?? !this.enabled;
if (shouldShow) {
this.enable();
} else {
this.disable();
}
}
/**
* 更新配置
*/
updateConfig(config: Partial<HeatmapConfig>): void {
this.config = { ...this.config, ...config };
if (config.gradient) {
this.kdeProcessor.buildGradientLUT(config.gradient);
this.gpuRenderer.setGradient(config.gradient);
}
if (config.canvasWidth || config.canvasHeight) {
this.renderCanvas.width = this.config.canvasWidth;
this.renderCanvas.height = this.config.canvasHeight;
this.gpuRenderer.resize(
this.config.canvasWidth,
this.config.canvasHeight
);
}
if (config.lodConfigs) {
this.spatialIndex = new MultiLevelSpatialIndex(config.lodConfigs);
}
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 调度渲染 (带节流)
*/
private scheduleRender(): void {
if (this.throttleTimer !== null) return;
this.throttleTimer = window.setTimeout(() => {
this.throttleTimer = null;
this.render();
}, this.throttleInterval);
}
/**
* 获取当前视口信息
*/
private getViewportInfo(): ViewportInfo | null {
const canvas = this.viewer.canvas;
const camera = this.viewer.camera;
const canvasWidth = canvas.clientWidth || canvas.width;
const canvasHeight = canvas.clientHeight || canvas.height;
// 获取相机高度
const cameraPosition = camera.positionCartographic;
const cameraHeight = cameraPosition.height;
// 获取视口矩形
const rect = camera.computeViewRectangle();
if (!rect) {
// 如果无法计算视口,使用相机位置估计
const lon = Cesium.Math.toDegrees(cameraPosition.longitude);
const lat = Cesium.Math.toDegrees(cameraPosition.latitude);
const span = cameraHeight / 111000; // 粗略估计
return {
west: lon - span,
east: lon + span,
south: lat - span,
north: lat + span,
cameraHeight,
canvasWidth,
canvasHeight,
};
}
return {
west: Cesium.Math.toDegrees(rect.west),
east: Cesium.Math.toDegrees(rect.east),
south: Cesium.Math.toDegrees(rect.south),
north: Cesium.Math.toDegrees(rect.north),
cameraHeight,
canvasWidth,
canvasHeight,
};
}
/**
* 将地理坐标转换为渲染画布坐标(屏幕空间)
*/
private geoToCanvasScreen(
longitude: number,
latitude: number,
viewport: ViewportInfo
): { x: number; y: number } | null {
// 使用 Cesium 的坐标转换
const cartesian = Cesium.Cartesian3.fromDegrees(longitude, latitude);
// 兼容不同版本的 Cesium API
const screenPosition =
(Cesium.SceneTransforms as any).wgs84ToWindowCoordinates?.(
this.viewer.scene,
cartesian
) ||
Cesium.SceneTransforms.worldToWindowCoordinates(
this.viewer.scene,
cartesian
);
if (!screenPosition) return null;
// 映射到渲染画布坐标
const scaleX = this.config.canvasWidth / viewport.canvasWidth;
const scaleY = this.config.canvasHeight / viewport.canvasHeight;
return {
x: screenPosition.x * scaleX,
y: screenPosition.y * scaleY,
};
}
/**
* 将地理坐标转换为渲染画布坐标(地理线性映射)
*/
private geoToCanvasGeographic(
longitude: number,
latitude: number,
viewport: ViewportInfo
): { x: number; y: number } | null {
const lonSpan = this.getLongitudeSpan(viewport);
const latSpan = viewport.north - viewport.south;
if (lonSpan <= 0 || latSpan <= 0) return null;
const normalizedLon = this.normalizeLongitude(longitude, viewport);
const x =
((normalizedLon - viewport.west) / lonSpan) * this.config.canvasWidth;
const y =
((viewport.north - latitude) / latSpan) * this.config.canvasHeight;
return { x, y };
}
private getLongitudeSpan(viewport: ViewportInfo): number {
const span = viewport.east - viewport.west;
return span >= 0 ? span : span + 360;
}
private normalizeLongitude(
longitude: number,
viewport: ViewportInfo
): number {
if (viewport.west <= viewport.east) return longitude;
return longitude < viewport.west ? longitude + 360 : longitude;
}
private shouldUseGeographicProjection(
viewport: ViewportInfo,
lodLevel: number
): boolean {
if (!this.isNorthUpNadirView()) return true;
if (lodLevel <= 1) return true;
const lonSpan = this.getLongitudeSpan(viewport);
const latSpan = Math.abs(viewport.north - viewport.south);
return lonSpan > 120 || latSpan > 60;
}
private isNorthUpNadirView(): boolean {
const camera = this.viewer.camera;
const pitchDeg = Cesium.Math.toDegrees(camera.pitch);
const rollDeg = Cesium.Math.toDegrees(camera.roll);
const headingDeg = Cesium.Math.toDegrees(
Cesium.Math.negativePiToPi(camera.heading)
);
const tolerance = 5;
return (
Math.abs(pitchDeg + 90) <= tolerance &&
Math.abs(rollDeg) <= tolerance &&
Math.abs(headingDeg) <= tolerance
);
}
/**
* 执行渲染
*/
private render(): void {
if (this.isRendering || !this.enabled) return;
this.isRendering = true;
const frameStartTime = performance.now();
try {
// 1. 获取视口信息
const viewport = this.getViewportInfo();
if (!viewport) {
console.warn("[MultiScaleHeatmap] Cannot get viewport info");
this.isRendering = false;
return;
}
// 2. 查询可见聚合点
const queryStartTime = performance.now();
const clusters = this.spatialIndex.queryViewport(viewport);
this.stats.spatialQueryTime = performance.now() - queryStartTime;
this.stats.clusteredPointCount = clusters.length;
console.log("[MultiScaleHeatmap] Rendering:", {
clusters: clusters.length,
cameraHeight: viewport.cameraHeight.toFixed(0),
bounds: `${viewport.west.toFixed(2)},${viewport.south.toFixed(
2
)} - ${viewport.east.toFixed(2)},${viewport.north.toFixed(2)}`,
});
if (clusters.length === 0) {
// 没有数据,清空并隐藏
this.renderCtx.clearRect(
0,
0,
this.config.canvasWidth,
this.config.canvasHeight
);
if (this.heatmapEntity) {
this.heatmapEntity.show = false;
}
this.isRendering = false;
return;
}
// 3. 检测 LOD 变化
const newLOD = this.spatialIndex.getCurrentLODLevel(
viewport.cameraHeight
);
if (newLOD !== this.currentLOD) {
this.previousLOD = this.currentLOD;
this.currentLOD = newLOD;
}
const useGeographicProjection = this.shouldUseGeographicProjection(
viewport,
newLOD
);
// 4. 获取当前 LOD 配置
const lodConfig = this.spatialIndex.getLODConfig(this.currentLOD);
if (!lodConfig) {
console.warn(
"[MultiScaleHeatmap] No LOD config for level:",
this.currentLOD
);
this.isRendering = false;
return;
}
// 5. 转换为屏幕坐标
const screenPoints: ScreenPoint[] = [];
for (const cluster of clusters) {
const screenPos = useGeographicProjection
? this.geoToCanvasGeographic(
cluster.longitude,
cluster.latitude,
viewport
)
: this.geoToCanvasScreen(
cluster.longitude,
cluster.latitude,
viewport
);
if (
screenPos &&
screenPos.x >= 0 &&
screenPos.x < this.config.canvasWidth &&
screenPos.y >= 0 &&
screenPos.y < this.config.canvasHeight
) {
screenPoints.push({
x: screenPos.x,
y: screenPos.y,
weight: cluster.totalWeight,
});
}
}
this.stats.visiblePointCount = screenPoints.length;
console.log(
"[MultiScaleHeatmap] Screen points:",
screenPoints.length,
"LOD:",
this.currentLOD
);
// 6. 渲染热力图
const renderStartTime = performance.now();
this.renderHeatmap(screenPoints, lodConfig.kernelRadius);
this.stats.renderTime = performance.now() - renderStartTime;
// 7. 更新 Cesium 图层
this.updateCesiumLayer(viewport);
this.stats.totalFrameTime = performance.now() - frameStartTime;
} catch (e) {
console.error("[MultiScaleHeatmap] Render error:", e);
} finally {
this.isRendering = false;
}
}
/**
* 渲染热力图到画布
*/
private renderHeatmap(points: ScreenPoint[], kernelRadius: number): void {
if (points.length === 0) {
// 清空画布
this.renderCtx.clearRect(
0,
0,
this.config.canvasWidth,
this.config.canvasHeight
);
return;
}
const kdeStartTime = performance.now();
// 先尝试 GPU 渲染
let useGPU = false;
if (this.config.useGPU && this.gpuRenderer.isGPUAvailable()) {
try {
this.gpuRenderer.resize(
this.config.canvasWidth,
this.config.canvasHeight
);
this.gpuRenderer.updatePoints(points);
const gpuCanvas = this.gpuRenderer.render(
kernelRadius,
this.config.intensityScale,
this.config.minOpacity,
this.config.maxOpacity
);
// 检查 GPU 渲染结果是否有效
const testCtx = gpuCanvas.getContext("2d");
if (testCtx) {
const testData = testCtx.getImageData(0, 0, 1, 1);
// 如果有非零像素说明渲染成功
if (testData.data[3] > 0) {
this.renderCtx.clearRect(
0,
0,
this.config.canvasWidth,
this.config.canvasHeight
);
this.renderCtx.drawImage(gpuCanvas, 0, 0);
useGPU = true;
}
}
} catch (e) {
console.warn("GPU render failed, falling back to CPU:", e);
}
}
// CPU 回退
if (!useGPU) {
this.kdeProcessor.setRadius(kernelRadius);
const density = this.kdeProcessor.computeKDEOptimized(
points,
this.config.canvasWidth,
this.config.canvasHeight,
this.config.intensityScale
);
const imageData = this.kdeProcessor.densityToImageData(
density,
this.config.canvasWidth,
this.config.canvasHeight,
this.config.minOpacity,
this.config.maxOpacity
);
this.renderCtx.putImageData(imageData, 0, 0);
}
this.stats.kdeTime = performance.now() - kdeStartTime;
}
/**
* 更新 Cesium 图层
*/
private updateCesiumLayer(viewport: ViewportInfo): void {
const rectangle = Cesium.Rectangle.fromDegrees(
viewport.west,
viewport.south,
viewport.east,
viewport.north
);
this.currentBounds = rectangle;
// 将 Canvas 转换为 Data URL 用于 Cesium 材质
const imageUrl = this.renderCanvas.toDataURL("image/png");
if (this.heatmapEntity) {
// 更新现有 Entity
this.heatmapEntity.show = true;
const rectGraphics = this.heatmapEntity.rectangle;
if (rectGraphics) {
rectGraphics.coordinates = new Cesium.ConstantProperty(rectangle);
// 尝试重用材质以避免闪烁
const existingMaterial = rectGraphics.material;
if (existingMaterial instanceof Cesium.ImageMaterialProperty) {
existingMaterial.image = new Cesium.ConstantProperty(imageUrl);
} else {
rectGraphics.material = new Cesium.ImageMaterialProperty({
image: imageUrl,
transparent: true,
});
}
}
} else {
// 创建新 Entity
this.heatmapEntity = this.viewer.entities.add({
rectangle: {
coordinates: rectangle,
material: new Cesium.ImageMaterialProperty({
image: imageUrl,
transparent: true,
}),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
});
console.log("[MultiScaleHeatmap] Created heatmap entity");
}
}
/**
* 移除热力图图层
*/
private removeHeatmapLayer(): void {
if (this.heatmapEntity) {
this.viewer.entities.remove(this.heatmapEntity);
this.heatmapEntity = null;
}
}
/**
* 获取性能统计
*/
getStats(): PerformanceStats {
return { ...this.stats };
}
/**
* 获取当前 LOD 级别
*/
getCurrentLOD(): number {
return this.currentLOD;
}
/**
* 获取数据点总数
*/
getPointCount(): number {
return this.spatialIndex.getPointCount();
}
/**
* 是否启用
*/
isEnabled(): boolean {
return this.enabled;
}
/**
* 设置颜色梯度
*/
setGradient(gradient: GradientConfig): void {
this.config.gradient = gradient;
this.kdeProcessor.buildGradientLUT(gradient);
this.gpuRenderer.setGradient(gradient);
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 设置强度缩放
*/
setIntensityScale(scale: number): void {
this.config.intensityScale = scale;
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 设置透明度范围
*/
setOpacity(min: number, max: number): void {
this.config.minOpacity = min;
this.config.maxOpacity = max;
if (this.enabled) {
this.scheduleRender();
}
}
/**
* 强制重新渲染
*/
forceRender(): void {
if (this.enabled) {
this.render();
}
}
/**
* 销毁
*/
destroy(): void {
this.disable();
this.gpuRenderer.destroy();
this.spatialIndex.clear();
}
}
// ==================== 方法总结 ====================
/**
* 本模块包含以下主要类及其方法:
*
* 1. MultiScaleHeatmapManager (主管理器)
* - constructor: 初始化管理器,创建空间索引、KDE处理器、GPU渲染器。
* - setData(points): 设置原始数据点,重建空间索引,并触发渲染。
* - addData(points): 增量添加数据点,更新空间索引,触发渲染。
* - clearData(): 清空所有数据,触发渲染清空图层。
* - enable(): 启用热力图,监听相机变化,开始渲染。
* - disable(): 禁用热力图,移除监听器,取消渲染,移除图层。
* - toggle(show): 切换启用/禁用状态。
* - updateConfig(config): 更新配置,重建必要组件,触发渲染。
* - scheduleRender(): 节流调度渲染。
* - getViewportInfo(): 获取当前视口信息(边界、相机高度、画布尺寸)。
* - geoToCanvasScreen(): 将地理坐标通过Cesium屏幕坐标转换映射到渲染画布。
* - geoToCanvasGeographic(): 将地理坐标通过线性投影映射到渲染画布(用于大范围或非正视视图)。
* - shouldUseGeographicProjection(): 判断是否应使用地理线性投影。
* - isNorthUpNadirView(): 判断当前相机是否为正北朝下的正视视图。
* - render(): 核心渲染流程:查询可见聚合点、转换坐标、渲染热力图、更新Cesium图层。
* - renderHeatmap(): 调用GPU或CPU渲染热力图到内部画布。
* - updateCesiumLayer(): 将内部画布转换为材质更新Cesium的Rectangle实体。
* - removeHeatmapLayer(): 移除Cesium中的热力图实体。
* - getStats(): 返回性能统计。
* - getCurrentLOD(): 返回当前LOD级别。
* - getPointCount(): 返回总数据点数。
* - isEnabled(): 返回是否启用。
* - setGradient(): 设置颜色梯度并重新渲染。
* - setIntensityScale(): 设置强度缩放并重新渲染。
* - setOpacity(): 设置透明度范围并重新渲染。
* - forceRender(): 强制立即渲染。
* - destroy(): 销毁管理器,释放资源。
*
* 2. MultiLevelSpatialIndex (空间索引)
* - constructor(lodConfigs): 初始化各级网格索引。
* - setData(points): 设置数据,重建索引。
* - addPoints(newPoints): 增量添加点,更新索引。
* - clear(): 清空所有数据。
* - computeDataHash(): 计算数据哈希用于检测变化。
* - rebuildIndex(): 完全重建索引。
* - updateIndexIncremental(): 增量更新索引。
* - indexPoint(): 将单个点加入各级网格。
* - getGridKey(): 生成网格键。
* - getCurrentLODLevel(cameraHeight): 根据相机高度返回LOD级别。
* - getLODConfig(level): 返回指定级别的配置。
* - queryViewport(viewport): 查询视口内的聚合点。
* - cellIntersectsViewport(): 判断网格单元是否与视口相交。
* - sampleClusters(): 对聚合点进行采样,限制数量。
* - getPointCount(): 返回总点数。
* - getGridCellCount(level): 返回指定级别网格数。
* - getAllPoints(): 返回所有原始点。
* - queryRawPointsInViewport(): 查询视口内的原始点(最高精度)。
*
* 3. KDEProcessor (CPU核密度估计)
* - constructor(radius, kernelType): 初始化,构建核函数LUT。
* - buildKernelLUT(): 预计算核函数查找表。
* - evaluateKernel(u): 评估核函数值。
* - getKernelValue(normalizedDistance): 从LUT获取核值。
* - setRadius(radius): 设置核半径。
* - getRadius(): 获取核半径。
* - buildGradientLUT(gradient): 根据配置构建颜色梯度LUT。
* - parseColor(): 解析CSS颜色字符串为RGBA。
* - interpolateGradient(): 颜色插值。
* - computeKDE(points, width, height, intensityScale): 基础KDE计算。
* - densityToImageData(): 将密度矩阵转换为ImageData。
* - computeKDEOptimized(): 优化版KDE(格子加速)。
* - getGradientLUT(): 返回梯度LUT。
*
* 4. GPUHeatmapRenderer (GPU渲染器)
* - constructor(width, height): 创建画布,初始化WebGL。
* - initWebGL(): 初始化WebGL上下文。
* - compileShader(): 编译着色器。
* - createProgram(): 创建着色器程序。
* - setupShaders(): 设置密度计算和颜色映射着色器。
* - setupBuffers(): 创建缓冲区。
* - setupFramebuffer(): 创建帧缓冲和纹理。
* - setGradient(gradient): 设置颜色梯度纹理。
* - parseColor(): 解析颜色。
* - interpolateGradient(): 插值颜色。
* - updatePoints(points): 上传点数据到GPU。
* - resize(width, height): 调整渲染尺寸。
* - render(kernelRadius, intensity, minOpacity, maxOpacity): 执行两遍渲染,返回画布。
* - estimateMaxDensity(): 采样估计最大密度。
* - getImageData(): 获取渲染结果的ImageData(Y轴翻转)。
* - isGPUAvailable(): 返回GPU是否可用。
* - getCanvas(): 返回内部画布。
* - destroy(): 释放WebGL资源。
*
* 5. 类型和常量 (types.ts, AMAP_GRADIENT, CLASSIC_GRADIENT, DEFAULT_LOD_CONFIGS)
* 定义了所有接口、类型和默认配置。
*/
热力图使用实例
以下是一个完整的HTML测试案例,演示如何使用整合后的MultiScaleHeatmapManager在Cesium中显示热力图。该案例生成5000个随机点(模拟北京市区周边数据),并包含简单的启用/禁用控制。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>多尺度热力图测试案例</title>
<script src="https://cesium.com/downloads/cesiumjs/releases/1.115/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.115/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<style>
#cesiumContainer {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
#controls {
position: absolute;
top: 10px;
right: 10px;
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
z-index: 100;
}
#stats {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px 10px;
border-radius: 3px;
font-family: monospace;
font-size: 12px;
z-index: 100;
pointer-events: none;
}
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<div id="controls">
<button id="toggleBtn">启用热力图</button>
<button id="updateDataBtn">更新随机数据</button>
<label><input type="checkbox" id="useGPUCb" checked> 使用GPU</label>
<br>
<label>强度缩放: <input type="range" id="intensitySlider" min="0.2" max="3" step="0.1" value="1.0"></label>
</div>
<div id="stats">
<div>数据点数: <span id="pointCount">0</span></div>
<div>可见聚合点: <span id="visiblePoints">0</span></div>
<div>当前LOD: <span id="currentLOD">0</span></div>
<div>帧耗时: <span id="frameTime">0</span> ms</div>
</div>
<script>
// ---------- 这里放置上述整合的整个模块代码 ----------
// 为了方便阅读,此处省略具体实现,实际使用时请将上面提供的所有类定义(类型、KDEProcessor、GPUHeatmapRenderer、MultiLevelSpatialIndex、MultiScaleHeatmapManager)复制到这里。
// 由于代码量较大,建议将整个模块保存为一个单独的文件(如 MultiScaleHeatmap.js)并通过 <script src="..."></script> 引入。
// 为演示完整性,下面假设已经通过某种方式加载了这些类(例如通过 import 或全局变量)。
// 实际测试时请确保以下类在全局作用域可用:
// - MultiScaleHeatmapManager
// - HeatPoint 等类型(可选,但 TypeScript 类型在运行时不需要)
// 为简化,我们假设类已经定义(例如通过 <script> 包含)。
// ---------- 结束模块占位 ----------
// 初始化 Cesium 访问令牌(可选,若使用Cesium Ion需设置)
Cesium.Ion.defaultAccessToken = 'your_token_here'; // 替换为你的token,或使用本地服务
// 创建 Viewer
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain(),
animation: false,
baseLayerPicker: false,
fullscreenButton: false,
vrButton: false,
geocoder: true,
homeButton: true,
infoBox: false,
sceneModePicker: true,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false
});
// 设置初始视角为北京附近
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(116.3, 39.9, 3000000) // 高度3000km
});
// 创建热力图管理器实例
const heatmapManager = new MultiScaleHeatmapManager(viewer, {
canvasWidth: 1024,
canvasHeight: 1024,
useGPU: true, // 默认启用GPU,可根据复选框调整
maxOpacity: 0.8,
minOpacity: 0.0,
intensityScale: 1.0,
gradient: {
"0.0": "rgba(0, 0, 255, 0)",
"0.2": "blue",
"0.4": "cyan",
"0.6": "green",
"0.8": "yellow",
"1.0": "red"
}
});
// 生成测试数据:5000个随机点,以北京为中心,经度范围116.0-116.6,纬度范围39.6-40.2,权重随机1-10
function generateRandomPoints(count = 5000) {
const points = [];
const centerLon = 116.3;
const centerLat = 39.9;
const lonRange = 0.6;
const latRange = 0.6;
for (let i = 0; i < count; i++) {
points.push({
longitude: centerLon + (Math.random() - 0.5) * lonRange,
latitude: centerLat + (Math.random() - 0.5) * latRange,
weight: Math.floor(Math.random() * 10) + 1 // 权重1~10
});
}
return points;
}
let testData = generateRandomPoints(5000);
heatmapManager.setData(testData);
updateStats();
// UI 控制
const toggleBtn = document.getElementById('toggleBtn');
const updateDataBtn = document.getElementById('updateDataBtn');
const useGPUCb = document.getElementById('useGPUCb');
const intensitySlider = document.getElementById('intensitySlider');
toggleBtn.addEventListener('click', () => {
if (heatmapManager.isEnabled()) {
heatmapManager.disable();
toggleBtn.textContent = '启用热力图';
} else {
heatmapManager.enable();
toggleBtn.textContent = '禁用热力图';
}
});
updateDataBtn.addEventListener('click', () => {
testData = generateRandomPoints(5000);
heatmapManager.setData(testData);
updateStats();
});
useGPUCb.addEventListener('change', (e) => {
heatmapManager.updateConfig({ useGPU: e.target.checked });
});
intensitySlider.addEventListener('input', (e) => {
heatmapManager.setIntensityScale(parseFloat(e.target.value));
});
// 定期更新统计信息
function updateStats() {
const stats = heatmapManager.getStats();
document.getElementById('pointCount').textContent = heatmapManager.getPointCount();
document.getElementById('visiblePoints').textContent = stats.visiblePointCount;
document.getElementById('currentLOD').textContent = heatmapManager.getCurrentLOD();
document.getElementById('frameTime').textContent = stats.totalFrameTime.toFixed(2);
requestAnimationFrame(updateStats);
}
// 可选:监听相机变化后手动更新统计(已在scheduleRender中自动处理)
console.log('测试案例已启动,请点击"启用热力图"查看效果');
</script>
</body>
</html>
使用说明
- 引入Cesium:通过CDN加载Cesium核心库和样式。
- 包含热力图模块 :需要将之前提供的所有类定义(
KDEProcessor、GPUHeatmapRenderer、MultiLevelSpatialIndex、MultiScaleHeatmapManager及类型接口)复制到<script>标签中,或者保存为单独文件并通过<script src="..."></script>引入。本例中为了简洁,仅用注释占位,实际运行时必须包含完整代码。 - 生成测试数据:在北京市区周围生成5000个随机点,每个点带有随机权重。
- 初始化管理器:传入Cesium Viewer实例和自定义配置(如颜色梯度、画布大小等)。
- UI交互 :
- 按钮"启用/禁用热力图"切换显示。
- "更新随机数据"重新生成点集。
- 复选框切换GPU/CPU渲染。
- 滑块调节强度缩放。
- 实时统计:在左下角显示当前数据点数、可见聚合点数、LOD级别和帧耗时。
注意事项
- 确保Cesium的访问令牌正确设置(如使用Cesium Ion服务)。如果本地开发,可省略或使用离线地形。
- 由于热力图渲染涉及WebGL和大量计算,建议在支持WebGL2的现代浏览器中测试。
- 若数据点过多导致性能问题,可调整
maxPoints等LOD参数。 - 首次启用时,相机需移动到数据可见区域(本例已设置初始视角)。
运行此页面后,点击"启用热力图"即可看到动态多尺度热力图效果。