更新: 引入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颜色插值,显示彩色的图像
封装的方法
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)
}
}