geotiff.js在cesium项目中使用,加载本地.tif文件渲染到地图上,使用d3-scale-chromatic解决单色文件只显示黑色问题

180 阅读8分钟

更新: 引入pro4j使用客户端重采样重投影解决转换后的坐标偏移问题

源坐标系非 4326 且存在投影非线性(如 UTM 等):只变换两个角并强行拉成经纬矩形,无法反映真实的弯曲边界与内部点非线性,面积越大偏差越明显。 我的tif文件是 EPSG:32651 ,使用pro4j转换

使用:

 const viewer = new Cesium.Viewer('mapContainer', {
        ......
    })
    
// 传入cesium实例    
// .tif文件放在public/static下面了,路径写死了,有需求的可以改在这里传文件路径
addBloomTif(viewer)

因为tif文件的坐标不同,需要用到api.maptiler.com的接口转换,需要去注册下,创建key

用于加载本地的.tif文件并渲染到地图上。我在项目中发现如果这个文件不是正常的有rgb三种颜色,按照网上教程做的话,只会显示黑色.

使用d3-scale-chromatic颜色插值,显示彩色的图像

25d2a41d-f264-4dc2-a21a-f85a22c60bf0.png

封装的方法

import { fromUrl, fromArrayBuffer, fromBlob } from 'geotiff'
import { interpolateTurbo, interpolateViridis, interpolatePlasma, interpolateInferno, interpolateMagma } from 'd3-scale-chromatic'

declare const Cesium: any

// 可选:按需加载 proj4 以进行客户端重投影(若不可用则走兜底矩形贴图)
let cachedProj4: any | null = null
async function getProj4 () {
  if (cachedProj4) return cachedProj4
  const g: any = (globalThis as any)
  if (g && g.proj4) {
    cachedProj4 = g.proj4
    return cachedProj4
  }
  try {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    const mod = await import('proj4')
    cachedProj4 = (mod as any).default || (mod as any)
    return cachedProj4
  } catch {
    return null
  }
}

function ensureEpsgDefinition (proj4: any, code: number) {
  if (!proj4) return
  const key = `EPSG:${code}`
  if (!(proj4 as any).defs) return
  if (!(proj4 as any).defs[key]) {
    if (code === 32651) {
      ;(proj4 as any).defs(key, '+proj=utm +zone=51 +datum=WGS84 +units=m +no_defs +type=crs')
    }
  }
}

// 读取 GeoTIFF 仿射参数(常见 north-up 情形:由 ModelPixelScale + ModelTiepoint 给出)
function getGeoTransform (image: any): {
  hasTransform: boolean,
  // 仿射:X = gt0 + gt1*col + gt2*row; Y = gt3 + gt4*col + gt5*row
  gt0: number, gt1: number, gt2: number, gt3: number, gt4: number, gt5: number,
  mapPixelToModel: (col: number, row: number) => { x: number, y: number },
  mapModelToPixel: (x: number, y: number) => { col: number, row: number }
} {
  const fd = image.getFileDirectory?.() || {}
  const scale: number[] | undefined = fd.ModelPixelScale
  const tie: number[] | undefined = fd.ModelTiepoint
  const matrix: number[] | undefined = fd.ModelTransformation // 16 numbers (少见)

  // 默认:无仿射
  let gt0 = 0; let gt1 = 1; let gt2 = 0; let gt3 = 0; let gt4 = 0; let gt5 = -1

  if (Array.isArray(matrix) && matrix.length === 16) {
    // 通用 3x3 仿射,简化为 2x2 + 平移(忽略透视分量)
    // [ a b c 0; d e f 0; 0 0 1 0; tx ty 1 1 ] 或变体,GeoTIFF 此处实现差异较大
    // 为避免错误,暂不使用该路径,回退到 scale+tie(更常见)。
  }

  if (Array.isArray(scale) && scale.length >= 2 && Array.isArray(tie) && tie.length >= 6) {
    const scaleX = scale[0]
    const scaleY = scale[1]
    // 取第一组 tiepoint(col,row,z, X,Y,Z)
    const px = tie[0]
    const py = tie[1]
    const mx = tie[3]
    const my = tie[4]
    // 常规 north-up:Y 轴向下,因此 gt5 为负
    gt0 = mx - px * scaleX
    gt1 = scaleX
    gt2 = 0
    gt3 = my - py * (-scaleY)
    gt4 = 0
    gt5 = -scaleY
  } else {
    // 尝试从 boundingBox 估计(退路,不精确)
    const [minX, minY, maxX, maxY] = image.getBoundingBox()
    const w = image.getWidth()
    const h = image.getHeight()
    gt0 = minX
    gt1 = (maxX - minX) / w
    gt2 = 0
    gt3 = maxY
    gt4 = 0
    gt5 = -(maxY - minY) / h
  }

  const det = gt1 * gt5 - gt2 * gt4
  const hasTransform = Number.isFinite(det) && Math.abs(det) > 1e-12

  const mapPixelToModel = (col: number, row: number) => {
    const x = gt0 + gt1 * col + gt2 * row
    const y = gt3 + gt4 * col + gt5 * row
    return { x, y }
  }
  const mapModelToPixel = (x: number, y: number) => {
    // 2x2 逆: [gt1 gt2; gt4 gt5]^-1 * ([x-gt0, y-gt3])
    const dx = x - gt0
    const dy = y - gt3
    const inv11 = gt5 / det
    const inv12 = -gt2 / det
    const inv21 = -gt4 / det
    const inv22 = gt1 / det
    const col = inv11 * dx + inv12 * dy
    const row = inv21 * dx + inv22 * dy
    return { col, row }
  }

  return { hasTransform, gt0, gt1, gt2, gt3, gt4, gt5, mapPixelToModel, mapModelToPixel }
}

