| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- 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 }
|