地图 热力图核心封装

文章目录

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>

使用说明

  1. 引入Cesium:通过CDN加载Cesium核心库和样式。
  2. 包含热力图模块 :需要将之前提供的所有类定义(KDEProcessorGPUHeatmapRendererMultiLevelSpatialIndexMultiScaleHeatmapManager及类型接口)复制到<script>标签中,或者保存为单独文件并通过<script src="..."></script>引入。本例中为了简洁,仅用注释占位,实际运行时必须包含完整代码。
  3. 生成测试数据:在北京市区周围生成5000个随机点,每个点带有随机权重。
  4. 初始化管理器:传入Cesium Viewer实例和自定义配置(如颜色梯度、画布大小等)。
  5. UI交互
    • 按钮"启用/禁用热力图"切换显示。
    • "更新随机数据"重新生成点集。
    • 复选框切换GPU/CPU渲染。
    • 滑块调节强度缩放。
  6. 实时统计:在左下角显示当前数据点数、可见聚合点数、LOD级别和帧耗时。

注意事项

  • 确保Cesium的访问令牌正确设置(如使用Cesium Ion服务)。如果本地开发,可省略或使用离线地形。
  • 由于热力图渲染涉及WebGL和大量计算,建议在支持WebGL2的现代浏览器中测试。
  • 若数据点过多导致性能问题,可调整maxPoints等LOD参数。
  • 首次启用时,相机需移动到数据可见区域(本例已设置初始视角)。

运行此页面后,点击"启用热力图"即可看到动态多尺度热力图效果。

相关推荐
窝子面2 小时前
Nestjs框架使用
javascript
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:@react-native-oh-tpl/react-native-fast-image
javascript·react native·react.js
朵朵奇葩向阳开#2 小时前
【无标题】
javascript·typescript·ruby·laravel·perl·composer
网络点点滴2 小时前
组件通信-provide和inject
javascript·vue.js·ecmascript
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:@react-native-oh-tpl/masked-view
javascript·react native·react.js
摸鱼的春哥3 小时前
Agent教程20:更适合编程工具的记忆方案——情景摘要
前端·javascript·后端
Hamm11 小时前
不想花一分钱玩 OpenClaw?来,一起折腾这个!
javascript·人工智能·agent
Setsuna_F_Seiei12 小时前
AI 对话应用之 JS 的流式接口数据处理
前端·javascript·ai编程
英俊潇洒美少年12 小时前
react如何实现 vue的$nextTick的效果
javascript·vue.js·react.js