export type ColorMapType = 'turbo' | 'viridis' | 'plasma' | 'inferno' | 'magma' | 'custom'

export interface AddGeoTiffOptions {
  url: string
  rectangle?: { west: number; south: number; east: number; north: number }
  band?: number // 单波段索引(从1开始);若不指定则尝试 interleave 读取
  noData?: number | number[]
  colorMap?: (value: number) => [number, number, number, number] // 单波段伪彩
  colorMapType?: ColorMapType // 预设色带类型,默认 'turbo'
  stretchPercentiles?: [number, number] // 拉伸百分位,默认 [2, 98]
}

export interface GeoTiffInspectionResult {
  url: string
  width: number
  height: number
  bbox: [number, number, number, number]
  geoKeys: any
  epsgCode?: number
  isLikelyDegrees: boolean
  suggestedRectangle?: { west: number; south: number; east: number; north: number }
}

async function transformPointEpsgIo (x: number, y: number, sourceEpsg: number, targetEpsg = 4326): Promise<{ lon: number; lat: number }> {
  // 优先使用 MapTiler(需在 .env 设置 VITE_MAPTILER_KEY),失败则回退 epsg.io
  const key = (import.meta as any).env?.VITE_MAPTILER_KEY
  const fetchWithTimeout = async (input: any, init: any = {}, timeoutMs = 6000) => {
    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), timeoutMs)
    try {
      const res = await fetch(input, { ...init, signal: controller.signal, mode: 'cors', cache: 'no-cache' })
      return res
    } finally {
      clearTimeout(timer)
    }
  }
  if (key) {
    const url = `https://api.maptiler.com/coordinates/transform/${encodeURIComponent(x)},${encodeURIComponent(y)}.json?key=${encodeURIComponent(key)}&s_srs=${encodeURIComponent(sourceEpsg)}&t_srs=${encodeURIComponent(targetEpsg)}`
    const res = await fetchWithTimeout(url)
    // console.log('%c[🚀🚀🚀 res ----geotiff.ts- line 45]', 'color: red;font-size:x-large', res)
    if (!res.ok) {
      throw new Error(`MapTiler 坐标转换失败: ${res.status}`)
    }
    const data = await res.json()
    // 常见返回: { coordinates: [lon, lat] } 或 { x, y } 或 { results: [{ x, y, z }] }
    if (Array.isArray(data?.coordinates) && data.coordinates.length >= 2) {
      return { lon: Number(data.coordinates[0]), lat: Number(data.coordinates[1]) }
    }
    if (typeof data?.x === 'number' && typeof data?.y === 'number') {
      return { lon: data.x, lat: data.y }
    }
    if (Array.isArray(data?.results) && data.results.length > 0) {
      const r = data.results[0]
      if (typeof r?.x === 'number' && typeof r?.y === 'number') {
        return { lon: r.x, lat: r.y }
      }
    }
    throw new Error('MapTiler 返回格式异常')
  }

  const url = `https://epsg.io/trans?x=${encodeURIComponent(x)}&y=${encodeURIComponent(y)}&s_srs=${encodeURIComponent(sourceEpsg)}&t_srs=${encodeURIComponent(targetEpsg)}`
  const res = await fetchWithTimeout(url)
  if (!res.ok) {
    throw new Error(`EPSG.io 请求失败: ${res.status}`)
  }
  const data = await res.json()
  const d = Array.isArray(data) ? data[0] : data
  if (typeof d?.x !== 'number' || typeof d?.y !== 'number') {
    throw new Error('EPSG.io 返回异常')
  }
  return { lon: d.x, lat: d.y }
}

