exportSource.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /**
  2. * 批量下载模型文件并解析数据脚本
  3. * 如果需要生成风场数,需:
  4. * justDownloadModel 与 justTrDataMode 改为 false
  5. * drgree 改为 gfs_1p00 后重启脚本
  6. * 待数据解析完成后 basePath 内的 wind 文件夹内就是需要的数据,及时备份好
  7. * 如需生成温度图等数据,需:
  8. * 先将 drgree 改为 gfs_0p25, justDownloadModel 改为 true 后运行脚本等待下载完成
  9. * 下载完成后,再将 justTrDataMode 改为 true 后运行脚本
  10. * 数据解析完成后 basePath 内会生成相关的图源数据,及时备份好
  11. */
  12. const express = require("express"); //Node.js的一个Web框架,可以用来快速构建Web应用程序
  13. const moment = require("moment"); //JavaScript日期处理库,可以用来格式化和解析日期。
  14. const http = require('http'); //Node.js的一个内置模块,可以用来创建HTTP服务器和客户端。
  15. const request = require('request'); //Node.js的HTTP客户端库,可以用来发送HTTP请求和处理响应。
  16. const fs = require('fs'); //Node.js的一个内置模块,可以用来读写文件和目录。
  17. const Q = require('q'); //JavaScript的Promise库,可以用来处理异步操作。
  18. const cors = require('cors'); //Node.js的中间件,可以用来处理跨域请求。
  19. const { exec, execSync } = require('child_process');
  20. const dayjs = require('dayjs');
  21. const path = require('path');
  22. const { createCloudImage } = require("./schema/createCloudImage_old.js");
  23. // 解析出的数据所存储的根目录
  24. // const basePath = path.join(__dirname, `./exportData`)
  25. const basePath = path.join("F:", `./exportData`)
  26. // 获取的模型文件名称
  27. const fileName = "f000"
  28. // 获取模型文件时间步进
  29. const dateStep = 6;
  30. // 获取模型文件的精度
  31. // const drgree = "gfs_0p25";
  32. const drgree = "gfs_1p00";
  33. // 仅下载模式
  34. const justDownloadModel = false;
  35. // 仅转换数据模式
  36. const justTrDataMode = false;
  37. // 模型下载根地址
  38. const baseDir = `http://nomads.ncep.noaa.gov/cgi-bin/filter_${drgree}.pl`;
  39. const windDirLayer = [];
  40. const modelDirLayer = [];
  41. function run(targetMoment) {
  42. if (justTrDataMode) {
  43. trModelData();
  44. console.log("所有转换数据转换工作已完成");
  45. } else {
  46. getGribData(targetMoment).then((response) => {
  47. if (response.stamp) {
  48. if (!justDownloadModel) {
  49. convertGribToJson(response.stamp, response.targetMoment);
  50. } else {
  51. run(moment(targetMoment).subtract(dateStep, 'hours'));
  52. }
  53. }
  54. });
  55. }
  56. }
  57. async function trModelData() {
  58. const fileList = JSON.parse(fs.readFileSync(`${basePath}/model/layer.json`, "utf8"));
  59. let mixData = [{
  60. lv: "500 mb", // 层级
  61. vr: "TCDC", // 变量
  62. type: "cloud", // 渲染图片类型
  63. dir: "cloud", // 导出文件夹名称
  64. rgb: { r: 255, g: 255, b: 255 }, // 渲染颜色
  65. typeName: "云层图" // layer.json文件内描述字段
  66. }, {
  67. lv: "50 mb",
  68. vr: "RWMR",
  69. type: "rain",
  70. dir: "rain",
  71. rgb: { r: 64, g: 158, b: 255 },
  72. typeName: "降雨图"
  73. }, {
  74. lv: "2 m above ground",
  75. vr: "TMP",
  76. type: "tmp",
  77. dir: "tmp",
  78. rgb: { r: 0, g: 0, b: 0 },
  79. typeName: "温度图"
  80. }];
  81. // [{
  82. // // 数据观测时间戳,可用此字段排序数据,值越大日期越靠后
  83. // "sort": 1757721600000,
  84. // // 数据观测时间格式化
  85. // "date": "2025-09-13 08:00",
  86. // // 此条数据对应的文件地址
  87. // "path": "/exportData/cloud/20250913/08.png",
  88. // // 当前取值的模型精度
  89. // "drgree": "gfs_0p25",
  90. // // 当前映射的中文描述
  91. // "description": "2025-09-13 08:00云层图片"
  92. // }]
  93. for (let j = 0; j < mixData.length; j++) {
  94. const { lv, vr, type, dir, rgb, typeName } = mixData[j];
  95. let dirLayer = [];
  96. for (let i = 0; i < fileList.length; i++) {
  97. console.log(`[[${type}:${lv}:${vr}]] 正在提取第 ${i + 1}/${fileList.length} 个模型`);
  98. const date = dayjs(fileList[i].date).format("YYYYMMDD");
  99. const hour = dayjs(fileList[i].date).format("HH");
  100. // 云层图
  101. try {
  102. const txtPath = `${mkDir(basePath + "/" + dir + "/" + date + "/")}${hour}.txt`;
  103. const command = `"${path.join(__dirname, "./wgrib2/wgrib2.exe")}" "${basePath + fileList[i].path.replace(/\/exportData/, "")}" -match "${vr}:${lv}" -text ${txtPath}`;
  104. execSync(command);
  105. await createCloudImage(txtPath, `${basePath}/${dir}/${date}/${hour}.png`, rgb, type);
  106. dirLayer.push({
  107. sort: fileList[i].sort,
  108. date: fileList[i].date,
  109. path: `/exportData/${type}/${date}/${hour}.png`,
  110. drgree,
  111. description: `${fileList[i].date}${typeName}`
  112. });
  113. execSync(`rm -rf ${txtPath}`);
  114. } catch (e) {
  115. console.log(`模型 "${fileList[i].path}" 数据类型 ${type} 生成失败...或许是因为所选层级与变量导出txt无内容`)
  116. }
  117. // 降雨图
  118. // const command = `"${path.join(__dirname, "../../wgrib2/wgrib2.exe")}" "${fileList.list[0]}" -match "RWMR:50 mb" -text ${tempTxtFilePath}`;
  119. // stdout = execSync(command);
  120. // await createCloudImage(tempTxtFilePath, path.join(__dirname, `../../tempDir/rain.png`), { r: 64, g: 158, b: 255 }, "rain");
  121. // // 降雨图
  122. // const command = `"${path.join(__dirname, "../../wgrib2/wgrib2.exe")}" "${fileList.list[0]}" -match "TMP:2 m above ground" -text ${tempTxtFilePath}`;
  123. // stdout = execSync(command);
  124. // await createCloudImage(tempTxtFilePath, path.join(__dirname, `../../tempDir/tmp.png`), { r: 0, g: 0, b: 0 }, "tmp");
  125. }
  126. fs.writeFileSync(`${mkDir(basePath + "/" + type)}/layer.json`, JSON.stringify(dirLayer), 'utf8');
  127. }
  128. }
  129. function getGribData(targetMoment) {
  130. var deferred = Q.defer();
  131. function runQuery(targetMoment) {
  132. // only go 2 weeks deep
  133. if (moment.utc().diff(targetMoment, 'days') > 30) {
  134. // 写入文件
  135. fs.writeFileSync(`${mkDir(basePath + "/model")}/layer.json`, JSON.stringify(modelDirLayer), 'utf8');
  136. if (!justDownloadModel) {
  137. fs.writeFileSync(`${mkDir(basePath + "/wind")}/layer.json`, JSON.stringify(windDirLayer), 'utf8');
  138. }
  139. console.log('命中极限,收获完成或数据存在较大缺口。');
  140. return;
  141. }
  142. var stamp = moment(targetMoment).format('YYYYMMDD') + roundHours(moment(targetMoment).hour(), dateStep);
  143. var years = moment(targetMoment).format('YYYYMMDD')
  144. var hour = roundHours(moment(targetMoment).hour(), dateStep);
  145. let requestQs = null;
  146. if (justDownloadModel) {
  147. requestQs = {
  148. file: `gfs.t${roundHours(moment(targetMoment).hour(), dateStep)}z.pgrb2.${drgree.split("_")[1]}.${fileName}`,//指定需要获取的气象预报数据文件的文件名。在这里,文件名是由多个参数组成的字符串,包括GFS模型的起报时间、预报时效、数据格式等信息。
  149. all_lev: "on",
  150. all_var: "on",
  151. leftlon: 0,//从0度经线开始 获取数据
  152. rightlon: 360,//到360度经线结束 获取数据
  153. toplat: 90, //到90度北纬结束 获取数据
  154. bottomlat: -90, //到90度南纬结束 获取数据
  155. dir: '/gfs.' + years + '/' + hour + '/atmos' //dir=%2Fgfs.20231127 %2F(转义:/) stamp:时间戳
  156. }
  157. } else {
  158. requestQs = {
  159. file: `gfs.t${roundHours(moment(targetMoment).hour(), dateStep)}z.pgrb2.${drgree.split("_")[1]}.${fileName}`,//指定需要获取的气象预报数据文件的文件名。在这里,文件名是由多个参数组成的字符串,包括GFS模型的起报时间、预报时效、数据格式等信息。
  160. lev_10_m_above_ground: 'on', //指定是否获取地面以上10米高度层的气象变量数据。这里设置为'on',表示需要获取该高度层的数据。
  161. lev_surface: 'on', //指定是否获取地面高度层的气象变量数据。这里设置为'on',表示需要获取该高度层的数据
  162. var_TMP: 'on', //指定是否获取温度(Temperature)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据
  163. var_UGRD: 'on', //指定是否获取东西向风速(Eastward Wind)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据
  164. var_VGRD: 'on', //指定是否获取南北向风速(Northward Wind)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据
  165. leftlon: 0,//从0度经线开始 获取数据
  166. rightlon: 360,//到360度经线结束 获取数据
  167. toplat: 90, //到90度北纬结束 获取数据
  168. bottomlat: -90, //到90度南纬结束 获取数据
  169. dir: '/gfs.' + years + '/' + hour + '/atmos' //dir=%2Fgfs.20231127 %2F(转义:/) stamp:时间戳
  170. }
  171. }
  172. request.get({
  173. url: baseDir,
  174. qs: requestQs
  175. }).on('error', function (err) {
  176. // console.log(err);
  177. runQuery(moment(targetMoment).subtract(dateStep, 'hours'));
  178. }).on('response', function (response) {
  179. console.log("🚀 ~ name:response", response.request.url.href)
  180. console.log('响应状态:' + response.statusCode + ' 时间节点: ' + stamp);
  181. if (response.statusCode != 200) {
  182. runQuery(moment(targetMoment).subtract(dateStep, 'hours'));
  183. } else {
  184. const windDir = mkDir(`${basePath}/wind/${stamp.substring(0, 8)}`);
  185. const modelDir = mkDir(`${basePath}/model/${stamp.substring(0, 8)}`);
  186. if (justDownloadModel) {
  187. var file = fs.createWriteStream(`${modelDir}/${stamp.substring(stamp.length - 2)}.${fileName}`);
  188. modelDirLayer.push({
  189. sort: dayjs(stamp).add(8, 'hour').valueOf(),
  190. date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"),
  191. path: `/exportData/model/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.${fileName}`,
  192. drgree,
  193. description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}气象模型文件`
  194. })
  195. response.pipe(file);
  196. file.on('finish', function () {
  197. file.close();
  198. deferred.resolve({ stamp: stamp, targetMoment: targetMoment });
  199. });
  200. } else {
  201. // don't rewrite stamps
  202. if (!checkPath(`${windDir}/${stamp.substring(stamp.length - 2)}.json`, false)) {
  203. console.log('piping ' + stamp);
  204. // mk sure we've got somewhere to put output
  205. // checkPath('grib-data', true);
  206. // pipe the file, resolve the valid time stamp
  207. var file = fs.createWriteStream(`${modelDir}/${stamp.substring(stamp.length - 2)}.${fileName}`);
  208. modelDirLayer.push({
  209. sort: dayjs(stamp).add(8, 'hour').valueOf(),
  210. date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"),
  211. path: `/exportData/model/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.${fileName}`,
  212. drgree,
  213. description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}气象模型文件`
  214. })
  215. response.pipe(file);
  216. file.on('finish', function () {
  217. file.close();
  218. deferred.resolve({ stamp: stamp, targetMoment: targetMoment });
  219. });
  220. } else {
  221. console.log('already have ' + stamp + ', not looking further');
  222. deferred.resolve({ stamp: false, targetMoment: false });
  223. }
  224. }
  225. }
  226. });
  227. }
  228. runQuery(targetMoment);
  229. return deferred.promise;
  230. }
  231. function convertGribToJson(stamp, targetMoment) {
  232. // mk sure we've got somewhere to put output
  233. // checkPath('json-data', true);
  234. const windDir = mkDir(`${basePath}/wind/${stamp.substring(0, 8)}`);
  235. const modelDir = mkDir(`${basePath}/model/${stamp.substring(0, 8)}`);
  236. const result = exec(`java\\bin\\grib2json --data --output ${windDir}\\${stamp.substring(stamp.length - 2)}.json --names --compact ${modelDir}\\${stamp.substring(stamp.length - 2)}.${fileName}`,
  237. { maxBuffer: 200 * 500 * 1024 },
  238. function (error, stdout, stderr) {
  239. if (stdout.error) {
  240. console.log('exec error: ' + error);
  241. } else {
  242. console.log("converted..");
  243. // don't keep raw grib data
  244. // exec('rm grib-data/*');
  245. windDirLayer.push({
  246. sort: dayjs(stamp).add(8, 'hour').valueOf(),
  247. date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"),
  248. path: `/exportData/wind/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.json`,
  249. drgree,
  250. description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}风场图数据Json`
  251. })
  252. // if we don't have older stamp, try and harvest one
  253. var prevMoment = moment(targetMoment).subtract(dateStep, 'hours');
  254. var prevStamp = prevMoment.format('YYYYMMDD') + roundHours(prevMoment.hour(), dateStep);
  255. if (!checkPath(`${basePath + "/wind/" + prevStamp.substring(0, 8)}/${prevStamp.substring(prevStamp.length - 2)}.json`, false)) {
  256. console.log("attempting to harvest older data " + stamp);
  257. run(prevMoment);
  258. } else {
  259. console.log('got older, no need to harvest further');
  260. }
  261. }
  262. }
  263. );
  264. }
  265. function checkPath(path, mkdir) {
  266. try {
  267. fs.statSync(path);
  268. return true;
  269. } catch (e) {
  270. if (mkdir) {
  271. fs.mkdirSync(path);
  272. }
  273. return false;
  274. }
  275. }
  276. function roundHours(hours, interval) {
  277. if (interval > 0) {
  278. var result = (Math.floor(hours / interval) * interval);
  279. return result < 10 ? '0' + result.toString() : result;
  280. }
  281. }
  282. // utc 时间转中国时间
  283. function convertUtcDateForChinaDate(date) {
  284. return dayjs(date).add(8, 'hour').format("YYYYMMDDHHmm");
  285. }
  286. function mkDir(dirPath) {
  287. if (!fs.existsSync(dirPath)) {
  288. fs.mkdirSync(dirPath, { recursive: true });
  289. }
  290. return dirPath;
  291. }
  292. run(moment.utc());