feat: 优化地图显示
This commit is contained in:
parent
8cfc1c0563
commit
2e62ce0501
187
main.go
187
main.go
@ -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("请使用新的启动程序:")
|
||||||
fmt.Println(" ./weatherstation_launcher # 同时启动UDP和Web服务器")
|
fmt.Println(" ./weatherstation_launcher # 同时启动UDP和Web服务器")
|
||||||
fmt.Println(" ./weatherstation_launcher -web # 只启动Web服务器")
|
fmt.Println(" ./weatherstation_launcher -web # 只启动Web服务器")
|
||||||
fmt.Println(" ./weatherstation_launcher -udp # 只启动UDP服务器")
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
12
static/images/marker-offline.svg
Normal file
12
static/images/marker-offline.svg
Normal 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 |
12
static/images/marker-online.svg
Normal file
12
static/images/marker-online.svg
Normal 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
12
static/images/marker.svg
Normal 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
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
4
static/js/ol.js
Normal file
File diff suppressed because one or more lines are too long
@ -4,10 +4,10 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="/static/js/chart.js"></script>
|
||||||
<!-- OpenLayers CSS and JS -->
|
<!-- OpenLayers CSS and JS -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@7.5.2/ol.css">
|
<link rel="stylesheet" href="/static/css/ol.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/ol@7.5.2/dist/ol.js"></script>
|
<script src="/static/js/ol.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
@ -272,19 +272,20 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- 系统信息 -->
|
<!-- 系统信息 -->
|
||||||
<div class="system-info">
|
<div class="system-info">
|
||||||
<strong>系统信息:</strong> 服务器时间: {{.ServerTime}} | 在线设备: <span id="onlineDevices">{{.OnlineDevices}}</span> 个
|
<strong>在线设备: </strong> <span id="onlineDevices">{{.OnlineDevices}}</span> 个
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 控制面板 -->
|
<!-- 控制面板 -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="station-input-group">
|
<div class="station-input-group">
|
||||||
<label for="stationInput">站点编号:</label>
|
<label for="stationInput">站点编号:</label>
|
||||||
<input type="text" id="stationInput" placeholder="输入十进制编号,如:10738" style="width: 200px;">
|
<input type="text" id="stationInput" placeholder="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="mapType">地图类型:</label>
|
<label for="mapType">地图类型:</label>
|
||||||
<select id="mapType" onchange="switchLayer(this.value)">
|
<select id="mapType" onchange="switchLayer(this.value)">
|
||||||
|
<option value="satellite">卫星图</option>
|
||||||
<option value="vector">矢量图</option>
|
<option value="vector">矢量图</option>
|
||||||
<option value="terrain">地形图</option>
|
<option value="terrain">地形图</option>
|
||||||
<option value="hybrid">混合地形图</option>
|
<option value="hybrid">混合地形图</option>
|
||||||
@ -357,8 +358,9 @@
|
|||||||
let stationLayer;
|
let stationLayer;
|
||||||
let clusterLayer;
|
let clusterLayer;
|
||||||
let clusterSource;
|
let clusterSource;
|
||||||
|
let singleStationLayer;
|
||||||
let combinedChart = null;
|
let combinedChart = null;
|
||||||
const CLUSTER_THRESHOLD = 11; // 缩放级别阈值,小于此值时启用集群
|
const CLUSTER_THRESHOLD = 10; // 缩放级别阈值,小于此值时启用集群
|
||||||
|
|
||||||
// 十六进制转十进制
|
// 十六进制转十进制
|
||||||
function hexToDecimal(hex) {
|
function hexToDecimal(hex) {
|
||||||
@ -373,6 +375,20 @@
|
|||||||
|
|
||||||
// 地图图层
|
// 地图图层
|
||||||
const layers = {
|
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({
|
vector: new ol.layer.Group({
|
||||||
layers: [
|
layers: [
|
||||||
new ol.layer.Tile({
|
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
|
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({
|
terrain: new ol.layer.Group({
|
||||||
layers: [
|
layers: [
|
||||||
@ -461,8 +478,13 @@
|
|||||||
|
|
||||||
// 创建集群源和图层
|
// 创建集群源和图层
|
||||||
clusterSource = new ol.source.Cluster({
|
clusterSource = new ol.source.Cluster({
|
||||||
distance: 40,
|
distance: 60, // 默认集群距离
|
||||||
source: stationLayer.getSource()
|
minDistance: 20, // 最小集群距离
|
||||||
|
source: stationLayer.getSource(),
|
||||||
|
geometryFunction: function(feature) {
|
||||||
|
// 使用原始几何形状进行聚类
|
||||||
|
return feature.getGeometry();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
clusterLayer = new ol.layer.Vector({
|
clusterLayer = new ol.layer.Vector({
|
||||||
@ -471,59 +493,54 @@
|
|||||||
const size = feature.get('features').length;
|
const size = feature.get('features').length;
|
||||||
const zoom = map.getView().getZoom();
|
const zoom = map.getView().getZoom();
|
||||||
|
|
||||||
// 低缩放:强制聚合显示(不显示单点详细标签)
|
// 低缩放级别:显示集群
|
||||||
if (zoom < CLUSTER_THRESHOLD) {
|
if (zoom < CLUSTER_THRESHOLD) {
|
||||||
if (size > 1) {
|
if (size > 1) {
|
||||||
return new ol.style.Style({
|
// 使用改进的集群样式
|
||||||
image: new ol.style.Circle({
|
return createClusterStyle(size, true);
|
||||||
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'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// size === 1 时,仅显示小点,不显示文本
|
// 单点在低缩放级别显示为简化标记
|
||||||
return new ol.style.Style({
|
return new ol.style.Style({
|
||||||
image: new ol.style.Circle({
|
image: new ol.style.Circle({
|
||||||
radius: 4,
|
radius: 6, // 稍大一些的点
|
||||||
fill: new ol.style.Fill({ color: '#6c757d' }),
|
fill: new ol.style.Fill({
|
||||||
stroke: new ol.style.Stroke({ color: '#fff', width: 1 })
|
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) {
|
if (size === 1) {
|
||||||
return createStationStyle(feature.get('features')[0]);
|
return createStationStyle(feature.get('features')[0]);
|
||||||
}
|
}
|
||||||
return new ol.style.Style({
|
|
||||||
image: new ol.style.Circle({
|
// 高缩放级别的集群
|
||||||
radius: 14,
|
return createClusterStyle(size, false);
|
||||||
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'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 创建单点图层(用于高缩放级别)
|
||||||
|
singleStationLayer = new ol.layer.Vector({
|
||||||
|
source: stationLayer.getSource(),
|
||||||
|
style: function(feature) {
|
||||||
|
return createStationStyle(feature);
|
||||||
|
},
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
|
|
||||||
// 创建地图
|
// 创建地图
|
||||||
map = new ol.Map({
|
map = new ol.Map({
|
||||||
target: 'map',
|
target: 'map',
|
||||||
layers: [
|
layers: [
|
||||||
|
layers.satellite,
|
||||||
layers.vector,
|
layers.vector,
|
||||||
layers.terrain,
|
layers.terrain,
|
||||||
layers.hybrid,
|
layers.hybrid,
|
||||||
clusterLayer
|
clusterLayer,
|
||||||
|
singleStationLayer
|
||||||
],
|
],
|
||||||
view: new ol.View({
|
view: new ol.View({
|
||||||
center: ol.proj.fromLonLat([108, 35]), // 中国中心
|
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() {
|
map.getView().on('change:resolution', function() {
|
||||||
const zoom = map.getView().getZoom();
|
const zoom = map.getView().getZoom();
|
||||||
updateClusterDistance(zoom);
|
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) {
|
features.forEach(function(feature) {
|
||||||
ol.extent.extend(extent, feature.getGeometry().getExtent());
|
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, {
|
map.getView().fit(extent, {
|
||||||
padding: [50, 50, 50, 50],
|
padding: [100, 100, 100, 100],
|
||||||
duration: 1000,
|
duration: 800,
|
||||||
maxZoom: CLUSTER_THRESHOLD
|
maxZoom: targetZoom
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 单个站点,获取十进制ID
|
// 单个站点
|
||||||
const singleFeature = features ? features[0] : feature;
|
const singleFeature = features ? features[0] : feature;
|
||||||
const decimalId = singleFeature.get('decimalId');
|
const decimalId = singleFeature.get('decimalId');
|
||||||
|
|
||||||
if (decimalId) {
|
if (decimalId) {
|
||||||
|
// 设置输入框值
|
||||||
document.getElementById('stationInput').value = 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小时粒度,自动查询
|
// 如果是1小时粒度,自动查询
|
||||||
if (document.getElementById('interval').value === '1hour') {
|
if (document.getElementById('interval').value === '1hour') {
|
||||||
queryHistoryData();
|
queryHistoryData();
|
||||||
@ -579,7 +637,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加鼠标悬停效果
|
// 只添加鼠标指针效果
|
||||||
map.on('pointermove', function(event) {
|
map.on('pointermove', function(event) {
|
||||||
const pixel = map.getEventPixel(event.originalEvent);
|
const pixel = map.getEventPixel(event.originalEvent);
|
||||||
const hit = map.hasFeatureAtPixel(pixel);
|
const hit = map.hasFeatureAtPixel(pixel);
|
||||||
@ -615,47 +673,89 @@
|
|||||||
function updateClusterDistance(zoom) {
|
function updateClusterDistance(zoom) {
|
||||||
// 动态调整聚合距离,让低缩放更容易聚合
|
// 动态调整聚合距离,让低缩放更容易聚合
|
||||||
let distance;
|
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.setDistance(distance);
|
||||||
|
|
||||||
|
// 强制刷新集群
|
||||||
|
clusterSource.refresh();
|
||||||
|
|
||||||
|
// 强制刷新集群层样式
|
||||||
|
setTimeout(() => {
|
||||||
|
clusterLayer.changed();
|
||||||
|
// 同时刷新单点图层
|
||||||
|
singleStationLayer.changed();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建站点样式
|
// 创建站点样式(使用本地SVG图标)
|
||||||
function createStationStyle(feature) {
|
const markerStyleCache = {};
|
||||||
const isOnline = new Date(feature.get('lastUpdate')) > new Date(Date.now() - 5*60*1000);
|
function getMarkerIconStyle(isOnline) {
|
||||||
const labelText = [
|
const key = isOnline ? 'online' : 'offline';
|
||||||
`十进制ID: ${feature.get('decimalId') || '未知'}`,
|
if (markerStyleCache[key]) return markerStyleCache[key];
|
||||||
`北斗设备ID: ${feature.get('name') || '未知'}`,
|
|
||||||
`位置: ${feature.get('location') || '未知'}`
|
const iconPath = isOnline ? '/static/images/marker-online.svg' : '/static/images/marker-offline.svg';
|
||||||
].join('|');
|
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({
|
return new ol.style.Style({
|
||||||
image: new ol.style.Circle({
|
image: new ol.style.Circle({
|
||||||
radius: 6,
|
radius: radius,
|
||||||
fill: new ol.style.Fill({
|
fill: new ol.style.Fill({ color: 'rgba(0, 123, 255, 0.8)' }),
|
||||||
color: isOnline ? '#007bff' : '#6c757d'
|
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
|
||||||
}),
|
|
||||||
stroke: new ol.style.Stroke({
|
|
||||||
color: '#fff',
|
|
||||||
width: 2
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
text: new ol.style.Text({
|
text: new ol.style.Text({
|
||||||
text: labelText,
|
text: String(size),
|
||||||
font: '12px Arial',
|
fill: new ol.style.Fill({ color: '#fff' }),
|
||||||
offsetY: -30,
|
font: `bold ${fontSize}px Arial`,
|
||||||
textAlign: 'left',
|
offsetY: 1
|
||||||
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)'
|
|
||||||
})
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '11px Arial',
|
||||||
|
offsetY: -24, // 更靠近图标
|
||||||
|
textAlign: 'center',
|
||||||
|
textBaseline: 'bottom',
|
||||||
|
fill: new ol.style.Fill({ color: '#666' }), // 淡灰色文字
|
||||||
|
stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) // 保留白色描边确保可读性
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -665,31 +765,55 @@
|
|||||||
const source = stationLayer.getSource();
|
const source = stationLayer.getSource();
|
||||||
source.clear();
|
source.clear();
|
||||||
|
|
||||||
|
// 计算在线和离线设备
|
||||||
|
const now = Date.now();
|
||||||
|
const fiveMinutesAgo = now - 5 * 60 * 1000;
|
||||||
|
let onlineCount = 0;
|
||||||
|
let offlineCount = 0;
|
||||||
|
|
||||||
stations.forEach(station => {
|
stations.forEach(station => {
|
||||||
if (station.latitude && station.longitude) {
|
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({
|
const feature = new ol.Feature({
|
||||||
geometry: new ol.geom.Point(ol.proj.fromLonLat([station.longitude, station.latitude])),
|
geometry: new ol.geom.Point(ol.proj.fromLonLat([station.longitude, station.latitude])),
|
||||||
stationId: station.station_id,
|
stationId: station.station_id,
|
||||||
decimalId: station.decimal_id,
|
decimalId: station.decimal_id,
|
||||||
name: station.name,
|
name: station.name,
|
||||||
location: station.location,
|
location: station.location,
|
||||||
lastUpdate: station.last_update
|
lastUpdate: station.last_update,
|
||||||
|
isOnline: isOnline
|
||||||
});
|
});
|
||||||
|
|
||||||
source.addFeature(feature);
|
source.addFeature(feature);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动缩放到所有站点
|
console.log(`已加载 ${stations.length} 个站点,在线: ${onlineCount},离线: ${offlineCount}`);
|
||||||
|
|
||||||
|
// 自动调整视图以适应所有站点
|
||||||
if (source.getFeatures().length > 0) {
|
if (source.getFeatures().length > 0) {
|
||||||
|
// 如果只有一个站点,设置适当的缩放级别
|
||||||
|
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();
|
const extent = source.getExtent();
|
||||||
map.getView().fit(extent, {
|
map.getView().fit(extent, {
|
||||||
padding: [50, 50, 50, 50],
|
padding: [50, 50, 50, 50],
|
||||||
duration: 1000
|
maxZoom: 10
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 强制刷新图层
|
||||||
|
updateClusterDistance(map.getView().getZoom());
|
||||||
|
}
|
||||||
|
|
||||||
// 切换地图显示
|
// 切换地图显示
|
||||||
function toggleMap() {
|
function toggleMap() {
|
||||||
const mapContainer = document.getElementById('mapContainer');
|
const mapContainer = document.getElementById('mapContainer');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user