async function transformBboxViaEpsgIo (bbox: [number, number, number, number], sourceEpsg: number): Promise<{ west: number; south: number; east: number; north: number }> {
  const [minX, minY, maxX, maxY] = bbox
  // 已在 WGS84
  if (!sourceEpsg || Number.isNaN(sourceEpsg) || sourceEpsg === 4326) {
    return { west: minX, south: minY, east: maxX, north: maxY }
  }

  // 对于投影坐标(如 UTM 32651),仅转换对角点会低估非线性,
  // 这里加密采样九个点:四角、四边中点、中心点
  const samplePoints: Array<[number, number]> = [
    [minX, minY], // SW
    [maxX, maxY], // NE
    [minX, maxY], // NW
    [maxX, minY], // SE
    [minX, (minY + maxY) / 2], // W mid
    [maxX, (minY + maxY) / 2], // E mid
    [(minX + maxX) / 2, minY], // S mid
    [(minX + maxX) / 2, maxY], // N mid
    [(minX + maxX) / 2, (minY + maxY) / 2] // center
  ]

  try {
    const transformed = await Promise.all(
      samplePoints.map(([x, y]) => transformPointEpsgIo(x, y, sourceEpsg, 4326))
    )
    let west = Infinity
    let south = Infinity
    let east = -Infinity
    let north = -Infinity
    for (const p of transformed) {
      if (Number.isFinite(p.lon) && Number.isFinite(p.lat)) {
        west = Math.min(west, p.lon)
        south = Math.min(south, p.lat)
        east = Math.max(east, p.lon)
        north = Math.max(north, p.lat)
      }
    }
    if (!Number.isFinite(west) || !Number.isFinite(south) || !Number.isFinite(east) || !Number.isFinite(north)) {
      throw new Error('无法计算有效的经纬度包络')
    }
    return { west, south, east, north }
  } catch (err) {
    // eslint-disable-next-line no-console
    console.error(`[GeoTIFF] 在线坐标转换失败,无法可靠得到经纬度范围(EPSG:${sourceEpsg})。`, err)
    throw err
  }
}

// 工具函数:计算百分位值
function computePercentile (arr: ArrayLike<number>, percentile: number, noDataSet: Set<number>): number {
  const validValues: number[] = []
  for (let i = 0; i < arr.length; i++) {
    const v = (arr as any)[i]
    if (Number.isNaN(v) || noDataSet.has(v)) continue
    validValues.push(v)
  }

  // console.log('%c[🚀🚀🚀 百分位计算 ----geotiff.ts]', 'color: cyan;font-size:x-large', {
  //   percentile,
  //   totalValues: arr.length,
  //   validValuesCount: validValues.length,
  //   noDataSet: Array.from(noDataSet),
  //   validValuesSample: validValues.slice(0, 10),
  //   validValuesMin: validValues.length > 0 ? Math.min(...validValues) : 'none',
  //   validValuesMax: validValues.length > 0 ? Math.max(...validValues) : 'none'
  // })

  if (validValues.length === 0) {
    console.warn('[GeoTIFF] 没有有效值用于百分位计算,返回 0')
    return 0
  }

  validValues.sort((a, b) => a - b)
  const index = Math.max(0, Math.min(validValues.length - 1, Math.floor((percentile / 100) * validValues.length)))
  const result = validValues[index]

  // console.log('%c[🚀🚀🚀 百分位结果 ----geotiff.ts]', 'color: cyan;font-size:x-large', {
  //   percentile,
  //   index,
  //   result,
  //   sortedSample: validValues.slice(0, 5).concat(validValues.slice(-5))
  // })

  return result
}

