const fs = require('fs'); const path = require('path'); const PNG = require('pngjs').PNG; const { createCanvas, loadImage } = require('canvas'); const sharp = require('sharp'); // 配置参数 // const WIDTH = 360; // 经度点数 // const HEIGHT = 181; // 纬度点数 // 配置 const TILE_SIZE = 256; // 瓦片大小 const MAX_ZOOM = 5; // 最大缩放级别 const TILES_DIR = 'F:/tiles/cloud'; // 瓦片输出目录 const MISSING_VALUE_THRESHOLD = 9.9e+19; // 空值填充值 // 增加Node.js内存限制 const MAX_MEMORY = 8192; if (process.env.NODE_OPTIONS === undefined) { process.env.NODE_OPTIONS = `--max-old-space-size=${MAX_MEMORY}`; } // 创建单张云层图 async function createCloudImage(inputFile, outputFile, rgbVar, type) { // 读取文件 const data = fs.readFileSync(inputFile, 'utf8'); const lines = data.split('\n').filter(line => line.trim() !== ''); const { r, g, b } = rgbVar; const needReCalcValue = ['DPT', 'HINDEX', 'ICETMP', 'TMAX', 'TMIN', 'TMP', 'TSOIL'].some(ele => { return ele.toLowerCase() === type; }); // 跳过第一行的尺寸信息,直接提取数据值 const values = []; let max = parseFloat(lines[1]); let min = parseFloat(lines[1]); for (let i = 1; i < lines.length; i++) { const value = parseFloat(lines[i]); if (!isNaN(value)) { values.push(needReCalcValue ? value - 273.15 : value); if (value > max) { max = value; } if (value < min) { min = value; } } } const WIDTH = lines[0].split(" ")[0]; const HEIGHT = lines[0].split(" ")[1]; // 创建PNG图像 const png = new PNG({ width: WIDTH, height: HEIGHT, filterType: -1 }); // 将数据映射到图像像素 for (let y = 0; y < HEIGHT; y++) { for (let x = 0; x < WIDTH; x++) { const idx = (WIDTH * y + x) * 4; const dataIdx = y * WIDTH + x; if (dataIdx < values.length) { const itemValue = values[dataIdx]; if (type === "cloud") { // 将云量值(0-100)映射到透明度(0-255) // 云量越高,越不透明(白色) const alpha = Math.round((itemValue / 100) * 255); png.data[idx] = r; // R png.data[idx + 1] = g; // G png.data[idx + 2] = b; // B png.data[idx + 3] = alpha; // A (透明度) } else if (type === "rain") { const bis = parseInt(((itemValue * 1000) / (max * 1000)) * 100); const alpha = Math.round((bis / 100) * 255); png.data[idx] = r; // R png.data[idx + 1] = g; // G png.data[idx + 2] = b; // B png.data[idx + 3] = alpha; // A (透明度) } else if (type === "tmp") { const { bis, vR, vG, vB } = getVarBis(itemValue); const alpha = Math.round((bis / 100) * 255); png.data[idx] = vR; // R png.data[idx + 1] = vG; // G png.data[idx + 2] = vB; // B png.data[idx + 3] = alpha; // A (透明度) } } else { // 数据不足时使用透明像素 png.data[idx] = 0; png.data[idx + 1] = 0; png.data[idx + 2] = 0; png.data[idx + 3] = 0; } } } // 保存PNG文件 png.pack().pipe(fs.createWriteStream(outputFile)) .on('finish', () => { console.log(`PNG图像已保存到 ${outputFile}`); // console.log('图像尺寸:', WIDTH, 'x', HEIGHT); }) .on('error', (err) => { console.error('保存图像时出错:', err); }); } const tempColors = [ { dy: -9999, xy: -20, rgb: [49, 54, 149] }, // 深蓝 { dy: -20, xy: -10, rgb: [69, 117, 180] }, // 蓝 { dy: -10, xy: 0, rgb: [116, 173, 209] }, // 浅蓝 { dy: 0, xy: 10, rgb: [171, 217, 233] }, // 更浅蓝 { dy: 10, xy: 20, rgb: [224, 243, 248] }, // 非常浅蓝 { dy: 20, xy: 25, rgb: [255, 255, 191] }, // 浅黄(过渡色) { dy: 25, xy: 30, rgb: [254, 224, 144] }, // 浅橙(分界点) { dy: 30, xy: 40, rgb: [253, 174, 97] }, // 橙色 { dy: 40, xy: 50, rgb: [244, 109, 67] }, // 橙红色 { dy: 50, xy: 9999, rgb: [215, 48, 39] } // 红色 ]; function getVarBis(value) { let findColor = tempColors.find(ele => { return value > ele.dy && value < ele.xy; }); let max = 0; if (findColor.dy === -9999 || findColor.xy === 9999) { max = value; } else { max = findColor.dy > findColor.xy ? findColor.dy : findColor.xy; } let bis = Math.abs(parseInt(value / max * 100)); bis = bis > 100 ? 100 : bis; return { bis: 100, vR: parseInt(findColor.rgb[0] * (bis / 100)), vG: parseInt(findColor.rgb[1] * (bis / 100)), vB: parseInt(findColor.rgb[2] * (bis / 100)), }; } // 创建金字塔云层图 async function createCloudImageDeep(file) { try { // 解析数据 const { lonCount, latCount, cloudData } = parseDataFile(file); // 创建输出目录 if (!fs.existsSync(TILES_DIR)) { fs.mkdirSync(TILES_DIR, { recursive: true }); } // 为每个缩放级别生成瓦片 for (let zoom = 0; zoom <= MAX_ZOOM; zoom++) { await generateTilesForZoom(zoom, lonCount, latCount, cloudData); } console.log('所有瓦片生成完成!'); } catch (error) { console.error('处理过程中发生错误:', error); } } // 解析数据文件 function parseDataFile(file) { console.log('正在解析数据文件...'); const data = fs.readFileSync(file, 'utf8').split('\n'); // 解析网格尺寸 const [lonCount, latCount] = data[0].split(' ').map(Number); console.log(`网格尺寸: ${lonCount} × ${latCount}`); // 解析云量数据并过滤缺测值 const cloudData = new Float32Array(lonCount * latCount); let missingCount = 0; let index = 0; for (let i = 1; i < data.length && index < cloudData.length; i++) { if (data[i].trim() !== '') { const value = parseFloat(data[i].trim()); // 过滤缺测值 if (value > MISSING_VALUE_THRESHOLD) { cloudData[index++] = NaN; missingCount++; } else { cloudData[index++] = value; } } } console.log(`成功读取 ${index} 个数据点,其中 ${missingCount} 个缺测值`); return { lonCount, latCount, cloudData }; } // 高精度经纬度到瓦片坐标转换 function lonLatToTile(lon, lat, zoom) { // 确保经度在[0, 360)范围内 const normalizedLon = ((lon % 360) + 360) % 360; const x = normalizedLon / 360 * Math.pow(2, zoom); const tileX = Math.floor(x); // 使用高精度墨卡托投影公式 const latRad = lat * Math.PI / 180; const y = (1 - Math.asinh(Math.tan(latRad)) / Math.PI) / 2 * Math.pow(2, zoom); const tileY = Math.floor(y); return { x: tileX, y: tileY }; } // 高精度瓦片坐标到经纬度范围 function tileToLonLat(x, y, zoom) { const n = Math.pow(2, zoom); // 计算经度范围 const lon_west = x / n * 360; const lon_east = (x + 1) / n * 360; // 计算纬度范围 (使用高精度反墨卡托投影) const lat_north = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI; const lat_south = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI; return { west: lon_west, east: lon_east, north: lat_north, south: lat_south }; } // 生成单个瓦片 - 使用直接数据映射方法 async function generateTile(zoom, x, y, lonCount, latCount, cloudData) { const tileBounds = tileToLonLat(x, y, zoom); const imageData = Buffer.alloc(TILE_SIZE * TILE_SIZE * 4); // 计算每个像素对应的经纬度增量 const lonIncrement = (tileBounds.east - tileBounds.west) / TILE_SIZE; const latIncrement = (tileBounds.north - tileBounds.south) / TILE_SIZE; // 预先计算数据网格的参数 const lonStep = 360 / lonCount; const latStep = 180 / (latCount - 1); for (let py = 0; py < TILE_SIZE; py++) { // 计算当前像素行的纬度 const lat = tileBounds.north - py * latIncrement; for (let px = 0; px < TILE_SIZE; px++) { // 计算当前像素列的经度 let lon = tileBounds.west + px * lonIncrement; // 确保经度在[0, 360)范围内 lon = ((lon % 360) + 360) % 360; // 计算数据网格索引 const lonIdx = Math.floor(lon / lonStep); const latIdx = Math.floor((90 - lat) / latStep); // 确保索引在有效范围内 const safeLonIdx = Math.min(Math.max(0, lonIdx), lonCount - 1); const safeLatIdx = Math.min(Math.max(0, latIdx), latCount - 1); // 获取云量值 let cloudValue = 0; const dataIdx = safeLatIdx * lonCount + safeLonIdx; if (dataIdx >= 0 && dataIdx < cloudData.length) { const value = cloudData[dataIdx]; // 过滤缺测值 if (isNaN(value) || value > MISSING_VALUE_THRESHOLD) { cloudValue = 0; } else { cloudValue = value; } } // 将云量值映射到RGBA const alpha = Math.min(255, Math.max(0, Math.round(cloudValue * 2.55))); const idx = (py * TILE_SIZE + px) * 4; imageData[idx] = 255; // R imageData[idx + 1] = 255; // G imageData[idx + 2] = 255; // B imageData[idx + 3] = alpha; // A (透明度) } } // 创建瓦片目录 const tileDir = path.join(TILES_DIR, `${zoom}`, `${x}`); fs.mkdirSync(tileDir, { recursive: true }); // 保存瓦片图像 await sharp(imageData, { raw: { width: TILE_SIZE, height: TILE_SIZE, channels: 4 } }) .png() .toFile(path.join(tileDir, `${y}.png`)); } // 生成指定缩放级别的所有瓦片 async function generateTilesForZoom(zoom, lonCount, latCount, cloudData) { console.log(`正在生成缩放级别 ${zoom} 的瓦片...`); const tileCount = Math.pow(2, zoom); let generated = 0; const totalTiles = tileCount * tileCount; // 使用更低的并发数以减少内存使用 const concurrency = 3; const queue = []; for (let x = 0; x < tileCount; x++) { for (let y = 0; y < tileCount; y++) { // 将任务加入队列 queue.push(() => generateTile(zoom, x, y, lonCount, latCount, cloudData)); // 当队列达到并发数量时,执行一批任务 if (queue.length >= concurrency) { await Promise.all(queue.map(fn => fn())); queue.length = 0; // 清空队列 generated += concurrency; // 显示进度 const progress = ((generated / totalTiles) * 100).toFixed(1); console.log(`缩放级别 ${zoom}: ${progress}% 完成 (${generated}/${totalTiles})`); // 手动触发垃圾回收 if (global.gc) { global.gc(); } } } } // 处理队列中剩余的任务 if (queue.length > 0) { await Promise.all(queue.map(fn => fn())); generated += queue.length; console.log(`缩放级别 ${zoom}: 100% 完成 (${generated}/${totalTiles})`); } console.log(`缩放级别 ${zoom} 完成,生成 ${tileCount * tileCount} 个瓦片`); } // 主函数 async function main() { try { // 解析数据 const { lonCount, latCount, cloudData } = parseDataFile(); // 创建输出目录 if (!fs.existsSync(TILES_DIR)) { fs.mkdirSync(TILES_DIR, { recursive: true }); } // 为每个缩放级别生成瓦片 for (let zoom = 0; zoom <= MAX_ZOOM; zoom++) { await generateTilesForZoom(zoom, lonCount, latCount, cloudData); } console.log('所有瓦片生成完成!'); } catch (error) { console.error('处理过程中发生错误:', error); } } module.exports = { createCloudImage, createCloudImageDeep }