feat: 新增彩云气象数据下载的时间控制
This commit is contained in:
parent
13b4117d75
commit
9f960c6411
17
config.yaml
17
config.yaml
@ -12,6 +12,23 @@ database:
|
|||||||
forecast:
|
forecast:
|
||||||
caiyun_token: "ZAcZq49qzibr10F0"
|
caiyun_token: "ZAcZq49qzibr10F0"
|
||||||
|
|
||||||
|
radar:
|
||||||
|
realtime_enabled: true
|
||||||
|
realtime_interval_minutes: 60
|
||||||
|
aliases:
|
||||||
|
- alias: "海珠雷达站"
|
||||||
|
lat: 23.09
|
||||||
|
lon: 113.35
|
||||||
|
z: 7
|
||||||
|
y: 40
|
||||||
|
x: 104
|
||||||
|
- alias: "番禺雷达站"
|
||||||
|
lat: 23.0225
|
||||||
|
lon: 113.3313
|
||||||
|
z: 7
|
||||||
|
y: 40
|
||||||
|
x: 104
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
port: 3306
|
port: 3306
|
||||||
|
|||||||
@ -28,6 +28,26 @@ type ForecastConfig struct {
|
|||||||
CaiyunToken string `yaml:"caiyun_token"`
|
CaiyunToken string `yaml:"caiyun_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RadarConfig 雷达相关配置
|
||||||
|
type RadarConfig struct {
|
||||||
|
// RealtimeIntervalMinutes 彩云实况拉取周期(分钟)。允许值:10、30、60。默认 10。
|
||||||
|
RealtimeIntervalMinutes int `yaml:"realtime_interval_minutes"`
|
||||||
|
// RealtimeEnabled 是否启用彩云实况定时任务。默认 false(不下载)。
|
||||||
|
RealtimeEnabled bool `yaml:"realtime_enabled"`
|
||||||
|
// Aliases 配置化的雷达别名列表(可用于前端选择与实况拉取)。
|
||||||
|
Aliases []RadarAlias `yaml:"aliases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadarAlias 配置中的雷达别名条目
|
||||||
|
type RadarAlias struct {
|
||||||
|
Alias string `yaml:"alias"`
|
||||||
|
Lat float64 `yaml:"lat"`
|
||||||
|
Lon float64 `yaml:"lon"`
|
||||||
|
Z int `yaml:"z"`
|
||||||
|
Y int `yaml:"y"`
|
||||||
|
X int `yaml:"x"`
|
||||||
|
}
|
||||||
|
|
||||||
// MySQLConfig MySQL 连接配置(用于 rtk_data)
|
// MySQLConfig MySQL 连接配置(用于 rtk_data)
|
||||||
type MySQLConfig struct {
|
type MySQLConfig struct {
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
@ -42,6 +62,7 @@ type Config struct {
|
|||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
Forecast ForecastConfig `yaml:"forecast"`
|
Forecast ForecastConfig `yaml:"forecast"`
|
||||||
|
Radar RadarConfig `yaml:"radar"`
|
||||||
MySQL MySQLConfig `yaml:"mysql"`
|
MySQL MySQLConfig `yaml:"mysql"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +124,12 @@ func (c *Config) validate() error {
|
|||||||
if c.MySQL.Port <= 0 {
|
if c.MySQL.Port <= 0 {
|
||||||
c.MySQL.Port = 3306
|
c.MySQL.Port = 3306
|
||||||
}
|
}
|
||||||
|
// Radar 默认拉取周期
|
||||||
|
if c.Radar.RealtimeIntervalMinutes != 10 && c.Radar.RealtimeIntervalMinutes != 30 && c.Radar.RealtimeIntervalMinutes != 60 {
|
||||||
|
c.Radar.RealtimeIntervalMinutes = 10
|
||||||
|
}
|
||||||
|
// 默认关闭实时抓取(可按需开启)
|
||||||
|
// 若用户已有旧配置未设置该字段,默认为 false
|
||||||
// CaiyunToken 允许为空:表示不启用彩云定时任务
|
// CaiyunToken 允许为空:表示不启用彩云定时任务
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,20 +85,32 @@ func Start(ctx context.Context, opts Options) error {
|
|||||||
}()
|
}()
|
||||||
// 瓦片:每3分钟查询一次
|
// 瓦片:每3分钟查询一次
|
||||||
go loop3(ctx, loc, opts)
|
go loop3(ctx, loc, opts)
|
||||||
// 实况:每10分钟一次
|
// 实况:按配置开关运行(默认关闭)
|
||||||
go loop10(ctx, loc, opts)
|
rtEnabled := config.GetConfig().Radar.RealtimeEnabled
|
||||||
log.Printf("[radar] scheduler started (tiles=3m, realtime=10m, dir=%s, tile=%d/%d/%d)", opts.OutputDir, opts.Z, opts.Y, opts.X)
|
rtMin := config.GetConfig().Radar.RealtimeIntervalMinutes
|
||||||
|
if rtEnabled {
|
||||||
|
if rtMin != 10 && rtMin != 30 && rtMin != 60 {
|
||||||
|
rtMin = 10
|
||||||
|
}
|
||||||
|
go loopRealtime(ctx, loc, opts, time.Duration(rtMin)*time.Minute)
|
||||||
|
}
|
||||||
|
if rtEnabled {
|
||||||
|
log.Printf("[radar] scheduler started (tiles=3m, realtime=%dm, dir=%s, tile=%d/%d/%d)", rtMin, opts.OutputDir, opts.Z, opts.Y, opts.X)
|
||||||
|
} else {
|
||||||
|
log.Printf("[radar] scheduler started (tiles=3m, realtime=disabled, dir=%s, tile=%d/%d/%d)", opts.OutputDir, opts.Z, opts.Y, opts.X)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loop10(ctx context.Context, loc *time.Location, opts Options) {
|
// loopRealtime 周期性拉取彩云实况,按 interval 对齐边界运行
|
||||||
|
func loopRealtime(ctx context.Context, loc *time.Location, opts Options, interval time.Duration) {
|
||||||
for {
|
for {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
now := time.Now().In(loc)
|
now := time.Now().In(loc)
|
||||||
// 对齐到10分钟边界
|
// 对齐到 interval 边界
|
||||||
runAt := roundDownN(now, 10*time.Minute).Add(10 * time.Minute)
|
runAt := roundDownN(now, interval).Add(interval)
|
||||||
sleep := time.Until(runAt)
|
sleep := time.Until(runAt)
|
||||||
if sleep < 0 {
|
if sleep < 0 {
|
||||||
sleep = 0
|
sleep = 0
|
||||||
@ -285,11 +297,13 @@ var reDigits17 = regexp.MustCompile(`([0-9]{17})`)
|
|||||||
|
|
||||||
// runOnceFromNMC fetches NMC JSON, extracts timestamp, shifts +8h, then downloads CMA tile for opts.Z/Y/X.
|
// runOnceFromNMC fetches NMC JSON, extracts timestamp, shifts +8h, then downloads CMA tile for opts.Z/Y/X.
|
||||||
func runOnceFromNMC(ctx context.Context, opts Options) error {
|
func runOnceFromNMC(ctx context.Context, opts Options) error {
|
||||||
// 保留原语义:两者都执行
|
|
||||||
if err := runTilesFromNMC(ctx, opts); err != nil {
|
if err := runTilesFromNMC(ctx, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return runRealtimeFromCaiyun(ctx)
|
if config.GetConfig().Radar.RealtimeEnabled {
|
||||||
|
return runRealtimeFromCaiyun(ctx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅瓦片下载:查询 NMC,解析时间,按该时刻下载 CMA 瓦片(若DB已存在则跳过)
|
// 仅瓦片下载:查询 NMC,解析时间,按该时刻下载 CMA 瓦片(若DB已存在则跳过)
|
||||||
@ -375,23 +389,18 @@ func runTilesFromNMC(ctx context.Context, opts Options) error {
|
|||||||
|
|
||||||
// 仅彩云实况(10分钟一次)
|
// 仅彩云实况(10分钟一次)
|
||||||
func runRealtimeFromCaiyun(ctx context.Context) error {
|
func runRealtimeFromCaiyun(ctx context.Context) error {
|
||||||
if err := fetchAndStoreRadarRealtimeFor(ctx, "南宁雷达站", 23.097234, 108.715433); err != nil {
|
// 1) 配置中的别名列表
|
||||||
log.Printf("[radar] realtime(NN) failed: %v", err)
|
cfg := config.GetConfig()
|
||||||
}
|
for _, a := range cfg.Radar.Aliases {
|
||||||
if err := fetchAndStoreRadarRealtimeFor(ctx, "广州雷达站", 23.146400, 113.341200); err != nil {
|
if err := fetchAndStoreRadarRealtimeFor(ctx, a.Alias, a.Lat, a.Lon); err != nil {
|
||||||
log.Printf("[radar] realtime(GZ) failed: %v", err)
|
log.Printf("[radar] realtime(alias=%s) failed: %v", a.Alias, err)
|
||||||
}
|
}
|
||||||
if err := fetchAndStoreRadarRealtimeFor(ctx, "海珠雷达站", 23.090000, 113.350000); err != nil {
|
|
||||||
log.Printf("[radar] realtime(HAIZHU) failed: %v", err)
|
|
||||||
}
|
|
||||||
if err := fetchAndStoreRadarRealtimeFor(ctx, "番禺雷达站", 23.022500, 113.331300); err != nil {
|
|
||||||
log.Printf("[radar] realtime(PANYU) failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WH65LP 设备批量
|
// 2) WH65LP 设备批量
|
||||||
token := os.Getenv("CAIYUN_TOKEN")
|
token := os.Getenv("CAIYUN_TOKEN")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
token = config.GetConfig().Forecast.CaiyunToken
|
token = cfg.Forecast.CaiyunToken
|
||||||
}
|
}
|
||||||
if token == "" {
|
if token == "" {
|
||||||
log.Printf("[radar] skip station realtime: missing CAIYUN_TOKEN")
|
log.Printf("[radar] skip station realtime: missing CAIYUN_TOKEN")
|
||||||
@ -537,12 +546,16 @@ func fetchAndStoreRadarRealtimeFor(ctx context.Context, alias string, lat, lon f
|
|||||||
return fmt.Errorf("realtime api status=%s", payload.Status)
|
return fmt.Errorf("realtime api status=%s", payload.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Align to 10-minute bucket in Asia/Shanghai
|
// Align to configured bucket in Asia/Shanghai
|
||||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
if loc == nil {
|
if loc == nil {
|
||||||
loc = time.FixedZone("CST", 8*3600)
|
loc = time.FixedZone("CST", 8*3600)
|
||||||
}
|
}
|
||||||
dt := roundDownN(time.Now().In(loc), 10*time.Minute)
|
bucketMin := config.GetConfig().Radar.RealtimeIntervalMinutes
|
||||||
|
if bucketMin != 10 && bucketMin != 30 && bucketMin != 60 {
|
||||||
|
bucketMin = 10
|
||||||
|
}
|
||||||
|
dt := roundDownN(time.Now().In(loc), time.Duration(bucketMin)*time.Minute)
|
||||||
|
|
||||||
// Store
|
// Store
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|||||||
@ -44,9 +44,12 @@ func StartGinServer() error {
|
|||||||
api.GET("/forecast", getForecastHandler)
|
api.GET("/forecast", getForecastHandler)
|
||||||
api.GET("/radar/latest", latestRadarTileHandler)
|
api.GET("/radar/latest", latestRadarTileHandler)
|
||||||
api.GET("/radar/at", radarTileAtHandler)
|
api.GET("/radar/at", radarTileAtHandler)
|
||||||
|
api.GET("/radar/nearest", nearestRadarTileHandler)
|
||||||
api.GET("/radar/times", radarTileTimesHandler)
|
api.GET("/radar/times", radarTileTimesHandler)
|
||||||
api.GET("/radar/weather_latest", latestRadarWeatherHandler)
|
api.GET("/radar/weather_latest", latestRadarWeatherHandler)
|
||||||
api.GET("/radar/weather_at", radarWeatherAtHandler)
|
api.GET("/radar/weather_at", radarWeatherAtHandler)
|
||||||
|
api.GET("/radar/weather_aliases", radarWeatherAliasesHandler)
|
||||||
|
api.GET("/radar/aliases", radarConfigAliasesHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取配置的Web端口
|
// 获取配置的Web端口
|
||||||
@ -74,45 +77,45 @@ func indexHandler(c *gin.Context) {
|
|||||||
// radarNanningHandler 南宁雷达站占位页
|
// radarNanningHandler 南宁雷达站占位页
|
||||||
func radarNanningHandler(c *gin.Context) {
|
func radarNanningHandler(c *gin.Context) {
|
||||||
data := types.PageData{
|
data := types.PageData{
|
||||||
Title: "南宁雷达站",
|
Title: "雷达页面",
|
||||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "radar_nanning.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// radarGuangzhouHandler 广州雷达站占位页
|
// radarGuangzhouHandler 广州雷达站占位页
|
||||||
func radarGuangzhouHandler(c *gin.Context) {
|
func radarGuangzhouHandler(c *gin.Context) {
|
||||||
data := types.PageData{
|
data := types.PageData{
|
||||||
Title: "广州雷达站",
|
Title: "雷达页面",
|
||||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "radar_guangzhou.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// radarHaizhuHandler 海珠雷达站占位页
|
// radarHaizhuHandler 海珠雷达站占位页
|
||||||
func radarHaizhuHandler(c *gin.Context) {
|
func radarHaizhuHandler(c *gin.Context) {
|
||||||
data := types.PageData{
|
data := types.PageData{
|
||||||
Title: "海珠雷达站",
|
Title: "雷达页面",
|
||||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "radar_haizhu.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// radarPanyuHandler 番禺雷达站占位页
|
// radarPanyuHandler 番禺雷达站占位页
|
||||||
func radarPanyuHandler(c *gin.Context) {
|
func radarPanyuHandler(c *gin.Context) {
|
||||||
data := types.PageData{
|
data := types.PageData{
|
||||||
Title: "番禺雷达站",
|
Title: "雷达页面",
|
||||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "radar_panyu.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imdroidRadarHandler(c *gin.Context) {
|
func imdroidRadarHandler(c *gin.Context) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
"weatherstation/internal/config"
|
||||||
"weatherstation/internal/database"
|
"weatherstation/internal/database"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -255,6 +256,104 @@ func radarTileTimesHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"times": times})
|
c.JSON(http.StatusOK, gin.H{"times": times})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nearestRadarTileHandler 返回最接近给定时间的瓦片(支持 z/y/x、容差分钟、偏好 lte 或 nearest)
|
||||||
|
func nearestRadarTileHandler(c *gin.Context) {
|
||||||
|
z := parseIntDefault(c.Query("z"), 7)
|
||||||
|
y := parseIntDefault(c.Query("y"), 40)
|
||||||
|
x := parseIntDefault(c.Query("x"), 102)
|
||||||
|
dtStr := c.Query("dt")
|
||||||
|
if dtStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 dt 参数(YYYY-MM-DD HH:MM:SS)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tolMin := parseIntDefault(c.Query("tolerance_min"), 30)
|
||||||
|
prefer := c.DefaultQuery("prefer", "nearest") // nearest|lte
|
||||||
|
|
||||||
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if loc == nil {
|
||||||
|
loc = time.FixedZone("CST", 8*3600)
|
||||||
|
}
|
||||||
|
target, err := time.ParseInLocation("2006-01-02 15:04:05", dtStr, loc)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 dt 格式"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
from := target.Add(-time.Duration(tolMin) * time.Minute)
|
||||||
|
to := target.Add(time.Duration(tolMin) * time.Minute)
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
var row *sql.Row
|
||||||
|
if prefer == "lte" {
|
||||||
|
const q = `
|
||||||
|
SELECT dt FROM radar_tiles
|
||||||
|
WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 AND dt <= $6
|
||||||
|
ORDER BY ($6 - dt) ASC
|
||||||
|
LIMIT 1`
|
||||||
|
row = db.QueryRow(q, z, y, x, from, to, target)
|
||||||
|
} else {
|
||||||
|
const q = `
|
||||||
|
SELECT dt FROM radar_tiles
|
||||||
|
WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5
|
||||||
|
ORDER BY ABS(EXTRACT(EPOCH FROM (dt - $6))) ASC
|
||||||
|
LIMIT 1`
|
||||||
|
row = db.QueryRow(q, z, y, x, from, to, target)
|
||||||
|
}
|
||||||
|
var picked time.Time
|
||||||
|
if err := row.Scan(&picked); err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "在容差范围内未找到匹配瓦片"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rec, err := getRadarTileAt(db, z, y, x, picked)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "未找到匹配瓦片数据"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码与 latest/at 相同
|
||||||
|
w, h := rec.Width, rec.Height
|
||||||
|
if len(rec.Data) < w*h*2 {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据长度异常"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vals := make([][]*float64, h)
|
||||||
|
off := 0
|
||||||
|
for rowi := 0; rowi < h; rowi++ {
|
||||||
|
rowVals := make([]*float64, w)
|
||||||
|
for col := 0; col < w; col++ {
|
||||||
|
v := int16(binary.BigEndian.Uint16(rec.Data[off : off+2]))
|
||||||
|
off += 2
|
||||||
|
if v >= 32766 {
|
||||||
|
rowVals[col] = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dbz := float64(v) / 10.0
|
||||||
|
if dbz < 0 {
|
||||||
|
dbz = 0
|
||||||
|
} else if dbz > 75 {
|
||||||
|
dbz = 75
|
||||||
|
}
|
||||||
|
vv := dbz
|
||||||
|
rowVals[col] = &vv
|
||||||
|
}
|
||||||
|
vals[rowi] = rowVals
|
||||||
|
}
|
||||||
|
resp := radarTileResponse{
|
||||||
|
DT: rec.DT.Format("2006-01-02 15:04:05"),
|
||||||
|
Z: rec.Z,
|
||||||
|
Y: rec.Y,
|
||||||
|
X: rec.X,
|
||||||
|
Width: rec.Width,
|
||||||
|
Height: rec.Height,
|
||||||
|
West: rec.West,
|
||||||
|
South: rec.South,
|
||||||
|
East: rec.East,
|
||||||
|
North: rec.North,
|
||||||
|
ResDeg: rec.ResDeg,
|
||||||
|
Values: vals,
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
func parseIntDefault(s string, def int) int {
|
func parseIntDefault(s string, def int) int {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return def
|
return def
|
||||||
@ -420,3 +519,51 @@ func radarWeatherAtHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, resp)
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// radarWeatherAliasesHandler 返回 radar_weather 中存在的站点别名及经纬度(按最近记录去重)
|
||||||
|
func radarWeatherAliasesHandler(c *gin.Context) {
|
||||||
|
const q = `
|
||||||
|
SELECT DISTINCT ON (alias) alias, lat, lon, dt
|
||||||
|
FROM radar_weather
|
||||||
|
ORDER BY alias, dt DESC`
|
||||||
|
rows, err := database.GetDB().Query(q)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询别名失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
type item struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
var list []item
|
||||||
|
for rows.Next() {
|
||||||
|
var a string
|
||||||
|
var lat, lon float64
|
||||||
|
var dt time.Time
|
||||||
|
if err := rows.Scan(&a, &lat, &lon, &dt); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list = append(list, item{Alias: a, Lat: lat, Lon: lon})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"aliases": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// radarConfigAliasesHandler 返回配置文件中的雷达别名列表(含 z/y/x 和经纬度)
|
||||||
|
func radarConfigAliasesHandler(c *gin.Context) {
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
type item struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
Z int `json:"z"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
X int `json:"x"`
|
||||||
|
}
|
||||||
|
out := make([]item, 0, len(cfg.Radar.Aliases))
|
||||||
|
for _, a := range cfg.Radar.Aliases {
|
||||||
|
out = append(out, item{Alias: a.Alias, Lat: a.Lat, Lon: a.Lon, Z: a.Z, Y: a.Y, X: a.X})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"aliases": out})
|
||||||
|
}
|
||||||
|
|||||||
@ -31,19 +31,61 @@
|
|||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = ''; opt.textContent = '请选择站点…';
|
opt.value = ''; opt.textContent = '请选择站点…';
|
||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
const res = await fetch('/api/stations');
|
try {
|
||||||
const stations = await res.json();
|
// 1) 实际设备站点
|
||||||
stations
|
const res = await fetch('/api/stations');
|
||||||
.filter(s => s.device_type === 'WH65LP' && s.latitude && s.longitude)
|
const stations = await res.json();
|
||||||
.forEach(s => {
|
stations
|
||||||
|
.filter(s => s.device_type === 'WH65LP' && s.latitude && s.longitude)
|
||||||
|
.forEach(s => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = s.station_id; // 用 station_id 作为联动主键
|
||||||
|
const alias = (s.station_alias && String(s.station_alias).trim().length > 0) ? s.station_alias : s.station_id;
|
||||||
|
o.textContent = alias; // 仅显示别名
|
||||||
|
o.dataset.z = s.z; o.dataset.y = s.y; o.dataset.x = s.x;
|
||||||
|
o.dataset.lat = s.latitude; o.dataset.lon = s.longitude;
|
||||||
|
o.dataset.kind = 'station';
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
// 2) 从配置读取别名(如 海珠/番禺),追加到同一下拉
|
||||||
|
const res2 = await fetch('/api/radar/aliases');
|
||||||
|
if (res2.ok) {
|
||||||
|
const j = await res2.json();
|
||||||
|
(j.aliases || []).forEach(a => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = a.alias;
|
||||||
|
o.textContent = a.alias;
|
||||||
|
o.dataset.z = a.z; o.dataset.y = a.y; o.dataset.x = a.x;
|
||||||
|
o.dataset.lat = a.lat; o.dataset.lon = a.lon;
|
||||||
|
o.dataset.kind = 'alias';
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAliases() {
|
||||||
|
const sel = document.getElementById('aliasSelect');
|
||||||
|
if (!sel) return;
|
||||||
|
sel.innerHTML = '';
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = ''; opt.textContent = '或选择雷达别名(海珠/番禺)…';
|
||||||
|
sel.appendChild(opt);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/radar/weather_aliases');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const j = await res.json();
|
||||||
|
(j.aliases || []).forEach(a => {
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = s.station_id; // 用 station_id 作为联动主键
|
o.value = a.alias;
|
||||||
const alias = (s.station_alias && String(s.station_alias).trim().length > 0) ? s.station_alias : s.station_id;
|
o.textContent = a.alias;
|
||||||
o.textContent = alias; // 仅显示别名
|
o.dataset.lat = a.lat;
|
||||||
o.dataset.z = s.z; o.dataset.y = s.y; o.dataset.x = s.x;
|
o.dataset.lon = a.lon;
|
||||||
o.dataset.lat = s.latitude; o.dataset.lon = s.longitude;
|
|
||||||
sel.appendChild(o);
|
sel.appendChild(o);
|
||||||
});
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRealtimeBox(j){
|
function setRealtimeBox(j){
|
||||||
@ -522,15 +564,15 @@
|
|||||||
})();
|
})();
|
||||||
document.getElementById('btnLoad').addEventListener('click', async ()=>{
|
document.getElementById('btnLoad').addEventListener('click', async ()=>{
|
||||||
const sel = document.getElementById('stationSelect');
|
const sel = document.getElementById('stationSelect');
|
||||||
const alias = sel.value;
|
if (!sel || !sel.value) return;
|
||||||
if (!alias) return;
|
const alias = sel.options[sel.selectedIndex].textContent || sel.value;
|
||||||
gAlias = alias;
|
|
||||||
gZ = Number(sel.options[sel.selectedIndex].dataset.z || 0);
|
gZ = Number(sel.options[sel.selectedIndex].dataset.z || 0);
|
||||||
gY = Number(sel.options[sel.selectedIndex].dataset.y || 0);
|
gY = Number(sel.options[sel.selectedIndex].dataset.y || 0);
|
||||||
gX = Number(sel.options[sel.selectedIndex].dataset.x || 0);
|
gX = Number(sel.options[sel.selectedIndex].dataset.x || 0);
|
||||||
const lat = Number(sel.options[sel.selectedIndex].dataset.lat);
|
const lat = Number(sel.options[sel.selectedIndex].dataset.lat);
|
||||||
const lon = Number(sel.options[sel.selectedIndex].dataset.lon);
|
const lon = Number(sel.options[sel.selectedIndex].dataset.lon);
|
||||||
gStLat = isNaN(lat)? null : lat; gStLon = isNaN(lon)? null : lon;
|
gStLat = isNaN(lat)? null : lat; gStLon = isNaN(lon)? null : lon;
|
||||||
|
gAlias = alias;
|
||||||
document.getElementById('rt_zyx').textContent = `z=${gZ}, y=${gY}, x=${gX}`;
|
document.getElementById('rt_zyx').textContent = `z=${gZ}, y=${gY}, x=${gX}`;
|
||||||
try { await loadRealtime(alias); } catch (e) { console.warn(e); }
|
try { await loadRealtime(alias); } catch (e) { console.warn(e); }
|
||||||
if (gZ && gY && gX) {
|
if (gZ && gY && gX) {
|
||||||
@ -615,7 +657,7 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="text-sm">选择站点:</label>
|
<label class="text-sm">选择站点:</label>
|
||||||
<select id="stationSelect" class="border rounded px-2 py-1 text-sm min-w-[280px]"></select>
|
<select id="stationSelect" class="border rounded px-2 py-1 text-sm min-w-[360px]"></select>
|
||||||
<button id="btnLoad" class="px-2.5 py-1 text-sm bg-blue-600 text-white rounded">加载数据</button>
|
<button id="btnLoad" class="px-2.5 py-1 text-sm bg-blue-600 text-white rounded">加载数据</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user