createCloudImage_old.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. const fs = require('fs');
  2. const path = require('path');
  3. const PNG = require('pngjs').PNG;
  4. const { createCanvas, loadImage } = require('canvas');
  5. const sharp = require('sharp');
  6. // 配置参数
  7. // const WIDTH = 360; // 经度点数
  8. // const HEIGHT = 181; // 纬度点数
  9. // 配置
  10. const TILE_SIZE = 256; // 瓦片大小
  11. const MAX_ZOOM = 5; // 最大缩放级别
  12. const TILES_DIR = 'F:/tiles/cloud'; // 瓦片输出目录
  13. const MISSING_VALUE_THRESHOLD = 9.9e+19; // 空值填充值
  14. // 增加Node.js内存限制
  15. const MAX_MEMORY = 8192;
  16. if (process.env.NODE_OPTIONS === undefined) {
  17. process.env.NODE_OPTIONS = `--max-old-space-size=${MAX_MEMORY}`;
  18. }
  19. // 创建单张云层图
  20. async function createCloudImage(inputFile, outputFile, rgbVar, type) {
  21. // 读取文件
  22. const data = fs.readFileSync(inputFile, 'utf8');
  23. const lines = data.split('\n').filter(line => line.trim() !== '');
  24. const { r, g, b } = rgbVar;
  25. const needReCalcValue = ['DPT', 'HINDEX', 'ICETMP', 'TMAX', 'TMIN', 'TMP', 'TSOIL'].some(ele => {
  26. return ele.toLowerCase() === type;
  27. });
  28. // 跳过第一行的尺寸信息,直接提取数据值
  29. const values = [];
  30. let max = parseFloat(lines[1]);
  31. let min = parseFloat(lines[1]);
  32. for (let i = 1; i < lines.length; i++) {
  33. const value = parseFloat(lines[i]);
  34. if (!isNaN(value)) {
  35. values.push(needReCalcValue ? value - 273.15 : value);
  36. if (value > max) {
  37. max = value;
  38. }
  39. if (value < min) {
  40. min = value;
  41. }
  42. }
  43. }
  44. const WIDTH = lines[0].split(" ")[0];
  45. const HEIGHT = lines[0].split(" ")[1];
  46. // 创建PNG图像
  47. const png = new PNG({
  48. width: WIDTH,
  49. height: HEIGHT,
  50. filterType: -1
  51. });
  52. // 将数据映射到图像像素
  53. for (let y = 0; y < HEIGHT; y++) {
  54. for (let x = 0; x < WIDTH; x++) {
  55. const idx = (WIDTH * y + x) * 4;
  56. const dataIdx = y * WIDTH + x;
  57. if (dataIdx < values.length) {
  58. const itemValue = values[dataIdx];
  59. if (type === "cloud") {
  60. // 将云量值(0-100)映射到透明度(0-255)
  61. // 云量越高,越不透明(白色)
  62. const alpha = Math.round((itemValue / 100) * 255);
  63. png.data[idx] = r; // R
  64. png.data[idx + 1] = g; // G
  65. png.data[idx + 2] = b; // B
  66. png.data[idx + 3] = alpha; // A (透明度)
  67. } else if (type === "rain") {
  68. const bis = parseInt(((itemValue * 1000) / (max * 1000)) * 100);
  69. const alpha = Math.round((bis / 100) * 255);
  70. png.data[idx] = r; // R
  71. png.data[idx + 1] = g; // G
  72. png.data[idx + 2] = b; // B
  73. png.data[idx + 3] = alpha; // A (透明度)
  74. } else if (type === "tmp") {
  75. const { bis, vR, vG, vB } = getVarBis(itemValue);
  76. const alpha = Math.round((bis / 100) * 255);
  77. png.data[idx] = vR; // R
  78. png.data[idx + 1] = vG; // G
  79. png.data[idx + 2] = vB; // B
  80. png.data[idx + 3] = alpha; // A (透明度)
  81. }
  82. } else {
  83. // 数据不足时使用透明像素
  84. png.data[idx] = 0;
  85. png.data[idx + 1] = 0;
  86. png.data[idx + 2] = 0;
  87. png.data[idx + 3] = 0;
  88. }
  89. }
  90. }
  91. // 保存PNG文件
  92. png.pack().pipe(fs.createWriteStream(outputFile))
  93. .on('finish', () => {
  94. console.log(`PNG图像已保存到 ${outputFile}`);
  95. // console.log('图像尺寸:', WIDTH, 'x', HEIGHT);
  96. })
  97. .on('error', (err) => {
  98. console.error('保存图像时出错:', err);
  99. });
  100. }
  101. const tempColors = [
  102. { dy: -9999, xy: -20, rgb: [49, 54, 149] }, // 深蓝
  103. { dy: -20, xy: -10, rgb: [69, 117, 180] }, // 蓝
  104. { dy: -10, xy: 0, rgb: [116, 173, 209] }, // 浅蓝
  105. { dy: 0, xy: 10, rgb: [171, 217, 233] }, // 更浅蓝
  106. { dy: 10, xy: 20, rgb: [224, 243, 248] }, // 非常浅蓝
  107. { dy: 20, xy: 25, rgb: [255, 255, 191] }, // 浅黄(过渡色)
  108. { dy: 25, xy: 30, rgb: [254, 224, 144] }, // 浅橙(分界点)
  109. { dy: 30, xy: 40, rgb: [253, 174, 97] }, // 橙色
  110. { dy: 40, xy: 50, rgb: [244, 109, 67] }, // 橙红色
  111. { dy: 50, xy: 9999, rgb: [215, 48, 39] } // 红色
  112. ];
  113. function getVarBis(value) {
  114. let findColor = tempColors.find(ele => {
  115. return value > ele.dy && value < ele.xy;
  116. });
  117. let max = 0;
  118. if (findColor.dy === -9999 || findColor.xy === 9999) {
  119. max = value;
  120. } else {
  121. max = findColor.dy > findColor.xy ? findColor.dy : findColor.xy;
  122. }
  123. let bis = Math.abs(parseInt(value / max * 100));
  124. bis = bis > 100 ? 100 : bis;
  125. return {
  126. bis: 100,
  127. vR: parseInt(findColor.rgb[0] * (bis / 100)),
  128. vG: parseInt(findColor.rgb[1] * (bis / 100)),
  129. vB: parseInt(findColor.rgb[2] * (bis / 100)),
  130. };
  131. }
  132. // 创建金字塔云层图
  133. async function createCloudImageDeep(file) {
  134. try {
  135. // 解析数据
  136. const { lonCount, latCount, cloudData } = parseDataFile(file);
  137. // 创建输出目录
  138. if (!fs.existsSync(TILES_DIR)) {
  139. fs.mkdirSync(TILES_DIR, { recursive: true });
  140. }
  141. // 为每个缩放级别生成瓦片
  142. for (let zoom = 0; zoom <= MAX_ZOOM; zoom++) {
  143. await generateTilesForZoom(zoom, lonCount, latCount, cloudData);
  144. }
  145. console.log('所有瓦片生成完成!');
  146. } catch (error) {
  147. console.error('处理过程中发生错误:', error);
  148. }
  149. }
  150. // 解析数据文件
  151. function parseDataFile(file) {
  152. console.log('正在解析数据文件...');
  153. const data = fs.readFileSync(file, 'utf8').split('\n');
  154. // 解析网格尺寸
  155. const [lonCount, latCount] = data[0].split(' ').map(Number);
  156. console.log(`网格尺寸: ${lonCount} × ${latCount}`);
  157. // 解析云量数据并过滤缺测值
  158. const cloudData = new Float32Array(lonCount * latCount);
  159. let missingCount = 0;
  160. let index = 0;
  161. for (let i = 1; i < data.length && index < cloudData.length; i++) {
  162. if (data[i].trim() !== '') {
  163. const value = parseFloat(data[i].trim());
  164. // 过滤缺测值
  165. if (value > MISSING_VALUE_THRESHOLD) {
  166. cloudData[index++] = NaN;
  167. missingCount++;
  168. } else {
  169. cloudData[index++] = value;
  170. }
  171. }
  172. }
  173. console.log(`成功读取 ${index} 个数据点,其中 ${missingCount} 个缺测值`);
  174. return { lonCount, latCount, cloudData };
  175. }
  176. // 高精度经纬度到瓦片坐标转换
  177. function lonLatToTile(lon, lat, zoom) {
  178. // 确保经度在[0, 360)范围内
  179. const normalizedLon = ((lon % 360) + 360) % 360;
  180. const x = normalizedLon / 360 * Math.pow(2, zoom);
  181. const tileX = Math.floor(x);
  182. // 使用高精度墨卡托投影公式
  183. const latRad = lat * Math.PI / 180;
  184. const y = (1 - Math.asinh(Math.tan(latRad)) / Math.PI) / 2 * Math.pow(2, zoom);
  185. const tileY = Math.floor(y);
  186. return { x: tileX, y: tileY };
  187. }
  188. // 高精度瓦片坐标到经纬度范围
  189. function tileToLonLat(x, y, zoom) {
  190. const n = Math.pow(2, zoom);
  191. // 计算经度范围
  192. const lon_west = x / n * 360;
  193. const lon_east = (x + 1) / n * 360;
  194. // 计算纬度范围 (使用高精度反墨卡托投影)
  195. const lat_north = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
  196. const lat_south = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
  197. return {
  198. west: lon_west,
  199. east: lon_east,
  200. north: lat_north,
  201. south: lat_south
  202. };
  203. }
  204. // 生成单个瓦片 - 使用直接数据映射方法
  205. async function generateTile(zoom, x, y, lonCount, latCount, cloudData) {
  206. const tileBounds = tileToLonLat(x, y, zoom);
  207. const imageData = Buffer.alloc(TILE_SIZE * TILE_SIZE * 4);
  208. // 计算每个像素对应的经纬度增量
  209. const lonIncrement = (tileBounds.east - tileBounds.west) / TILE_SIZE;
  210. const latIncrement = (tileBounds.north - tileBounds.south) / TILE_SIZE;
  211. // 预先计算数据网格的参数
  212. const lonStep = 360 / lonCount;
  213. const latStep = 180 / (latCount - 1);
  214. for (let py = 0; py < TILE_SIZE; py++) {
  215. // 计算当前像素行的纬度
  216. const lat = tileBounds.north - py * latIncrement;
  217. for (let px = 0; px < TILE_SIZE; px++) {
  218. // 计算当前像素列的经度
  219. let lon = tileBounds.west + px * lonIncrement;
  220. // 确保经度在[0, 360)范围内
  221. lon = ((lon % 360) + 360) % 360;
  222. // 计算数据网格索引
  223. const lonIdx = Math.floor(lon / lonStep);
  224. const latIdx = Math.floor((90 - lat) / latStep);
  225. // 确保索引在有效范围内
  226. const safeLonIdx = Math.min(Math.max(0, lonIdx), lonCount - 1);
  227. const safeLatIdx = Math.min(Math.max(0, latIdx), latCount - 1);
  228. // 获取云量值
  229. let cloudValue = 0;
  230. const dataIdx = safeLatIdx * lonCount + safeLonIdx;
  231. if (dataIdx >= 0 && dataIdx < cloudData.length) {
  232. const value = cloudData[dataIdx];
  233. // 过滤缺测值
  234. if (isNaN(value) || value > MISSING_VALUE_THRESHOLD) {
  235. cloudValue = 0;
  236. } else {
  237. cloudValue = value;
  238. }
  239. }
  240. // 将云量值映射到RGBA
  241. const alpha = Math.min(255, Math.max(0, Math.round(cloudValue * 2.55)));
  242. const idx = (py * TILE_SIZE + px) * 4;
  243. imageData[idx] = 255; // R
  244. imageData[idx + 1] = 255; // G
  245. imageData[idx + 2] = 255; // B
  246. imageData[idx + 3] = alpha; // A (透明度)
  247. }
  248. }
  249. // 创建瓦片目录
  250. const tileDir = path.join(TILES_DIR, `${zoom}`, `${x}`);
  251. fs.mkdirSync(tileDir, { recursive: true });
  252. // 保存瓦片图像
  253. await sharp(imageData, {
  254. raw: {
  255. width: TILE_SIZE,
  256. height: TILE_SIZE,
  257. channels: 4
  258. }
  259. })
  260. .png()
  261. .toFile(path.join(tileDir, `${y}.png`));
  262. }
  263. // 生成指定缩放级别的所有瓦片
  264. async function generateTilesForZoom(zoom, lonCount, latCount, cloudData) {
  265. console.log(`正在生成缩放级别 ${zoom} 的瓦片...`);
  266. const tileCount = Math.pow(2, zoom);
  267. let generated = 0;
  268. const totalTiles = tileCount * tileCount;
  269. // 使用更低的并发数以减少内存使用
  270. const concurrency = 3;
  271. const queue = [];
  272. for (let x = 0; x < tileCount; x++) {
  273. for (let y = 0; y < tileCount; y++) {
  274. // 将任务加入队列
  275. queue.push(() => generateTile(zoom, x, y, lonCount, latCount, cloudData));
  276. // 当队列达到并发数量时,执行一批任务
  277. if (queue.length >= concurrency) {
  278. await Promise.all(queue.map(fn => fn()));
  279. queue.length = 0; // 清空队列
  280. generated += concurrency;
  281. // 显示进度
  282. const progress = ((generated / totalTiles) * 100).toFixed(1);
  283. console.log(`缩放级别 ${zoom}: ${progress}% 完成 (${generated}/${totalTiles})`);
  284. // 手动触发垃圾回收
  285. if (global.gc) {
  286. global.gc();
  287. }
  288. }
  289. }
  290. }
  291. // 处理队列中剩余的任务
  292. if (queue.length > 0) {
  293. await Promise.all(queue.map(fn => fn()));
  294. generated += queue.length;
  295. console.log(`缩放级别 ${zoom}: 100% 完成 (${generated}/${totalTiles})`);
  296. }
  297. console.log(`缩放级别 ${zoom} 完成,生成 ${tileCount * tileCount} 个瓦片`);
  298. }
  299. // 主函数
  300. async function main() {
  301. try {
  302. // 解析数据
  303. const { lonCount, latCount, cloudData } = parseDataFile();
  304. // 创建输出目录
  305. if (!fs.existsSync(TILES_DIR)) {
  306. fs.mkdirSync(TILES_DIR, { recursive: true });
  307. }
  308. // 为每个缩放级别生成瓦片
  309. for (let zoom = 0; zoom <= MAX_ZOOM; zoom++) {
  310. await generateTilesForZoom(zoom, lonCount, latCount, cloudData);
  311. }
  312. console.log('所有瓦片生成完成!');
  313. } catch (error) {
  314. console.error('处理过程中发生错误:', error);
  315. }
  316. }
  317. module.exports = { createCloudImage, createCloudImageDeep }