feat: 优化地图显示

This commit is contained in:
yarnom 2025-08-09 01:36:34 +08:00
parent 8cfc1c0563
commit 2e62ce0501
7 changed files with 266 additions and 275 deletions

189
main.go
View File

@ -1,192 +1,5 @@
package main
import (
"encoding/hex"
"fmt"
"log"
"os"
"strings"
"time"
"weatherstation/model"
)
func main() {
// 检查是否有命令行参数
if len(os.Args) > 1 && os.Args[1] == "parse" {
if len(os.Args) > 2 {
// 解析指定的十六进制数据
hexData := os.Args[2]
parseHexData(hexData)
} else {
fmt.Println("用法: ./weatherstation parse <十六进制数据>")
fmt.Println("示例: ./weatherstation parse \"24 F2 10 02 C7 48 10 03 00 6A 03 E8 05 F5 96 10 3F 01 83 2D B1 00 29 9B A4\"")
}
} else {
fmt.Println("请使用新的启动程序:")
fmt.Println(" ./weatherstation_launcher # 同时启动UDP和Web服务器")
fmt.Println(" ./weatherstation_launcher -web # 只启动Web服务器")
fmt.Println(" ./weatherstation_launcher -udp # 只启动UDP服务器")
}
}
// parseHexData 解析十六进制字符串数据
func parseHexData(hexStr string) {
// 移除所有空格
hexStr = strings.ReplaceAll(hexStr, " ", "")
// 将十六进制字符串转换为字节数组
data, err := hex.DecodeString(hexStr)
if err != nil {
log.Printf("解析十六进制字符串失败: %v", err)
return
}
// 打印原始数据
log.Println("=== 原始数据分析 ===")
log.Printf("输入的十六进制字符串: %s", hexStr)
log.Printf("解析后的字节数组长度: %d", len(data))
log.Printf("字节数组内容: %v", data)
// 按索引打印每个字节
for i, b := range data {
log.Printf("索引[%2d]: 0x%02X (%d)", i, b, b)
}
// 检查数据有效性
if !model.ValidateRS485Data(data) {
log.Printf("无效的RS485数据格式: 长度=%d, 起始字节=%02X", len(data), data[0])
return
}
log.Println("\n=== 使用Protocol.go标准解析 ===")
// 创建协议解析器
protocol := model.NewProtocol(data)
rs485Protocol := model.NewRS485Protocol(data)
// 1. 解析设备ID
log.Println("--- 设备ID解析 ---")
idParts, err := protocol.GetCompleteID()
if err != nil {
log.Printf("获取设备ID失败: %v", err)
return
}
log.Printf("HSB (索引21): 0x%02X = %d", data[21], data[21])
log.Printf("MSB (索引22): 0x%02X = %d", data[22], data[22])
log.Printf("LSB (索引1): 0x%02X = %d", data[1], data[1])
log.Printf("完整设备ID: %s", idParts.Complete.Hex)
// 2. 解析温度
log.Println("--- 温度解析 ---")
temp, err := protocol.GetTemperature()
if err != nil {
log.Printf("获取温度失败: %v", err)
} else {
log.Printf("TMP_H (bit29-31): %s (0x%X)", temp.TmpH.Binary, temp.TmpH.Value)
log.Printf("TMP_M (bit32-35): %s (0x%X)", temp.TmpM.Binary, temp.TmpM.Value)
log.Printf("TMP_L (bit36-39): %s (0x%X)", temp.TmpL.Binary, temp.TmpL.Value)
log.Printf("原始值: 0x%X = %d", temp.Complete.RawValue, temp.Complete.RawValue)
log.Printf("温度: %.2f°C", temp.Complete.Value)
}
// 3. 解析湿度
log.Println("--- 湿度解析 ---")
humidity, err := protocol.GetHumidity()
if err != nil {
log.Printf("获取湿度失败: %v", err)
} else {
log.Printf("HM_H (bit40-43): %s (0x%X)", humidity.HmH.Binary, humidity.HmH.Value)
log.Printf("HM_L (bit44-47): %s (0x%X)", humidity.HmL.Binary, humidity.HmL.Value)
log.Printf("原始值: 0x%02X = %d", humidity.Complete.RawValue, humidity.Complete.RawValue)
log.Printf("湿度: %d%%", humidity.Complete.Value)
}
// 4. 解析风速
log.Println("--- 风速解析 ---")
windSpeed, err := protocol.GetWindSpeed()
if err != nil {
log.Printf("获取风速失败: %v", err)
} else {
log.Printf("WSP_FLAG: %v", windSpeed.WspFlag.Value)
log.Printf("WSP_H (bit48-51): %s (0x%X)", windSpeed.WspH.Binary, windSpeed.WspH.Value)
log.Printf("WSP_L (bit52-55): %s (0x%X)", windSpeed.WspL.Binary, windSpeed.WspL.Value)
log.Printf("原始值: 0x%X = %d", windSpeed.Complete.RawValue, windSpeed.Complete.RawValue)
log.Printf("风速: %.5f m/s", windSpeed.Complete.Value)
}
// 5. 解析风向
log.Println("--- 风向解析 ---")
windDir, err := protocol.GetWindDirection()
if err != nil {
log.Printf("获取风向失败: %v", err)
} else {
log.Printf("DIR_H: %s (0x%X)", windDir.DirH.Binary, windDir.DirH.Value)
log.Printf("DIR_M: %s (0x%X)", windDir.DirM.Binary, windDir.DirM.Value)
log.Printf("DIR_L: %s (0x%X)", windDir.DirL.Binary, windDir.DirL.Value)
log.Printf("原始值: 0x%X = %d", windDir.Complete.Value, windDir.Complete.Value)
log.Printf("风向: %.1f°", windDir.Complete.Degree)
}
// 6. 解析降雨量
log.Println("--- 降雨量解析 ---")
rainfall, err := protocol.GetRainfall()
if err != nil {
log.Printf("获取降雨量失败: %v", err)
} else {
log.Printf("原始值: 0x%X = %d", rainfall.Complete.RawValue, rainfall.Complete.RawValue)
log.Printf("降雨量: %.3f mm", rainfall.Complete.Value)
}
// 7. 解析光照
log.Println("--- 光照解析 ---")
light, err := protocol.GetLight()
if err != nil {
log.Printf("获取光照失败: %v", err)
} else {
log.Printf("原始值: 0x%X = %d", light.Complete.RawValue, light.Complete.RawValue)
log.Printf("光照: %.1f lux", light.Complete.Value)
}
// 8. 解析UV指数
log.Println("--- UV指数解析 ---")
uv, err := protocol.GetUVIndex()
if err != nil {
log.Printf("获取UV指数失败: %v", err)
} else {
log.Printf("原始值: 0x%X = %d", uv.Complete.RawValue, uv.Complete.RawValue)
log.Printf("UV指数: %.1f uW/c㎡", uv.Complete.Value)
}
// 9. 解析气压
log.Println("--- 气压解析 ---")
pressure, err := protocol.GetPressure()
if err != nil {
log.Printf("获取气压失败: %v", err)
} else {
log.Printf("原始值: 0x%X = %d", pressure.Complete.RawValue, pressure.Complete.RawValue)
log.Printf("气压: %.2f hPa", pressure.Complete.Value)
}
log.Println("\n=== RS485协议统一解析结果 ===")
// 使用修正后的RS485解析
rs485Data, err := rs485Protocol.ParseRS485Data()
if err != nil {
log.Printf("解析RS485数据失败: %v", err)
return
} else {
rs485Data.DeviceID = idParts.Complete.Hex
rs485Data.ReceivedAt = time.Now()
rs485Data.RawDataHex = fmt.Sprintf("%X", data)
log.Printf("设备ID: RS485-%s", rs485Data.DeviceID)
log.Printf("温度: %.2f°C", rs485Data.Temperature)
log.Printf("湿度: %.1f%%", rs485Data.Humidity)
log.Printf("风速: %.5f m/s", rs485Data.WindSpeed)
log.Printf("风向: %.1f°", rs485Data.WindDirection)
log.Printf("降雨量: %.3f mm", rs485Data.Rainfall)
log.Printf("光照: %.1f lux", rs485Data.Light)
log.Printf("紫外线: %.1f", rs485Data.UV)
log.Printf("气压: %.2f hPa", rs485Data.Pressure)
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26" height="26" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-color="#000" flood-opacity="0.35"/>
</filter>
</defs>
<g filter="url(#shadow)">
<path d="M20 2 C11 2 4 9 4 18 C4 29 20 38 20 38 C20 38 36 29 36 18 C36 9 29 2 20 2 Z" fill="#6c757d" stroke="#ffffff" stroke-width="2.5"/>
<circle cx="20" cy="18" r="6" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 557 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26" height="26" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-color="#000" flood-opacity="0.35"/>
</filter>
</defs>
<g filter="url(#shadow)">
<path d="M20 2 C11 2 4 9 4 18 C4 29 20 38 20 38 C20 38 36 29 36 18 C36 9 29 2 20 2 Z" fill="#007bff" stroke="#ffffff" stroke-width="2.5"/>
<circle cx="20" cy="18" r="6" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 557 B

12
static/images/marker.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26" height="26" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-color="#000" flood-opacity="0.35"/>
</filter>
</defs>
<g filter="url(#shadow)">
<path d="M20 2 C11 2 4 9 4 18 C4 29 20 38 20 38 C20 38 36 29 36 18 C36 9 29 2 20 2 Z" fill="#007bff" stroke="#ffffff" stroke-width="2.5"/>
<circle cx="20" cy="18" r="6" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 557 B

14
static/js/chart.js Normal file

File diff suppressed because one or more lines are too long

4
static/js/ol.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,10 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/js/chart.js"></script>
<!-- OpenLayers CSS and JS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@7.5.2/ol.css">
<script src="https://cdn.jsdelivr.net/npm/ol@7.5.2/dist/ol.js"></script>
<link rel="stylesheet" href="/static/css/ol.css">
<script src="/static/js/ol.js"></script>
<style>
body {
font-family: Arial, sans-serif;
@ -272,19 +272,20 @@
<div class="container">
<!-- 系统信息 -->
<div class="system-info">
<strong>系统信息:</strong> 服务器时间: {{.ServerTime}} | 在线设备: <span id="onlineDevices">{{.OnlineDevices}}</span>
<strong>在线设备: </strong> <span id="onlineDevices">{{.OnlineDevices}}</span>
</div>
<!-- 控制面板 -->
<div class="controls">
<div class="station-input-group">
<label for="stationInput">站点编号:</label>
<input type="text" id="stationInput" placeholder="输入十进制编号10738" style="width: 200px;">
<input type="text" id="stationInput" placeholder="">
</div>
<div class="control-group">
<label for="mapType">地图类型:</label>
<select id="mapType" onchange="switchLayer(this.value)">
<option value="satellite">卫星图</option>
<option value="vector">矢量图</option>
<option value="terrain">地形图</option>
<option value="hybrid">混合地形图</option>
@ -357,8 +358,9 @@
let stationLayer;
let clusterLayer;
let clusterSource;
let singleStationLayer;
let combinedChart = null;
const CLUSTER_THRESHOLD = 11; // 缩放级别阈值,小于此值时启用集群
const CLUSTER_THRESHOLD = 10; // 缩放级别阈值,小于此值时启用集群
// 十六进制转十进制
function hexToDecimal(hex) {
@ -373,6 +375,20 @@
// 地图图层
const layers = {
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=' + TIANDITU_KEY
})
}),
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=' + TIANDITU_KEY
})
})
]
}),
vector: new ol.layer.Group({
layers: [
new ol.layer.Tile({
@ -385,7 +401,8 @@
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=' + TIANDITU_KEY
})
})
]
],
visible: false
}),
terrain: new ol.layer.Group({
layers: [
@ -461,8 +478,13 @@
// 创建集群源和图层
clusterSource = new ol.source.Cluster({
distance: 40,
source: stationLayer.getSource()
distance: 60, // 默认集群距离
minDistance: 20, // 最小集群距离
source: stationLayer.getSource(),
geometryFunction: function(feature) {
// 使用原始几何形状进行聚类
return feature.getGeometry();
}
});
clusterLayer = new ol.layer.Vector({
@ -471,59 +493,54 @@
const size = feature.get('features').length;
const zoom = map.getView().getZoom();
// 低缩放:强制聚合显示(不显示单点详细标签)
// 低缩放级别:显示集群
if (zoom < CLUSTER_THRESHOLD) {
if (size > 1) {
return new ol.style.Style({
image: new ol.style.Circle({
radius: 16,
fill: new ol.style.Fill({ color: '#007bff' }),
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 12px Arial'
})
});
// 使用改进的集群样式
return createClusterStyle(size, true);
}
// size === 1 时,仅显示小点,不显示文本
// 单点在低缩放级别显示为简化标记
return new ol.style.Style({
image: new ol.style.Circle({
radius: 4,
fill: new ol.style.Fill({ color: '#6c757d' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 1 })
radius: 6, // 稍大一些的点
fill: new ol.style.Fill({
color: new Date(feature.get('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 createStationStyle(feature.get('features')[0]);
}
return new ol.style.Style({
image: new ol.style.Circle({
radius: 14,
fill: new ol.style.Fill({ color: '#007bff' }),
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 12px Arial'
})
});
// 高缩放级别的集群
return createClusterStyle(size, false);
}
});
// 创建单点图层(用于高缩放级别)
singleStationLayer = new ol.layer.Vector({
source: stationLayer.getSource(),
style: function(feature) {
return createStationStyle(feature);
},
visible: false
});
// 创建地图
map = new ol.Map({
target: 'map',
layers: [
layers.satellite,
layers.vector,
layers.terrain,
layers.hybrid,
clusterLayer
clusterLayer,
singleStationLayer
],
view: new ol.View({
center: ol.proj.fromLonLat([108, 35]), // 中国中心
@ -533,15 +550,32 @@
})
});
// 初始化时根据当前缩放设置集群距离
updateClusterDistance(map.getView().getZoom());
// 初始化时根据当前缩放设置集群距离和图层可见性
const initialZoom = map.getView().getZoom();
updateClusterDistance(initialZoom);
// 设置初始图层可见性
if (initialZoom >= CLUSTER_THRESHOLD) {
clusterLayer.setVisible(false);
singleStationLayer.setVisible(true);
} else {
clusterLayer.setVisible(true);
singleStationLayer.setVisible(false);
}
// 监听缩放事件
map.getView().on('change:resolution', function() {
const zoom = map.getView().getZoom();
updateClusterDistance(zoom);
// 强制样式刷新以应用显示/隐藏规则
clusterLayer.changed();
// 根据缩放级别切换图层显示
if (zoom >= CLUSTER_THRESHOLD) {
clusterLayer.setVisible(false);
singleStationLayer.setVisible(true);
} else {
clusterLayer.setVisible(true);
singleStationLayer.setVisible(false);
}
});
// 添加点击事件
@ -558,18 +592,42 @@
features.forEach(function(feature) {
ol.extent.extend(extent, feature.getGeometry().getExtent());
});
// 计算适当的缩放级别
const zoom = map.getView().getZoom();
const targetZoom = Math.min(
// 如果集群内点数较少,直接放大到显示单点的级别
features.length <= 5 ? CLUSTER_THRESHOLD : zoom + 2,
CLUSTER_THRESHOLD
);
map.getView().fit(extent, {
padding: [50, 50, 50, 50],
duration: 1000,
maxZoom: CLUSTER_THRESHOLD
padding: [100, 100, 100, 100],
duration: 800,
maxZoom: targetZoom
});
} else {
// 单个站点获取十进制ID
// 单个站点
const singleFeature = features ? features[0] : feature;
const decimalId = singleFeature.get('decimalId');
if (decimalId) {
// 设置输入框值
document.getElementById('stationInput').value = decimalId;
// 高亮显示选中的站点
const center = singleFeature.getGeometry().getCoordinates();
const currentZoom = map.getView().getZoom();
// 如果缩放级别不够高,增加缩放
if (currentZoom < CLUSTER_THRESHOLD) {
map.getView().animate({
center: center,
zoom: CLUSTER_THRESHOLD,
duration: 500
});
}
// 如果是1小时粒度自动查询
if (document.getElementById('interval').value === '1hour') {
queryHistoryData();
@ -579,7 +637,7 @@
}
});
// 添加鼠标悬停效果
// 只添加鼠标指针效果
map.on('pointermove', function(event) {
const pixel = map.getEventPixel(event.originalEvent);
const hit = map.hasFeatureAtPixel(pixel);
@ -615,47 +673,89 @@
function updateClusterDistance(zoom) {
// 动态调整聚合距离,让低缩放更容易聚合
let distance;
if (zoom < 6) distance = 100; else if (zoom < 8) distance = 70; else if (zoom < 11) distance = 40; else distance = 0;
if (zoom < 5) distance = 120; // 国家级别视图大范围聚合
else if (zoom < 7) distance = 90; // 省级别视图较大范围聚合
else if (zoom < 9) distance = 60; // 市级别视图中等范围聚合
else if (zoom < CLUSTER_THRESHOLD) distance = 40; // /区级别视图小范围聚合
else distance = 0; // 高缩放级别,不聚合
clusterSource.setDistance(distance);
// 强制刷新集群
clusterSource.refresh();
// 强制刷新集群层样式
setTimeout(() => {
clusterLayer.changed();
// 同时刷新单点图层
singleStationLayer.changed();
}, 100);
}
// 创建站点样式
function createStationStyle(feature) {
const isOnline = new Date(feature.get('lastUpdate')) > new Date(Date.now() - 5*60*1000);
const labelText = [
`十进制ID: ${feature.get('decimalId') || '未知'}`,
`北斗设备ID: ${feature.get('name') || '未知'}`,
`位置: ${feature.get('location') || '未知'}`
].join('|');
// 创建站点样式使用本地SVG图标
const markerStyleCache = {};
function getMarkerIconStyle(isOnline) {
const key = isOnline ? 'online' : 'offline';
if (markerStyleCache[key]) return 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 // 适当缩小图标比例
})
});
markerStyleCache[key] = style;
return style;
}
// 创建集群样式
function createClusterStyle(size, isLowZoom) {
// 根据点数量动态调整大小,但整体缩小一些
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: 6,
fill: new ol.style.Fill({
color: isOnline ? '#007bff' : '#6c757d'
}),
stroke: new ol.style.Stroke({
color: '#fff',
width: 2
})
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
})
});
}
function createStationStyle(feature) {
const isOnline = new Date(feature.get('lastUpdate')) > new Date(Date.now() - 5*60*1000);
// 根据缩放级别决定是否显示详细信息
const zoom = map ? map.getView().getZoom() : 10;
let labelText = '';
// 显示完整信息,但是单行显示
if (zoom >= CLUSTER_THRESHOLD - 2) {
labelText = `${feature.get('decimalId') || '未知'} | ${feature.get('name') || '未知'} | ${feature.get('location') || '未知'}`;
}
// 低缩放级别不显示文本
return new ol.style.Style({
image: getMarkerIconStyle(isOnline).getImage(),
text: new ol.style.Text({
text: labelText,
font: '12px Arial',
offsetY: -30,
textAlign: 'left',
font: '11px Arial',
offsetY: -24, // 更靠近图标
textAlign: 'center',
textBaseline: 'bottom',
fill: new ol.style.Fill({
color: '#333'
}),
stroke: new ol.style.Stroke({
color: '#fff',
width: 3
}),
padding: [5, 5, 5, 5],
backgroundFill: new ol.style.Fill({
color: 'rgba(255, 255, 255, 0.8)'
})
fill: new ol.style.Fill({ color: '#666' }), // 淡灰色文字
stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) // 保留白色描边确保可读性
})
});
}
@ -665,29 +765,53 @@
const source = stationLayer.getSource();
source.clear();
// 计算在线和离线设备
const now = Date.now();
const fiveMinutesAgo = now - 5 * 60 * 1000;
let onlineCount = 0;
let offlineCount = 0;
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
lastUpdate: station.last_update,
isOnline: isOnline
});
source.addFeature(feature);
}
});
// 自动缩放到所有站点
console.log(`已加载 ${stations.length} 个站点,在线: ${onlineCount},离线: ${offlineCount}`);
// 自动调整视图以适应所有站点
if (source.getFeatures().length > 0) {
const extent = source.getExtent();
map.getView().fit(extent, {
padding: [50, 50, 50, 50],
duration: 1000
});
// 如果只有一个站点,设置适当的缩放级别
if (source.getFeatures().length === 1) {
const feature = source.getFeatures()[0];
map.getView().setCenter(feature.getGeometry().getCoordinates());
map.getView().setZoom(12); // 单个站点时缩放级别较高
} else {
// 多个站点时,自动适应所有站点的范围
const extent = source.getExtent();
map.getView().fit(extent, {
padding: [50, 50, 50, 50],
maxZoom: 10
});
}
}
// 强制刷新图层
updateClusterDistance(map.getView().getZoom());
}
// 切换地图显示