// 工具函数:获取颜色映射函数
function getColorMapFunction (type: ColorMapType, customColorMap?: (value: number) => [number, number, number, number]) {
  if (type === 'custom' && customColorMap) {
    return customColorMap
  }

  const interpolators: Record<Exclude<ColorMapType, 'custom'>, (t: number) => string> = {
    turbo: interpolateTurbo,
    viridis: interpolateViridis,
    plasma: interpolatePlasma,
    inferno: interpolateInferno,
    magma: interpolateMagma
  }

  const interpolator = interpolators[type as Exclude<ColorMapType, 'custom'>] || interpolateTurbo

  return (t: number): [number, number, number, number] => {
    // 处理 NaN 或无效的 t 值
    if (Number.isNaN(t) || !Number.isFinite(t)) {
      console.warn('[GeoTIFF] 颜色映射接收到无效的 t 值:', t, '使用默认值 0.5')
      t = 0.5
    }

    // 确保 t 在 [0, 1] 范围内
    t = Math.min(1, Math.max(0, t))

    const color = interpolator(t)

    // 调试信息 - 总是打印前几次调用
    if (Math.random() < 0.01) { // 增加打印频率到 1%
      // console.log('%c[🚀🚀🚀 颜色映射调试 ----geotiff.ts]', 'color: magenta;font-size:x-large', {
      //   t,
      //   rawColor: color,
      //   colorType: typeof color,
      //   colorLength: color?.length
      // })
    }

    // 将颜色转换为 RGB
    let r: number, g: number, b: number

    if (typeof color === 'string') {
      if (color.startsWith('rgb(')) {
        // 处理 rgb(r, g, b) 格式
        const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
        if (rgbMatch) {
          r = parseInt(rgbMatch[1], 10)
          g = parseInt(rgbMatch[2], 10)
          b = parseInt(rgbMatch[3], 10)
        } else {
          console.error('[GeoTIFF] rgb 颜色格式解析失败:', color)
          return [255, 0, 255, 255] // 返回洋红色作为错误指示
        }
      } else if (color.startsWith('#')) {
        // 处理 #RRGGBB 格式
        const hex = color.substring(1)
        if (hex.length < 6) {
          console.error('[GeoTIFF] hex 颜色长度不足:', hex, '长度:', hex.length)
          return [255, 0, 255, 255] // 返回洋红色作为错误指示
        }
        r = parseInt(hex.substring(0, 2), 16)
        g = parseInt(hex.substring(2, 4), 16)
        b = parseInt(hex.substring(4, 6), 16)
      } else {
        console.error('[GeoTIFF] 不支持的颜色格式:', color)
        return [255, 0, 255, 255] // 返回洋红色作为错误指示
      }
    } else {
      console.error('[GeoTIFF] 颜色映射返回了无效的颜色格式:', color)
      return [255, 0, 255, 255] // 返回洋红色作为错误指示
    }

    // 检查解析结果
    if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
      console.error('[GeoTIFF] 颜色解析失败:', {
        r,
        g,
        b,
        originalColor: color
      })
      return [255, 0, 255, 255] // 返回洋红色作为错误指示
    }

    return [r, g, b, 255]
  }
}

// 获取 GeoTIFF Image;优先使用 fromBlob,避免 Range 请求;失败再逐步回退
async function getTiffImageWithFallback (url: string) {
  // 1) try fromBlob first
  try {
    const resp = await fetch(url, { cache: 'no-cache' })
    if (!resp.ok) throw new Error(`获取 Blob 失败: ${resp.status}`)
    const blob = await resp.blob()
    const tiff = await fromBlob(blob)
    return await tiff.getImage()
  } catch (blobErr) {
    // eslint-disable-next-line no-console
    console.warn('[GeoTIFF] fromBlob 失败,尝试 fromArrayBuffer', blobErr)
    try {
      const resp = await fetch(url, { cache: 'no-cache' })
      if (!resp.ok) throw new Error(`获取 ArrayBuffer 失败: ${resp.status}`)
      const buf = await resp.arrayBuffer()
      const tiff = await fromArrayBuffer(buf)
      return await tiff.getImage()
    } catch (bufErr) {
      // eslint-disable-next-line no-console
      console.warn('[GeoTIFF] fromArrayBuffer 失败,最后尝试 fromUrl', bufErr)
      const tiff = await fromUrl(url)
      return await tiff.getImage()
    }
  }
}

/**
 * 将 GeoTIFF 以单张影像的形式叠加到 Cesium,适合中小分辨率或快速预览
 * - 优先读取 GeoTIFF 内的地理范围;若无则使用外部传入的 rectangle
 * - 支持单波段伪彩映射
 */
