createCloudImage.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. const fs = require('fs');
  2. const path = require('path');
  3. const { createCanvas, ImageData } = require('canvas');
  4. const sharp = require('sharp');
  5. // 配置参数
  6. const config = {
  7. outputDir: 'G:/tiles/cloud', // 输出目录
  8. emptyValue: 9.999e+20, // 空值标识
  9. minZoom: 0, // 最小缩放级别
  10. maxZoom: 5 // 最大缩放级别
  11. };
  12. async function createCloudImageDeep(inputFile) {
  13. try {
  14. // 确保输出目录存在
  15. if (!fs.existsSync(inputFile)) {
  16. fs.mkdirSync(inputFile, { recursive: true });
  17. }
  18. // 解析数据文件
  19. const { grid, width, height } = parseDataFile(inputFile);
  20. // 生成各级别瓦片
  21. for (let zoom = config.minZoom; zoom <= config.maxZoom; zoom++) {
  22. await generateTiles(grid, width, height, zoom);
  23. }
  24. // 生成图例
  25. generateLegend();
  26. console.log('瓦片生成完成!');
  27. console.log(`输出目录: ${path.resolve(config.outputDir)}`);
  28. } catch (error) {
  29. console.error('处理过程中发生错误:', error);
  30. }
  31. }
  32. // 读取并解析数据文件
  33. function parseDataFile(filePath) {
  34. console.log('正在读取数据文件...');
  35. const data = fs.readFileSync(filePath, 'utf8').split('\n');
  36. // 解析第一行获取尺寸信息
  37. const dimensions = data[0].split(' ').map(Number);
  38. const dataWidth = dimensions[0];
  39. const dataHeight = dimensions[1];
  40. console.log(`数据尺寸: ${dataWidth}x${dataHeight}`);
  41. // 解析数据值
  42. const grid = [];
  43. for (let i = 1; i < data.length; i++) {
  44. if (data[i].trim() === '') continue;
  45. const value = parseFloat(data[i]);
  46. // 处理空值
  47. grid.push(value === config.emptyValue ? NaN : value);
  48. }
  49. console.log(`已读取 ${grid.length} 个数据点`);
  50. return { grid, width: dataWidth, height: dataHeight };
  51. }
  52. // 将经纬度转换为瓦片坐标
  53. function latLonToTileIndex(lat, lon, zoom) {
  54. const x = Math.floor((lon + 180) / 360 * Math.pow(2, zoom));
  55. const y = Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) +
  56. 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom));
  57. return { x, y };
  58. }
  59. // 将瓦片坐标转换为经纬度范围
  60. function tileToLatLon(x, y, zoom) {
  61. const n = Math.pow(2, zoom);
  62. const lon1 = x / n * 360 - 180;
  63. const lat1 = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
  64. const lon2 = (x + 1) / n * 360 - 180;
  65. const lat2 = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
  66. return {
  67. west: lon1,
  68. east: lon2,
  69. south: lat2,
  70. north: lat1
  71. };
  72. }
  73. // 生成指定缩放级别的瓦片
  74. async function generateTiles(data, dataWidth, dataHeight, zoom) {
  75. console.log(`正在生成第 ${zoom} 级瓦片...`);
  76. const tileSize = 256;
  77. const totalTiles = Math.pow(2, zoom);
  78. // 创建进度跟踪
  79. let processed = 0;
  80. const totalToProcess = totalTiles * totalTiles;
  81. for (let x = 0; x < totalTiles; x++) {
  82. for (let y = 0; y < totalTiles; y++) {
  83. const canvas = createCanvas(tileSize, tileSize);
  84. const ctx = canvas.getContext('2d');
  85. const imageData = ctx.createImageData(tileSize, tileSize);
  86. // 获取当前瓦片的经纬度范围
  87. const bounds = tileToLatLon(x, y, zoom);
  88. // 为瓦片中的每个像素计算数据值
  89. for (let py = 0; py < tileSize; py++) {
  90. for (let px = 0; px < tileSize; px++) {
  91. // 计算当前像素的经纬度
  92. const pixelLon = bounds.west + (px / tileSize) * (bounds.east - bounds.west);
  93. const pixelLat = bounds.south + (py / tileSize) * (bounds.north - bounds.south);
  94. // 将经纬度转换为数据网格索引
  95. const dataX = Math.floor((pixelLon + 180) / 360 * dataWidth);
  96. const dataY = Math.floor((90 - pixelLat) / 180 * dataHeight);
  97. // 确保索引在有效范围内
  98. const safeX = Math.max(0, Math.min(dataWidth - 1, dataX));
  99. const safeY = Math.max(0, Math.min(dataHeight - 1, dataY));
  100. const dataIndex = safeY * dataWidth + safeX;
  101. // 获取数据值并处理空值
  102. let value = data[dataIndex];
  103. if (isNaN(value)) {
  104. // 空值处理:设置为完全透明
  105. setPixel(imageData, px, py, 0, 0, 0, 0);
  106. } else {
  107. // 将云量值(0-100)映射到颜色(蓝色到白色)
  108. const intensity = Math.min(255, Math.max(0, Math.floor(value * 2.55)));
  109. // 使用蓝色渐变表示云量
  110. setPixel(imageData, px, py, intensity, intensity, 255, 180);
  111. }
  112. }
  113. }
  114. ctx.putImageData(imageData, 0, 0);
  115. // 创建目录并保存瓦片
  116. const tileDir = path.join(config.outputDir, zoom.toString(), x.toString());
  117. if (!fs.existsSync(tileDir)) {
  118. fs.mkdirSync(tileDir, { recursive: true });
  119. }
  120. const tilePath = path.join(tileDir, `${y}.png`);
  121. const buffer = canvas.toBuffer('image/png');
  122. await sharp(buffer).png().toFile(tilePath);
  123. // 更新进度
  124. processed++;
  125. if (processed % 100 === 0) {
  126. console.log(`进度: ${Math.round(processed / totalToProcess * 100)}%`);
  127. }
  128. }
  129. }
  130. }
  131. // 设置像素颜色
  132. function setPixel(imageData, x, y, r, g, b, a) {
  133. const index = (y * imageData.width + x) * 4;
  134. imageData.data[index] = r;
  135. imageData.data[index + 1] = g;
  136. imageData.data[index + 2] = b;
  137. imageData.data[index + 3] = a;
  138. }
  139. // 生成图例
  140. function generateLegend() {
  141. const legend = `
  142. <!DOCTYPE html>
  143. <html>
  144. <head>
  145. <title>云量图例</title>
  146. <style>
  147. .legend {
  148. position: absolute;
  149. bottom: 20px;
  150. right: 20px;
  151. background: white;
  152. padding: 10px;
  153. border-radius: 5px;
  154. box-shadow: 0 0 10px rgba(0,0,0,0.2);
  155. }
  156. .color-bar {
  157. height: 20px;
  158. width: 200px;
  159. background: linear-gradient(to right, #0000ff, #ffffff);
  160. margin-bottom: 5px;
  161. }
  162. .labels {
  163. display: flex;
  164. justify-content: space-between;
  165. }
  166. </style>
  167. </head>
  168. <body>
  169. <div class="legend">
  170. <div>云量百分比</div>
  171. <div class="color-bar"></div>
  172. <div class="labels">
  173. <span>0%</span>
  174. <span>100%</span>
  175. </div>
  176. </div>
  177. </body>
  178. </html>
  179. `;
  180. fs.writeFileSync(path.join(config.outputDir, 'legend.html'), legend);
  181. }
  182. // 主函数
  183. async function main() {
  184. try {
  185. // 确保输出目录存在
  186. if (!fs.existsSync(config.outputDir)) {
  187. fs.mkdirSync(config.outputDir, { recursive: true });
  188. }
  189. // 解析数据文件
  190. const { grid, width, height } = parseDataFile(config.inputFile);
  191. // 生成各级别瓦片
  192. for (let zoom = config.minZoom; zoom <= config.maxZoom; zoom++) {
  193. await generateTiles(grid, width, height, zoom);
  194. }
  195. // 生成图例
  196. generateLegend();
  197. console.log('瓦片生成完成!');
  198. console.log(`输出目录: ${path.resolve(config.outputDir)}`);
  199. } catch (error) {
  200. console.error('处理过程中发生错误:', error);
  201. }
  202. }
  203. module.exports = {
  204. createCloudImageDeep
  205. };