feat: 调整前端页面。加入 Angular 框架,以期减少前端耦合
This commit is contained in:
parent
28ce15ce13
commit
dc03e83562
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/log/*
|
||||||
|
/rain_data/*
|
||||||
|
export_data/*
|
||||||
|
/.gopath/*
|
||||||
|
/tech/*
|
||||||
4
core/frontend/.gitignore
vendored
Normal file
4
core/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.angular/
|
||||||
|
|
||||||
64
core/frontend/angular.json
Normal file
64
core/frontend/angular.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/angular-cli",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"weatherstation-ui": {
|
||||||
|
"projectType": "application",
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/ui",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": [],
|
||||||
|
"baseHref": "/ui/"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "weatherstation-ui:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "weatherstation-ui:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "weatherstation-ui:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12103
core/frontend/package-lock.json
generated
Normal file
12103
core/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
core/frontend/package.json
Normal file
30
core/frontend/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "weatherstation-ui",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build --configuration production --base-href /ui/",
|
||||||
|
"dev": "ng serve",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^17.3.0",
|
||||||
|
"@angular/common": "^17.3.0",
|
||||||
|
"@angular/compiler": "^17.3.0",
|
||||||
|
"@angular/core": "^17.3.0",
|
||||||
|
"@angular/forms": "^17.3.0",
|
||||||
|
"@angular/platform-browser": "^17.3.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"zone.js": "^0.14.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^17.3.0",
|
||||||
|
"@angular/cli": "^17.3.0",
|
||||||
|
"@angular/compiler-cli": "^17.3.0",
|
||||||
|
"typescript": "~5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
4
core/frontend/src/app.component.html
Normal file
4
core/frontend/src/app.component.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div style="font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans, 'Apple Color Emoji', 'Segoe UI Emoji'; padding: 24px;">
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
12
core/frontend/src/index.html
Normal file
12
core/frontend/src/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>WeatherStation UI</title>
|
||||||
|
<base href="/ui/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root>Loading...</app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
core/frontend/src/polyfills.ts
Normal file
3
core/frontend/src/polyfills.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Angular zone support (default change detection)
|
||||||
|
import 'zone.js';
|
||||||
|
|
||||||
4
core/frontend/src/styles.css
Normal file
4
core/frontend/src/styles.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* Global styles (optional) */
|
||||||
|
html, body { height: 100%; margin: 0; }
|
||||||
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans; }
|
||||||
|
|
||||||
14
core/frontend/tsconfig.app.json
Normal file
14
core/frontend/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
core/frontend/tsconfig.json
Normal file
27
core/frontend/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./out-tsc",
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"module": "es2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"lib": [
|
||||||
|
"es2022",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -4,8 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"st
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"os"
|
||||||
"weatherstation/internal/config"
|
"weatherstation/internal/config"
|
||||||
"weatherstation/internal/database"
|
"weatherstation/internal/database"
|
||||||
"weatherstation/pkg/types"
|
"weatherstation/pkg/types"
|
||||||
@ -27,6 +29,23 @@ func StartGinServer() error {
|
|||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
r.Static("/static", "./static")
|
r.Static("/static", "./static")
|
||||||
|
|
||||||
|
// 前端SPA(Angular)静态资源与路由回退
|
||||||
|
// 构建产物目录:./core/frontend/dist/ui
|
||||||
|
r.GET("/ui/*filepath", func(c *gin.Context) {
|
||||||
|
// 物理文件优先,否则回退到 index.html(支持前端路由)
|
||||||
|
requested := c.Param("filepath")
|
||||||
|
if requested == "" || requested == "/" {
|
||||||
|
c.File("./core/frontend/dist/ui/index.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
full := ".//core/frontend/dist/ui" + requested
|
||||||
|
if _, err := os.Stat(full); err == nil {
|
||||||
|
c.File(full)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.File("./core/frontend/dist/ui/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
// 路由设置
|
// 路由设置
|
||||||
r.GET("/", indexHandler)
|
r.GET("/", indexHandler)
|
||||||
r.GET("/radar/nanning", radarNanningHandler)
|
r.GET("/radar/nanning", radarNanningHandler)
|
||||||
|
|||||||
@ -15,6 +15,10 @@ const WeatherMap = {
|
|||||||
this.initializeLayers();
|
this.initializeLayers();
|
||||||
this.initializeMap(tiandituKey);
|
this.initializeMap(tiandituKey);
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
this.setupTileControls();
|
||||||
|
// 默认不显示(需用户选择),但准备好默认索引
|
||||||
|
this.tileZ = 7; this.tileY = 40; this.tileX = 102;
|
||||||
|
this.tileProduct = 'none';
|
||||||
},
|
},
|
||||||
|
|
||||||
// 初始化图层
|
// 初始化图层
|
||||||
@ -56,6 +60,8 @@ const WeatherMap = {
|
|||||||
layers.vector,
|
layers.vector,
|
||||||
layers.terrain,
|
layers.terrain,
|
||||||
layers.hybrid,
|
layers.hybrid,
|
||||||
|
// 栅格瓦片叠加层(动态)
|
||||||
|
this.createTileOverlayLayer(),
|
||||||
this.clusterLayer,
|
this.clusterLayer,
|
||||||
this.singleStationLayer
|
this.singleStationLayer
|
||||||
],
|
],
|
||||||
@ -73,6 +79,17 @@ const WeatherMap = {
|
|||||||
this.updateLayerVisibility(initialZoom);
|
this.updateLayerVisibility(initialZoom);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createTileOverlayLayer(){
|
||||||
|
const layer = new ol.layer.Image({
|
||||||
|
visible: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
source: null,
|
||||||
|
zIndex: 999
|
||||||
|
});
|
||||||
|
this.tileOverlayLayer = layer;
|
||||||
|
return layer;
|
||||||
|
},
|
||||||
|
|
||||||
// 创建地图图层
|
// 创建地图图层
|
||||||
createMapLayers(tiandituKey) {
|
createMapLayers(tiandituKey) {
|
||||||
return {
|
return {
|
||||||
@ -138,6 +155,220 @@ const WeatherMap = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- 瓦片联动:控件与渲染 ----
|
||||||
|
setupTileControls(){
|
||||||
|
const prodSel = document.getElementById('tileProduct');
|
||||||
|
const timeSel = document.getElementById('tileTimeSelect');
|
||||||
|
const prevBtn = document.getElementById('btnTilePrev');
|
||||||
|
const nextBtn = document.getElementById('btnTileNext');
|
||||||
|
if (!prodSel || !timeSel || !prevBtn || !nextBtn) return;
|
||||||
|
|
||||||
|
prodSel.addEventListener('change', async ()=>{
|
||||||
|
this.tileProduct = prodSel.value;
|
||||||
|
// 切换产品:none 则隐藏图层
|
||||||
|
if (this.tileProduct === 'none') {
|
||||||
|
this.tileTimes = [];
|
||||||
|
this.tileCurrentIdx = -1;
|
||||||
|
if (this.tileOverlayLayer) this.tileOverlayLayer.setSource(null);
|
||||||
|
this.updateTileCountInfo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.reloadTileTimesAndShow();
|
||||||
|
});
|
||||||
|
const sEl = document.getElementById('startDate');
|
||||||
|
const eEl = document.getElementById('endDate');
|
||||||
|
const onRangeChange = async ()=>{
|
||||||
|
if (this.tileProduct && this.tileProduct !== 'none') {
|
||||||
|
await this.reloadTileTimesAndShow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (sEl) sEl.addEventListener('change', onRangeChange);
|
||||||
|
if (eEl) eEl.addEventListener('change', onRangeChange);
|
||||||
|
timeSel.addEventListener('change', async ()=>{
|
||||||
|
const dt = timeSel.value;
|
||||||
|
if (!dt) return;
|
||||||
|
const idx = (this.tileTimes||[]).indexOf(dt);
|
||||||
|
if (idx >= 0) this.tileCurrentIdx = idx;
|
||||||
|
await this.loadAndRenderTile(dt);
|
||||||
|
this.updateTileCountInfo();
|
||||||
|
});
|
||||||
|
prevBtn.addEventListener('click', async ()=>{
|
||||||
|
if (!this.tileTimes || this.tileTimes.length === 0) return;
|
||||||
|
// times 为倒序:上一时次=更老 => idx+1
|
||||||
|
if (this.tileCurrentIdx < this.tileTimes.length - 1) {
|
||||||
|
this.tileCurrentIdx += 1;
|
||||||
|
const dt = this.tileTimes[this.tileCurrentIdx];
|
||||||
|
timeSel.value = dt;
|
||||||
|
await this.loadAndRenderTile(dt);
|
||||||
|
this.updateTileCountInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nextBtn.addEventListener('click', async ()=>{
|
||||||
|
if (!this.tileTimes || this.tileTimes.length === 0) return;
|
||||||
|
if (this.tileCurrentIdx > 0) {
|
||||||
|
this.tileCurrentIdx -= 1;
|
||||||
|
const dt = this.tileTimes[this.tileCurrentIdx];
|
||||||
|
timeSel.value = dt;
|
||||||
|
await this.loadAndRenderTile(dt);
|
||||||
|
this.updateTileCountInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadTileTimesAndShow(){
|
||||||
|
try{
|
||||||
|
const times = await this.fetchTileTimes(this.tileProduct, this.tileZ, this.tileY, this.tileX);
|
||||||
|
this.tileTimes = times || [];
|
||||||
|
const timeSel = document.getElementById('tileTimeSelect');
|
||||||
|
if (timeSel){
|
||||||
|
timeSel.innerHTML = '';
|
||||||
|
if (this.tileTimes.length === 0){
|
||||||
|
const opt = document.createElement('option'); opt.value=''; opt.textContent='无可用时次'; timeSel.appendChild(opt);
|
||||||
|
} else {
|
||||||
|
this.tileTimes.forEach(dt=>{ const o=document.createElement('option'); o.value=dt; o.textContent=dt; timeSel.appendChild(o); });
|
||||||
|
this.tileCurrentIdx = 0;
|
||||||
|
timeSel.value = this.tileTimes[0];
|
||||||
|
await this.loadAndRenderTile(this.tileTimes[0]);
|
||||||
|
}
|
||||||
|
this.updateTileCountInfo();
|
||||||
|
}
|
||||||
|
}catch(e){ console.error('加载瓦片时次失败', e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTileCountInfo(){
|
||||||
|
const el = document.getElementById('tileCountInfo');
|
||||||
|
if (!el) return;
|
||||||
|
const total = this.tileTimes ? this.tileTimes.length : 0;
|
||||||
|
const idxDisp = (this.tileCurrentIdx != null && this.tileCurrentIdx >= 0) ? (this.tileCurrentIdx+1) : 0;
|
||||||
|
el.textContent = `共${total}条,第${idxDisp}条`;
|
||||||
|
const prevBtn = document.getElementById('btnTilePrev');
|
||||||
|
const nextBtn = document.getElementById('btnTileNext');
|
||||||
|
if (prevBtn) prevBtn.disabled = !(total>0 && this.tileCurrentIdx < total-1);
|
||||||
|
if (nextBtn) nextBtn.disabled = !(total>0 && this.tileCurrentIdx > 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchTileTimes(product, z,y,x){
|
||||||
|
const sEl = document.getElementById('startDate');
|
||||||
|
const eEl = document.getElementById('endDate');
|
||||||
|
let url;
|
||||||
|
if (product==='rain') url = `/api/rain/times?z=${z}&y=${y}&x=${x}`; else url = `/api/radar/times?z=${z}&y=${y}&x=${x}`;
|
||||||
|
if (sEl && eEl && sEl.value && eEl.value) {
|
||||||
|
const from = sEl.value.replace('T',' ') + ':00';
|
||||||
|
const to = eEl.value.replace('T',' ') + ':00';
|
||||||
|
url += `&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
|
||||||
|
} else {
|
||||||
|
url += '&limit=60';
|
||||||
|
}
|
||||||
|
const r = await fetch(url);
|
||||||
|
if(!r.ok) return [];
|
||||||
|
const j = await r.json();
|
||||||
|
return j.times || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadAndRenderTile(dtStr){
|
||||||
|
try{
|
||||||
|
const z=this.tileZ,y=this.tileY,x=this.tileX;
|
||||||
|
const url = this.tileProduct==='rain' ? `/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}` : `/api/radar/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`;
|
||||||
|
const r = await fetch(url);
|
||||||
|
if(!r.ok){ console.warn('瓦片未找到', dtStr); return; }
|
||||||
|
const t = await r.json();
|
||||||
|
await this.renderTileOnMap(this.tileProduct, t);
|
||||||
|
}catch(e){ console.error('加载/渲染瓦片失败', e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTileOnMap(product, t){
|
||||||
|
if(!t || !t.values) return;
|
||||||
|
const w=t.width, h=t.height, resDeg=t.res_deg;
|
||||||
|
const west=t.west, south=t.south, east=t.east, north=t.north;
|
||||||
|
// 生成画布
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = w; canvas.height = h;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const imgData = ctx.createImageData(w, h);
|
||||||
|
// 色带
|
||||||
|
let colorFunc;
|
||||||
|
if (product==='rain'){
|
||||||
|
const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity];
|
||||||
|
const colors = [
|
||||||
|
[255,255,255], [126,212,121], [110,200,109], [97,169,97], [81,148,76], [90,158,112],
|
||||||
|
[143,194,254], [92,134,245], [66,87,240], [45,48,214], [26,15,166], [63,22,145], [191,70,148], [213,1,146], [213,1,146]
|
||||||
|
];
|
||||||
|
colorFunc = (mm)=>{
|
||||||
|
if (mm===0) return [0,0,0,0]; // 0 值透明
|
||||||
|
let idx=0; while(idx<edges.length-1 && !(mm>=edges[idx] && mm<edges[idx+1])) idx++;
|
||||||
|
const c = colors[Math.min(idx, colors.length-1)]; return [c[0],c[1],c[2],220];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// radar dBZ,0..75 映射 15 段
|
||||||
|
const colors = [[0,0,255],[0,191,255],[0,255,255],[127,255,212],[124,252,0],[173,255,47],[255,255,0],[255,215,0],[255,165,0],[255,140,0],[255,69,0],[255,0,0],[220,20,60],[199,21,133],[139,0,139]];
|
||||||
|
colorFunc = (dbz)=>{
|
||||||
|
let v = Math.max(0, Math.min(75, dbz));
|
||||||
|
if (v===0) return [0,0,0,0]; // 0 值透明
|
||||||
|
let bin = Math.floor(v/5); if (bin>=colors.length) bin=colors.length-1;
|
||||||
|
const c = colors[bin]; return [c[0],c[1],c[2],220];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// API 行从南到北?我们按数组行序逐行,从上到下绘制需倒置行
|
||||||
|
let p=0;
|
||||||
|
for(let row=0; row<h; row++){
|
||||||
|
const srcRow = t.values[row] || [];
|
||||||
|
const dstRow = (h-1-row); // 翻转以匹配北向上
|
||||||
|
for(let col=0; col<w; col++){
|
||||||
|
const v = srcRow[col];
|
||||||
|
let rgba;
|
||||||
|
if (v==null){ rgba=[0,0,0,0]; }
|
||||||
|
else { rgba = colorFunc(Number(v)); }
|
||||||
|
const off = (dstRow*w + col)*4;
|
||||||
|
imgData.data[off+0]=rgba[0];
|
||||||
|
imgData.data[off+1]=rgba[1];
|
||||||
|
imgData.data[off+2]=rgba[2];
|
||||||
|
imgData.data[off+3]=rgba[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.putImageData(imgData, 0, 0);
|
||||||
|
// 去除数值文本叠加(应用户要求)
|
||||||
|
const dataURL = canvas.toDataURL('image/png');
|
||||||
|
const extent4326 = [west, south, east, north];
|
||||||
|
const proj = this.map.getView().getProjection();
|
||||||
|
const extentProj = ol.proj.transformExtent(extent4326, 'EPSG:4326', proj);
|
||||||
|
const src = new ol.source.ImageStatic({ url: dataURL, imageExtent: extentProj, projection: proj });
|
||||||
|
this.tileOverlayLayer.setSource(src);
|
||||||
|
// 保存最近一次用于拾取
|
||||||
|
this.tileLast = { product, meta: { west, south, east, north, resDeg, width:w, height:h }, values: t.values };
|
||||||
|
this.setupTileHover();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupTileHover(){
|
||||||
|
if (this._hoverBound) return;
|
||||||
|
const tip = document.getElementById('tileValueTooltip');
|
||||||
|
if (!tip) return;
|
||||||
|
this.map.on('pointermove', (evt)=>{
|
||||||
|
try{
|
||||||
|
if (!this.tileLast || !this.tileOverlayLayer || !this.tileOverlayLayer.getSource()) { tip.style.display='none'; return; }
|
||||||
|
const coord = this.map.getEventCoordinate(evt.originalEvent);
|
||||||
|
const lonlat = ol.proj.transform(coord, this.map.getView().getProjection(), 'EPSG:4326');
|
||||||
|
const {west,south,east,north,resDeg} = this.tileLast.meta;
|
||||||
|
const lon = lonlat[0], lat = lonlat[1];
|
||||||
|
if (lon<west || lon>east || lat<south || lat>north) { tip.style.display='none'; return; }
|
||||||
|
const col = Math.floor((lon - west)/resDeg);
|
||||||
|
const row = Math.floor((lat - south)/resDeg);
|
||||||
|
if (row<0 || row>=this.tileLast.meta.height || col<0 || col>=this.tileLast.meta.width) { tip.style.display='none'; return; }
|
||||||
|
const v = this.tileLast.values[row]?.[col];
|
||||||
|
if (v==null) { tip.style.display='none'; return; }
|
||||||
|
const val = Number(v);
|
||||||
|
// 构建文本
|
||||||
|
const txt = this.tileLast.product==='rain' ? `${val.toFixed(1)} mm` : `${val.toFixed(1)} dBZ`;
|
||||||
|
tip.textContent = txt;
|
||||||
|
// 位置
|
||||||
|
const pixel = this.map.getPixelFromCoordinate(coord);
|
||||||
|
tip.style.left = (pixel[0]+10) + 'px';
|
||||||
|
tip.style.top = (pixel[1]+10) + 'px';
|
||||||
|
tip.style.display = 'block';
|
||||||
|
}catch{ tip.style.display='none'; }
|
||||||
|
});
|
||||||
|
this._hoverBound = true;
|
||||||
|
},
|
||||||
|
|
||||||
// 设置事件监听
|
// 设置事件监听
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// 监听缩放事件
|
// 监听缩放事件
|
||||||
@ -441,4 +672,4 @@ const WeatherMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 导出地图对象
|
// 导出地图对象
|
||||||
window.WeatherMap = WeatherMap;
|
window.WeatherMap = WeatherMap;
|
||||||
|
|||||||
@ -517,11 +517,35 @@
|
|||||||
<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>
|
<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>
|
</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" selected>不显示</option>
|
||||||
|
<option value="rain">一小时降雨</option>
|
||||||
|
<option value="radar">组合反射率</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
|
||||||
<div class="map-container border border-gray-200 rounded" id="mapContainer">
|
<div class="map-container border border-gray-200 rounded" id="mapContainer">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
|
<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>
|
||||||
|
|
||||||
<div class="chart-container" id="chartContainer">
|
<div class="chart-container" id="chartContainer">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user