export async function addGeoTiffAsSingleTile (viewer: any, options: AddGeoTiffOptions) {
  const {
    url,
    rectangle,
    band,
    noData,
    colorMap,
    colorMapType = 'turbo',
    stretchPercentiles = [2, 98]
  } = options
  // console.log('%c[🚀🚀🚀 Prediction.vue ----geotiff.ts- line 20]', 'color: red;font-size:x-large', url)
  const image = await getTiffImageWithFallback(url)

  const [west, south, east, north] = image.getBoundingBox() // [minX, minY, maxX, maxY]
  const code =
  image.geoKeys.ProjectedCSTypeGeoKey ||
  image.geoKeys.GeographicTypeGeoKey
  // 参考: 直接从 GeoTIFF 获取 bbox 并贴图的做法,见文中示例
  // console.log('%c[🚀🚀🚀 Prediction.vue ----geotiff.ts- line 38]', 'color: red;font-size:x-large', west, south, east, north)
  // console.log('%c[🚀🚀🚀 Prediction.vue ----geotiff.ts- line 39]', 'color: red;font-size:x-large', code)
  // https://download.csdn.net/blog/column/11250060/122294171
  // console.log('%c[🚀🚀🚀 image ----geotiff.ts- line 134]', 'color: red;font-size:x-large', image)

  // 计算经纬度 rectangle(若没传入)
  let rectangleWgs84: { west: number; south: number; east: number; north: number }
  try {
    rectangleWgs84 = await transformBboxViaEpsgIo([west, south, east, north], code)
  } catch (e) {
    if (rectangle) {
      // 使用调用方提供的范围作为兜底
      rectangleWgs84 = rectangle
    } else {
      throw e
    }
  }
  // console.log('%c[🚀🚀🚀 Prediction.vue ----geotiff.ts- line 121]', 'color: red;font-size:x-large', rectangleWgs84)

  // 读取像素信息
  const rasters = await image.readRasters()
  // console.log('%c[🚀🚀🚀 Prediction.vue ----geotiff.ts- line 142]', 'color: red;font-size:x-large', rasters)

  const width = image.getWidth()
  const height = image.getHeight()
  // console.log('%c[🚀🚀🚀 Prediction.vue ----geotiff.ts- line 132]', 'color: red;font-size:x-large', width, height)

  // 若为投影坐标(如 UTM 32651)且为单波段数据,尝试客户端重投影到 WGS84 规则网格
  if (typeof code === 'number' && code !== 4326 && (rasters?.length === 1)) {
    const proj4 = await getProj4()
    if (proj4) {
      ensureEpsgDefinition(proj4, code)
      const srcCrs = `EPSG:${code}`
      const dstCrs = 'EPSG:4326'
      const band0: any = rasters[0]

      // 输出分辨率(提升清晰度):允许适度上采样至原图 2x,且不超过 8192
      const maxSize = 8192
      const qualityScale = 3 // 可视需要调整为 1.5~3
      const outWidth = Math.min(maxSize, Math.round(width * qualityScale))
      const outHeight = Math.min(maxSize, Math.round(height * qualityScale))

      const canvasRe = document.createElement('canvas')
      canvasRe.width = outWidth
      canvasRe.height = outHeight
      const ctxRe = canvasRe.getContext('2d')!
      const outData = ctxRe.createImageData(outWidth, outHeight)

      // 使用 GeoTIFF 仿射参数进行像素<->投影坐标互转,尽量还原形状
      const gt = getGeoTransform(image)

      // noData 处理
      const noDataSet = new Set<number>()
      if (noData != null) {
        if (Array.isArray(noData)) noData.forEach(v => noDataSet.add(v))
        else noDataSet.add(noData)
      }

      // 拉伸范围
      let minVal = computePercentile(band0, stretchPercentiles[0], noDataSet)
      let maxVal = computePercentile(band0, stretchPercentiles[1], noDataSet)
      if (minVal === maxVal) {
        const validValues = Array.from(band0).filter((v: any) => !noDataSet.has(v) && !Number.isNaN(v)) as number[]
        if (validValues.length > 0) {
          const u = validValues[0]
          minVal = u - 0.1
          maxVal = u + 0.1
        }
      }
      const colorMapFunc = getColorMapFunction(colorMapType, colorMap)

      for (let j = 0; j < outHeight; j++) {
        const v = j / (outHeight - 1)
        const lat = rectangleWgs84.north * (1 - v) + rectangleWgs84.south * v
        for (let i = 0; i < outWidth; i++) {
          const u = i / (outWidth - 1)
          const lon = rectangleWgs84.west * (1 - u) + rectangleWgs84.east * u
          let srcXY: [number, number]
          try {
            srcXY = proj4(dstCrs, srcCrs, [lon, lat]) as [number, number]
          } catch {
            const idx = (j * outWidth + i) * 4
            outData.data[idx + 3] = 0
            continue
          }
          // 投影坐标 → 源像素
          const pr = gt.mapModelToPixel(srcXY[0], srcXY[1])
          const srcCol = Math.round(pr.col)
          const srcRow = Math.round(pr.row)
          const outIdx = (j * outWidth + i) * 4
          if (srcCol < 0 || srcCol >= width || srcRow < 0 || srcRow >= height) {
            outData.data[outIdx + 3] = 0
            continue
          }
          // 双线性采样(在源像素网格上插值)
          const fx = pr.col
          const fy = pr.row
          const x0 = Math.floor(fx)
          const y0 = Math.floor(fy)
          const x1 = Math.min(width - 1, x0 + 1)
          const y1 = Math.min(height - 1, y0 + 1)
          const tx = fx - x0
          const ty = fy - y0
          const idx00 = y0 * width + x0
          const idx10 = y0 * width + x1
          const idx01 = y1 * width + x0
          const idx11 = y1 * width + x1
          const v00 = band0[idx00]
          const v10 = band0[idx10]
          const v01 = band0[idx01]
          const v11 = band0[idx11]
          // 若邻域包含无效值,则退化为最近邻
          let value: number
          if (
            noDataSet.has(v00) || Number.isNaN(v00) ||
            noDataSet.has(v10) || Number.isNaN(v10) ||
            noDataSet.has(v01) || Number.isNaN(v01) ||
            noDataSet.has(v11) || Number.isNaN(v11)
          ) {
            const nnCol = Math.round(fx)
            const nnRow = Math.round(fy)
            const nnIdx = nnRow * width + nnCol
            value = band0[nnIdx]
            if (noDataSet.has(value) || Number.isNaN(value)) {
              outData.data[outIdx + 3] = 0
              continue
            }
          } else {
            const v0 = v00 * (1 - tx) + v10 * tx
            const v1 = v01 * (1 - tx) + v11 * tx
            value = v0 * (1 - ty) + v1 * ty
          }
          const norm = maxVal === minVal ? 0.5 : Math.min(1, Math.max(0, (value - minVal) / (maxVal - minVal)))
          const [rr, gg, bb, aa] = colorMapFunc(norm)
          outData.data[outIdx + 0] = rr
          outData.data[outIdx + 1] = gg
          outData.data[outIdx + 2] = bb
          outData.data[outIdx + 3] = aa
        }
      }

      ctxRe.putImageData(outData, 0, 0)
      const duRe = canvasRe.toDataURL()
      return {
        url: duRe,
        rectangle: Cesium.Rectangle.fromDegrees(rectangleWgs84.west, rectangleWgs84.south, rectangleWgs84.east, rectangleWgs84.north)
      }
    }
  }

  const canvas = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  const ctx = canvas.getContext('2d')!
  const imageData = ctx.createImageData(width, height)

  // 处理 noData 值
  const noDataSet = new Set<number>()
  if (noData != null) {
    if (Array.isArray(noData)) {
      noData.forEach(v => noDataSet.add(v))
    } else {
      noDataSet.add(noData)
    }
  }

  console.time('写入像素')

  // console.log('%c[🚀🚀🚀 rasters.length ----geotiff.ts- line 221]', 'color: red;font-size:x-large', rasters.length)
  if (rasters.length === 1) {
    // 单波段伪彩色映射
    const band0: any = rasters[0]
    // console.log('%c[🚀🚀🚀 单波段数据 ----geotiff.ts]', 'color: blue;font-size:x-large', {
    //   band0Type: typeof band0,
    //   band0Length: band0?.length,
    //   band0Constructor: band0?.constructor?.name,
    //   first10Values: band0?.slice ? band0.slice(0, 10) : 'no slice method',
    //   sampleValues: Array.from(band0).slice(0, 20)
    // })

    // 计算有效值用于调试
    const validValues = Array.from(band0).filter((v: any) => !noDataSet.has(v) && !Number.isNaN(v)) as number[]

    // 计算拉伸范围(使用百分位避免极值影响)
    let minVal = computePercentile(band0, stretchPercentiles[0], noDataSet)
    let maxVal = computePercentile(band0, stretchPercentiles[1], noDataSet)

    // 如果所有有效值都相同,使用不同的拉伸策略
    if (minVal === maxVal) {
      // console.log('%c[🚀🚀🚀 数据值范围问题 ----geotiff.ts]', 'color: red;font-size:x-large', {
      //   message: '所有有效值都相同,使用替代拉伸策略',
      //   minVal,
      //   maxVal,
      //   validValuesCount: validValues.length
      // })

      // 策略1:如果只有一个值,使用固定颜色
      if (validValues.length > 0) {
        const uniqueValue = validValues[0]
        minVal = uniqueValue - 0.1 // 稍微扩展范围
        maxVal = uniqueValue + 0.1
        // console.log('%c[🚀🚀🚀 使用扩展范围 ----geotiff.ts]', 'color: yellow;font-size:x-large', {
        //   originalValue: uniqueValue,
        //   newMinVal: minVal,
        //   newMaxVal: maxVal
        // })
      }
    }

    // 计算实际的最小最大值用于调试
    const band0Min = validValues.length > 0 ? Math.min(...validValues) : 0
    const band0Max = validValues.length > 0 ? Math.max(...validValues) : 0

    // console.log('%c[🚀🚀🚀 拉伸范围计算 ----geotiff.ts]', 'color: green;font-size:x-large', {
    //   minVal,
    //   maxVal,
    //   stretchPercentiles,
    //   noDataSet: Array.from(noDataSet),
    //   band0Min,
    //   band0Max,
    //   validValuesCount: validValues.length
    // })

    // 获取颜色映射函数
    const colorMapFunc = getColorMapFunction(colorMapType, colorMap)
    // console.log('%c[🚀🚀🚀 颜色映射函数 ----geotiff.ts]', 'color: purple;font-size:x-large', {
    //   colorMapType,
    //   hasCustomColorMap: !!colorMap,
    //   testColor: colorMapFunc(0.5) // 测试中间值颜色
    // })

    let processedPixels = 0
    let noDataPixels = 0
    const colorSamples: Array<{value: number, normalized: number, color: [number, number, number, number]}> = []

    for (let i = 0; i < band0.length; i++) {
      const value = band0[i]

      // 处理 noData 值
      if (noDataSet.has(value) || Number.isNaN(value)) {
        imageData.data[i * 4 + 0] = 0
        imageData.data[i * 4 + 1] = 0
        imageData.data[i * 4 + 2] = 0
        imageData.data[i * 4 + 3] = 0 // 透明
        noDataPixels++
        continue
      }

      // 归一化到 [0, 1]
      let normalized: number
      if (maxVal === minVal) {
        // 如果所有有效值都相同,使用固定值
        normalized = 0.5 // 使用中间值
        // console.log('%c[🚀🚀🚀 警告:所有有效值相同 ----geotiff.ts]', 'color: orange;font-size:x-large', {
        //   value,
        //   minVal,
        //   maxVal,
        //   normalized
        // })
      } else {
        normalized = Math.min(1, Math.max(0, (value - minVal) / (maxVal - minVal)))
      }

      // 应用颜色映射
      const [r, g, b, a] = colorMapFunc(normalized)
      imageData.data[i * 4 + 0] = r
      imageData.data[i * 4 + 1] = g
      imageData.data[i * 4 + 2] = b
      imageData.data[i * 4 + 3] = a

      processedPixels++

      // 收集一些样本用于调试
      if (colorSamples.length < 10 && i % Math.floor(band0.length / 20) === 0) {
        colorSamples.push({ value, normalized, color: [r, g, b, a] })
      }

      // 强制收集前几个有效像素的样本
      if (colorSamples.length < 5 && processedPixels <= 5) {
        colorSamples.push({ value, normalized, color: [r, g, b, a] })
      }
    }

    // console.log('%c[🚀🚀🚀 像素处理结果 ----geotiff.ts]', 'color: orange;font-size:x-large', {
    //   totalPixels: band0.length,
    //   processedPixels,
    //   noDataPixels,
    //   colorSamples
    // })
  } else {
    // 多波段 RGB 处理
    const red: any = rasters[0] || []
    const green: any = rasters[1] || red
    const blue: any = rasters[2] || red

    for (let i = 0; i < imageData.data.length / 4; i++) {
      const r = red[i] || 0
      const g = green[i] || 0
      const b = blue[i] || 0
      imageData.data[i * 4 + 0] = r
      imageData.data[i * 4 + 1] = g
      imageData.data[i * 4 + 2] = b
      imageData.data[i * 4 + 3] = 255
    }
  }

  ctx.putImageData(imageData, 0, 0)

  // console.timeEnd('写入像素')

  const du = canvas.toDataURL()
  return {
    url: du,
    rectangle: Cesium.Rectangle.fromDegrees(rectangleWgs84.west, rectangleWgs84.south, rectangleWgs84.east, rectangleWgs84.north),
  }
  // // 预览图像
  // const img = document.createElement('img')
  // img.src = du
  // img.style.position = 'fixed'
  // img.style.right = '20px'
  // img.style.bottom = '20px'
  // img.style.width = '200px'
  // img.style.border = '2px solid #fff'
  // img.style.borderRadius = '4px'
  // img.style.zIndex = '999'
  // document.body.appendChild(img)

  // console.log('%c[🚀🚀🚀 geotiff ----geotiff.ts- line 150]', 'color: red;font-size:x-large', du)

  // viewer.imageryLayers.addImageryProvider(
  //   new Cesium.SingleTileImageryProvider({
  //     url: du,
  //     rectangle: Cesium.Rectangle.fromDegrees(rectangleWgs84.west, rectangleWgs84.south, rectangleWgs84.east, rectangleWgs84.north),
  //   })
  // )
  // viewer.camera.setView({
  //   destination: Cesium.Rectangle.fromDegrees(rectangleWgs84.west, rectangleWgs84.south, rectangleWgs84.east, rectangleWgs84.north),
  // })
}
/**
 * 针对项目内的 Bloom.tif 的便捷加载器
 */
