feat: 新增彩云气象数据下载的时间控制
This commit is contained in:
parent
13b4117d75
commit
9f960c6411
17
config.yaml
17
config.yaml
@ -12,6 +12,23 @@ database:
|
||||
forecast:
|
||||
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:
|
||||
host: "127.0.0.1"
|
||||
port: 3306
|
||||
|
||||
@ -28,6 +28,26 @@ type ForecastConfig struct {
|
||||
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)
|
||||
type MySQLConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
@ -42,6 +62,7 @@ type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Forecast ForecastConfig `yaml:"forecast"`
|
||||
Radar RadarConfig `yaml:"radar"`
|
||||
MySQL MySQLConfig `yaml:"mysql"`
|
||||
}
|
||||
|
||||
@ -103,6 +124,12 @@ func (c *Config) validate() error {
|
||||
if c.MySQL.Port <= 0 {
|
||||
c.MySQL.Port = 3306
|
||||
}
|
||||
// Radar 默认拉取周期
|
||||
if c.Radar.RealtimeIntervalMinutes != 10 && c.Radar.RealtimeIntervalMinutes != 30 && c.Radar.RealtimeIntervalMinutes != 60 {
|
||||
c.Radar.RealtimeIntervalMinutes = 10
|
||||
}
|
||||
// 默认关闭实时抓取(可按需开启)
|
||||
// 若用户已有旧配置未设置该字段,默认为 false
|
||||
// CaiyunToken 允许为空:表示不启用彩云定时任务
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -85,20 +85,32 @@ func Start(ctx context.Context, opts Options) error {
|
||||
}()
|
||||
// 瓦片:每3分钟查询一次
|
||||
go loop3(ctx, loc, opts)
|
||||
// 实况:每10分钟一次
|
||||
go loop10(ctx, loc, opts)
|
||||
log.Printf("[radar] scheduler started (tiles=3m, realtime=10m, dir=%s, tile=%d/%d/%d)", opts.OutputDir, opts.Z, opts.Y, opts.X)
|
||||
// 实况:按配置开关运行(默认关闭)
|
||||
rtEnabled := config.GetConfig().Radar.RealtimeEnabled
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
// 对齐到10分钟边界
|
||||
runAt := roundDownN(now, 10*time.Minute).Add(10 * time.Minute)
|
||||
// 对齐到 interval 边界
|
||||
runAt := roundDownN(now, interval).Add(interval)
|
||||
sleep := time.Until(runAt)
|
||||
if sleep < 0 {
|
||||
sleep = 0
|
||||
@ -285,12 +297,14 @@ var reDigits17 = regexp.MustCompile(`([0-9]{17})`)
|
||||
|
||||
// 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 {
|
||||
// 保留原语义:两者都执行
|
||||
if err := runTilesFromNMC(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.GetConfig().Radar.RealtimeEnabled {
|
||||
return runRealtimeFromCaiyun(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 仅瓦片下载:查询 NMC,解析时间,按该时刻下载 CMA 瓦片(若DB已存在则跳过)
|
||||
func runTilesFromNMC(ctx context.Context, opts Options) error {
|
||||
@ -375,23 +389,18 @@ func runTilesFromNMC(ctx context.Context, opts Options) error {
|
||||
|
||||
// 仅彩云实况(10分钟一次)
|
||||
func runRealtimeFromCaiyun(ctx context.Context) error {
|
||||
if err := fetchAndStoreRadarRealtimeFor(ctx, "南宁雷达站", 23.097234, 108.715433); err != nil {
|
||||
log.Printf("[radar] realtime(NN) failed: %v", err)
|
||||
// 1) 配置中的别名列表
|
||||
cfg := config.GetConfig()
|
||||
for _, a := range cfg.Radar.Aliases {
|
||||
if err := fetchAndStoreRadarRealtimeFor(ctx, a.Alias, a.Lat, a.Lon); err != nil {
|
||||
log.Printf("[radar] realtime(alias=%s) failed: %v", a.Alias, err)
|
||||
}
|
||||
if err := fetchAndStoreRadarRealtimeFor(ctx, "广州雷达站", 23.146400, 113.341200); err != nil {
|
||||
log.Printf("[radar] realtime(GZ) failed: %v", 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")
|
||||
if token == "" {
|
||||
token = config.GetConfig().Forecast.CaiyunToken
|
||||
token = cfg.Forecast.CaiyunToken
|
||||
}
|
||||
if 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)
|
||||
}
|
||||
|
||||
// Align to 10-minute bucket in Asia/Shanghai
|
||||
// Align to configured bucket in Asia/Shanghai
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
if loc == nil {
|
||||
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
|
||||
db := database.GetDB()
|
||||
|
||||
@ -44,9 +44,12 @@ func StartGinServer() error {
|
||||
api.GET("/forecast", getForecastHandler)
|
||||
api.GET("/radar/latest", latestRadarTileHandler)
|
||||
api.GET("/radar/at", radarTileAtHandler)
|
||||
api.GET("/radar/nearest", nearestRadarTileHandler)
|
||||
api.GET("/radar/times", radarTileTimesHandler)
|
||||
api.GET("/radar/weather_latest", latestRadarWeatherHandler)
|
||||
api.GET("/radar/weather_at", radarWeatherAtHandler)
|
||||
api.GET("/radar/weather_aliases", radarWeatherAliasesHandler)
|
||||
api.GET("/radar/aliases", radarConfigAliasesHandler)
|
||||
}
|
||||
|
||||
// 获取配置的Web端口
|
||||
@ -74,45 +77,45 @@ func indexHandler(c *gin.Context) {
|
||||
// radarNanningHandler 南宁雷达站占位页
|
||||
func radarNanningHandler(c *gin.Context) {
|
||||
data := types.PageData{
|
||||
Title: "南宁雷达站",
|
||||
Title: "雷达页面",
|
||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||
}
|
||||
c.HTML(http.StatusOK, "radar_nanning.html", data)
|
||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||
}
|
||||
|
||||
// radarGuangzhouHandler 广州雷达站占位页
|
||||
func radarGuangzhouHandler(c *gin.Context) {
|
||||
data := types.PageData{
|
||||
Title: "广州雷达站",
|
||||
Title: "雷达页面",
|
||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||
}
|
||||
c.HTML(http.StatusOK, "radar_guangzhou.html", data)
|
||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||
}
|
||||
|
||||
// radarHaizhuHandler 海珠雷达站占位页
|
||||
func radarHaizhuHandler(c *gin.Context) {
|
||||
data := types.PageData{
|
||||
Title: "海珠雷达站",
|
||||
Title: "雷达页面",
|
||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||
}
|
||||
c.HTML(http.StatusOK, "radar_haizhu.html", data)
|
||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||
}
|
||||
|
||||
// radarPanyuHandler 番禺雷达站占位页
|
||||
func radarPanyuHandler(c *gin.Context) {
|
||||
data := types.PageData{
|
||||
Title: "番禺雷达站",
|
||||
Title: "雷达页面",
|
||||
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
|
||||
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
|
||||
}
|
||||
c.HTML(http.StatusOK, "radar_panyu.html", data)
|
||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||
}
|
||||
|
||||
func imdroidRadarHandler(c *gin.Context) {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
"weatherstation/internal/config"
|
||||
"weatherstation/internal/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -255,6 +256,104 @@ func radarTileTimesHandler(c *gin.Context) {
|
||||
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 {
|
||||
if s == "" {
|
||||
return def
|
||||
@ -420,3 +519,51 @@ func radarWeatherAtHandler(c *gin.Context) {
|
||||
}
|
||||
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,6 +31,8 @@
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ''; opt.textContent = '请选择站点…';
|
||||
sel.appendChild(opt);
|
||||
try {
|
||||
// 1) 实际设备站点
|
||||
const res = await fetch('/api/stations');
|
||||
const stations = await res.json();
|
||||
stations
|
||||
@ -42,8 +44,48 @@
|
||||
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');
|
||||
o.value = a.alias;
|
||||
o.textContent = a.alias;
|
||||
o.dataset.lat = a.lat;
|
||||
o.dataset.lon = a.lon;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function setRealtimeBox(j){
|
||||
@ -522,15 +564,15 @@
|
||||
})();
|
||||
document.getElementById('btnLoad').addEventListener('click', async ()=>{
|
||||
const sel = document.getElementById('stationSelect');
|
||||
const alias = sel.value;
|
||||
if (!alias) return;
|
||||
gAlias = alias;
|
||||
if (!sel || !sel.value) return;
|
||||
const alias = sel.options[sel.selectedIndex].textContent || sel.value;
|
||||
gZ = Number(sel.options[sel.selectedIndex].dataset.z || 0);
|
||||
gY = Number(sel.options[sel.selectedIndex].dataset.y || 0);
|
||||
gX = Number(sel.options[sel.selectedIndex].dataset.x || 0);
|
||||
const lat = Number(sel.options[sel.selectedIndex].dataset.lat);
|
||||
const lon = Number(sel.options[sel.selectedIndex].dataset.lon);
|
||||
gStLat = isNaN(lat)? null : lat; gStLon = isNaN(lon)? null : lon;
|
||||
gAlias = alias;
|
||||
document.getElementById('rt_zyx').textContent = `z=${gZ}, y=${gY}, x=${gX}`;
|
||||
try { await loadRealtime(alias); } catch (e) { console.warn(e); }
|
||||
if (gZ && gY && gX) {
|
||||
@ -615,7 +657,7 @@
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user