| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- /**
- * 批量下载模型文件并解析数据脚本
- * 如果需要生成风场数,需:
- * justDownloadModel 与 justTrDataMode 改为 false
- * drgree 改为 gfs_1p00 后重启脚本
- * 待数据解析完成后 basePath 内的 wind 文件夹内就是需要的数据,及时备份好
- * 如需生成温度图等数据,需:
- * 先将 drgree 改为 gfs_0p25, justDownloadModel 改为 true 后运行脚本等待下载完成
- * 下载完成后,再将 justTrDataMode 改为 true 后运行脚本
- * 数据解析完成后 basePath 内会生成相关的图源数据,及时备份好
- */
- const express = require("express"); //Node.js的一个Web框架,可以用来快速构建Web应用程序
- const moment = require("moment"); //JavaScript日期处理库,可以用来格式化和解析日期。
- const http = require('http'); //Node.js的一个内置模块,可以用来创建HTTP服务器和客户端。
- const request = require('request'); //Node.js的HTTP客户端库,可以用来发送HTTP请求和处理响应。
- const fs = require('fs'); //Node.js的一个内置模块,可以用来读写文件和目录。
- const Q = require('q'); //JavaScript的Promise库,可以用来处理异步操作。
- const cors = require('cors'); //Node.js的中间件,可以用来处理跨域请求。
- const { exec, execSync } = require('child_process');
- const dayjs = require('dayjs');
- const path = require('path');
- const { createCloudImage } = require("./schema/createCloudImage_old.js");
- // 解析出的数据所存储的根目录
- // const basePath = path.join(__dirname, `./exportData`)
- const basePath = path.join("F:", `./exportData`)
- // 获取的模型文件名称
- const fileName = "f000"
- // 获取模型文件时间步进
- const dateStep = 6;
- // 获取模型文件的精度
- // const drgree = "gfs_0p25";
- const drgree = "gfs_1p00";
- // 仅下载模式
- const justDownloadModel = false;
- // 仅转换数据模式
- const justTrDataMode = false;
- // 模型下载根地址
- const baseDir = `http://nomads.ncep.noaa.gov/cgi-bin/filter_${drgree}.pl`;
- const windDirLayer = [];
- const modelDirLayer = [];
- function run(targetMoment) {
- if (justTrDataMode) {
- trModelData();
- console.log("所有转换数据转换工作已完成");
- } else {
- getGribData(targetMoment).then((response) => {
- if (response.stamp) {
- if (!justDownloadModel) {
- convertGribToJson(response.stamp, response.targetMoment);
- } else {
- run(moment(targetMoment).subtract(dateStep, 'hours'));
- }
- }
- });
- }
- }
- async function trModelData() {
- const fileList = JSON.parse(fs.readFileSync(`${basePath}/model/layer.json`, "utf8"));
- let mixData = [{
- lv: "500 mb", // 层级
- vr: "TCDC", // 变量
- type: "cloud", // 渲染图片类型
- dir: "cloud", // 导出文件夹名称
- rgb: { r: 255, g: 255, b: 255 }, // 渲染颜色
- typeName: "云层图" // layer.json文件内描述字段
- }, {
- lv: "50 mb",
- vr: "RWMR",
- type: "rain",
- dir: "rain",
- rgb: { r: 64, g: 158, b: 255 },
- typeName: "降雨图"
- }, {
- lv: "2 m above ground",
- vr: "TMP",
- type: "tmp",
- dir: "tmp",
- rgb: { r: 0, g: 0, b: 0 },
- typeName: "温度图"
- }];
- // [{
- // // 数据观测时间戳,可用此字段排序数据,值越大日期越靠后
- // "sort": 1757721600000,
- // // 数据观测时间格式化
- // "date": "2025-09-13 08:00",
- // // 此条数据对应的文件地址
- // "path": "/exportData/cloud/20250913/08.png",
- // // 当前取值的模型精度
- // "drgree": "gfs_0p25",
- // // 当前映射的中文描述
- // "description": "2025-09-13 08:00云层图片"
- // }]
- for (let j = 0; j < mixData.length; j++) {
- const { lv, vr, type, dir, rgb, typeName } = mixData[j];
- let dirLayer = [];
- for (let i = 0; i < fileList.length; i++) {
- console.log(`[[${type}:${lv}:${vr}]] 正在提取第 ${i + 1}/${fileList.length} 个模型`);
- const date = dayjs(fileList[i].date).format("YYYYMMDD");
- const hour = dayjs(fileList[i].date).format("HH");
- // 云层图
- try {
- const txtPath = `${mkDir(basePath + "/" + dir + "/" + date + "/")}${hour}.txt`;
- const command = `"${path.join(__dirname, "./wgrib2/wgrib2.exe")}" "${basePath + fileList[i].path.replace(/\/exportData/, "")}" -match "${vr}:${lv}" -text ${txtPath}`;
- execSync(command);
- await createCloudImage(txtPath, `${basePath}/${dir}/${date}/${hour}.png`, rgb, type);
- dirLayer.push({
- sort: fileList[i].sort,
- date: fileList[i].date,
- path: `/exportData/${type}/${date}/${hour}.png`,
- drgree,
- description: `${fileList[i].date}${typeName}`
- });
- execSync(`rm -rf ${txtPath}`);
- } catch (e) {
- console.log(`模型 "${fileList[i].path}" 数据类型 ${type} 生成失败...或许是因为所选层级与变量导出txt无内容`)
- }
- // 降雨图
- // const command = `"${path.join(__dirname, "../../wgrib2/wgrib2.exe")}" "${fileList.list[0]}" -match "RWMR:50 mb" -text ${tempTxtFilePath}`;
- // stdout = execSync(command);
- // await createCloudImage(tempTxtFilePath, path.join(__dirname, `../../tempDir/rain.png`), { r: 64, g: 158, b: 255 }, "rain");
- // // 降雨图
- // const command = `"${path.join(__dirname, "../../wgrib2/wgrib2.exe")}" "${fileList.list[0]}" -match "TMP:2 m above ground" -text ${tempTxtFilePath}`;
- // stdout = execSync(command);
- // await createCloudImage(tempTxtFilePath, path.join(__dirname, `../../tempDir/tmp.png`), { r: 0, g: 0, b: 0 }, "tmp");
- }
- fs.writeFileSync(`${mkDir(basePath + "/" + type)}/layer.json`, JSON.stringify(dirLayer), 'utf8');
- }
- }
- function getGribData(targetMoment) {
- var deferred = Q.defer();
- function runQuery(targetMoment) {
- // only go 2 weeks deep
- if (moment.utc().diff(targetMoment, 'days') > 30) {
- // 写入文件
- fs.writeFileSync(`${mkDir(basePath + "/model")}/layer.json`, JSON.stringify(modelDirLayer), 'utf8');
- if (!justDownloadModel) {
- fs.writeFileSync(`${mkDir(basePath + "/wind")}/layer.json`, JSON.stringify(windDirLayer), 'utf8');
- }
- console.log('命中极限,收获完成或数据存在较大缺口。');
- return;
- }
- var stamp = moment(targetMoment).format('YYYYMMDD') + roundHours(moment(targetMoment).hour(), dateStep);
- var years = moment(targetMoment).format('YYYYMMDD')
- var hour = roundHours(moment(targetMoment).hour(), dateStep);
- let requestQs = null;
- if (justDownloadModel) {
- requestQs = {
- file: `gfs.t${roundHours(moment(targetMoment).hour(), dateStep)}z.pgrb2.${drgree.split("_")[1]}.${fileName}`,//指定需要获取的气象预报数据文件的文件名。在这里,文件名是由多个参数组成的字符串,包括GFS模型的起报时间、预报时效、数据格式等信息。
- all_lev: "on",
- all_var: "on",
- leftlon: 0,//从0度经线开始 获取数据
- rightlon: 360,//到360度经线结束 获取数据
- toplat: 90, //到90度北纬结束 获取数据
- bottomlat: -90, //到90度南纬结束 获取数据
- dir: '/gfs.' + years + '/' + hour + '/atmos' //dir=%2Fgfs.20231127 %2F(转义:/) stamp:时间戳
- }
- } else {
- requestQs = {
- file: `gfs.t${roundHours(moment(targetMoment).hour(), dateStep)}z.pgrb2.${drgree.split("_")[1]}.${fileName}`,//指定需要获取的气象预报数据文件的文件名。在这里,文件名是由多个参数组成的字符串,包括GFS模型的起报时间、预报时效、数据格式等信息。
- lev_10_m_above_ground: 'on', //指定是否获取地面以上10米高度层的气象变量数据。这里设置为'on',表示需要获取该高度层的数据。
- lev_surface: 'on', //指定是否获取地面高度层的气象变量数据。这里设置为'on',表示需要获取该高度层的数据
- var_TMP: 'on', //指定是否获取温度(Temperature)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据
- var_UGRD: 'on', //指定是否获取东西向风速(Eastward Wind)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据
- var_VGRD: 'on', //指定是否获取南北向风速(Northward Wind)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据
- leftlon: 0,//从0度经线开始 获取数据
- rightlon: 360,//到360度经线结束 获取数据
- toplat: 90, //到90度北纬结束 获取数据
- bottomlat: -90, //到90度南纬结束 获取数据
- dir: '/gfs.' + years + '/' + hour + '/atmos' //dir=%2Fgfs.20231127 %2F(转义:/) stamp:时间戳
- }
- }
- request.get({
- url: baseDir,
- qs: requestQs
- }).on('error', function (err) {
- // console.log(err);
- runQuery(moment(targetMoment).subtract(dateStep, 'hours'));
- }).on('response', function (response) {
- console.log("🚀 ~ name:response", response.request.url.href)
- console.log('响应状态:' + response.statusCode + ' 时间节点: ' + stamp);
- if (response.statusCode != 200) {
- runQuery(moment(targetMoment).subtract(dateStep, 'hours'));
- } else {
- const windDir = mkDir(`${basePath}/wind/${stamp.substring(0, 8)}`);
- const modelDir = mkDir(`${basePath}/model/${stamp.substring(0, 8)}`);
- if (justDownloadModel) {
- var file = fs.createWriteStream(`${modelDir}/${stamp.substring(stamp.length - 2)}.${fileName}`);
- modelDirLayer.push({
- sort: dayjs(stamp).add(8, 'hour').valueOf(),
- date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"),
- path: `/exportData/model/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.${fileName}`,
- drgree,
- description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}气象模型文件`
- })
- response.pipe(file);
- file.on('finish', function () {
- file.close();
- deferred.resolve({ stamp: stamp, targetMoment: targetMoment });
- });
- } else {
- // don't rewrite stamps
- if (!checkPath(`${windDir}/${stamp.substring(stamp.length - 2)}.json`, false)) {
- console.log('piping ' + stamp);
- // mk sure we've got somewhere to put output
- // checkPath('grib-data', true);
- // pipe the file, resolve the valid time stamp
- var file = fs.createWriteStream(`${modelDir}/${stamp.substring(stamp.length - 2)}.${fileName}`);
- modelDirLayer.push({
- sort: dayjs(stamp).add(8, 'hour').valueOf(),
- date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"),
- path: `/exportData/model/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.${fileName}`,
- drgree,
- description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}气象模型文件`
- })
- response.pipe(file);
- file.on('finish', function () {
- file.close();
- deferred.resolve({ stamp: stamp, targetMoment: targetMoment });
- });
- } else {
- console.log('already have ' + stamp + ', not looking further');
- deferred.resolve({ stamp: false, targetMoment: false });
- }
- }
- }
- });
- }
- runQuery(targetMoment);
- return deferred.promise;
- }
- function convertGribToJson(stamp, targetMoment) {
- // mk sure we've got somewhere to put output
- // checkPath('json-data', true);
- const windDir = mkDir(`${basePath}/wind/${stamp.substring(0, 8)}`);
- const modelDir = mkDir(`${basePath}/model/${stamp.substring(0, 8)}`);
- const result = exec(`java\\bin\\grib2json --data --output ${windDir}\\${stamp.substring(stamp.length - 2)}.json --names --compact ${modelDir}\\${stamp.substring(stamp.length - 2)}.${fileName}`,
- { maxBuffer: 200 * 500 * 1024 },
- function (error, stdout, stderr) {
- if (stdout.error) {
- console.log('exec error: ' + error);
- } else {
- console.log("converted..");
- // don't keep raw grib data
- // exec('rm grib-data/*');
- windDirLayer.push({
- sort: dayjs(stamp).add(8, 'hour').valueOf(),
- date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"),
- path: `/exportData/wind/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.json`,
- drgree,
- description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}风场图数据Json`
- })
- // if we don't have older stamp, try and harvest one
- var prevMoment = moment(targetMoment).subtract(dateStep, 'hours');
- var prevStamp = prevMoment.format('YYYYMMDD') + roundHours(prevMoment.hour(), dateStep);
- if (!checkPath(`${basePath + "/wind/" + prevStamp.substring(0, 8)}/${prevStamp.substring(prevStamp.length - 2)}.json`, false)) {
- console.log("attempting to harvest older data " + stamp);
- run(prevMoment);
- } else {
- console.log('got older, no need to harvest further');
- }
- }
- }
- );
- }
- function checkPath(path, mkdir) {
- try {
- fs.statSync(path);
- return true;
- } catch (e) {
- if (mkdir) {
- fs.mkdirSync(path);
- }
- return false;
- }
- }
- function roundHours(hours, interval) {
- if (interval > 0) {
- var result = (Math.floor(hours / interval) * interval);
- return result < 10 ? '0' + result.toString() : result;
- }
- }
- // utc 时间转中国时间
- function convertUtcDateForChinaDate(date) {
- return dayjs(date).add(8, 'hour').format("YYYYMMDDHHmm");
- }
- function mkDir(dirPath) {
- if (!fs.existsSync(dirPath)) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- return dirPath;
- }
- run(moment.utc());
|