export async function addBloomTif (viewer: any, url: string) {
  // 使用打包友好的方式解析资源 URL
  // const url = '../static/t1.tif' // public 根下
  // 若该 TIF 没有 GeoTransform,可在此指定范围
  return addGeoTiffAsSingleTile(viewer, {
    url,
    colorMapType: 'turbo', // 使用 Turbo 色带
    stretchPercentiles: [2, 98], // 使用 2%-98% 百分位拉伸
    noData: 0 // 假设 0 是无效值
  })
}

/**
 * 便捷函数:使用不同色带加载单波段 TIFF
 */
export async function addGeoTiffWithColorMap (viewer: any, url: string, colorMapType: ColorMapType = 'turbo') {
  return addGeoTiffAsSingleTile(viewer, {
    url,
    colorMapType,
    stretchPercentiles: [2, 98],
    noData: 0
  })
}

/**
 * 测试颜色映射函数 - 用于调试
 */
export function testColorMapping () {
  // console.log('%c[🚀🚀🚀 测试颜色映射 ----geotiff.ts]', 'color: red;font-size:x-large', '开始测试颜色映射')

  const testValues = [0, 0.25, 0.5, 0.75, 1.0]
  const colorMaps: ColorMapType[] = ['turbo', 'viridis', 'plasma', 'inferno', 'magma']

  colorMaps.forEach(colorMapType => {
    console.log(`\n=== 测试 ${colorMapType} 色带 ===`)
    const colorMapFunc = getColorMapFunction(colorMapType)

    testValues.forEach(t => {
      const [r, g, b, a] = colorMapFunc(t)
      console.log(`t=${t}: RGB(${r}, ${g}, ${b}, ${a})`)
    })
  })

  // 测试自定义颜色映射
  console.log('\n=== 测试自定义颜色映射 ===')
  const customColorMap = (t: number): [number, number, number, number] => {
    return [Math.round(255 * t), Math.round(255 * (1 - t)), 0, 255]
  }
  const customFunc = getColorMapFunction('custom', customColorMap)

  testValues.forEach(t => {
    const [r, g, b, a] = customFunc(t)
    console.log(`t=${t}: RGB(${r}, ${g}, ${b}, ${a})`)
  })
}

