增加地图展示 在地图上面展示kml边界
This commit is contained in:
parent
65f95662ca
commit
9df02f5b5f
47
README.md
47
README.md
@ -1,47 +0,0 @@
|
|||||||
# 气象站数据接收系统
|
|
||||||
|
|
||||||
UDP接收气象站数据,存PostgreSQL。
|
|
||||||
|
|
||||||
## 数据库字段转换
|
|
||||||
|
|
||||||
### 温度
|
|
||||||
- `temp_f`, `dewpoint_f`, `windchill_f`, `indoor_temp_f`: 存储值=实际值×10 (°F)
|
|
||||||
|
|
||||||
### 湿度
|
|
||||||
- `humidity`, `indoor_humidity`: 直接存整数百分比 (%)
|
|
||||||
|
|
||||||
### 风速
|
|
||||||
- `wind_dir`: 直接存角度 (°)
|
|
||||||
- `wind_speed_mph`, `wind_gust_mph`: 存储值=实际值×100 (mph)
|
|
||||||
|
|
||||||
### 降雨量
|
|
||||||
- 所有rain字段: 存储值=实际值×1000 (英寸)
|
|
||||||
|
|
||||||
### 气压
|
|
||||||
- `abs_barometer_in`, `barometer_in`: 存储值=实际值×1000 (英寸汞柱)
|
|
||||||
|
|
||||||
### 其他
|
|
||||||
- `solar_radiation`: 存储值=实际值×100 (W/m²)
|
|
||||||
- `uv`: 直接存整数
|
|
||||||
- `low_battery`: 布尔值
|
|
||||||
|
|
||||||
## 单位转换
|
|
||||||
- 华氏→摄氏: (°F - 32) * 5/9
|
|
||||||
- 英里→公里: mph * 1.60934
|
|
||||||
- 英寸→毫米: in * 25.4
|
|
||||||
- 英寸汞柱→百帕: inHg * 33.8639
|
|
||||||
|
|
||||||
## 查询示例
|
|
||||||
```sql
|
|
||||||
SELECT
|
|
||||||
station_id,
|
|
||||||
timestamp AT TIME ZONE 'Asia/Shanghai' as local_time,
|
|
||||||
temp_f::float/10 as temp_f,
|
|
||||||
(temp_f::float/10 - 32) * 5/9 as temp_c,
|
|
||||||
humidity,
|
|
||||||
wind_speed_mph::float/100 as wind_speed_mph,
|
|
||||||
barometer_in::float/1000 * 33.8639 as barometer_hpa
|
|
||||||
FROM weather_data
|
|
||||||
ORDER BY timestamp DESC
|
|
||||||
LIMIT 10;
|
|
||||||
```
|
|
||||||
@ -1,12 +1,16 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"weatherstation/internal/config"
|
"weatherstation/internal/config"
|
||||||
"weatherstation/internal/database"
|
"weatherstation/internal/database"
|
||||||
@ -15,6 +19,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var staticBaseDir string
|
||||||
|
|
||||||
// StartGinServer 启动Gin Web服务器
|
// StartGinServer 启动Gin Web服务器
|
||||||
func StartGinServer() error {
|
func StartGinServer() error {
|
||||||
// 设置Gin模式
|
// 设置Gin模式
|
||||||
@ -61,6 +67,7 @@ func StartGinServer() error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
staticBaseDir = staticDir
|
||||||
r.Static("/static", staticDir)
|
r.Static("/static", staticDir)
|
||||||
|
|
||||||
// 前端SPA(Angular)静态资源与路由回退
|
// 前端SPA(Angular)静态资源与路由回退
|
||||||
@ -159,6 +166,7 @@ func indexHandler(c *gin.Context) {
|
|||||||
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",
|
||||||
|
KmlLayersJSON: buildKmlLayersJSON(),
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "index.html", data)
|
c.HTML(http.StatusOK, "index.html", data)
|
||||||
}
|
}
|
||||||
@ -170,6 +178,7 @@ func radarNanningHandler(c *gin.Context) {
|
|||||||
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",
|
||||||
|
KmlLayersJSON: buildKmlLayersJSON(),
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
@ -181,6 +190,7 @@ func radarGuangzhouHandler(c *gin.Context) {
|
|||||||
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",
|
||||||
|
KmlLayersJSON: buildKmlLayersJSON(),
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
@ -192,6 +202,7 @@ func radarHaizhuHandler(c *gin.Context) {
|
|||||||
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",
|
||||||
|
KmlLayersJSON: buildKmlLayersJSON(),
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
@ -203,6 +214,7 @@ func radarPanyuHandler(c *gin.Context) {
|
|||||||
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",
|
||||||
|
KmlLayersJSON: buildKmlLayersJSON(),
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
@ -213,10 +225,54 @@ func imdroidRadarHandler(c *gin.Context) {
|
|||||||
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",
|
||||||
|
KmlLayersJSON: buildKmlLayersJSON(),
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
c.HTML(http.StatusOK, "imdroid_radar.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildKmlLayersJSON() template.JS {
|
||||||
|
layers := loadKmlLayers()
|
||||||
|
if len(layers) == 0 {
|
||||||
|
return template.JS("[]")
|
||||||
|
}
|
||||||
|
bytes, err := json.Marshal(layers)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("序列化KML列表失败: %v", err)
|
||||||
|
return template.JS("[]")
|
||||||
|
}
|
||||||
|
return template.JS(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadKmlLayers() []types.KmlLayer {
|
||||||
|
if staticBaseDir == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
kmlDir := filepath.Join(staticBaseDir, "kml")
|
||||||
|
entries, err := os.ReadDir(kmlDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
layers := make([]types.KmlLayer, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasSuffix(strings.ToLower(name), ".kml") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
display := strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
layers = append(layers, types.KmlLayer{
|
||||||
|
Name: display,
|
||||||
|
URL: "/static/kml/" + name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(layers, func(i, j int) bool {
|
||||||
|
return strings.ToLower(layers[i].Name) < strings.ToLower(layers[j].Name)
|
||||||
|
})
|
||||||
|
return layers
|
||||||
|
}
|
||||||
|
|
||||||
// systemStatusHandler 处理系统状态API请求
|
// systemStatusHandler 处理系统状态API请求
|
||||||
func systemStatusHandler(c *gin.Context) {
|
func systemStatusHandler(c *gin.Context) {
|
||||||
status := types.SystemStatus{
|
status := types.SystemStatus{
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
|
import "html/template"
|
||||||
|
|
||||||
// Station 站点信息
|
// Station 站点信息
|
||||||
type Station struct {
|
type Station struct {
|
||||||
StationID string `json:"station_id"`
|
StationID string `json:"station_id"`
|
||||||
@ -31,12 +33,19 @@ type WeatherPoint struct {
|
|||||||
RainTotal float64 `json:"rain_total"`
|
RainTotal float64 `json:"rain_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KmlLayer 描述一个可供前端加载的KML图层
|
||||||
|
type KmlLayer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
// PageData 页面数据结构
|
// PageData 页面数据结构
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Title string
|
Title string
|
||||||
ServerTime string
|
ServerTime string
|
||||||
OnlineDevices int
|
OnlineDevices int
|
||||||
TiandituKey string
|
TiandituKey string
|
||||||
|
KmlLayersJSON template.JS
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemStatus 系统状态结构
|
// SystemStatus 系统状态结构
|
||||||
|
|||||||
@ -9,6 +9,12 @@ const WeatherMap = {
|
|||||||
combinedChart: null,
|
combinedChart: null,
|
||||||
CLUSTER_THRESHOLD: 10,
|
CLUSTER_THRESHOLD: 10,
|
||||||
isMapCollapsed: false,
|
isMapCollapsed: false,
|
||||||
|
kmlLayers: [],
|
||||||
|
kmlLayerGroup: null,
|
||||||
|
activeKmlLayer: null,
|
||||||
|
kmlLastExtent: null,
|
||||||
|
kmlFitButton: null,
|
||||||
|
_kmlControlsBound: false,
|
||||||
|
|
||||||
// 初始化地图
|
// 初始化地图
|
||||||
init(tiandituKey) {
|
init(tiandituKey) {
|
||||||
@ -16,6 +22,7 @@ const WeatherMap = {
|
|||||||
this.initializeMap(tiandituKey);
|
this.initializeMap(tiandituKey);
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.setupTileControls();
|
this.setupTileControls();
|
||||||
|
this.setupKmlControls();
|
||||||
// 默认叠加:组合反射率(radar),并准备默认索引
|
// 默认叠加:组合反射率(radar),并准备默认索引
|
||||||
this.tileZ = 7; this.tileY = 40; this.tileX = 102;
|
this.tileZ = 7; this.tileY = 40; this.tileX = 102;
|
||||||
this.tileProduct = 'radar';
|
this.tileProduct = 'radar';
|
||||||
@ -50,13 +57,20 @@ const WeatherMap = {
|
|||||||
style: (feature) => this.createStationStyle(feature),
|
style: (feature) => this.createStationStyle(feature),
|
||||||
visible: false
|
visible: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.kmlLayerGroup = new ol.layer.Group({
|
||||||
|
layers: [],
|
||||||
|
zIndex: 1200,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 初始化地图
|
// 初始化地图
|
||||||
initializeMap(tiandituKey) {
|
initializeMap(tiandituKey) {
|
||||||
const layers = this.createMapLayers(tiandituKey);
|
const layers = this.createMapLayers(tiandituKey);
|
||||||
this.layers = layers;
|
this.layers = layers;
|
||||||
|
|
||||||
|
const tileOverlayGroup = this.createTileOverlayLayer();
|
||||||
this.map = new ol.Map({
|
this.map = new ol.Map({
|
||||||
target: 'map',
|
target: 'map',
|
||||||
layers: [
|
layers: [
|
||||||
@ -65,7 +79,8 @@ const WeatherMap = {
|
|||||||
layers.terrain,
|
layers.terrain,
|
||||||
layers.hybrid,
|
layers.hybrid,
|
||||||
// 栅格瓦片叠加层(动态)
|
// 栅格瓦片叠加层(动态)
|
||||||
this.createTileOverlayLayer(),
|
tileOverlayGroup,
|
||||||
|
this.kmlLayerGroup,
|
||||||
this.clusterLayer,
|
this.clusterLayer,
|
||||||
this.singleStationLayer
|
this.singleStationLayer
|
||||||
],
|
],
|
||||||
@ -219,6 +234,52 @@ const WeatherMap = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setupKmlControls(){
|
||||||
|
if (this._kmlControlsBound) return;
|
||||||
|
this._kmlControlsBound = true;
|
||||||
|
|
||||||
|
const select = document.getElementById('kmlLayerSelect');
|
||||||
|
const fitBtn = document.getElementById('btnKmlFit');
|
||||||
|
this.kmlFitButton = fitBtn || null;
|
||||||
|
this.kmlLayers = Array.isArray(window.KML_LAYERS) ? window.KML_LAYERS : [];
|
||||||
|
|
||||||
|
if (select) {
|
||||||
|
select.innerHTML = '';
|
||||||
|
const defaultOption = document.createElement('option');
|
||||||
|
defaultOption.value = '';
|
||||||
|
defaultOption.textContent = '不显示';
|
||||||
|
select.appendChild(defaultOption);
|
||||||
|
|
||||||
|
if (this.kmlLayers.length === 0) {
|
||||||
|
const emptyOption = document.createElement('option');
|
||||||
|
emptyOption.value = '';
|
||||||
|
emptyOption.textContent = '暂无KML图层';
|
||||||
|
select.appendChild(emptyOption);
|
||||||
|
select.disabled = true;
|
||||||
|
} else {
|
||||||
|
this.kmlLayers.forEach(layer => {
|
||||||
|
if (!layer || !layer.url) return;
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = layer.url;
|
||||||
|
option.textContent = layer.name || layer.url;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
select.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', (event)=>{
|
||||||
|
const url = event.target.value || '';
|
||||||
|
this.switchKmlLayer(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.kmlFitButton) {
|
||||||
|
this.kmlFitButton.addEventListener('click', ()=>this.fitToKmlExtent());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateKmlFitButton();
|
||||||
|
},
|
||||||
|
|
||||||
async reloadTileTimesAndShow(){
|
async reloadTileTimesAndShow(){
|
||||||
try{
|
try{
|
||||||
const times = await this.fetchTileTimes(this.tileProduct, this.tileZ, this.tileY, this.tileX);
|
const times = await this.fetchTileTimes(this.tileProduct, this.tileZ, this.tileY, this.tileX);
|
||||||
@ -318,6 +379,15 @@ const WeatherMap = {
|
|||||||
this.tileLastList = [];
|
this.tileLastList = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearKmlLayer(){
|
||||||
|
if (!this.kmlLayerGroup) return;
|
||||||
|
const layers = this.kmlLayerGroup.getLayers();
|
||||||
|
if (layers) layers.clear();
|
||||||
|
this.activeKmlLayer = null;
|
||||||
|
this.kmlLastExtent = null;
|
||||||
|
this.updateKmlFitButton();
|
||||||
|
},
|
||||||
|
|
||||||
addImageOverlayFromCanvas(canvas, extent4326){
|
addImageOverlayFromCanvas(canvas, extent4326){
|
||||||
const proj = this.map.getView().getProjection();
|
const proj = this.map.getView().getProjection();
|
||||||
const extentProj = ol.proj.transformExtent(extent4326, 'EPSG:4326', proj);
|
const extentProj = ol.proj.transformExtent(extent4326, 'EPSG:4326', proj);
|
||||||
@ -479,6 +549,126 @@ const WeatherMap = {
|
|||||||
this._hoverBound = true;
|
this._hoverBound = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
switchKmlLayer(url){
|
||||||
|
this.clearKmlLayer();
|
||||||
|
if (!url || !this.map || !this.kmlLayerGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vectorSource = new ol.source.Vector({
|
||||||
|
url: url,
|
||||||
|
format: new ol.format.KML({
|
||||||
|
extractStyles: true,
|
||||||
|
showPointNames: true
|
||||||
|
}),
|
||||||
|
crossOrigin: 'anonymous'
|
||||||
|
});
|
||||||
|
|
||||||
|
const vectorLayer = new ol.layer.Vector({
|
||||||
|
source: vectorSource,
|
||||||
|
style: null,
|
||||||
|
visible: true,
|
||||||
|
opacity: 0.95,
|
||||||
|
zIndex: 1300
|
||||||
|
});
|
||||||
|
|
||||||
|
this.kmlLayerGroup.getLayers().push(vectorLayer);
|
||||||
|
this.activeKmlLayer = vectorLayer;
|
||||||
|
|
||||||
|
const handleSourceReady = ()=>{
|
||||||
|
if (vectorSource.getState && vectorSource.getState() !== 'ready') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vectorSource.un('change', handleSourceReady);
|
||||||
|
const extent = vectorSource.getExtent ? vectorSource.getExtent() : null;
|
||||||
|
if (extent && !ol.extent.isEmpty(extent)) {
|
||||||
|
this.kmlLastExtent = extent.slice ? extent.slice() : extent;
|
||||||
|
} else {
|
||||||
|
this.kmlLastExtent = null;
|
||||||
|
}
|
||||||
|
this.updateKmlFitButton();
|
||||||
|
if (this.kmlLastExtent) {
|
||||||
|
this.fitToKmlExtent({ duration: 600, padding: [80, 80, 80, 80] });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vectorSource.on('change', handleSourceReady);
|
||||||
|
vectorSource.on('error', ()=>{
|
||||||
|
console.error('KML图层加载失败:', url);
|
||||||
|
this.clearKmlLayer();
|
||||||
|
});
|
||||||
|
|
||||||
|
const computeExtentAndEnable = ()=>{
|
||||||
|
try{
|
||||||
|
const features = vectorSource.getFeatures ? vectorSource.getFeatures() : [];
|
||||||
|
if (!features || features.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let extent = ol.extent.createEmpty();
|
||||||
|
features.forEach((feature)=>{
|
||||||
|
const geom = feature && feature.getGeometry ? feature.getGeometry() : null;
|
||||||
|
if (geom) {
|
||||||
|
const geomExtent = geom.getExtent();
|
||||||
|
if (geomExtent) {
|
||||||
|
extent = ol.extent.extend(extent, geomExtent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!extent || ol.extent.isEmpty(extent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.kmlLastExtent = extent.slice ? extent.slice() : extent;
|
||||||
|
this.updateKmlFitButton();
|
||||||
|
this.fitToKmlExtent({ duration: 600, padding: [80, 80, 80, 80] });
|
||||||
|
return true;
|
||||||
|
}catch(err){
|
||||||
|
console.warn('计算KML范围失败:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryEnableWithDelay = (attempt = 0)=>{
|
||||||
|
if (computeExtentAndEnable()) return;
|
||||||
|
if (attempt > 10) return;
|
||||||
|
setTimeout(()=>tryEnableWithDelay(attempt+1), 120);
|
||||||
|
};
|
||||||
|
|
||||||
|
vectorSource.once('change', ()=>{
|
||||||
|
tryEnableWithDelay();
|
||||||
|
});
|
||||||
|
vectorSource.once('addfeature', ()=>{
|
||||||
|
tryEnableWithDelay();
|
||||||
|
});
|
||||||
|
vectorSource.once('featuresloadend', ()=>{
|
||||||
|
tryEnableWithDelay();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vectorSource.getState && vectorSource.getState() === 'ready') {
|
||||||
|
tryEnableWithDelay();
|
||||||
|
handleSourceReady();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateKmlFitButton(){
|
||||||
|
if (!this.kmlFitButton) return;
|
||||||
|
this.kmlFitButton.disabled = !this.kmlLastExtent;
|
||||||
|
},
|
||||||
|
|
||||||
|
fitToKmlExtent(options = {}){
|
||||||
|
if (!this.map || !this.kmlLastExtent) return;
|
||||||
|
const view = this.map.getView();
|
||||||
|
if (!view) return;
|
||||||
|
const padding = Array.isArray(options.padding) ? options.padding : [60, 60, 60, 60];
|
||||||
|
const duration = typeof options.duration === 'number' ? options.duration : 500;
|
||||||
|
const maxZoom = view.getMaxZoom();
|
||||||
|
const targetMaxZoom = typeof maxZoom === 'number' ? Math.min(maxZoom, 17) : 17;
|
||||||
|
view.fit(this.kmlLastExtent, {
|
||||||
|
padding: padding,
|
||||||
|
duration: duration,
|
||||||
|
maxZoom: targetMaxZoom
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// 设置事件监听
|
// 设置事件监听
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// 监听缩放事件
|
// 监听缩放事件
|
||||||
|
|||||||
256
static/kml/selected_polygons.kml
Normal file
256
static/kml/selected_polygons.kml
Normal file
File diff suppressed because one or more lines are too long
@ -541,6 +541,13 @@
|
|||||||
<option value="radar" selected>水汽含量</option>
|
<option value="radar" selected>水汽含量</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="text-sm text-gray-600">区域图层:</label>
|
||||||
|
<select id="kmlLayerSelect" class="px-2 py-1 border border-gray-300 rounded text-sm min-w-[200px]">
|
||||||
|
<option value="">不显示</option>
|
||||||
|
</select>
|
||||||
|
<button id="btnKmlFit" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed" disabled>定位</button>
|
||||||
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button id="btnTilePrev" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100">上一时次</button>
|
<button id="btnTilePrev" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100">上一时次</button>
|
||||||
<span id="tileCountInfo" class="text-xs text-gray-800">共0条,第0条</span>
|
<span id="tileCountInfo" class="text-xs text-gray-800">共0条,第0条</span>
|
||||||
@ -602,6 +609,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.TIANDITU_KEY = '{{.TiandituKey}}';
|
window.TIANDITU_KEY = '{{.TiandituKey}}';
|
||||||
|
window.KML_LAYERS = {{.KmlLayersJSON}};
|
||||||
</script>
|
</script>
|
||||||
<script defer src="/static/js/alpinejs.min.js"></script>
|
<script defer src="/static/js/alpinejs.min.js"></script>
|
||||||
<script src="/static/js/utils.js"></script>
|
<script src="/static/js/utils.js"></script>
|
||||||
|
|||||||
@ -264,6 +264,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// ===== 前端直接配置KML图层 =====
|
||||||
|
window.TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac'; // 天地图Key
|
||||||
|
window.KML_LAYERS = [
|
||||||
|
{
|
||||||
|
name: "昭君镇示范社区",
|
||||||
|
url: "../static/kml/selected_polygons.kml"
|
||||||
|
}
|
||||||
|
// 如果有更多KML文件,继续添加:
|
||||||
|
// { name: "另一个区域", url: "../static/kml/another.kml" }
|
||||||
|
];
|
||||||
|
|
||||||
let selectedStation = null;
|
let selectedStation = null;
|
||||||
let combinedChart = null;
|
let combinedChart = null;
|
||||||
|
|
||||||
|
|||||||
628
web/index_local.html
Normal file
628
web/index_local.html
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<script src="/static/js/chart.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/css/ol.css">
|
||||||
|
<script src="/static/js/ol.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/css/tailwind.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-7p {
|
||||||
|
padding-left: 7%;
|
||||||
|
padding-right: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-narrow {
|
||||||
|
width: 86%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content-narrow {
|
||||||
|
width: 92%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stationInput {
|
||||||
|
width: 120px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, input, button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
height: 60vh;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container.collapsed {
|
||||||
|
height: 38vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-control-btn {
|
||||||
|
padding: 3px 8px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-control-btn:hover {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-control-btn.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1001;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-info-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container.show {
|
||||||
|
display: block;
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accuracy-panel {
|
||||||
|
display: none;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #374151; /* 灰色文字 */
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.accuracy-panel .item { margin-left: 8px; }
|
||||||
|
.accuracy-panel .label { color: #6b7280; margin-right: 4px; }
|
||||||
|
.accuracy-panel .value { font-weight: 600; color: #111827; }
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
height: 500px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container.show {
|
||||||
|
display: block;
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 12px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-info {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff;
|
||||||
|
height: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 720px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-list-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-list-footer {
|
||||||
|
padding: 10px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-item:hover {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-marker {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-marker.offline {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-label {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body x-data="{ showPastForecast: false, deviceModalOpen: false }" class="text-[14px] md:text-[15px]" x-init="window.addEventListener('close-device-modal', () => { deviceModalOpen = false })">
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
<div id="deviceModal" class="device-modal" x-show="deviceModalOpen" x-transition.opacity @click.self="deviceModalOpen=false">
|
||||||
|
<div class="device-modal-content bg-white shadow-xl" x-transition.scale.duration.150ms>
|
||||||
|
<div class="device-list-header flex items-center justify-between border-b">
|
||||||
|
<div class="text-sm">设备列表</div>
|
||||||
|
<span class="close-modal" @click="deviceModalOpen=false">×</span>
|
||||||
|
</div>
|
||||||
|
<div id="deviceList" class="device-list">
|
||||||
|
</div>
|
||||||
|
<div class="device-list-footer">
|
||||||
|
<div class="pagination">
|
||||||
|
<button class="pagination-btn" id="prevPage" :disabled="window.WeatherApp.currentPage <= 1" @click="window.WeatherApp.updateDeviceList(window.WeatherApp.currentPage - 1)">< 上一页</button>
|
||||||
|
<span>第 <span id="currentPage">1</span> 页,共 <span id="totalPages">1</span> 页</span>
|
||||||
|
<button class="pagination-btn" id="nextPage" :disabled="window.WeatherApp.currentPage >= Math.ceil(window.WeatherApp.filteredDevices.length / window.WeatherApp.itemsPerPage)" @click="window.WeatherApp.updateDeviceList(window.WeatherApp.currentPage + 1)">下一页 ></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container content-narrow py-5">
|
||||||
|
<div class="system-info bg-gray-100 p-3 mb-5 rounded text-sm">
|
||||||
|
<strong>在线设备: </strong> <span id="onlineDevices">{{.OnlineDevices}}</span> 个 |
|
||||||
|
<strong>总设备: </strong> <a href="#" id="showDeviceList" class="text-blue-600 hover:text-blue-700 underline-offset-2" @click.prevent="deviceModalOpen = true; window.WeatherApp.updateDeviceList(1)"><span id="wh65lpCount">0</span> 个</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls flex flex-col gap-4 mb-5 p-4 border rounded bg-white">
|
||||||
|
<div class="control-row flex items-center gap-4 flex-wrap">
|
||||||
|
<div class="station-input-group flex items-center gap-1">
|
||||||
|
<label for="stationInput" class="text-sm text-gray-600">站点编号:</label>
|
||||||
|
<input type="text" id="stationInput" placeholder="" class="w-32 px-2 py-1 border border-gray-300 rounded text-sm font-mono">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="mapType" class="text-sm text-gray-600">地图类型:</label>
|
||||||
|
<select id="mapType" onchange="switchLayer(this.value)" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<option value="satellite">卫星图</option>
|
||||||
|
<option value="vector">矢量图</option>
|
||||||
|
<option value="terrain">地形图</option>
|
||||||
|
<option value="hybrid">混合地形图</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="forecastProvider" class="text-sm text-gray-600">预报源:</label>
|
||||||
|
<select id="forecastProvider" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<option value="">不显示预报</option>
|
||||||
|
<option value="imdroid_mix" selected>英卓 V4</option>
|
||||||
|
<option value="open-meteo">英卓 V3</option>
|
||||||
|
<option value="caiyun">英卓 V2</option>
|
||||||
|
<option value="imdroid">英卓 V1</option>
|
||||||
|
|
||||||
|
<!-- <option value="cma">中央气象台</option>-->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="legendMode" class="text-sm text-gray-600">图例展示:</label>
|
||||||
|
<select id="legendMode" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<option value="combo_standard" selected>综合</option>
|
||||||
|
<option value="verify_all">全部气象要素对比</option>
|
||||||
|
<option value="temp_compare">温度对比</option>
|
||||||
|
<option value="hum_compare">湿度对比</option>
|
||||||
|
<option value="rain_all">降水(+1/+2/+3h)</option>
|
||||||
|
<option value="pressure_compare">气压对比</option>
|
||||||
|
<option value="wind_compare">风速对比</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row flex items-center gap-4 flex-wrap">
|
||||||
|
<div class="control-group flex items-center gap-1">
|
||||||
|
<label for="interval" class="text-sm text-gray-600">数据粒度:</label>
|
||||||
|
<select id="interval" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<option value="raw">原始(16s)</option>
|
||||||
|
<option value="10min">10分钟</option>
|
||||||
|
<option value="30min">30分钟</option>
|
||||||
|
<option value="1hour" selected>1小时</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group" id="timeRangeGroup">
|
||||||
|
<label for="startDate" class="text-sm text-gray-600">开始时间:</label>
|
||||||
|
<input type="datetime-local" id="startDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<label for="endDate" class="text-sm text-gray-600">结束时间:</label>
|
||||||
|
<input type="datetime-local" id="endDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<button onclick="queryHistoryData()" id="queryBtn" class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white px-3 py-1 rounded">查看历史数据</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-row flex items-center gap-3 flex-wrap">
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="text-sm text-gray-600">叠加显示:</label>
|
||||||
|
<select id="tileProduct" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<option value="none">不显示</option>
|
||||||
|
<option value="rain">1h 实际降雨</option>
|
||||||
|
<option value="radar" selected>水汽含量</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="text-sm text-gray-600">区域图层:</label>
|
||||||
|
<select id="kmlLayerSelect" class="px-2 py-1 border border-gray-300 rounded text-sm min-w-[200px]">
|
||||||
|
<option value="">不显示</option>
|
||||||
|
</select>
|
||||||
|
<button id="btnKmlFit" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed" disabled>定位</button>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<button id="btnTilePrev" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100">上一时次</button>
|
||||||
|
<span id="tileCountInfo" class="text-xs text-gray-800">共0条,第0条</span>
|
||||||
|
<button id="btnTileNext" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100">下一时次</button>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="text-sm text-gray-600">时间:</label>
|
||||||
|
<select id="tileTimeSelect" class="px-2 py-1 border border-gray-300 rounded text-sm min-w-[220px]">
|
||||||
|
<option value="">请选择时间</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="map-container border border-gray-200 rounded" id="mapContainer">
|
||||||
|
<div id="map"></div>
|
||||||
|
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
|
||||||
|
<div id="tileValueTooltip" style="position:absolute;pointer-events:none;z-index:1003;display:none;background:rgba(0,0,0,0.65);color:#fff;font-size:12px;padding:4px 6px;border-radius:4px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container" id="chartContainer">
|
||||||
|
<div id="stationInfoTitle" class="station-info-title"></div>
|
||||||
|
<div id="accuracyPanel" class="accuracy-panel">
|
||||||
|
<span class="item"><span class="label">+1h</span><span id="accH1" class="value">--</span></span>
|
||||||
|
<span class="item"><span class="label">+2h</span><span id="accH2" class="value">--</span></span>
|
||||||
|
<span class="item"><span class="label">+3h</span><span id="accH3" class="value">--</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="combinedChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" id="tableContainer">
|
||||||
|
<div id="forecastToggleContainer" style="padding: 8px 12px;font-size: 12px;color: #666;display: none;display: flex;justify-content: flex-start;align-items: center;align-content: center;">
|
||||||
|
<label style="display: flex;align-items: center;gap: 5px;">
|
||||||
|
<input type="checkbox" id="showPastForecast" style="vertical-align: middle;" x-model="showPastForecast" @change="window.WeatherTable.display(window.WeatherApp.cachedHistoryData, window.WeatherApp.cachedForecastData)">
|
||||||
|
显示历史预报
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<table class="min-w-full text-sm text-center">
|
||||||
|
<thead>
|
||||||
|
<tr id="tableHeader">
|
||||||
|
<th>时间</th>
|
||||||
|
<th>温度 (°C)</th>
|
||||||
|
<th>湿度 (%)</th>
|
||||||
|
<th>气压 (hPa)</th>
|
||||||
|
<th>风速 (m/s)</th>
|
||||||
|
<th>风向 (°)</th>
|
||||||
|
<th>雨量 (mm)</th>
|
||||||
|
<th>光照 (lux)</th>
|
||||||
|
<th>紫外线</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac';
|
||||||
|
window.KML_LAYERS = [
|
||||||
|
{
|
||||||
|
name: "昭君镇示范社区(典型防控区+社区边界)",
|
||||||
|
url: "/static/kml/selected_polygons.kml"
|
||||||
|
}
|
||||||
|
// 添加更多KML文件:
|
||||||
|
// { name: "其他区域", url: "./static/kml/other.kml" }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<script defer src="/static/js/alpinejs.min.js"></script>
|
||||||
|
<script src="/static/js/utils.js"></script>
|
||||||
|
<script src="/static/js/weather-app.js"></script>
|
||||||
|
<script src="/static/js/weather-chart.js"></script>
|
||||||
|
<script src="/static/js/weather-table.js"></script>
|
||||||
|
<script src="/static/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user