weather-station/static/js/weather-app.js

786 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 地图相关功能
const WeatherMap = {
map: null,
stations: [],
stationLayer: null,
clusterLayer: null,
clusterSource: null,
singleStationLayer: null,
combinedChart: null,
CLUSTER_THRESHOLD: 10,
isMapCollapsed: false,
// 初始化地图
init(tiandituKey) {
this.initializeLayers();
this.initializeMap(tiandituKey);
this.setupEventListeners();
this.setupTileControls();
// 默认叠加组合反射率radar并准备默认索引
this.tileZ = 7; this.tileY = 40; this.tileX = 102;
this.tileProduct = 'radar';
const prodSel = document.getElementById('tileProduct');
if (prodSel) prodSel.value = 'radar';
// 触发首次加载
try { this.reloadTileTimesAndShow(); } catch(e) { console.warn('init tiles load failed', e); }
},
// 初始化图层
initializeLayers() {
this.stationLayer = new ol.layer.Vector({
source: new ol.source.Vector()
});
this.clusterSource = new ol.source.Cluster({
distance: 60,
minDistance: 20,
source: this.stationLayer.getSource(),
geometryFunction: function(feature) {
return feature.getGeometry();
}
});
this.clusterLayer = new ol.layer.Vector({
source: this.clusterSource,
style: (feature) => this.createClusterStyle(feature)
});
this.singleStationLayer = new ol.layer.Vector({
source: this.stationLayer.getSource(),
style: (feature) => this.createStationStyle(feature),
visible: false
});
},
// 初始化地图
initializeMap(tiandituKey) {
const layers = this.createMapLayers(tiandituKey);
this.layers = layers;
this.map = new ol.Map({
target: 'map',
layers: [
layers.satellite,
layers.vector,
layers.terrain,
layers.hybrid,
// 栅格瓦片叠加层(动态)
this.createTileOverlayLayer(),
this.clusterLayer,
this.singleStationLayer
],
view: new ol.View({
center: ol.proj.fromLonLat([108, 35]),
zoom: 5,
minZoom: 3,
maxZoom: 18
})
});
// 初始化时设置图层状态
const initialZoom = this.map.getView().getZoom();
this.updateClusterDistance(initialZoom);
this.updateLayerVisibility(initialZoom);
},
createTileOverlayLayer(){
// 使用分组图层以支持同一时次叠加多块瓦片
const group = new ol.layer.Group({
layers: [],
zIndex: 999,
visible: true
});
this.tileOverlayGroup = group;
return group;
},
// 创建地图图层
createMapLayers(tiandituKey) {
return {
satellite: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
]
}),
vector: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
],
visible: false
}),
terrain: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
],
visible: false
}),
hybrid: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
],
visible: false
})
};
},
// ---- 瓦片联动:控件与渲染 ----
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;
this.clearTileOverlays();
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;
if (this.tileProduct === 'rain') {
// 雨量:同一时次下叠加所有 y/x 瓦片
const url = `/api/rain/tiles_at?z=${z}&dt=${encodeURIComponent(dtStr)}`;
const r = await fetch(url);
if(!r.ok){
// 回退单块接口
const url1 = `/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`;
const r1 = await fetch(url1);
if(!r1.ok){ console.warn('雨量瓦片未找到', dtStr); return; }
const t1 = await r1.json();
await this.renderTileOnMap('rain', t1);
return;
}
const j = await r.json();
const tiles = Array.isArray(j.tiles) ? j.tiles : [];
if (tiles.length === 0) { console.warn('该时次无雨量瓦片集合'); this.clearTileOverlays(); return; }
await this.renderTilesOnMap('rain', tiles);
} else {
// radar: 取同一时次该 z 下的所有 y/x 瓦片
const url = `/api/radar/tiles_at?z=${z}&dt=${encodeURIComponent(dtStr)}`;
const r = await fetch(url);
if(!r.ok){
// 兼容后备:退回单块接口
const url1 = `/api/radar/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`;
const r1 = await fetch(url1);
if(!r1.ok){ console.warn('雷达瓦片未找到', dtStr); return; }
const t1 = await r1.json();
await this.renderTileOnMap('radar', t1);
return;
}
const j = await r.json();
const tiles = Array.isArray(j.tiles) ? j.tiles : [];
if (tiles.length === 0) { console.warn('该时次无雷达瓦片集合'); this.clearTileOverlays(); return; }
await this.renderTilesOnMap('radar', tiles);
}
}catch(e){ console.error('加载/渲染瓦片失败', e); }
},
clearTileOverlays(){
if (!this.tileOverlayGroup) return;
// 清空子图层
const coll = this.tileOverlayGroup.getLayers();
if (coll) coll.clear();
this.tileLastList = [];
},
addImageOverlayFromCanvas(canvas, extent4326){
const proj = this.map.getView().getProjection();
const extentProj = ol.proj.transformExtent(extent4326, 'EPSG:4326', proj);
const dataURL = canvas.toDataURL('image/png');
const src = new ol.source.ImageStatic({ url: dataURL, imageExtent: extentProj, projection: proj });
const layer = new ol.layer.Image({ source: src, opacity: 0.8, visible: true });
this.tileOverlayGroup.getLayers().push(layer);
},
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 = [
// 0值透明(0,5) 用绿色,不再用白色
[126,212,121], [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 dBZ0..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);
// 清空并添加唯一图层
this.clearTileOverlays();
this.addImageOverlayFromCanvas(canvas, [west, south, east, north]);
// 保存用于拾取(单块)
this.tileLastList = [{ product, meta: { west, south, east, north, resDeg, width:w, height:h }, values: t.values }];
this.setupTileHover();
},
renderTilesOnMap(product, tiles){
// tiles: radarTileResponse[]
this.clearTileOverlays();
const lastList = [];
for (const t of tiles){
if(!t || !t.values) continue;
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 = [
// 0值透明(0,5) 用绿色,不再用白色
[126,212,121], [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];
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 {
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];
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];
};
}
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);
this.addImageOverlayFromCanvas(canvas, [west, south, east, north]);
lastList.push({ product, meta: { west, south, east, north, resDeg, width:w, height:h }, values: t.values });
}
this.tileLastList = lastList;
this.setupTileHover();
},
setupTileHover(){
if (this._hoverBound) return;
const tip = document.getElementById('tileValueTooltip');
if (!tip) return;
this.map.on('pointermove', (evt)=>{
try{
// 需要至少一个叠加瓦片
if (!this.tileLastList || !this.tileOverlayGroup || this.tileOverlayGroup.getLayers().getLength()===0) { 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 lon = lonlat[0], lat = lonlat[1];
// 选中包含该点的第一块瓦片
let pickedVal = null, pickedProd = null;
for (const it of this.tileLastList){
const {west,south,east,north,resDeg,width,height} = it.meta;
if (lon<west || lon>east || lat<south || lat>north) continue;
const col = Math.floor((lon - west)/resDeg);
const row = Math.floor((lat - south)/resDeg);
if (row<0 || row>=height || col<0 || col>=width) continue;
const v = it.values[row]?.[col];
if (v==null) continue;
pickedVal = Number(v); pickedProd = it.product; break;
}
if (pickedVal==null){ tip.style.display='none'; return; }
const txt = pickedProd==='rain' ? `${pickedVal.toFixed(1)} mm` : `${pickedVal.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() {
// 监听缩放事件
this.map.getView().on('change:resolution', () => {
const zoom = this.map.getView().getZoom();
this.updateClusterDistance(zoom);
this.updateLayerVisibility(zoom);
});
// 添加点击事件
this.map.on('click', (event) => this.handleMapClick(event));
// 添加鼠标移动事件
this.map.on('pointermove', (event) => {
const pixel = this.map.getEventPixel(event.originalEvent);
const hit = this.map.hasFeatureAtPixel(pixel);
this.map.getTargetElement().style.cursor = hit ? 'pointer' : '';
});
},
// 处理地图点击
handleMapClick(event) {
const feature = this.map.forEachFeatureAtPixel(event.pixel, feature => feature);
if (!feature) return;
const features = feature.get('features');
if (features && features.length > 1) {
this.handleClusterClick(features);
} else {
this.handleStationClick(features ? features[0] : feature);
}
},
// 处理集群点击
handleClusterClick(features) {
const extent = ol.extent.createEmpty();
features.forEach(feature => {
ol.extent.extend(extent, feature.getGeometry().getExtent());
});
const zoom = this.map.getView().getZoom();
const targetZoom = Math.min(
features.length <= 5 ? this.CLUSTER_THRESHOLD : zoom + 2,
this.CLUSTER_THRESHOLD
);
this.map.getView().fit(extent, {
padding: [100, 100, 100, 100],
duration: 800,
maxZoom: targetZoom
});
},
// 处理站点点击
handleStationClick(feature) {
const decimalId = feature.get('decimalId');
if (!decimalId) return;
document.getElementById('stationInput').value = decimalId;
const center = feature.getGeometry().getCoordinates();
const currentZoom = this.map.getView().getZoom();
if (currentZoom < this.CLUSTER_THRESHOLD) {
this.map.getView().animate({
center: center,
zoom: this.CLUSTER_THRESHOLD,
duration: 500
});
}
if (document.getElementById('interval').value === '1hour') {
// 触发查询事件
window.dispatchEvent(new CustomEvent('query-history-data'));
}
},
// 更新集群距离
updateClusterDistance(zoom) {
let distance;
if (zoom < 5) distance = 120;
else if (zoom < 7) distance = 90;
else if (zoom < 9) distance = 60;
else if (zoom < this.CLUSTER_THRESHOLD) distance = 40;
else distance = 0;
this.clusterSource.setDistance(distance);
this.clusterSource.refresh();
setTimeout(() => {
this.clusterLayer.changed();
this.singleStationLayer.changed();
}, 100);
},
// 更新图层可见性
updateLayerVisibility(zoom) {
if (zoom >= this.CLUSTER_THRESHOLD) {
this.clusterLayer.setVisible(false);
this.singleStationLayer.setVisible(true);
} else {
this.clusterLayer.setVisible(true);
this.singleStationLayer.setVisible(false);
}
},
// 创建站点样式
createStationStyle(feature) {
const isOnline = new Date(feature.get('lastUpdate')) > new Date(Date.now() - 5*60*1000);
const zoom = this.map ? this.map.getView().getZoom() : 10;
let labelText = '';
if (zoom >= this.CLUSTER_THRESHOLD - 2) {
labelText = `${feature.get('decimalId') || '未知'} | ${feature.get('name') || '未知'} | ${feature.get('location') || '未知'}`;
}
return new ol.style.Style({
image: this.getMarkerIconStyle(isOnline).getImage(),
text: new ol.style.Text({
text: labelText,
font: '11px Arial',
offsetY: -24,
textAlign: 'center',
textBaseline: 'bottom',
fill: new ol.style.Fill({ color: '#666' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
})
});
},
// 创建集群样式
createClusterStyle(feature) {
const features = feature.get('features');
const size = features.length;
const zoom = this.map.getView().getZoom();
if (zoom < this.CLUSTER_THRESHOLD) {
if (size > 1) {
const radius = Math.min(16 + size * 0.8, 32);
const fontSize = Math.min(11 + size/12, 16);
return new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({ color: 'rgba(0, 123, 255, 0.8)' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
}),
text: new ol.style.Text({
text: String(size),
fill: new ol.style.Fill({ color: '#fff' }),
font: `bold ${fontSize}px Arial`,
offsetY: 1
})
});
}
return new ol.style.Style({
image: new ol.style.Circle({
radius: 6,
fill: new ol.style.Fill({
color: new Date(features[0].get('lastUpdate')) >
new Date(Date.now() - 5*60*1000) ? '#007bff' : '#6c757d'
}),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
})
});
}
if (size === 1) {
return this.createStationStyle(features[0]);
}
const radius = Math.min(16 + size * 0.8, 32);
const fontSize = Math.min(11 + size/12, 16);
return new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({ color: 'rgba(0, 123, 255, 0.8)' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
}),
text: new ol.style.Text({
text: String(size),
fill: new ol.style.Fill({ color: '#fff' }),
font: `bold ${fontSize}px Arial`,
offsetY: 1
})
});
},
// 获取标记图标样式
getMarkerIconStyle(isOnline) {
const key = isOnline ? 'online' : 'offline';
if (this.markerStyleCache?.[key]) return this.markerStyleCache[key];
const iconPath = isOnline ? '/static/images/marker-online.svg' : '/static/images/marker-offline.svg';
const style = new ol.style.Style({
image: new ol.style.Icon({
src: iconPath,
anchor: [0.5, 1],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
scale: 0.9
})
});
if (!this.markerStyleCache) this.markerStyleCache = {};
this.markerStyleCache[key] = style;
return style;
},
// 切换地图图层
switchLayer(layerType) {
Object.keys(this.layers).forEach(key => {
this.layers[key].setVisible(key === layerType);
});
},
// 切换地图折叠状态
toggleMap() {
this.isMapCollapsed = !this.isMapCollapsed;
const mapContainer = document.getElementById('mapContainer');
const toggleBtn = document.getElementById('toggleMapBtn');
if (this.isMapCollapsed) {
mapContainer.classList.add('collapsed');
toggleBtn.textContent = '展开地图';
} else {
mapContainer.classList.remove('collapsed');
toggleBtn.textContent = '折叠地图';
}
setTimeout(() => {
this.map.updateSize();
}, 300);
},
// 加载站点数据
async loadStations() {
try {
const response = await fetch('/api/stations');
this.stations = await response.json();
// 更新WH65LP设备数量
const wh65lpDevices = this.stations.filter(station => station.device_type === 'WH65LP');
document.getElementById('wh65lpCount').textContent = wh65lpDevices.length;
this.displayStationsOnMap();
} catch (error) {
console.error('加载站点失败:', error);
}
},
// 在地图上显示站点
displayStationsOnMap() {
const source = this.stationLayer.getSource();
source.clear();
const now = Date.now();
const fiveMinutesAgo = now - 5 * 60 * 1000;
let onlineCount = 0;
let offlineCount = 0;
this.stations.forEach(station => {
if (station.latitude && station.longitude) {
const isOnline = new Date(station.last_update) > new Date(fiveMinutesAgo);
if (isOnline) onlineCount++; else offlineCount++;
const feature = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([station.longitude, station.latitude])),
stationId: station.station_id,
decimalId: station.decimal_id,
name: station.name,
location: station.location,
lastUpdate: station.last_update,
isOnline: isOnline
});
source.addFeature(feature);
}
});
console.log(`已加载 ${this.stations.length} 个站点,在线: ${onlineCount},离线: ${offlineCount}`);
if (source.getFeatures().length > 0) {
if (source.getFeatures().length === 1) {
const feature = source.getFeatures()[0];
this.map.getView().setCenter(feature.getGeometry().getCoordinates());
this.map.getView().setZoom(12);
} else {
const extent = source.getExtent();
this.map.getView().fit(extent, {
padding: [50, 50, 50, 50],
maxZoom: 10
});
}
}
this.updateClusterDistance(this.map.getView().getZoom());
}
};
// 导出地图对象
window.WeatherMap = WeatherMap;