/**
 * 测试单值数据的颜色映射 - 专门针对你的问题
 */
export function testSingleValueColorMapping () {
  // console.log('%c[🚀🚀🚀 测试单值数据颜色映射 ----geotiff.ts]', 'color: red;font-size:x-large', '开始测试单值数据')

  // 模拟你的数据情况:所有有效值都是 1
  const testValue = 1
  const minVal = 1
  const maxVal = 1

  console.log('数据情况:', { testValue, minVal, maxVal })

  // 测试修复后的归一化逻辑
  let normalized: number
  if (maxVal === minVal) {
    normalized = 0.5 // 使用中间值
    console.log('使用固定归一化值:', normalized)
  } else {
    normalized = (testValue - minVal) / (maxVal - minVal)
    console.log('计算归一化值:', normalized)
  }

  // 测试颜色映射
  const colorMapFunc = getColorMapFunction('turbo')
  const [r, g, b, a] = colorMapFunc(normalized)

  console.log('最终颜色:', { normalized, r, g, b, a })

  // 测试扩展范围的情况
  const extendedMinVal = testValue - 0.1
  const extendedMaxVal = testValue + 0.1
  const extendedNormalized = (testValue - extendedMinVal) / (extendedMaxVal - extendedMinVal)
  const [r2, g2, b2, a2] = colorMapFunc(extendedNormalized)

  console.log('扩展范围后:', {
    extendedMinVal,
    extendedMaxVal,
    extendedNormalized,
    r2,
    g2,
    b2,
    a2
  })
}

/**
 * 测试 d3-scale-chromatic 是否正常工作
 */
export function testD3ScaleChromatic () {
  // console.log('%c[🚀🚀🚀 测试 d3-scale-chromatic ----geotiff.ts]', 'color: red;font-size:x-large', '开始测试 d3-scale-chromatic')

  try {
    // 直接测试 d3 函数
    console.log('直接测试 interpolateTurbo:')
    console.log('interpolateTurbo(0):', interpolateTurbo(0))
    console.log('interpolateTurbo(0.5):', interpolateTurbo(0.5))
    console.log('interpolateTurbo(1):', interpolateTurbo(1))

    console.log('直接测试 interpolateViridis:')
    console.log('interpolateViridis(0):', interpolateViridis(0))
    console.log('interpolateViridis(0.5):', interpolateViridis(0.5))
    console.log('interpolateViridis(1):', interpolateViridis(1))

    // 测试我们的颜色映射函数
    console.log('测试我们的颜色映射函数:')
    const colorMapFunc = getColorMapFunction('turbo')
    console.log('colorMapFunc(0):', colorMapFunc(0))
    console.log('colorMapFunc(0.5):', colorMapFunc(0.5))
    console.log('colorMapFunc(1):', colorMapFunc(1))
  } catch (error) {
    console.error('d3-scale-chromatic 测试失败:', error)
  }
}