Compare commits
49 Commits
feature/rt
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 01427e230e | |||
| b6629696d6 | |||
|
|
c8678bc0a0 | ||
| 9d54bdcb1a | |||
| bc89ccca79 | |||
| efe8787d53 | |||
| e7c6b36f6d | |||
| 9913b8a732 | |||
| 4ceac73519 | |||
| 702c6a87f4 | |||
| 3e99132ad7 | |||
| efaf5fe8f4 | |||
| 6ecc833a2c | |||
| e4ec7a13eb | |||
| 94b91c1290 | |||
| 0d88e161d9 | |||
| d694ea8835 | |||
| c79c868c41 | |||
| 1c4633881d | |||
| 5fef8eec51 | |||
| 3cbeba9832 | |||
| c0d25ef0e7 | |||
| c0ba5fc4b9 | |||
| 71c56c6eb3 | |||
| 5f83223d9a | |||
| 1336bb16e6 | |||
| a70852fdc9 | |||
| 4425fcd7e6 | |||
| a4cf6130c3 | |||
| e256cd5e68 | |||
|
|
3a503701af | ||
|
|
cbb1a22161 | ||
|
|
281c11b1e7 | ||
| 15b14a94f6 | |||
| a580eb204a | |||
|
|
9f5fa31bb1 | ||
|
|
40813356a6 | ||
| 8115e54fcc | |||
| e272988ada | |||
|
|
137603cddb | ||
| feedaaabb4 | |||
|
|
4a394849a1 | ||
|
|
425e4b158e | ||
|
|
b6418ffeae | ||
| 1293f891db | |||
|
|
aac2bcbfd5 | ||
|
|
3e15355609 | ||
|
|
499037fca6 | ||
|
|
4570a8f9b3 |
@ -17,7 +17,7 @@ public interface RtcmClient {
|
|||||||
@GetMapping(value = "/get_device_info")
|
@GetMapping(value = "/get_device_info")
|
||||||
public HttpResp getDeviceInfo(@RequestParam(name = "deviceId") String deviceId, @RequestParam(name = "cmd") String cmd);
|
public HttpResp getDeviceInfo(@RequestParam(name = "deviceId") String deviceId, @RequestParam(name = "cmd") String cmd);
|
||||||
@PostMapping("/device_param_changed")
|
@PostMapping("/device_param_changed")
|
||||||
HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId);
|
HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId,@RequestParam(name = "oldParentId1") String oldParentId1);
|
||||||
|
|
||||||
@PostMapping("/group_param_changed")
|
@PostMapping("/group_param_changed")
|
||||||
HttpResp groupParamChanged();
|
HttpResp groupParamChanged();
|
||||||
|
|||||||
@ -81,6 +81,8 @@ public class GnssDevice {
|
|||||||
// 参数改变
|
// 参数改变
|
||||||
private Integer change_flag = 0;
|
private Integer change_flag = 0;
|
||||||
|
|
||||||
|
private String parentid1;
|
||||||
|
|
||||||
private String getBatteryConfigCmd(){
|
private String getBatteryConfigCmd(){
|
||||||
return "d3130006"+ HexUtil.Int2HexString(Integer.parseInt(deviceid))+
|
return "d3130006"+ HexUtil.Int2HexString(Integer.parseInt(deviceid))+
|
||||||
"55"+(has_battery?"01":"00");
|
"55"+(has_battery?"01":"00");
|
||||||
|
|||||||
@ -21,13 +21,13 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Layton
|
* @author Layton
|
||||||
* @date 2023/2/2 20:49
|
* @date 2023/2/2 20:49
|
||||||
@ -37,6 +37,15 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
|||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
// 基站状态管理
|
||||||
|
private static final Map<String, BaseStationStatus> baseStationStatusMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 测站当前使用的基站映射
|
||||||
|
private static final Map<String, String> deviceCurrentBaseMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 基站切换监听器列表
|
||||||
|
private static final List<BaseStationSwitchListener> switchListeners = new ArrayList<>();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DeviceService deviceService;
|
private DeviceService deviceService;
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -48,23 +57,133 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
|||||||
|
|
||||||
// 添加一个成员变量用于追踪每个测站最后一次转发D300数据的时间
|
// 添加一个成员变量用于追踪每个测站最后一次转发D300数据的时间
|
||||||
private final Map<String, Long> lastD300ForwardTimeMap = new ConcurrentHashMap<>();
|
private final Map<String, Long> lastD300ForwardTimeMap = new ConcurrentHashMap<>();
|
||||||
private static final long D300_FORWARD_INTERVAL = 10000; // 10秒,单位毫秒
|
private static final long D300_FORWARD_INTERVAL = 5000; // 5秒,单位毫秒
|
||||||
|
|
||||||
|
private static final Map<String, Long> deviceLastTime = new ConcurrentHashMap<>();
|
||||||
|
private static final long LOG_INTERVAL = 10 * 60 * 1000L;
|
||||||
|
|
||||||
|
// 基站在线检查间隔 - 修改为秒
|
||||||
|
private static final long BASE_STATION_CHECK_INTERVAL = 30; // 30秒
|
||||||
|
private static final long BASE_STATION_OFFLINE_TIMEOUT = 5 * 60; // 5分钟,单位:秒
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基站状态枚举
|
||||||
|
*/
|
||||||
|
public enum BaseStationStatusEnum {
|
||||||
|
ONLINE,
|
||||||
|
OFFLINE,
|
||||||
|
SWITCHING
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基站状态信息
|
||||||
|
*/
|
||||||
|
public static class BaseStationStatus {
|
||||||
|
private String baseStationId;
|
||||||
|
private BaseStationStatusEnum status;
|
||||||
|
private LocalDateTime lastActiveTime;
|
||||||
|
private LocalDateTime statusChangeTime;
|
||||||
|
private Set<String> servingDevices; // 当前服务的测站列表
|
||||||
|
|
||||||
|
public BaseStationStatus(String baseStationId) {
|
||||||
|
this.baseStationId = baseStationId;
|
||||||
|
this.status = BaseStationStatusEnum.OFFLINE;
|
||||||
|
this.lastActiveTime = LocalDateTime.now();
|
||||||
|
this.statusChangeTime = LocalDateTime.now();
|
||||||
|
this.servingDevices = ConcurrentHashMap.newKeySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getBaseStationId() { return baseStationId; }
|
||||||
|
public BaseStationStatusEnum getStatus() { return status; }
|
||||||
|
public void setStatus(BaseStationStatusEnum status) {
|
||||||
|
if (this.status != status) {
|
||||||
|
this.status = status;
|
||||||
|
this.statusChangeTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public LocalDateTime getLastActiveTime() { return lastActiveTime; }
|
||||||
|
public void setLastActiveTime(LocalDateTime lastActiveTime) { this.lastActiveTime = lastActiveTime; }
|
||||||
|
public LocalDateTime getStatusChangeTime() { return statusChangeTime; }
|
||||||
|
public Set<String> getServingDevices() { return servingDevices; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基站切换监听器接口
|
||||||
|
*/
|
||||||
|
public interface BaseStationSwitchListener {
|
||||||
|
void onBaseStationSwitch(String deviceId, String fromBaseId, String toBaseId, String reason);
|
||||||
|
void onBaseStationStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认的基站切换监听器实现
|
||||||
|
*/
|
||||||
|
private class DefaultBaseStationSwitchListener implements BaseStationSwitchListener {
|
||||||
|
@Override
|
||||||
|
public void onBaseStationSwitch(String deviceId, String fromBaseId, String toBaseId, String reason) {
|
||||||
|
logger.info("Base station switch - Device: {}, From: {} To: {}, Reason: {}",
|
||||||
|
deviceId, fromBaseId, toBaseId, reason);
|
||||||
|
|
||||||
|
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||||
|
try {
|
||||||
|
notifyBaseStationSwitch(deviceId, fromBaseId, toBaseId, reason);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to send base station switch notification", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBaseStationStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus) {
|
||||||
|
logger.info("Base station status change - BaseStation: {}, From: {} To: {}",
|
||||||
|
baseStationId, oldStatus, newStatus);
|
||||||
|
|
||||||
|
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||||
|
try {
|
||||||
|
notifyBaseStationStatusChange(baseStationId, oldStatus, newStatus);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to send base station status notification", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化默认监听器
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
switchListeners.add(new DefaultBaseStationSwitchListener());
|
||||||
|
logger.info("D331RtcmMessageExecutor initialized");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Void execute(D331RtcmMessage message) {
|
public Void execute(D331RtcmMessage message) {
|
||||||
String id = message.getId();
|
String id = message.getId();
|
||||||
|
|
||||||
|
// 更新基站状态
|
||||||
|
updateBaseStationStatus(id);
|
||||||
|
|
||||||
// 补齐tenantId
|
// 补齐tenantId
|
||||||
Device deviceBs = deviceService.findByDeviceId(id);
|
Device deviceBs = deviceService.findByDeviceId(id);
|
||||||
if(deviceBs == null || deviceBs.getOpMode() == GnssDevice.OP_MODE_UNUSE) return null;
|
if(deviceBs == null || deviceBs.getOpMode() == GnssDevice.OP_MODE_UNUSE || deviceBs.getOpMode() == GnssDevice.OP_MODE_CHECK) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 推送基站数据
|
// 推送基站数据
|
||||||
if(deviceBs.getOpMode() == GnssDevice.OP_MODE_USE) {
|
if(deviceBs.getOpMode() == GnssDevice.OP_MODE_USE) {
|
||||||
byte[] forwardBytes = message.getSrcData();
|
byte[] forwardBytes = message.getSrcData();
|
||||||
// 要求快速转发,因此用缓存,不要每次都查数据库
|
|
||||||
List<Device> deviceList = deviceService.findByParentId(id);
|
// 获取使用该基站(包括作为主基站和备选基站)的所有测站
|
||||||
//logger.debug("base station {} has {} rovers: ", message.getId(),deviceList.size());
|
List<Device> primaryDevices = deviceService.findByParentId(id);
|
||||||
|
List<Device> backupDevices = deviceService.findByParentId1(id);
|
||||||
|
|
||||||
|
// 合并两个列表
|
||||||
|
List<Device> allDevices = new ArrayList<>();
|
||||||
|
allDevices.addAll(primaryDevices);
|
||||||
|
allDevices.addAll(backupDevices);
|
||||||
|
|
||||||
DeviceChannel deviceChannel = null;
|
DeviceChannel deviceChannel = null;
|
||||||
for (Device device : deviceList) {
|
for (Device device : allDevices) {
|
||||||
if (device.getOpMode() != GnssDevice.OP_MODE_USE) continue;
|
if (device.getOpMode() != GnssDevice.OP_MODE_USE) continue;
|
||||||
if ((device.getModel()==GnssDevice.MODEL_G510) &&
|
if ((device.getModel()==GnssDevice.MODEL_G510) &&
|
||||||
(device.getFixedNum()>100) && (device.getGnssSampleRate()>1)
|
(device.getFixedNum()>100) && (device.getGnssSampleRate()>1)
|
||||||
@ -73,6 +192,24 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String deviceId = device.getDeviceId();
|
String deviceId = device.getDeviceId();
|
||||||
|
|
||||||
|
// 智能基站选择和切换
|
||||||
|
String selectedBaseId = selectOptimalBaseStation(device);
|
||||||
|
|
||||||
|
if (selectedBaseId == null || !selectedBaseId.equals(id)) {
|
||||||
|
continue; // 当前基站不是该设备的最优选择
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证兼容性(双重检查)
|
||||||
|
if (!isBaseStationCompatibleWithDevice(selectedBaseId, device)) {
|
||||||
|
logger.warn("Base station {} selected but not compatible with device {}, skipping",
|
||||||
|
selectedBaseId, deviceId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取要转发的数据(可能需要修改基站ID)
|
||||||
|
byte[] deviceForwardBytes = prepareForwardData(forwardBytes, device, selectedBaseId);
|
||||||
|
|
||||||
// 获取设备通道并发送数据
|
// 获取设备通道并发送数据
|
||||||
if(device.getDataChannelType() == Device.CHANNEL_TYPE_UDP) {
|
if(device.getDataChannelType() == Device.CHANNEL_TYPE_UDP) {
|
||||||
deviceChannel = OnlineChannels.INSTANCE.getDataChannel(deviceId);
|
deviceChannel = OnlineChannels.INSTANCE.getDataChannel(deviceId);
|
||||||
@ -80,168 +217,365 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
|||||||
deviceChannel = OnlineChannels.INSTANCE.getConfigChannel(deviceId);
|
deviceChannel = OnlineChannels.INSTANCE.getConfigChannel(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取数据库中model字段,判断基站类型
|
// 根据基站和设备类型处理数据转发
|
||||||
Short baseStationModel = deviceBs.getModel();
|
if (shouldForwardData(deviceBs, device)) {
|
||||||
// 如果model为null,使用默认值0
|
forwardDataToDevice(deviceChannel, deviceForwardBytes, device, deviceBs, id, deviceId);
|
||||||
if (baseStationModel == null) {
|
|
||||||
baseStationModel = 0;
|
|
||||||
logger.warn("Base station model is null for device: {}, using default value 0", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseStationModel == 1) {
|
|
||||||
// 基站类型为1,正常执行
|
|
||||||
if(deviceChannel != null && deviceChannel.isOnline()) {
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("forward d331 rtcm from {} to device {}", id, deviceId);
|
|
||||||
}
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
buf.writeBytes(forwardBytes);
|
|
||||||
deviceChannel.writeAndFlush(buf);
|
|
||||||
}
|
|
||||||
} else if (baseStationModel == 0) {
|
|
||||||
//logger.info("Base station model is 0 for device: {}", deviceId);
|
|
||||||
|
|
||||||
Short deviceModel = device.getModel();
|
|
||||||
// 如果model为null,使用默认值0
|
|
||||||
if (deviceModel == null) {
|
|
||||||
deviceModel = 0;
|
|
||||||
//logger.warn("Device model is null for device: {}, using default value 0", deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(deviceModel == 0){
|
|
||||||
// 测站类型为0,正常执行
|
|
||||||
if(deviceId.startsWith("2307")){
|
|
||||||
// 处理2307型号的测站
|
|
||||||
forwardBytes[2] = (byte) (forwardBytes[2] & 0x07);//兼容不带序号的测站
|
|
||||||
}
|
|
||||||
// 对所有测站类型为0的设备执行转发
|
|
||||||
if(deviceChannel != null && deviceChannel.isOnline()) {
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("forward d331 rtcm from {} to device {}", id, deviceId);
|
|
||||||
}
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
buf.writeBytes(forwardBytes);
|
|
||||||
deviceChannel.writeAndFlush(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(deviceModel == 1){
|
|
||||||
//logger.info("Device model is 1 for device: {}", deviceId);
|
|
||||||
|
|
||||||
if(deviceChannel != null && deviceChannel.isOnline()) {
|
|
||||||
//logger.info("Device channel is online for device: {}", deviceId);
|
|
||||||
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
buf.writeBytes(forwardBytes);
|
|
||||||
|
|
||||||
// 检查是否满足10秒转发间隔,只有满足条件时才添加D300字符串
|
|
||||||
long currentTime = System.currentTimeMillis();
|
|
||||||
Long lastForwardTime = lastD300ForwardTimeMap.getOrDefault(deviceId, 0L);
|
|
||||||
|
|
||||||
if(currentTime - lastForwardTime >= D300_FORWARD_INTERVAL) {
|
|
||||||
//logger.info("Adding D300 string for device: {}", deviceId);
|
|
||||||
|
|
||||||
// 获取当前buf中的数据
|
|
||||||
byte[] originalData = buf.array();
|
|
||||||
String originalHex = ByteUtil.bytesToHexString(originalData);
|
|
||||||
|
|
||||||
// 找到D300和D301的位置
|
|
||||||
int d300Index = originalHex.indexOf("d300");
|
|
||||||
int d301Index = originalHex.indexOf("d301");
|
|
||||||
|
|
||||||
// 确定插入位置:如果两个都存在,取位置靠前的;如果只存在一个,就用那个位置
|
|
||||||
int insertIndex = -1;
|
|
||||||
if (d300Index != -1 && d301Index != -1) {
|
|
||||||
// 两个都存在,取位置靠前的
|
|
||||||
insertIndex = (d300Index < d301Index) ? d300Index : d301Index;
|
|
||||||
//logger.info("Found both D300 and D301, D300 at {}, D301 at {}, will insert before position {}",
|
|
||||||
/// d300Index, d301Index, insertIndex);
|
|
||||||
} else if (d300Index != -1) {
|
|
||||||
insertIndex = d300Index;
|
|
||||||
//logger.info("Found D300 at position {}", d300Index);
|
|
||||||
} else if (d301Index != -1) {
|
|
||||||
insertIndex = d301Index;
|
|
||||||
//logger.info("Found D301 at position {}", d301Index);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (insertIndex != -1) {
|
|
||||||
// 创建新的buf
|
|
||||||
ByteBuf newBuf = Unpooled.buffer();
|
|
||||||
// 写入D300/D301之前的数据
|
|
||||||
newBuf.writeBytes(originalData, 0, insertIndex / 2);
|
|
||||||
|
|
||||||
// 使用f9p坐标生成1005消息,并插入
|
|
||||||
double[] ecef =new double[3];
|
|
||||||
ecef[0] = deviceBs.getEcefx();
|
|
||||||
ecef[1] = deviceBs.getEcefy();
|
|
||||||
ecef[2] = deviceBs.getEcefz();
|
|
||||||
String rtcm1005 = Rtcm1005.generateRtcm1005Hex(ecef, 0);
|
|
||||||
if (rtcm1005 != null) {
|
|
||||||
// 写入RTCM 1005消息
|
|
||||||
byte[] rtcm1005Bytes = ByteUtil.hexStringTobyte(rtcm1005);
|
|
||||||
newBuf.writeBytes(rtcm1005Bytes);
|
|
||||||
// logger.info("Generated RTCM 1005 message for base station {}: {}", deviceBs.getDeviceId(), rtcm1005);
|
|
||||||
} else {
|
|
||||||
//logger.warn("Failed to generate RTCM 1005 message for base station: {}", deviceBs.getDeviceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入剩余的数据
|
|
||||||
newBuf.writeBytes(originalData, insertIndex / 2, originalData.length - insertIndex / 2);
|
|
||||||
|
|
||||||
// 更新buf
|
|
||||||
buf = newBuf;
|
|
||||||
|
|
||||||
// 添加日志,记录插入位置和完整数据
|
|
||||||
// logger.info("Inserted RTCM 1005 message before position {}, complete data: {}",
|
|
||||||
// insertIndex,
|
|
||||||
// ByteUtil.bytesToHexString(buf.array()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后转发时间
|
|
||||||
lastD300ForwardTimeMap.put(deviceId, currentTime);
|
|
||||||
|
|
||||||
// 添加日志,记录测站转发的完整数据
|
|
||||||
//logger.info("Forward data to device: {}, time: {}, complete data: {}",
|
|
||||||
// deviceId,
|
|
||||||
// LocalDateTime.now(),
|
|
||||||
// ByteUtil.bytesToHexString(buf.array()));
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceChannel.writeAndFlush(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果30分钟内收到不到d3f0和d3f2,则根据UDP最后一个报文触发状态更新和统计
|
// 处理设备状态和统计信息
|
||||||
if(deviceBs.getD3xxbytes()>0){
|
processDeviceStatistics(deviceBs, message);
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
if(deviceBs.getLastRxTime().isBefore(now.minusMinutes(1)) &&
|
|
||||||
(deviceBs.getLastD3f2Time() == null ||
|
|
||||||
deviceBs.getLastD3f2Time().isBefore(now.minusMinutes(30)))) {
|
|
||||||
// new cycle
|
|
||||||
logger.info("device {} rx {} d331 in a cycle while not d3f0f2",deviceBs.getDeviceId(),deviceBs.getD3xxCount());
|
|
||||||
|
|
||||||
Device lastCycleDevice = new Device();
|
// 添加NTRIP处理
|
||||||
lastCycleDevice.setDeviceId(deviceBs.getDeviceId());
|
byte[] srcdata = message.getSrcData();
|
||||||
lastCycleDevice.setDeviceType(deviceBs.getDeviceType());
|
String rtcm = ByteUtil.bytesToHexString(srcdata);
|
||||||
lastCycleDevice.setTenantId(deviceBs.getTenantId());
|
sendToNtrip(id, rtcm);
|
||||||
lastCycleDevice.setD341bytes(deviceBs.getD341bytes());
|
|
||||||
lastCycleDevice.setD341Count(deviceBs.getD341Count());
|
// 日志记录
|
||||||
lastCycleDevice.setFixedNum(deviceBs.getFixedNum());
|
logMessageIfNeeded(deviceBs, message);
|
||||||
lastCycleDevice.setFloatNum(deviceBs.getFloatNum());
|
|
||||||
lastCycleDevice.setNoFixedAndFloatResult(deviceBs.getNoFixedAndFloatResult());
|
return null;
|
||||||
lastCycleDevice.setLastRxTime(deviceBs.getLastRxTime());
|
}
|
||||||
lastCycleDevice.setLastValidCalcDataTime(deviceBs.getLastValidCalcDataTime());
|
|
||||||
lastCycleDevice.setSatelitesInUse(deviceBs.getSatelitesInUse());
|
/**
|
||||||
|
* 更新基站状态
|
||||||
|
*/
|
||||||
|
private void updateBaseStationStatus(String baseStationId) {
|
||||||
|
BaseStationStatus status = baseStationStatusMap.computeIfAbsent(baseStationId,
|
||||||
|
k -> new BaseStationStatus(baseStationId));
|
||||||
|
|
||||||
|
BaseStationStatusEnum oldStatus = status.getStatus();
|
||||||
|
status.setLastActiveTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
if (oldStatus == BaseStationStatusEnum.OFFLINE) {
|
||||||
|
status.setStatus(BaseStationStatusEnum.ONLINE);
|
||||||
|
logger.info("Base station {} changed status from {} to {}", baseStationId, oldStatus, BaseStationStatusEnum.ONLINE);
|
||||||
|
notifyStatusChange(baseStationId, oldStatus, BaseStationStatusEnum.ONLINE);
|
||||||
|
|
||||||
|
// 检查是否有设备需要切换回主基站
|
||||||
|
checkAndSwitchBackToPrimary(baseStationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并切换回主基站
|
||||||
|
*/
|
||||||
|
private void checkAndSwitchBackToPrimary(String primaryBaseId) {
|
||||||
|
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||||
|
try {
|
||||||
|
List<Device> devicesUsingPrimary = deviceService.findByParentId(primaryBaseId);
|
||||||
|
|
||||||
|
for (Device device : devicesUsingPrimary) {
|
||||||
|
String currentBase = deviceCurrentBaseMap.get(device.getDeviceId());
|
||||||
|
|
||||||
|
if (currentBase != null && !currentBase.equals(primaryBaseId)) {
|
||||||
|
// 设备当前使用备用基站,需要切换回主基站
|
||||||
|
logger.info("Switching device {} back to primary base station {} from {}",
|
||||||
|
device.getDeviceId(), primaryBaseId, currentBase);
|
||||||
|
switchDeviceToBaseStation(device.getDeviceId(), primaryBaseId,
|
||||||
|
"Primary base station back online");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error checking devices for primary base station switch back", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能选择最优基站
|
||||||
|
*/
|
||||||
|
private String selectOptimalBaseStation(Device device) {
|
||||||
|
String primaryBaseId = device.getParentId();
|
||||||
|
String backupBaseId = device.getParentId1();
|
||||||
|
String deviceId = device.getDeviceId();
|
||||||
|
|
||||||
|
// 获取当前使用的基站
|
||||||
|
String currentBaseId = deviceCurrentBaseMap.get(deviceId);
|
||||||
|
|
||||||
|
// 获取所有候选基站,按优先级排序
|
||||||
|
List<String> candidateBases = getCandidateBaseStations(device);
|
||||||
|
|
||||||
|
for (String baseId : candidateBases) {
|
||||||
|
// 检查基站是否在线
|
||||||
|
boolean isOnline = isBaseStationOnline(baseId);
|
||||||
|
|
||||||
|
if (!isOnline) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查基站与设备的兼容性
|
||||||
|
if (!isBaseStationCompatibleWithDevice(baseId, device)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前基站发生变化,记录切换
|
||||||
|
if (currentBaseId != null && !currentBaseId.equals(baseId)) {
|
||||||
|
switchDeviceToBaseStation(deviceId, baseId,
|
||||||
|
String.format("Switch from %s to %s (priority/availability)", currentBaseId, baseId));
|
||||||
|
} else if (currentBaseId == null) {
|
||||||
|
// 首次连接
|
||||||
|
deviceCurrentBaseMap.put(deviceId, baseId);
|
||||||
|
BaseStationStatus status = baseStationStatusMap.get(baseId);
|
||||||
|
if (status != null) {
|
||||||
|
status.getServingDevices().add(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("No compatible and available base station found for device: {}", deviceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查基站与设备的兼容性
|
||||||
|
*/
|
||||||
|
private boolean isBaseStationCompatibleWithDevice(String baseStationId, Device device) {
|
||||||
|
// 获取基站设备信息
|
||||||
|
Device baseStation = deviceService.findByDeviceId(baseStationId);
|
||||||
|
if (baseStation == null) {
|
||||||
|
logger.warn("Base station {} not found", baseStationId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查基站是否处于可用状态
|
||||||
|
if (baseStation.getOpMode() != GnssDevice.OP_MODE_USE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用已有的兼容性判断逻辑
|
||||||
|
return shouldForwardData(baseStation, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取候选基站列表(按优先级排序)
|
||||||
|
*/
|
||||||
|
private List<String> getCandidateBaseStations(Device device) {
|
||||||
|
List<String> candidates = new ArrayList<>();
|
||||||
|
|
||||||
|
// 主基站优先级最高
|
||||||
|
if (device.getParentId() != null) {
|
||||||
|
candidates.add(device.getParentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用基站
|
||||||
|
if (device.getParentId1() != null) {
|
||||||
|
candidates.add(device.getParentId1());
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换设备到指定基站
|
||||||
|
*/
|
||||||
|
private void switchDeviceToBaseStation(String deviceId, String newBaseId, String reason) {
|
||||||
|
String oldBaseId = deviceCurrentBaseMap.get(deviceId);
|
||||||
|
|
||||||
|
logger.info("Executing base station switch for device {}: {} -> {}, Reason: {}",
|
||||||
|
deviceId, oldBaseId, newBaseId, reason);
|
||||||
|
|
||||||
|
// 更新映射
|
||||||
|
deviceCurrentBaseMap.put(deviceId, newBaseId);
|
||||||
|
|
||||||
|
// 更新基站服务列表
|
||||||
|
if (oldBaseId != null) {
|
||||||
|
BaseStationStatus oldStatus = baseStationStatusMap.get(oldBaseId);
|
||||||
|
if (oldStatus != null) {
|
||||||
|
oldStatus.getServingDevices().remove(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseStationStatus newStatus = baseStationStatusMap.get(newBaseId);
|
||||||
|
if (newStatus != null) {
|
||||||
|
newStatus.getServingDevices().add(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知监听器
|
||||||
|
for (BaseStationSwitchListener listener : switchListeners) {
|
||||||
|
try {
|
||||||
|
listener.onBaseStationSwitch(deviceId, oldBaseId, newBaseId, reason);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error notifying base station switch listener", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备转发数据(可能需要修改基站ID)
|
||||||
|
*/
|
||||||
|
private byte[] prepareForwardData(byte[] originalData, Device device, String selectedBaseId) {
|
||||||
|
String primaryBaseId = device.getParentId();
|
||||||
|
|
||||||
|
// 如果选择的基站不是主基站,需要修改RTCM数据中的基站ID
|
||||||
|
if (primaryBaseId != null && !selectedBaseId.equals(primaryBaseId)) {
|
||||||
|
try {
|
||||||
|
byte[] modifyData = originalData.clone();
|
||||||
|
// 修复:使用Long.parseLong而不是Integer.parseInt,因为基站ID可能很大
|
||||||
|
long primaryBaseIdLong = Long.parseLong(primaryBaseId);
|
||||||
|
String hexPrimaryBase = String.format("%06x", primaryBaseIdLong);
|
||||||
|
|
||||||
|
if (hexPrimaryBase.length() >= 6) {
|
||||||
|
modifyData[5] = (byte) Integer.parseInt(hexPrimaryBase.substring(0, 2), 16);
|
||||||
|
modifyData[6] = (byte) Integer.parseInt(hexPrimaryBase.substring(2, 4), 16);
|
||||||
|
modifyData[7] = (byte) Integer.parseInt(hexPrimaryBase.substring(4, 6), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifyData;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.error("Error parsing base station ID: {}", primaryBaseId, e);
|
||||||
|
return originalData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否应该转发数据
|
||||||
|
* 基站类型说明:
|
||||||
|
* - model = 1: f9p基站,所有设备都可以接收
|
||||||
|
* - model = 0: 博通基站,只有博通设备(model=0)和兼容设备(model=1)可以接收
|
||||||
|
*/
|
||||||
|
private boolean shouldForwardData(Device baseStation, Device device) {
|
||||||
|
Short baseStationModel = baseStation.getModel();
|
||||||
|
if (baseStationModel == null) {
|
||||||
|
baseStationModel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Short deviceModel = device.getModel();
|
||||||
|
if (deviceModel == null) {
|
||||||
|
deviceModel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseStationModel == 1) {
|
||||||
|
// f9p基站,所有设备都可以接收
|
||||||
|
return true;
|
||||||
|
} else if (baseStationModel == 0) {
|
||||||
|
// 博通基站,只支持特定设备
|
||||||
|
return (deviceModel == 0 || deviceModel == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转发数据到设备
|
||||||
|
*/
|
||||||
|
private void forwardDataToDevice(DeviceChannel deviceChannel, byte[] forwardBytes,
|
||||||
|
Device device, Device deviceBs, String baseStationId, String deviceId) {
|
||||||
|
if (deviceChannel == null || !deviceChannel.isOnline()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Short deviceModel = device.getModel();
|
||||||
|
if (deviceModel == null) {
|
||||||
|
deviceModel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("forward d331 rtcm from {} to device {}", baseStationId, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuf buf = Unpooled.buffer();
|
||||||
|
buf.writeBytes(forwardBytes);
|
||||||
|
|
||||||
|
// 特殊处理设备类型
|
||||||
|
if (deviceModel == 0 && deviceId.startsWith("2307")) {
|
||||||
|
// 处理2307型号的测站
|
||||||
|
byte[] modifiedData = buf.array();
|
||||||
|
modifiedData[2] = (byte) (modifiedData[2] & 0x07); // 兼容不带序号的测站
|
||||||
|
} else if (deviceModel == 1) {
|
||||||
|
// 处理类型1的设备,可能需要添加D300数据
|
||||||
|
buf = handleDeviceType1(buf, device, deviceBs, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceChannel.writeAndFlush(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理设备类型1的特殊逻辑
|
||||||
|
*/
|
||||||
|
private ByteBuf handleDeviceType1(ByteBuf buf, Device device, Device deviceBs, String deviceId) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
Long lastForwardTime = lastD300ForwardTimeMap.getOrDefault(deviceId, 0L);
|
||||||
|
|
||||||
|
if (currentTime - lastForwardTime >= D300_FORWARD_INTERVAL) {
|
||||||
|
byte[] originalData = buf.array();
|
||||||
|
String originalHex = ByteUtil.bytesToHexString(originalData);
|
||||||
|
|
||||||
|
int d300Index = originalHex.indexOf("d300");
|
||||||
|
int d301Index = originalHex.indexOf("d301");
|
||||||
|
|
||||||
|
int insertIndex = -1;
|
||||||
|
if (d300Index != -1 && d301Index != -1) {
|
||||||
|
insertIndex = Math.min(d300Index, d301Index);
|
||||||
|
} else if (d300Index != -1) {
|
||||||
|
insertIndex = d300Index;
|
||||||
|
} else if (d301Index != -1) {
|
||||||
|
insertIndex = d301Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertIndex != -1) {
|
||||||
|
// 检查基站是否有有效的ECEF坐标
|
||||||
|
Double ecefX = deviceBs.getEcefx();
|
||||||
|
Double ecefY = deviceBs.getEcefy();
|
||||||
|
Double ecefZ = deviceBs.getEcefz();
|
||||||
|
|
||||||
|
// 验证坐标有效性
|
||||||
|
if (isValidEcefCoordinates(ecefX, ecefY, ecefZ)) {
|
||||||
|
ByteBuf newBuf = Unpooled.buffer();
|
||||||
|
newBuf.writeBytes(originalData, 0, insertIndex / 2);
|
||||||
|
|
||||||
|
double[] ecef = new double[3];
|
||||||
|
ecef[0] = ecefX.doubleValue();
|
||||||
|
ecef[1] = ecefY.doubleValue();
|
||||||
|
ecef[2] = ecefZ.doubleValue();
|
||||||
|
|
||||||
|
String rtcm1005 = Rtcm1005.generateRtcm1005Hex(ecef, 0);
|
||||||
|
if (rtcm1005 != null && !rtcm1005.isEmpty()) {
|
||||||
|
byte[] rtcm1005Bytes = ByteUtil.hexStringTobyte(rtcm1005);
|
||||||
|
newBuf.writeBytes(rtcm1005Bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
newBuf.writeBytes(originalData, insertIndex / 2, originalData.length - insertIndex / 2);
|
||||||
|
buf = newBuf;
|
||||||
|
} else {
|
||||||
|
logger.warn("Base station {} has invalid ECEF coordinates, skipping RTCM 1005 generation for device {}",
|
||||||
|
deviceBs.getDeviceId(), deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastD300ForwardTimeMap.put(deviceId, currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理设备统计信息
|
||||||
|
*/
|
||||||
|
private void processDeviceStatistics(Device deviceBs, D331RtcmMessage message) {
|
||||||
|
if (deviceBs.getD3xxbytes() > 0) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
if (deviceBs.getLastRxTime() != null &&
|
||||||
|
deviceBs.getLastRxTime().isBefore(now.minusMinutes(1)) &&
|
||||||
|
(deviceBs.getLastD3f2Time() == null ||
|
||||||
|
deviceBs.getLastD3f2Time().isBefore(now.minusMinutes(30)))) {
|
||||||
|
|
||||||
|
logger.info("device {} rx {} d331 in a cycle while not d3f0f2",
|
||||||
|
deviceBs.getDeviceId(), deviceBs.getD3xxCount());
|
||||||
|
|
||||||
|
Device lastCycleDevice = createLastCycleDevice(deviceBs);
|
||||||
deviceBs.clearStat();
|
deviceBs.clearStat();
|
||||||
|
|
||||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||||
// 通知上线
|
|
||||||
try {
|
try {
|
||||||
beidouClient.onDeviceActive(deviceBs.getDeviceId(), deviceBs.getTenantId());
|
beidouClient.onDeviceActive(deviceBs.getDeviceId(), deviceBs.getTenantId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error(e.toString());
|
logger.error("Error notifying device active", e);
|
||||||
}
|
}
|
||||||
dataPersistService.updateDeviceState(lastCycleDevice);
|
dataPersistService.updateDeviceState(lastCycleDevice);
|
||||||
});
|
});
|
||||||
@ -249,53 +583,231 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update trx
|
// update trx
|
||||||
deviceBs.updateRx(message.getHeader(),message.getLen(),message.getPacketNum());
|
deviceBs.updateRx(message.getHeader(), message.getLen(), message.getPacketNum());
|
||||||
|
|
||||||
// update gga
|
// update gga
|
||||||
Gga gga = message.getGga();
|
Gga gga = message.getGga();
|
||||||
if(gga != null) {
|
if (gga != null) {
|
||||||
deviceBs.updateSatelitesNum(gga.getSatellitesInUsed());
|
deviceBs.updateSatelitesNum(gga.getSatellitesInUsed());
|
||||||
|
// 修复:正确处理Double类型的坐标
|
||||||
deviceBs.setLatitude(gga.getLatitude());
|
deviceBs.setLatitude(gga.getLatitude());
|
||||||
deviceBs.setLongitude(gga.getLongitude());
|
deviceBs.setLongitude(gga.getLongitude());
|
||||||
deviceBs.setAltitude(gga.getAltitude());
|
deviceBs.setAltitude(gga.getAltitude());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加NTRIP处理
|
|
||||||
byte[] srcdata = message.getSrcData();
|
|
||||||
String rtcm = ByteUtil.bytesToHexString(srcdata);
|
|
||||||
sendToNtrip(id, rtcm);
|
|
||||||
|
|
||||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
|
||||||
// 原始码流输出到日志文件 -- INFO 级别
|
|
||||||
// 只有测站开了日志记录,或者消息来自基站,才将原码记录到日志文件
|
|
||||||
if(deviceBs.getLoggingmode() == GnssDevice.LOGGING_MODE_FULL){
|
|
||||||
logger.info("receive {} d331 message: {}", message.getId(), DataTypeUtil.getHexString(message.getSrcData()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendToNtrip(String mountpoint, String hexData) {
|
/**
|
||||||
try {
|
* 创建上一周期设备信息
|
||||||
|
*/
|
||||||
|
private Device createLastCycleDevice(Device deviceBs) {
|
||||||
|
Device lastCycleDevice = new Device();
|
||||||
|
lastCycleDevice.setDeviceId(deviceBs.getDeviceId());
|
||||||
|
lastCycleDevice.setDeviceType(deviceBs.getDeviceType());
|
||||||
|
lastCycleDevice.setTenantId(deviceBs.getTenantId());
|
||||||
|
lastCycleDevice.setD341bytes(deviceBs.getD341bytes());
|
||||||
|
lastCycleDevice.setD341Count(deviceBs.getD341Count());
|
||||||
|
lastCycleDevice.setFixedNum(deviceBs.getFixedNum());
|
||||||
|
lastCycleDevice.setFloatNum(deviceBs.getFloatNum());
|
||||||
|
lastCycleDevice.setNoFixedAndFloatResult(deviceBs.getNoFixedAndFloatResult());
|
||||||
|
lastCycleDevice.setLastRxTime(deviceBs.getLastRxTime());
|
||||||
|
lastCycleDevice.setLastValidCalcDataTime(deviceBs.getLastValidCalcDataTime());
|
||||||
|
lastCycleDevice.setSatelitesInUse(deviceBs.getSatelitesInUse());
|
||||||
|
return lastCycleDevice;
|
||||||
|
}
|
||||||
|
|
||||||
// 将原始字节转换为16进制字符串用于RTCM提取
|
/**
|
||||||
//String hexData = ByteUtil.bytesToHexString(rawData);
|
* 记录日志(如果需要)
|
||||||
//System.out.println(hexData);
|
*/
|
||||||
|
private void logMessageIfNeeded(Device deviceBs, D331RtcmMessage message) {
|
||||||
// 提取RTCM数据并发送到NtripServer,使用设备ID作为挂载点
|
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||||
Optional.ofNullable(RtcmGgaUtil.getRtcms(hexData))
|
if (deviceBs.getLoggingmode() != null && deviceBs.getLoggingmode() == GnssDevice.LOGGING_MODE_FULL) {
|
||||||
.ifPresent(rtcm -> {
|
logger.info("receive {} d331 message: {}", message.getId(),
|
||||||
//System.out.println("挂载点: " + mountpoint);
|
DataTypeUtil.getHexString(message.getSrcData()));
|
||||||
//System.out.println("RTCM数据: " + rtcm);
|
}
|
||||||
ntripServer.send(mountpoint, rtcm);
|
});
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("处理NTRIP数据失败, 挂载点: {}, 错误: {}", mountpoint, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> getMessageType() {
|
public Class<?> getMessageType() {
|
||||||
return D331RtcmMessage.class;
|
return D331RtcmMessage.class;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 发送数据到NTRIP
|
||||||
|
*/
|
||||||
|
private void sendToNtrip(String mountpoint, String hexData) {
|
||||||
|
try {
|
||||||
|
Optional.ofNullable(RtcmGgaUtil.getRtcms(hexData))
|
||||||
|
.ifPresent(rtcm -> ntripServer.send(mountpoint, rtcm));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理NTRIP数据失败, 挂载点: {}, 错误: {}", mountpoint, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断基站是否在线
|
||||||
|
*/
|
||||||
|
private boolean isBaseStationOnline(String baseStationId) {
|
||||||
|
if (baseStationId == null) return false;
|
||||||
|
|
||||||
|
BaseStationStatus status = baseStationStatusMap.get(baseStationId);
|
||||||
|
if (status == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
return status.getLastActiveTime() != null &&
|
||||||
|
status.getLastActiveTime().isAfter(now.minusSeconds(BASE_STATION_OFFLINE_TIMEOUT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断基站设备是否在线(兼容原有方法)
|
||||||
|
*/
|
||||||
|
private boolean isBaseStationOnline(Device baseStation) {
|
||||||
|
if (baseStation == null) return false;
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
return baseStation.getLastRxTime() != null &&
|
||||||
|
baseStation.getLastRxTime().isAfter(now.minusMinutes(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知状态变化
|
||||||
|
*/
|
||||||
|
private void notifyStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus) {
|
||||||
|
for (BaseStationSwitchListener listener : switchListeners) {
|
||||||
|
try {
|
||||||
|
listener.onBaseStationStatusChange(baseStationId, oldStatus, newStatus);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error notifying base station status change listener", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知基站切换(可扩展为调用外部服务)
|
||||||
|
*/
|
||||||
|
private void notifyBaseStationSwitch(String deviceId, String fromBaseId, String toBaseId, String reason) {
|
||||||
|
logger.info("Sending external notification for base station switch: Device={}, From={}, To={}, Reason={}",
|
||||||
|
deviceId, fromBaseId, toBaseId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知基站状态变化(可扩展为调用外部服务)
|
||||||
|
*/
|
||||||
|
private void notifyBaseStationStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus) {
|
||||||
|
logger.info("Sending external notification for base station status change: BaseStation={}, From={}, To={}",
|
||||||
|
baseStationId, oldStatus, newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定期检查基站状态
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 30000) // 30秒执行一次
|
||||||
|
public void checkBaseStationStatus() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
for (BaseStationStatus status : baseStationStatusMap.values()) {
|
||||||
|
if (status.getStatus() == BaseStationStatusEnum.ONLINE) {
|
||||||
|
// 检查是否超时离线
|
||||||
|
if (status.getLastActiveTime().isBefore(now.minusSeconds(BASE_STATION_OFFLINE_TIMEOUT))) {
|
||||||
|
BaseStationStatusEnum oldStatus = status.getStatus();
|
||||||
|
status.setStatus(BaseStationStatusEnum.OFFLINE);
|
||||||
|
|
||||||
|
logger.warn("Base station {} went offline (timeout). Last active: {}",
|
||||||
|
status.getBaseStationId(), status.getLastActiveTime());
|
||||||
|
|
||||||
|
notifyStatusChange(status.getBaseStationId(), oldStatus, BaseStationStatusEnum.OFFLINE);
|
||||||
|
|
||||||
|
// 触发设备切换到备用基站
|
||||||
|
triggerDeviceSwitchToBackup(status.getBaseStationId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发设备切换到备用基站
|
||||||
|
*/
|
||||||
|
private void triggerDeviceSwitchToBackup(String offlineBaseId) {
|
||||||
|
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||||
|
try {
|
||||||
|
BaseStationStatus status = baseStationStatusMap.get(offlineBaseId);
|
||||||
|
if (status != null) {
|
||||||
|
Set<String> affectedDevices = new HashSet<>(status.getServingDevices());
|
||||||
|
|
||||||
|
for (String deviceId : affectedDevices) {
|
||||||
|
// 查找设备信息并触发切换
|
||||||
|
Device device = deviceService.findByDeviceId(deviceId);
|
||||||
|
if (device != null) {
|
||||||
|
String newBaseId = selectOptimalBaseStation(device);
|
||||||
|
if (newBaseId != null && !newBaseId.equals(offlineBaseId)) {
|
||||||
|
switchDeviceToBaseStation(deviceId, newBaseId,
|
||||||
|
"Primary base station offline: " + offlineBaseId);
|
||||||
|
} else {
|
||||||
|
logger.warn("No suitable backup base station found for device {} (was using {})",
|
||||||
|
deviceId, offlineBaseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error triggering device switch to backup base station", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加基站切换监听器
|
||||||
|
*/
|
||||||
|
public static void addBaseStationSwitchListener(BaseStationSwitchListener listener) {
|
||||||
|
switchListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除基站切换监听器
|
||||||
|
*/
|
||||||
|
public static void removeBaseStationSwitchListener(BaseStationSwitchListener listener) {
|
||||||
|
switchListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基站状态信息
|
||||||
|
*/
|
||||||
|
public static BaseStationStatus getBaseStationStatus(String baseStationId) {
|
||||||
|
return baseStationStatusMap.get(baseStationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有基站状态
|
||||||
|
*/
|
||||||
|
public static Map<String, BaseStationStatus> getAllBaseStationStatus() {
|
||||||
|
return new HashMap<>(baseStationStatusMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备当前使用的基站
|
||||||
|
*/
|
||||||
|
public static String getDeviceCurrentBase(String deviceId) {
|
||||||
|
return deviceCurrentBaseMap.get(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证ECEF坐标是否有效
|
||||||
|
*/
|
||||||
|
private boolean isValidEcefCoordinates(Double x, Double y, Double z) {
|
||||||
|
if (x == null || y == null || z == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为零坐标
|
||||||
|
if (x == 0.0 && y == 0.0 && z == 0.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查坐标是否在地球表面合理范围内(大致6.3M到6.4M米)
|
||||||
|
double distance = Math.sqrt(x * x + y * y + z * z);
|
||||||
|
return distance >= 6300000 && distance <= 6400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -90,6 +90,8 @@ public class Device {
|
|||||||
byte cfgChannelType = CHANNEL_TYPE_TCP; // 0:TCP;1:DUP
|
byte cfgChannelType = CHANNEL_TYPE_TCP; // 0:TCP;1:DUP
|
||||||
byte dataChannelType = CHANNEL_TYPE_UDP; // 0:TCP;1:DUP
|
byte dataChannelType = CHANNEL_TYPE_UDP; // 0:TCP;1:DUP
|
||||||
|
|
||||||
|
private String parentId1;
|
||||||
|
|
||||||
public void updateRx(int head, int bytes,int count){
|
public void updateRx(int head, int bytes,int count){
|
||||||
lastRxHead = head;
|
lastRxHead = head;
|
||||||
|
|
||||||
|
|||||||
@ -11,4 +11,6 @@ public interface DeviceService {
|
|||||||
Device findByDeviceId(String deviceId);
|
Device findByDeviceId(String deviceId);
|
||||||
|
|
||||||
List<Device> findByParentId(String parentId);
|
List<Device> findByParentId(String parentId);
|
||||||
|
|
||||||
|
List<Device> findByParentId1(String parentId1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@ public class LocalDeviceServiceImpl implements DeviceService {
|
|||||||
device.setEcefy(gnssDevice.getEcefy());
|
device.setEcefy(gnssDevice.getEcefy());
|
||||||
device.setEcefz(gnssDevice.getEcefz());
|
device.setEcefz(gnssDevice.getEcefz());
|
||||||
device.setLoggingmode(gnssDevice.getLoggingmode());
|
device.setLoggingmode(gnssDevice.getLoggingmode());
|
||||||
|
device.setParentId1(gnssDevice.getParentid1());
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +66,19 @@ public class LocalDeviceServiceImpl implements DeviceService {
|
|||||||
}
|
}
|
||||||
return deviceList;
|
return deviceList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Device> getDeviceListFromDBByParentId1(String parentId1) {
|
||||||
|
QueryWrapper<GnssDevice> query = new QueryWrapper();
|
||||||
|
query.eq("parentid1", parentId1);
|
||||||
|
List<GnssDevice> gnssDeviceList = gnssDeviceMapper.selectList(query);
|
||||||
|
List<Device> deviceList = new ArrayList<>(gnssDeviceList.size());
|
||||||
|
for (GnssDevice gnssDevice : gnssDeviceList) {
|
||||||
|
Device device = findByDeviceId(gnssDevice.getDeviceid());
|
||||||
|
if(device!=null) deviceList.add(device);
|
||||||
|
}
|
||||||
|
return deviceList;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Device findByDeviceId(String deviceId) {
|
public Device findByDeviceId(String deviceId) {
|
||||||
Device device = deviceCache.getIfPresent(deviceId);
|
Device device = deviceCache.getIfPresent(deviceId);
|
||||||
@ -79,23 +93,43 @@ public class LocalDeviceServiceImpl implements DeviceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Device> findByParentId(String parentId) {
|
public List<Device> findByParentId(String parentId) {
|
||||||
List<Device> devices = subDeviceCache.getIfPresent(parentId);
|
String cacheKey = "PARENTID_" + parentId;
|
||||||
|
List<Device> devices = subDeviceCache.getIfPresent(cacheKey);
|
||||||
if (devices == null) {
|
if (devices == null) {
|
||||||
devices = getDeviceListFromDBByParentId(parentId);
|
devices = getDeviceListFromDBByParentId(parentId);
|
||||||
if (devices != null) {
|
if (devices != null) {
|
||||||
subDeviceCache.put(parentId, devices);
|
subDeviceCache.put(cacheKey, devices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refresh(String deviceId, String oldParentId){
|
@Override
|
||||||
|
public List<Device> findByParentId1(String parentId1) {
|
||||||
|
String cacheKey = "PARENTID1_" + parentId1;
|
||||||
|
List<Device> devices = subDeviceCache.getIfPresent(cacheKey);
|
||||||
|
if (devices == null) {
|
||||||
|
devices = getDeviceListFromDBByParentId1(parentId1);
|
||||||
|
if (devices != null) {
|
||||||
|
subDeviceCache.put(cacheKey, devices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh(String deviceId, String oldParentId, String oldParentId1){
|
||||||
Device device = deviceCache.getIfPresent(deviceId);
|
Device device = deviceCache.getIfPresent(deviceId);
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
if(device.getParentId()!=null) {
|
if(device.getParentId()!=null) {
|
||||||
subDeviceCache.invalidate(device.getParentId());
|
subDeviceCache.invalidate("PARENTID_" + device.getParentId());
|
||||||
if(oldParentId!=null && !oldParentId.equals(device.getParentId())){
|
if(oldParentId!=null && !oldParentId.equals(device.getParentId())){
|
||||||
subDeviceCache.invalidate(oldParentId);
|
subDeviceCache.invalidate("PARENTID_" + oldParentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(device.getParentId1()!=null) {
|
||||||
|
subDeviceCache.invalidate("PARENTID1_" + device.getParentId1());
|
||||||
|
if(oldParentId1!=null && !oldParentId1.equals(device.getParentId1())){
|
||||||
|
subDeviceCache.invalidate("PARENTID1_" + oldParentId1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceCache.invalidate(deviceId);
|
deviceCache.invalidate(deviceId);
|
||||||
|
|||||||
@ -154,9 +154,9 @@ public class ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/device_param_changed")
|
@PostMapping("/device_param_changed")
|
||||||
public HttpResp deviceParamChanged(String deviceId, String oldParentId) {
|
public HttpResp deviceParamChanged(String deviceId, String oldParentId, String oldParentId1) {
|
||||||
// 更新设备缓存
|
// 更新设备缓存
|
||||||
localDeviceService.refresh(deviceId, oldParentId);
|
localDeviceService.refresh(deviceId, oldParentId, oldParentId1);
|
||||||
calcService.refreshDeviceCalc(deviceId);
|
calcService.refreshDeviceCalc(deviceId);
|
||||||
|
|
||||||
HttpResp resp = new HttpResp();
|
HttpResp resp = new HttpResp();
|
||||||
|
|||||||
@ -287,7 +287,7 @@ public class GnssDeviceController extends BasicController{
|
|||||||
} else {
|
} else {
|
||||||
// 更新组参数的关联个数
|
// 更新组参数的关联个数
|
||||||
updateBasicGroupAssociatedNum(device,old_device);
|
updateBasicGroupAssociatedNum(device,old_device);
|
||||||
rtcmClient.deviceParamChanged(device.getDeviceid(), device.getParentid());
|
rtcmClient.deviceParamChanged(device.getDeviceid(), old_device != null ? old_device.getParentid() : null, old_device != null ? old_device.getParentid1() : null);
|
||||||
return HttpResult.ok();
|
return HttpResult.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -409,7 +409,7 @@ public class GnssDeviceController extends BasicController{
|
|||||||
if (num == 0) {
|
if (num == 0) {
|
||||||
return HttpResult.failed();
|
return HttpResult.failed();
|
||||||
} else{
|
} else{
|
||||||
rtcmClient.deviceParamChanged(del_id, null);
|
rtcmClient.deviceParamChanged(del_id, null, null);
|
||||||
return HttpResult.ok();
|
return HttpResult.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.imdroid.beidou.controller;
|
package com.imdroid.beidou.controller;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.imdroid.beidou.service.CommonExcelService;
|
import com.imdroid.beidou.service.CommonExcelService;
|
||||||
import com.imdroid.secapi.dto.GnssStatusMsg;
|
import com.imdroid.secapi.dto.GnssStatusMsg;
|
||||||
@ -11,12 +12,13 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态消息 控制器
|
* 状态消息 控制器
|
||||||
@ -32,6 +34,10 @@ public class GnssMsgStatusController extends BasicController implements CommonEx
|
|||||||
GnssStatusMsgMapper statusMsgMapper;
|
GnssStatusMsgMapper statusMsgMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
OpLogManager opLogManager;
|
OpLogManager opLogManager;
|
||||||
|
|
||||||
|
// 滑动窗口大小
|
||||||
|
private static final int WINDOW_SIZE = 5;
|
||||||
|
|
||||||
@RequestMapping("/page/gnss_msg_status")
|
@RequestMapping("/page/gnss_msg_status")
|
||||||
public String gnssStatusMsg(Model m, HttpSession session) {
|
public String gnssStatusMsg(Model m, HttpSession session) {
|
||||||
initModel(m, session);
|
initModel(m, session);
|
||||||
@ -53,6 +59,118 @@ public class GnssMsgStatusController extends BasicController implements CommonEx
|
|||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取特定设备的数据,并应用滑动平均
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param limit 获取条数,默认150条
|
||||||
|
* @return 处理后的数据
|
||||||
|
*/
|
||||||
|
@RequestMapping("/gnss/msg/status/device_data")
|
||||||
|
@ResponseBody
|
||||||
|
public JSONObject getDeviceData(@RequestParam String deviceId,
|
||||||
|
@RequestParam(required = false, defaultValue = "150") Integer limit) {
|
||||||
|
// 查询指定设备的最近数据
|
||||||
|
QueryWrapper<GnssStatusMsg> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("deviceid", deviceId)
|
||||||
|
.orderByDesc("createtime")
|
||||||
|
.last("limit " + limit);
|
||||||
|
|
||||||
|
List<GnssStatusMsg> dataList = statusMsgMapper.selectList(queryWrapper);
|
||||||
|
// 反转列表,使其按时间升序排列
|
||||||
|
Collections.reverse(dataList);
|
||||||
|
|
||||||
|
// 应用滑动平均
|
||||||
|
List<GnssStatusMsg> smoothedData = applyMovingAverage(dataList, WINDOW_SIZE);
|
||||||
|
|
||||||
|
JSONObject result = new JSONObject();
|
||||||
|
result.put("code", 0);
|
||||||
|
result.put("msg", "");
|
||||||
|
result.put("count", smoothedData.size());
|
||||||
|
result.put("data", smoothedData);
|
||||||
|
result.put("originalData", dataList);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用滑动平均算法
|
||||||
|
*
|
||||||
|
* @param dataList 原始数据列表
|
||||||
|
* @param windowSize 窗口大小
|
||||||
|
* @return 处理后的数据列表
|
||||||
|
*/
|
||||||
|
private List<GnssStatusMsg> applyMovingAverage(List<GnssStatusMsg> dataList, int windowSize) {
|
||||||
|
if (dataList.size() <= 1) {
|
||||||
|
return new ArrayList<>(dataList);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GnssStatusMsg> result = new ArrayList<>(dataList.size());
|
||||||
|
|
||||||
|
// 初始窗口
|
||||||
|
Deque<Float> rollWindow = new LinkedList<>();
|
||||||
|
Deque<Float> pitchWindow = new LinkedList<>();
|
||||||
|
Deque<Float> yawWindow = new LinkedList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < dataList.size(); i++) {
|
||||||
|
GnssStatusMsg currentMsg = dataList.get(i);
|
||||||
|
GnssStatusMsg smoothedMsg = new GnssStatusMsg();
|
||||||
|
|
||||||
|
// 复制基本属性
|
||||||
|
smoothedMsg.setId(currentMsg.getId());
|
||||||
|
smoothedMsg.setDeviceid(currentMsg.getDeviceid());
|
||||||
|
smoothedMsg.setCreatetime(currentMsg.getCreatetime());
|
||||||
|
smoothedMsg.setDevicetime(currentMsg.getDevicetime());
|
||||||
|
smoothedMsg.setTenantid(currentMsg.getTenantid());
|
||||||
|
smoothedMsg.setDtustate(currentMsg.getDtustate());
|
||||||
|
smoothedMsg.setRssi(currentMsg.getRssi());
|
||||||
|
smoothedMsg.setVoltage(currentMsg.getVoltage());
|
||||||
|
smoothedMsg.setSolarvoltage(currentMsg.getSolarvoltage());
|
||||||
|
smoothedMsg.setChargecurrency(currentMsg.getChargecurrency());
|
||||||
|
smoothedMsg.setChargewatt(currentMsg.getChargewatt());
|
||||||
|
smoothedMsg.setTemperature(currentMsg.getTemperature());
|
||||||
|
smoothedMsg.setHumidity(currentMsg.getHumidity());
|
||||||
|
|
||||||
|
// 添加当前值到窗口
|
||||||
|
if (currentMsg.getRoll() != null) rollWindow.addLast(currentMsg.getRoll());
|
||||||
|
if (currentMsg.getPitch() != null) pitchWindow.addLast(currentMsg.getPitch());
|
||||||
|
if (currentMsg.getYaw() != null) yawWindow.addLast(currentMsg.getYaw());
|
||||||
|
|
||||||
|
// 保持窗口大小
|
||||||
|
if (rollWindow.size() > windowSize) rollWindow.removeFirst();
|
||||||
|
if (pitchWindow.size() > windowSize) pitchWindow.removeFirst();
|
||||||
|
if (yawWindow.size() > windowSize) yawWindow.removeFirst();
|
||||||
|
|
||||||
|
// 计算平均值
|
||||||
|
if (!rollWindow.isEmpty()) {
|
||||||
|
float rollSum = 0;
|
||||||
|
for (Float val : rollWindow) rollSum += val;
|
||||||
|
smoothedMsg.setRoll(rollSum / rollWindow.size());
|
||||||
|
} else {
|
||||||
|
smoothedMsg.setRoll(currentMsg.getRoll());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pitchWindow.isEmpty()) {
|
||||||
|
float pitchSum = 0;
|
||||||
|
for (Float val : pitchWindow) pitchSum += val;
|
||||||
|
smoothedMsg.setPitch(pitchSum / pitchWindow.size());
|
||||||
|
} else {
|
||||||
|
smoothedMsg.setPitch(currentMsg.getPitch());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yawWindow.isEmpty()) {
|
||||||
|
float yawSum = 0;
|
||||||
|
for (Float val : yawWindow) yawSum += val;
|
||||||
|
smoothedMsg.setYaw(yawSum / yawWindow.size());
|
||||||
|
} else {
|
||||||
|
smoothedMsg.setYaw(currentMsg.getYaw());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(smoothedMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出excel
|
* 导出excel
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 坐标转换工具模块
|
||||||
|
* 提供WGS84和GCJ-02坐标系之间的转换功能
|
||||||
|
*/
|
||||||
|
var CoordinateUtils = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var pi = 3.14159265358979324;
|
||||||
|
var a = 6378245.0;
|
||||||
|
var ee = 0.00669342162296594323;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否在国内,不在国内则不做偏移
|
||||||
|
*/
|
||||||
|
function outOfChina(lon, lat) {
|
||||||
|
if ((lon < 72.004 || lon > 137.8347) && (lat < 0.8293 || lat > 55.8271)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformLat(x, y) {
|
||||||
|
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
|
||||||
|
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
|
||||||
|
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
|
||||||
|
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformLon(x, y) {
|
||||||
|
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
|
||||||
|
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
|
||||||
|
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
|
||||||
|
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function transform(wgLat, wgLon) {
|
||||||
|
var mars_point = {lon: 0, lat: 0};
|
||||||
|
if (outOfChina(wgLon, wgLat)) {
|
||||||
|
mars_point.lat = wgLat;
|
||||||
|
mars_point.lon = wgLon;
|
||||||
|
return mars_point;
|
||||||
|
}
|
||||||
|
var dLat = transformLat(wgLon - 105.0, wgLat - 35.0);
|
||||||
|
var dLon = transformLon(wgLon - 105.0, wgLat - 35.0);
|
||||||
|
var radLat = wgLat / 180.0 * pi;
|
||||||
|
var magic = Math.sin(radLat);
|
||||||
|
magic = 1 - ee * magic * magic;
|
||||||
|
var sqrtMagic = Math.sqrt(magic);
|
||||||
|
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
|
||||||
|
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
|
||||||
|
mars_point.lat = wgLat + dLat;
|
||||||
|
mars_point.lon = wgLon + dLon;
|
||||||
|
return mars_point;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getMapCoordinates(lat, lon, mapType) {
|
||||||
|
var coordinates;
|
||||||
|
if (mapType === 'amap' || mapType === 'amap_satellite') {
|
||||||
|
// 高德地图 WGS84 转换为 GCJ-02
|
||||||
|
var gcjCoord = transform(lat, lon);
|
||||||
|
coordinates = ol.proj.fromLonLat([gcjCoord.lon, gcjCoord.lat]);
|
||||||
|
} else if (mapType.startsWith('google_')) {
|
||||||
|
// Google地图使用WGS84坐标系
|
||||||
|
coordinates = ol.proj.fromLonLat([lon, lat]);
|
||||||
|
} else {
|
||||||
|
// 天地图 CGCS2000,与WGS84实质一样
|
||||||
|
coordinates = ol.proj.fromLonLat([lon, lat]);
|
||||||
|
}
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: transform,
|
||||||
|
getMapCoordinates: getMapCoordinates,
|
||||||
|
outOfChina: outOfChina
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -0,0 +1,369 @@
|
|||||||
|
/**
|
||||||
|
* 设备标记管理模块
|
||||||
|
* 负责设备标记的创建、分类和显示管理
|
||||||
|
*/
|
||||||
|
var DeviceMarkers = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var greenFeatures = [];
|
||||||
|
var orangeFeatures = [];
|
||||||
|
var redFeatures = [];
|
||||||
|
var allFeatures = [];
|
||||||
|
var myLocationFeature = null;
|
||||||
|
var myLocationInterval = null;
|
||||||
|
var showDeviceId = true;
|
||||||
|
var minZoomForLabels = 4;
|
||||||
|
|
||||||
|
|
||||||
|
var map = null;
|
||||||
|
var vectorSource = null;
|
||||||
|
var vectorLayer = null;
|
||||||
|
|
||||||
|
|
||||||
|
function init(mapInstance, vectorSourceInstance, vectorLayerInstance) {
|
||||||
|
map = mapInstance;
|
||||||
|
vectorSource = vectorSourceInstance;
|
||||||
|
vectorLayer = vectorLayerInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createDeviceStyle(feature) {
|
||||||
|
if (feature.get('isMyLocation')) {
|
||||||
|
return new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: [0.5, 1],
|
||||||
|
src: '../images/loc_blue.png',
|
||||||
|
scale: 0.7
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceInfo = feature.get('deviceInfo');
|
||||||
|
if (!deviceInfo) return null;
|
||||||
|
|
||||||
|
var iconSrc;
|
||||||
|
var color = '#000';
|
||||||
|
var isHovered = feature.get('hovered') === true;
|
||||||
|
var scale = isHovered ? 0.85 : 0.7;
|
||||||
|
|
||||||
|
// 根据告警级别选择图标
|
||||||
|
if (deviceInfo.warning == 2) {
|
||||||
|
iconSrc = '../images/loc1_red.png';
|
||||||
|
} else if (deviceInfo.warning == 1) {
|
||||||
|
iconSrc = '../images/loc1_orange.png';
|
||||||
|
} else {
|
||||||
|
iconSrc = '../images/loc1_green.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = new ol.style.Style({
|
||||||
|
image: new ol.style.Icon({
|
||||||
|
anchor: [0.5, 1],
|
||||||
|
src: iconSrc,
|
||||||
|
scale: scale
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据缩放级别和设置决定是否显示设备ID
|
||||||
|
if (showDeviceId && map && map.getView().getZoom() >= minZoomForLabels) {
|
||||||
|
style.setText(new ol.style.Text({
|
||||||
|
text: deviceInfo.deviceid,
|
||||||
|
offsetY: -30,
|
||||||
|
fill: new ol.style.Fill({
|
||||||
|
color: isHovered ? '#1aa094' : color
|
||||||
|
}),
|
||||||
|
stroke: new ol.style.Stroke({
|
||||||
|
color: '#fff',
|
||||||
|
width: isHovered ? 3 : 2
|
||||||
|
}),
|
||||||
|
font: isHovered ? 'bold 12px Arial' : '12px Arial'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addDeviceMarkers(deviceList) {
|
||||||
|
if (!vectorSource || !deviceList) return;
|
||||||
|
|
||||||
|
var savedMyLocationFeature = myLocationFeature;
|
||||||
|
|
||||||
|
vectorSource.clear();
|
||||||
|
greenFeatures = [];
|
||||||
|
orangeFeatures = [];
|
||||||
|
redFeatures = [];
|
||||||
|
allFeatures = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < deviceList.length; i++) {
|
||||||
|
var device = deviceList[i];
|
||||||
|
var currentMapType = getCurrentMapType();
|
||||||
|
var mapCoordinates = CoordinateUtils.getMapCoordinates(
|
||||||
|
device.latitude,
|
||||||
|
device.longitude,
|
||||||
|
currentMapType
|
||||||
|
);
|
||||||
|
|
||||||
|
var feature = new ol.Feature({
|
||||||
|
geometry: new ol.geom.Point(mapCoordinates),
|
||||||
|
deviceInfo: device
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按告警级别分类
|
||||||
|
if (device.warning == 2) {
|
||||||
|
redFeatures.push(feature);
|
||||||
|
} else if (device.warning == 1) {
|
||||||
|
orangeFeatures.push(feature);
|
||||||
|
} else {
|
||||||
|
greenFeatures.push(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
allFeatures.push(feature);
|
||||||
|
vectorSource.addFeature(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedMyLocationFeature) {
|
||||||
|
vectorSource.addFeature(savedMyLocationFeature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制更新样式
|
||||||
|
if (vectorLayer) {
|
||||||
|
vectorLayer.changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getCurrentMapType() {
|
||||||
|
var mapTypeSelect = document.getElementById('mapTypeSelectNew');
|
||||||
|
return mapTypeSelect ? mapTypeSelect.value : 'tianditu_satellite';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showAllDevices() {
|
||||||
|
if (!vectorSource) return;
|
||||||
|
|
||||||
|
var savedMyLocationFeature = myLocationFeature;
|
||||||
|
vectorSource.clear();
|
||||||
|
|
||||||
|
if (savedMyLocationFeature) {
|
||||||
|
vectorSource.addFeature(savedMyLocationFeature);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < allFeatures.length; i++) {
|
||||||
|
vectorSource.addFeature(allFeatures[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWarning1Devices() {
|
||||||
|
if (!vectorSource) return;
|
||||||
|
|
||||||
|
var savedMyLocationFeature = myLocationFeature;
|
||||||
|
vectorSource.clear();
|
||||||
|
|
||||||
|
if (savedMyLocationFeature) {
|
||||||
|
vectorSource.addFeature(savedMyLocationFeature);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < allFeatures.length; i++) {
|
||||||
|
vectorSource.addFeature(allFeatures[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideGreenFeatures();
|
||||||
|
hideRedFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showWarning2Devices() {
|
||||||
|
if (!vectorSource) return;
|
||||||
|
|
||||||
|
var savedMyLocationFeature = myLocationFeature;
|
||||||
|
vectorSource.clear();
|
||||||
|
|
||||||
|
if (savedMyLocationFeature) {
|
||||||
|
vectorSource.addFeature(savedMyLocationFeature);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < allFeatures.length; i++) {
|
||||||
|
vectorSource.addFeature(allFeatures[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideGreenFeatures();
|
||||||
|
hideOrangeFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGreenFeatures() {
|
||||||
|
for (var i = 0; i < greenFeatures.length; i++) {
|
||||||
|
vectorSource.removeFeature(greenFeatures[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOrangeFeatures() {
|
||||||
|
for (var i = 0; i < orangeFeatures.length; i++) {
|
||||||
|
vectorSource.removeFeature(orangeFeatures[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRedFeatures() {
|
||||||
|
for (var i = 0; i < redFeatures.length; i++) {
|
||||||
|
vectorSource.removeFeature(redFeatures[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDeviceById(deviceId) {
|
||||||
|
// console.log('搜索设备:', deviceId, '(类型:', typeof deviceId, ')');
|
||||||
|
// console.log('当前设备总数:', allFeatures.length);
|
||||||
|
|
||||||
|
var searchTerm = String(deviceId).trim();
|
||||||
|
|
||||||
|
for (var i = 0; i < allFeatures.length; i++) {
|
||||||
|
var feature = allFeatures[i];
|
||||||
|
var deviceInfo = feature.get('deviceInfo');
|
||||||
|
if (deviceInfo) {
|
||||||
|
var currentDeviceId = String(deviceInfo.deviceid);
|
||||||
|
// console.log('检查设备:', currentDeviceId, '(类型:', typeof deviceInfo.deviceid, ')');
|
||||||
|
|
||||||
|
if (currentDeviceId === searchTerm) {
|
||||||
|
// console.log('精确匹配找到设备:', currentDeviceId);
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDeviceId.indexOf(searchTerm) !== -1) {
|
||||||
|
// console.log('部分匹配找到设备:', currentDeviceId);
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDeviceId.replace(/\s+/g, '') === searchTerm.replace(/\s+/g, '')) {
|
||||||
|
// console.log('去除空格后匹配找到设备:', currentDeviceId);
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('未找到设备:', deviceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function locateDevice(deviceId) {
|
||||||
|
var feature = findDeviceById(deviceId);
|
||||||
|
if (feature && map) {
|
||||||
|
var geometry = feature.getGeometry();
|
||||||
|
var coordinates = geometry.getCoordinates();
|
||||||
|
|
||||||
|
map.getView().animate({
|
||||||
|
center: coordinates,
|
||||||
|
zoom: Math.max(map.getView().getZoom(), 15),
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的位置
|
||||||
|
*/
|
||||||
|
function getMyLocation() {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
function(position) {
|
||||||
|
var lat = position.coords.latitude;
|
||||||
|
var lon = position.coords.longitude;
|
||||||
|
|
||||||
|
var currentMapType = getCurrentMapType();
|
||||||
|
var coordinates = CoordinateUtils.getMapCoordinates(lat, lon, currentMapType);
|
||||||
|
|
||||||
|
if (myLocationFeature) {
|
||||||
|
vectorSource.removeFeature(myLocationFeature);
|
||||||
|
}
|
||||||
|
|
||||||
|
myLocationFeature = new ol.Feature({
|
||||||
|
geometry: new ol.geom.Point(coordinates),
|
||||||
|
isMyLocation: true
|
||||||
|
});
|
||||||
|
|
||||||
|
vectorSource.addFeature(myLocationFeature);
|
||||||
|
|
||||||
|
map.getView().animate({
|
||||||
|
center: coordinates,
|
||||||
|
zoom: 15,
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.error('获取位置失败:', error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function startLocationUpdates() {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
myLocationInterval = setInterval(function() {
|
||||||
|
getMyLocation();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function stopLocationUpdates() {
|
||||||
|
if (myLocationInterval) {
|
||||||
|
clearInterval(myLocationInterval);
|
||||||
|
myLocationInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateMyLocationForMapType(mapType) {
|
||||||
|
if (myLocationFeature) {
|
||||||
|
var geometry = myLocationFeature.getGeometry();
|
||||||
|
var coordinates = geometry.getCoordinates();
|
||||||
|
var lonLat = ol.proj.toLonLat(coordinates);
|
||||||
|
|
||||||
|
var newCoordinates = CoordinateUtils.getMapCoordinates(lonLat[1], lonLat[0], mapType);
|
||||||
|
|
||||||
|
myLocationFeature.setGeometry(new ol.geom.Point(newCoordinates));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setShowDeviceId(show) {
|
||||||
|
showDeviceId = show;
|
||||||
|
if (vectorLayer) {
|
||||||
|
vectorLayer.changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getDeviceStats() {
|
||||||
|
return {
|
||||||
|
total: allFeatures.length,
|
||||||
|
green: greenFeatures.length,
|
||||||
|
orange: orangeFeatures.length,
|
||||||
|
red: redFeatures.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
createDeviceStyle: createDeviceStyle,
|
||||||
|
addDeviceMarkers: addDeviceMarkers,
|
||||||
|
showAllDevices: showAllDevices,
|
||||||
|
showWarning1Devices: showWarning1Devices,
|
||||||
|
showWarning2Devices: showWarning2Devices,
|
||||||
|
hideGreenFeatures: hideGreenFeatures,
|
||||||
|
hideOrangeFeatures: hideOrangeFeatures,
|
||||||
|
hideRedFeatures: hideRedFeatures,
|
||||||
|
findDeviceById: findDeviceById,
|
||||||
|
locateDevice: locateDevice,
|
||||||
|
getMyLocation: getMyLocation,
|
||||||
|
startLocationUpdates: startLocationUpdates,
|
||||||
|
stopLocationUpdates: stopLocationUpdates,
|
||||||
|
updateMyLocationForMapType: updateMyLocationForMapType,
|
||||||
|
setShowDeviceId: setShowDeviceId,
|
||||||
|
getDeviceStats: getDeviceStats,
|
||||||
|
|
||||||
|
getAllFeatures: function() { return allFeatures; },
|
||||||
|
getGreenFeatures: function() { return greenFeatures; },
|
||||||
|
getOrangeFeatures: function() { return orangeFeatures; },
|
||||||
|
getRedFeatures: function() { return redFeatures; },
|
||||||
|
getMyLocationFeature: function() { return myLocationFeature; }
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* 设备总览主入口文件
|
||||||
|
* 负责整个设备总览模块的初始化和全局函数暴露
|
||||||
|
*/
|
||||||
|
var DeviceOverview = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 全局变量(从原HTML中提取)
|
||||||
|
var deviceList = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化设备总览模块
|
||||||
|
* @param {Array} devices - 设备列表数据
|
||||||
|
* @param {Object} options - 配置选项
|
||||||
|
*/
|
||||||
|
function init(devices, options) {
|
||||||
|
deviceList = devices || [];
|
||||||
|
|
||||||
|
// 设置全局变量供其他模块使用
|
||||||
|
window.deviceList = deviceList;
|
||||||
|
window.userRole = options && options.role ? options.role : 'USER';
|
||||||
|
|
||||||
|
// 等待layui加载完成后初始化
|
||||||
|
layui.use(['form'], function(){
|
||||||
|
var form = layui.form;
|
||||||
|
|
||||||
|
// 绑定表单事件
|
||||||
|
form.on('select(mapTypeNew)', function(data){
|
||||||
|
MapCore.switchMapType(data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化地图核心
|
||||||
|
MapCore.initialize(deviceList);
|
||||||
|
|
||||||
|
// 默认显示所有设备
|
||||||
|
document.getElementById('warningFilter').value = 'all';
|
||||||
|
SearchFilter.showAllDevices();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露给HTML使用的全局函数
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地图类型变化处理
|
||||||
|
*/
|
||||||
|
function onMapTypeChange() {
|
||||||
|
return MapCore.onMapTypeChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索设备
|
||||||
|
*/
|
||||||
|
function searchDeviceNew() {
|
||||||
|
return MapCore.searchDeviceNew();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 告警过滤变化处理
|
||||||
|
*/
|
||||||
|
function onWarningFilterChange() {
|
||||||
|
return MapCore.onWarningFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换地图功能菜单
|
||||||
|
*/
|
||||||
|
function toggleMapFunctionsMenu() {
|
||||||
|
return MapCore.toggleMapFunctionsMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换设备ID显示
|
||||||
|
*/
|
||||||
|
function toggleDeviceId() {
|
||||||
|
return MapCore.toggleDeviceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换集群显示
|
||||||
|
*/
|
||||||
|
function toggleCluster() {
|
||||||
|
return MapCore.toggleCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换测距功能
|
||||||
|
*/
|
||||||
|
function toggleMeasureDistance() {
|
||||||
|
return MeasureTools.toggleMeasureDistance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成测量
|
||||||
|
*/
|
||||||
|
function finishMeasuring() {
|
||||||
|
return MeasureTools.finishMeasuring();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除测距
|
||||||
|
*/
|
||||||
|
function clearMeasure() {
|
||||||
|
return MeasureTools.clearMeasure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换天气预报功能
|
||||||
|
*/
|
||||||
|
function toggleWeatherForecast() {
|
||||||
|
return WeatherForecast.toggleWeatherForecast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭天气卡片
|
||||||
|
*/
|
||||||
|
function closeWeatherCard() {
|
||||||
|
return WeatherForecast.closeWeatherCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示上一个天气预报
|
||||||
|
*/
|
||||||
|
function showPrevForecast() {
|
||||||
|
return WeatherForecast.showPrevForecast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示下一个天气预报
|
||||||
|
*/
|
||||||
|
function showNextForecast() {
|
||||||
|
return WeatherForecast.showNextForecast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备
|
||||||
|
* @param {string} statusType - 状态类型
|
||||||
|
*/
|
||||||
|
function queryDevices(statusType) {
|
||||||
|
return SearchFilter.queryDevices(statusType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位设备到地图
|
||||||
|
* @param {string} deviceId - 设备ID
|
||||||
|
* @param {number} latitude - 纬度
|
||||||
|
* @param {number} longitude - 经度
|
||||||
|
*/
|
||||||
|
function locateDeviceOnMap(deviceId, latitude, longitude) {
|
||||||
|
return SearchFilter.locateDeviceOnMap(deviceId, latitude, longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接定位设备
|
||||||
|
* @param {string} deviceId - 设备ID
|
||||||
|
*/
|
||||||
|
function locateDeviceDirectly(deviceId) {
|
||||||
|
return SearchFilter.locateDeviceDirectly(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换地图类型
|
||||||
|
* @param {string} mapType - 地图类型
|
||||||
|
*/
|
||||||
|
function switchMapType(mapType) {
|
||||||
|
return MapCore.switchMapType(mapType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公开API
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
|
||||||
|
// 地图相关
|
||||||
|
onMapTypeChange: onMapTypeChange,
|
||||||
|
switchMapType: switchMapType,
|
||||||
|
|
||||||
|
// 搜索和过滤
|
||||||
|
searchDeviceNew: searchDeviceNew,
|
||||||
|
onWarningFilterChange: onWarningFilterChange,
|
||||||
|
queryDevices: queryDevices,
|
||||||
|
locateDeviceOnMap: locateDeviceOnMap,
|
||||||
|
locateDeviceDirectly: locateDeviceDirectly,
|
||||||
|
|
||||||
|
// 地图功能
|
||||||
|
toggleMapFunctionsMenu: toggleMapFunctionsMenu,
|
||||||
|
toggleDeviceId: toggleDeviceId,
|
||||||
|
toggleCluster: toggleCluster,
|
||||||
|
|
||||||
|
// 测距工具
|
||||||
|
toggleMeasureDistance: toggleMeasureDistance,
|
||||||
|
finishMeasuring: finishMeasuring,
|
||||||
|
clearMeasure: clearMeasure,
|
||||||
|
|
||||||
|
// 天气预报
|
||||||
|
toggleWeatherForecast: toggleWeatherForecast,
|
||||||
|
closeWeatherCard: closeWeatherCard,
|
||||||
|
showPrevForecast: showPrevForecast,
|
||||||
|
showNextForecast: showNextForecast
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 将主要函数暴露到全局作用域,供HTML中的onclick等使用
|
||||||
|
window.DeviceOverview = DeviceOverview;
|
||||||
|
window.onMapTypeChange = DeviceOverview.onMapTypeChange;
|
||||||
|
window.searchDeviceNew = DeviceOverview.searchDeviceNew;
|
||||||
|
window.onWarningFilterChange = DeviceOverview.onWarningFilterChange;
|
||||||
|
window.toggleMapFunctionsMenu = DeviceOverview.toggleMapFunctionsMenu;
|
||||||
|
window.toggleDeviceId = DeviceOverview.toggleDeviceId;
|
||||||
|
window.toggleCluster = DeviceOverview.toggleCluster;
|
||||||
|
window.toggleMeasureDistance = DeviceOverview.toggleMeasureDistance;
|
||||||
|
window.finishMeasuring = DeviceOverview.finishMeasuring;
|
||||||
|
window.clearMeasure = DeviceOverview.clearMeasure;
|
||||||
|
window.toggleWeatherForecast = DeviceOverview.toggleWeatherForecast;
|
||||||
|
window.closeWeatherCard = DeviceOverview.closeWeatherCard;
|
||||||
|
window.showPrevForecast = DeviceOverview.showPrevForecast;
|
||||||
|
window.showNextForecast = DeviceOverview.showNextForecast;
|
||||||
|
window.queryDevices = DeviceOverview.queryDevices;
|
||||||
|
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
|
||||||
|
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;
|
||||||
@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* 地图核心模块
|
||||||
|
* 负责地图初始化、事件处理和核心功能管理
|
||||||
|
*/
|
||||||
|
var MapCore = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var map = null;
|
||||||
|
var vectorSource = null;
|
||||||
|
var vectorLayer = null;
|
||||||
|
var clusterSource = null;
|
||||||
|
var clusterLayer = null;
|
||||||
|
var currentBaseLayer = null;
|
||||||
|
var showDeviceId = true;
|
||||||
|
var showCluster = true;
|
||||||
|
var minZoomForLabels = 4;
|
||||||
|
var maxZoomForClustering = 8;
|
||||||
|
var hoveredFeature = null;
|
||||||
|
|
||||||
|
|
||||||
|
function initialize(deviceList) {
|
||||||
|
// 创建矢量数据源和图层
|
||||||
|
vectorSource = new ol.source.Vector();
|
||||||
|
vectorLayer = new ol.layer.Vector({
|
||||||
|
source: vectorSource,
|
||||||
|
style: function(feature) {
|
||||||
|
return DeviceMarkers.createDeviceStyle(feature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建集群数据源和图层
|
||||||
|
clusterSource = new ol.source.Cluster({
|
||||||
|
distance: 40,
|
||||||
|
source: vectorSource
|
||||||
|
});
|
||||||
|
|
||||||
|
clusterLayer = new ol.layer.Vector({
|
||||||
|
source: clusterSource,
|
||||||
|
style: function(feature) {
|
||||||
|
var size = feature.get('features').length;
|
||||||
|
var style = new ol.style.Style({
|
||||||
|
image: new ol.style.Circle({
|
||||||
|
radius: 15,
|
||||||
|
fill: new ol.style.Fill({
|
||||||
|
color: '#3399CC'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
text: new ol.style.Text({
|
||||||
|
text: size.toString(),
|
||||||
|
fill: new ol.style.Fill({
|
||||||
|
color: '#fff'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var initialMapType = document.getElementById('mapTypeSelectNew').value || 'tianditu_satellite';
|
||||||
|
currentBaseLayer = MapLayers.getLayer(initialMapType);
|
||||||
|
|
||||||
|
map = new ol.Map({
|
||||||
|
target: 'map-container',
|
||||||
|
layers: [
|
||||||
|
currentBaseLayer,
|
||||||
|
clusterLayer,
|
||||||
|
vectorLayer
|
||||||
|
],
|
||||||
|
view: new ol.View({
|
||||||
|
center: ol.proj.fromLonLat([116.404, 39.915]),
|
||||||
|
zoom: 7
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
DeviceMarkers.init(map, vectorSource, vectorLayer);
|
||||||
|
MeasureTools.init(map);
|
||||||
|
WeatherForecast.init();
|
||||||
|
|
||||||
|
var scaleLineControl = new ol.control.ScaleLine();
|
||||||
|
map.addControl(scaleLineControl);
|
||||||
|
|
||||||
|
var initialZoom = map.getView().getZoom();
|
||||||
|
updateLayerVisibility(initialZoom);
|
||||||
|
|
||||||
|
map.getView().on('change:resolution', function() {
|
||||||
|
var zoom = map.getView().getZoom();
|
||||||
|
updateLayerVisibility(zoom);
|
||||||
|
vectorLayer.changed();
|
||||||
|
});
|
||||||
|
|
||||||
|
bindMouseEvents();
|
||||||
|
|
||||||
|
if (deviceList && deviceList.length > 0) {
|
||||||
|
setCenterFromDevices(deviceList);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceMarkers.addDeviceMarkers(deviceList);
|
||||||
|
|
||||||
|
DeviceMarkers.getMyLocation();
|
||||||
|
DeviceMarkers.startLocationUpdates();
|
||||||
|
|
||||||
|
document.getElementById('showDeviceIdSwitch').checked = showDeviceId;
|
||||||
|
document.getElementById('showClusterSwitch').checked = showCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLayerVisibility(zoom) {
|
||||||
|
if (showCluster) {
|
||||||
|
if (zoom >= maxZoomForClustering) {
|
||||||
|
clusterLayer.setVisible(false);
|
||||||
|
vectorLayer.setVisible(true);
|
||||||
|
} else {
|
||||||
|
clusterLayer.setVisible(true);
|
||||||
|
vectorLayer.setVisible(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clusterLayer.setVisible(false);
|
||||||
|
vectorLayer.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindMouseEvents() {
|
||||||
|
map.on('pointermove', function(evt) {
|
||||||
|
if (evt.dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixel = map.getEventPixel(evt.originalEvent);
|
||||||
|
var hit = map.hasFeatureAtPixel(pixel);
|
||||||
|
|
||||||
|
map.getTargetElement().style.cursor = hit ? 'pointer' : '';
|
||||||
|
|
||||||
|
var feature = map.forEachFeatureAtPixel(pixel, function(feature) {
|
||||||
|
return feature;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hoveredFeature && hoveredFeature !== feature) {
|
||||||
|
hoveredFeature.set('hovered', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature) {
|
||||||
|
// 处理集群
|
||||||
|
var features = feature.get('features');
|
||||||
|
if (features && features.length === 1) {
|
||||||
|
features[0].set('hovered', true);
|
||||||
|
hoveredFeature = features[0];
|
||||||
|
} else if (!features && feature.get('deviceInfo')) {
|
||||||
|
feature.set('hovered', true);
|
||||||
|
hoveredFeature = feature;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vectorLayer.changed();
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('click', function(evt) {
|
||||||
|
var feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) {
|
||||||
|
return feature;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (feature) {
|
||||||
|
var features = feature.get('features');
|
||||||
|
if (features && features.length > 1) {
|
||||||
|
// 集群点击,扩展视图
|
||||||
|
var extent = vectorSource.getExtent();
|
||||||
|
map.getView().fit(extent, {
|
||||||
|
padding: [50, 50, 50, 50],
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
} else if (features && features.length === 1) {
|
||||||
|
// 单个设备点击
|
||||||
|
var deviceInfo = features[0].get('deviceInfo');
|
||||||
|
if (deviceInfo) {
|
||||||
|
showDeviceInfo(deviceInfo);
|
||||||
|
// 如果天气预测开启,显示天气卡片
|
||||||
|
if (WeatherForecast.isEnabled()) {
|
||||||
|
WeatherForecast.showWeatherForecast(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (feature.get('deviceInfo')) {
|
||||||
|
var deviceInfo = feature.get('deviceInfo');
|
||||||
|
showDeviceInfo(deviceInfo);
|
||||||
|
// 如果天气预测开启,显示天气卡片
|
||||||
|
if (WeatherForecast.isEnabled()) {
|
||||||
|
WeatherForecast.showWeatherForecast(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showDeviceInfo(deviceInfo) {
|
||||||
|
var statusText = '';
|
||||||
|
|
||||||
|
if (deviceInfo.warning === 2) {
|
||||||
|
statusText = '严重告警';
|
||||||
|
} else if (deviceInfo.warning === 1) {
|
||||||
|
statusText = '一般告警';
|
||||||
|
} else {
|
||||||
|
statusText = '正常';
|
||||||
|
}
|
||||||
|
|
||||||
|
var infoMsg = ' 设备: ' + deviceInfo.deviceid +
|
||||||
|
' | 状态: ' + statusText +
|
||||||
|
' | 坐标: ' + deviceInfo.latitude.toFixed(4) + ', ' + deviceInfo.longitude.toFixed(4);
|
||||||
|
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg(infoMsg, {
|
||||||
|
time: 3000,
|
||||||
|
area: ['auto', 'auto'],
|
||||||
|
offset: 'auto'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCenterFromDevices(deviceList) {
|
||||||
|
if (!deviceList || deviceList.length === 0) return;
|
||||||
|
|
||||||
|
var minLat = deviceList[0].latitude;
|
||||||
|
var maxLat = deviceList[0].latitude;
|
||||||
|
var minLon = deviceList[0].longitude;
|
||||||
|
var maxLon = deviceList[0].longitude;
|
||||||
|
|
||||||
|
for (var i = 1; i < deviceList.length; i++) {
|
||||||
|
var device = deviceList[i];
|
||||||
|
minLat = Math.min(minLat, device.latitude);
|
||||||
|
maxLat = Math.max(maxLat, device.latitude);
|
||||||
|
minLon = Math.min(minLon, device.longitude);
|
||||||
|
maxLon = Math.max(maxLon, device.longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
var centerLat = (minLat + maxLat) / 2;
|
||||||
|
var centerLon = (minLon + maxLon) / 2;
|
||||||
|
|
||||||
|
map.getView().setCenter(ol.proj.fromLonLat([centerLon, centerLat]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchMapType(mapType) {
|
||||||
|
if (!MapLayers.hasLayer(mapType)) {
|
||||||
|
console.error('未知的地图类型:', mapType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.removeLayer(currentBaseLayer);
|
||||||
|
currentBaseLayer = MapLayers.getLayer(mapType);
|
||||||
|
map.getLayers().insertAt(0, currentBaseLayer);
|
||||||
|
|
||||||
|
DeviceMarkers.updateMyLocationForMapType(mapType);
|
||||||
|
|
||||||
|
// 重新获取设备列表并添加标记
|
||||||
|
var deviceList = window.deviceList || [];
|
||||||
|
DeviceMarkers.addDeviceMarkers(deviceList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDeviceId() {
|
||||||
|
showDeviceId = document.getElementById('showDeviceIdSwitch').checked;
|
||||||
|
DeviceMarkers.setShowDeviceId(showDeviceId);
|
||||||
|
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg(showDeviceId ? '已显示设备信息' : '已隐藏设备信息');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCluster() {
|
||||||
|
showCluster = document.getElementById('showClusterSwitch').checked;
|
||||||
|
var zoom = map.getView().getZoom();
|
||||||
|
updateLayerVisibility(zoom);
|
||||||
|
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg(showCluster ? '已启用集群显示' : '已禁用集群显示');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMapFunctionsMenu() {
|
||||||
|
var menu = document.getElementById('mapFunctionsMenu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.toggle('show');
|
||||||
|
if (menu.classList.contains('show')) {
|
||||||
|
document.addEventListener('click', closeMapFunctionsMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭地图功能菜单
|
||||||
|
* @param {Event} event - 点击事件
|
||||||
|
*/
|
||||||
|
function closeMapFunctionsMenu(event) {
|
||||||
|
var dropdown = document.getElementById('mapFunctionsMenu');
|
||||||
|
var toggleBtn = document.querySelector('.dropdown-toggle');
|
||||||
|
|
||||||
|
if (dropdown && !dropdown.contains(event.target) && !toggleBtn.contains(event.target)) {
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
document.removeEventListener('click', closeMapFunctionsMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地图类型变化处理
|
||||||
|
*/
|
||||||
|
function onMapTypeChange() {
|
||||||
|
var mapType = document.getElementById('mapTypeSelectNew').value;
|
||||||
|
switchMapType(mapType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWarningFilterChange() {
|
||||||
|
var filterValue = document.getElementById('warningFilter').value;
|
||||||
|
switch(filterValue) {
|
||||||
|
case 'all':
|
||||||
|
SearchFilter.showAllDevices();
|
||||||
|
break;
|
||||||
|
case 'warning1':
|
||||||
|
SearchFilter.showWarning1Devices();
|
||||||
|
break;
|
||||||
|
case 'warning2':
|
||||||
|
SearchFilter.showWarning2Devices();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchDeviceNew() {
|
||||||
|
var searchInput = document.getElementById('deviceSearchNew');
|
||||||
|
if (searchInput) {
|
||||||
|
var deviceId = searchInput.value.trim();
|
||||||
|
if (deviceId) {
|
||||||
|
SearchFilter.searchDevice(deviceId);
|
||||||
|
} else {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('请输入设备编号');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMap() {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVectorSource() {
|
||||||
|
return vectorSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVectorLayer() {
|
||||||
|
return vectorLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize: initialize,
|
||||||
|
switchMapType: switchMapType,
|
||||||
|
toggleDeviceId: toggleDeviceId,
|
||||||
|
toggleCluster: toggleCluster,
|
||||||
|
toggleMapFunctionsMenu: toggleMapFunctionsMenu,
|
||||||
|
onMapTypeChange: onMapTypeChange,
|
||||||
|
onWarningFilterChange: onWarningFilterChange,
|
||||||
|
searchDeviceNew: searchDeviceNew,
|
||||||
|
getMap: getMap,
|
||||||
|
getVectorSource: getVectorSource,
|
||||||
|
getVectorLayer: getVectorLayer
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* 地图图层管理模块
|
||||||
|
* 管理不同类型的地图图层(天地图、高德、谷歌等)
|
||||||
|
*/
|
||||||
|
var MapLayers = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 天地图 API 密钥 (fengyarnom@gmail.com)
|
||||||
|
var TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac';
|
||||||
|
|
||||||
|
function createTiandituTileLoadFunction() {
|
||||||
|
return function(imageTile, src) {
|
||||||
|
imageTile.getImage().src = src;
|
||||||
|
imageTile.getImage().onerror = function() {
|
||||||
|
// 天地图加载失败时切换到高德地图
|
||||||
|
var mapTypeSelect = document.getElementById('mapTypeSelectNew');
|
||||||
|
if(mapTypeSelect && mapTypeSelect.value.startsWith('tianditu_')) {
|
||||||
|
mapTypeSelect.value = 'amap';
|
||||||
|
if (window.DeviceOverview && typeof window.DeviceOverview.switchMapType === 'function') {
|
||||||
|
window.DeviceOverview.switchMapType('amap');
|
||||||
|
}
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('天地图加载失败,已自动切换到高德地图');
|
||||||
|
}
|
||||||
|
if (window.layui && window.layui.form) {
|
||||||
|
window.layui.form.render('select');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var mapLayers = {
|
||||||
|
// 高德地图
|
||||||
|
amap: new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 高德卫星图
|
||||||
|
amap_satellite: new ol.layer.Group({
|
||||||
|
layers: [
|
||||||
|
new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 谷歌卫星图
|
||||||
|
google_satellite: new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://mt{0-3}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
|
||||||
|
crossOrigin: 'anonymous'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 谷歌地形图
|
||||||
|
google_terrain: new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://mt{0-3}.google.com/vt/lyrs=p&x={x}&y={y}&z={z}',
|
||||||
|
crossOrigin: 'anonymous'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 谷歌道路图
|
||||||
|
google_roadmap: new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://mt{0-3}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
|
||||||
|
crossOrigin: 'anonymous'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 谷歌混合图
|
||||||
|
google_hybrid: new ol.layer.Tile({
|
||||||
|
source: new ol.source.XYZ({
|
||||||
|
url: 'https://mt{0-3}.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
|
||||||
|
crossOrigin: 'anonymous'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 天地图卫星影像
|
||||||
|
tianditu_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,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
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,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 天地图矢量图
|
||||||
|
tianditu_normal: 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=' + TIANDITU_KEY,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
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=' + TIANDITU_KEY,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 天地图地形图
|
||||||
|
tianditu_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=' + TIANDITU_KEY,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 天地图地形混合图
|
||||||
|
tianditu_terrain_hybrid: 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=' + TIANDITU_KEY,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
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=' + TIANDITU_KEY,
|
||||||
|
tileLoadFunction: createTiandituTileLoadFunction()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function getLayer(mapType) {
|
||||||
|
return mapLayers[mapType];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getAllLayers() {
|
||||||
|
return mapLayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hasLayer(mapType) {
|
||||||
|
return mapLayers.hasOwnProperty(mapType);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
getLayer: getLayer,
|
||||||
|
getAllLayers: getAllLayers,
|
||||||
|
hasLayer: hasLayer
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -0,0 +1,303 @@
|
|||||||
|
/**
|
||||||
|
* 测距工具模块
|
||||||
|
* 提供地图测距功能
|
||||||
|
*/
|
||||||
|
var MeasureTools = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var measureActive = false;
|
||||||
|
var measureDraw = null;
|
||||||
|
var measureSource = null;
|
||||||
|
var measureLayer = null;
|
||||||
|
var measureTooltips = [];
|
||||||
|
var measureFeatures = [];
|
||||||
|
var currentMeasureTooltips = [];
|
||||||
|
var currentSketch = null;
|
||||||
|
var currentListener = null;
|
||||||
|
var segmentTooltips = [];
|
||||||
|
var map = null;
|
||||||
|
|
||||||
|
function init(mapInstance) {
|
||||||
|
map = mapInstance;
|
||||||
|
|
||||||
|
// 测距专用图层
|
||||||
|
measureSource = new ol.source.Vector();
|
||||||
|
measureLayer = new ol.layer.Vector({
|
||||||
|
source: measureSource,
|
||||||
|
style: new ol.style.Style({
|
||||||
|
fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.2)' }),
|
||||||
|
stroke: new ol.style.Stroke({ color: '#ffcc33', width: 2 }),
|
||||||
|
image: new ol.style.RegularShape({
|
||||||
|
points: 4,
|
||||||
|
radius: 8,
|
||||||
|
radius2: 0,
|
||||||
|
angle: Math.PI / 4,
|
||||||
|
stroke: new ol.style.Stroke({ color: '#ed8936', width: 2 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (map) {
|
||||||
|
map.addLayer(measureLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMeasureDistance() {
|
||||||
|
if (measureActive) {
|
||||||
|
deactivateMeasure();
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('测距功能已关闭');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activateMeasure();
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('点击左键添加距离节点,点击右键结束测量 :)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var menu = document.getElementById('mapFunctionsMenu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishMeasuring() {
|
||||||
|
if (measureActive && currentSketch && measureDraw) {
|
||||||
|
measureDraw.finishDrawing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateMeasure() {
|
||||||
|
if (!map || !measureSource) return;
|
||||||
|
|
||||||
|
measureActive = true;
|
||||||
|
|
||||||
|
// 显示测量状态指示器
|
||||||
|
var measureStatus = document.getElementById('measureStatus');
|
||||||
|
if (measureStatus) {
|
||||||
|
measureStatus.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
measureDraw = new ol.interaction.Draw({
|
||||||
|
source: measureSource,
|
||||||
|
type: 'LineString',
|
||||||
|
style: new ol.style.Style({
|
||||||
|
fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.2)' }),
|
||||||
|
stroke: new ol.style.Stroke({ color: '#ffcc33', width: 2 }),
|
||||||
|
image: new ol.style.RegularShape({
|
||||||
|
points: 4,
|
||||||
|
radius: 8,
|
||||||
|
radius2: 0,
|
||||||
|
angle: Math.PI / 4,
|
||||||
|
stroke: new ol.style.Stroke({ color: '#ed8936', width: 2 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addInteraction(measureDraw);
|
||||||
|
|
||||||
|
map.getViewport().addEventListener('contextmenu', function(e) {
|
||||||
|
if (measureActive && currentSketch) {
|
||||||
|
e.preventDefault();
|
||||||
|
measureDraw.finishDrawing();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
measureDraw.on('drawstart', function(evt) {
|
||||||
|
currentSketch = evt.feature;
|
||||||
|
currentMeasureTooltips = [];
|
||||||
|
segmentTooltips = [];
|
||||||
|
|
||||||
|
currentListener = currentSketch.getGeometry().on('change', function(e) {
|
||||||
|
var geom = e.target;
|
||||||
|
var coords = geom.getCoordinates();
|
||||||
|
|
||||||
|
clearTemporaryTooltips();
|
||||||
|
clearSegmentTooltips();
|
||||||
|
|
||||||
|
// 计算并显示每个节点的距离
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < coords.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
// 起点
|
||||||
|
var startTooltip = createMeasureTooltip(coords[0], '起点');
|
||||||
|
currentMeasureTooltips.push(startTooltip);
|
||||||
|
} else {
|
||||||
|
// 计算段距离
|
||||||
|
var seg = new ol.geom.LineString([coords[i-1], coords[i]]);
|
||||||
|
var segmentLength = ol.sphere.getLength(seg);
|
||||||
|
total += segmentLength;
|
||||||
|
|
||||||
|
// 显示每个节点的累计距离
|
||||||
|
var output = formatLength(total);
|
||||||
|
var tooltip = createMeasureTooltip(coords[i], output);
|
||||||
|
segmentTooltips.push(tooltip);
|
||||||
|
|
||||||
|
if (i === coords.length - 1) {
|
||||||
|
currentMeasureTooltips.push(tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绘制结束
|
||||||
|
measureDraw.on('drawend', function(evt) {
|
||||||
|
var coords = evt.feature.getGeometry().getCoordinates();
|
||||||
|
|
||||||
|
clearTemporaryTooltips();
|
||||||
|
clearSegmentTooltips();
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < coords.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
var startTooltip = createMeasureTooltip(coords[0], '起点', true);
|
||||||
|
measureTooltips.push(startTooltip);
|
||||||
|
} else {
|
||||||
|
var seg = new ol.geom.LineString([coords[i-1], coords[i]]);
|
||||||
|
total += ol.sphere.getLength(seg);
|
||||||
|
|
||||||
|
var output = formatLength(total);
|
||||||
|
var tooltip = createMeasureTooltip(coords[i], output, true);
|
||||||
|
measureTooltips.push(tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
measureFeatures.push(evt.feature);
|
||||||
|
|
||||||
|
if (currentListener) {
|
||||||
|
ol.Observable.unByKey(currentListener);
|
||||||
|
}
|
||||||
|
currentSketch = null;
|
||||||
|
currentListener = null;
|
||||||
|
|
||||||
|
var measureStatus = document.getElementById('measureStatus');
|
||||||
|
if (measureStatus) {
|
||||||
|
measureStatus.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
map.removeInteraction(measureDraw);
|
||||||
|
measureActive = false;
|
||||||
|
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('测量完成');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateMeasure() {
|
||||||
|
measureActive = false;
|
||||||
|
|
||||||
|
var measureStatus = document.getElementById('measureStatus');
|
||||||
|
if (measureStatus) {
|
||||||
|
measureStatus.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (measureDraw && map) {
|
||||||
|
map.removeInteraction(measureDraw);
|
||||||
|
measureDraw = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentListener) {
|
||||||
|
ol.Observable.unByKey(currentListener);
|
||||||
|
currentListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSketch = null;
|
||||||
|
clearTemporaryTooltips();
|
||||||
|
clearSegmentTooltips();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMeasureTooltip(coord, text, isStatic) {
|
||||||
|
var elem = document.createElement('div');
|
||||||
|
elem.className = isStatic ? 'ol-tooltip ol-tooltip-static' : 'ol-tooltip ol-tooltip-measure';
|
||||||
|
elem.innerHTML = text;
|
||||||
|
|
||||||
|
var overlay = new ol.Overlay({
|
||||||
|
element: elem,
|
||||||
|
offset: [0, -15],
|
||||||
|
positioning: 'bottom-center'
|
||||||
|
});
|
||||||
|
|
||||||
|
overlay.setPosition(coord);
|
||||||
|
|
||||||
|
if (map) {
|
||||||
|
map.addOverlay(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLength(length) {
|
||||||
|
if (length > 1000) {
|
||||||
|
return (Math.round(length / 100) / 10) + ' km';
|
||||||
|
} else {
|
||||||
|
return (Math.round(length * 10) / 10) + ' m';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllMeasureTooltips() {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < measureTooltips.length; i++) {
|
||||||
|
map.removeOverlay(measureTooltips[i]);
|
||||||
|
}
|
||||||
|
measureTooltips = [];
|
||||||
|
|
||||||
|
clearTemporaryTooltips();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTemporaryTooltips() {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < currentMeasureTooltips.length; i++) {
|
||||||
|
map.removeOverlay(currentMeasureTooltips[i]);
|
||||||
|
}
|
||||||
|
currentMeasureTooltips = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSegmentTooltips() {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < segmentTooltips.length; i++) {
|
||||||
|
map.removeOverlay(segmentTooltips[i]);
|
||||||
|
}
|
||||||
|
segmentTooltips = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMeasure() {
|
||||||
|
if (measureSource) {
|
||||||
|
measureSource.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllMeasureTooltips();
|
||||||
|
measureFeatures = [];
|
||||||
|
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('测距标记已清除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭地图功能菜单
|
||||||
|
var menu = document.getElementById('mapFunctionsMenu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive() {
|
||||||
|
return measureActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMeasureCount() {
|
||||||
|
return measureFeatures.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
toggleMeasureDistance: toggleMeasureDistance,
|
||||||
|
finishMeasuring: finishMeasuring,
|
||||||
|
clearMeasure: clearMeasure,
|
||||||
|
isActive: isActive,
|
||||||
|
getMeasureCount: getMeasureCount
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* 搜索和过滤功能模块
|
||||||
|
* 负责设备搜索、过滤和查询功能
|
||||||
|
*/
|
||||||
|
var SearchFilter = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var currentSearchedDevice = null;
|
||||||
|
var markerState = 3; // 1:all; 2:orange; 3:red
|
||||||
|
|
||||||
|
function searchDevice(deviceId) {
|
||||||
|
if (!deviceId || !deviceId.trim()) {
|
||||||
|
clearSearch();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceId = deviceId.trim();
|
||||||
|
currentSearchedDevice = deviceId;
|
||||||
|
|
||||||
|
var success = DeviceMarkers.locateDevice(deviceId);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('已定位到设备: ' + deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备信息并显示天气预测(如果启用)
|
||||||
|
var targetFeature = DeviceMarkers.findDeviceById(deviceId);
|
||||||
|
if (targetFeature && window.WeatherForecast && window.WeatherForecast.isEnabled()) {
|
||||||
|
var deviceInfo = targetFeature.get('deviceInfo');
|
||||||
|
if (deviceInfo) {
|
||||||
|
window.WeatherForecast.showWeatherForecast(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('未找到设备: ' + deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
currentSearchedDevice = null;
|
||||||
|
var searchInput = document.getElementById('deviceSearchNew');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复到当前的过滤状态
|
||||||
|
applyCurrentFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCurrentFilter() {
|
||||||
|
switch(markerState) {
|
||||||
|
case 1:
|
||||||
|
showAllDevices();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
showWarning1Devices();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
showWarning2Devices();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAllDevices() {
|
||||||
|
currentSearchedDevice = null;
|
||||||
|
clearSearchInput();
|
||||||
|
DeviceMarkers.showAllDevices();
|
||||||
|
markerState = 1;
|
||||||
|
updateFilterSelect('all');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWarning1Devices() {
|
||||||
|
currentSearchedDevice = null;
|
||||||
|
clearSearchInput();
|
||||||
|
DeviceMarkers.showWarning1Devices();
|
||||||
|
markerState = 2;
|
||||||
|
updateFilterSelect('warning1');
|
||||||
|
}
|
||||||
|
function showWarning2Devices() {
|
||||||
|
currentSearchedDevice = null;
|
||||||
|
clearSearchInput();
|
||||||
|
DeviceMarkers.showWarning2Devices();
|
||||||
|
markerState = 3;
|
||||||
|
updateFilterSelect('warning2');
|
||||||
|
}
|
||||||
|
function clearSearchInput() {
|
||||||
|
var searchInput = document.getElementById('deviceSearchNew');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterSelect(filterValue) {
|
||||||
|
var filterSelect = document.getElementById('warningFilter');
|
||||||
|
if (filterSelect) {
|
||||||
|
filterSelect.value = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDevicesByStatus(statusType) {
|
||||||
|
switch(statusType) {
|
||||||
|
case 'warning1':
|
||||||
|
showWarning1Devices();
|
||||||
|
break;
|
||||||
|
case 'warning2':
|
||||||
|
showWarning2Devices();
|
||||||
|
break;
|
||||||
|
case 'offline':
|
||||||
|
case 'no_fwd':
|
||||||
|
case 'nofixed':
|
||||||
|
case 'nogga':
|
||||||
|
// 对于这些状态,显示所有设备,让用户在弹窗中选择
|
||||||
|
showAllDevices();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showAllDevices();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryDevices(statusType) {
|
||||||
|
filterDevicesByStatus(statusType);
|
||||||
|
|
||||||
|
// 打开设备列表弹窗
|
||||||
|
if (window.layer && typeof window.layer.open === 'function') {
|
||||||
|
var index = window.layer.open({
|
||||||
|
title: '设备列表',
|
||||||
|
type: 2,
|
||||||
|
shade: 0.2,
|
||||||
|
maxmin: true,
|
||||||
|
shadeClose: true,
|
||||||
|
anim: 2,
|
||||||
|
offset: 'rb',
|
||||||
|
area: ['100%', '50%'],
|
||||||
|
content: '../page/gnss_q_status?query=' + statusType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function locateDeviceOnMap(deviceId, latitude, longitude) {
|
||||||
|
currentSearchedDevice = deviceId;
|
||||||
|
var success = DeviceMarkers.locateDevice(deviceId);
|
||||||
|
|
||||||
|
if (success && window.layer && typeof window.layer.closeAll === 'function') {
|
||||||
|
window.layer.closeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
function locateDeviceDirectly(deviceId) {
|
||||||
|
currentSearchedDevice = deviceId;
|
||||||
|
return DeviceMarkers.locateDevice(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSearchedDevice() {
|
||||||
|
return currentSearchedDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkerState() {
|
||||||
|
return markerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMarkerState(state) {
|
||||||
|
markerState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchDevice: searchDevice,
|
||||||
|
clearSearch: clearSearch,
|
||||||
|
showAllDevices: showAllDevices,
|
||||||
|
showWarning1Devices: showWarning1Devices,
|
||||||
|
showWarning2Devices: showWarning2Devices,
|
||||||
|
filterDevicesByStatus: filterDevicesByStatus,
|
||||||
|
queryDevices: queryDevices,
|
||||||
|
locateDeviceOnMap: locateDeviceOnMap,
|
||||||
|
locateDeviceDirectly: locateDeviceDirectly,
|
||||||
|
getCurrentSearchedDevice: getCurrentSearchedDevice,
|
||||||
|
getMarkerState: getMarkerState,
|
||||||
|
setMarkerState: setMarkerState
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -0,0 +1,307 @@
|
|||||||
|
var WeatherForecast = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var weatherApiKey = 'Uxh4IdMuAvhSiBnsf4UUDVGF4e3YAp2B';
|
||||||
|
var weatherEnabled = false;
|
||||||
|
var weatherData = null;
|
||||||
|
var currentForecastIndex = 0;
|
||||||
|
var currentWeatherDevice = null;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// 天气预报模块初始化完成
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWeatherForecast() {
|
||||||
|
var role = window.userRole || 'USER';
|
||||||
|
if (role !== 'SUPER_ADMIN') {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('您没有权限使用此功能');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enableSwitch = document.getElementById('enableWeatherSwitch');
|
||||||
|
weatherEnabled = enableSwitch ? enableSwitch.checked : false;
|
||||||
|
|
||||||
|
if (weatherEnabled) {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg(
|
||||||
|
'搜索设备或点击地图设备图标即可自动查询天气预测',
|
||||||
|
{time: 3000, area: ['300px', '80px']}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('天气预测功能已关闭');
|
||||||
|
}
|
||||||
|
closeWeatherCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
var menu = document.getElementById('mapFunctionsMenu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWeatherForecast(deviceInfo) {
|
||||||
|
currentWeatherDevice = deviceInfo;
|
||||||
|
weatherData = null;
|
||||||
|
|
||||||
|
var role = window.userRole || 'USER';
|
||||||
|
if (role !== 'SUPER_ADMIN') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!weatherEnabled) {
|
||||||
|
if (window.layer && typeof window.layer.msg === 'function') {
|
||||||
|
window.layer.msg('天气预测功能未启用', {time: 2000});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceIdElement = document.getElementById('weatherDeviceId');
|
||||||
|
var deviceCoordsElement = document.getElementById('weatherDeviceCoords');
|
||||||
|
|
||||||
|
if (deviceIdElement) {
|
||||||
|
deviceIdElement.textContent = '设备: ' + deviceInfo.deviceid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceCoordsElement) {
|
||||||
|
deviceCoordsElement.textContent =
|
||||||
|
'坐标: ' + deviceInfo.latitude.toFixed(4) + ', ' + deviceInfo.longitude.toFixed(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
var weatherCard = document.getElementById('weatherForecastCard');
|
||||||
|
if (weatherCard) {
|
||||||
|
weatherCard.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentElement = document.getElementById('weatherForecastContent');
|
||||||
|
if (contentElement) {
|
||||||
|
contentElement.innerHTML =
|
||||||
|
'<div class="weather-loading">' +
|
||||||
|
'<i class="layui-icon layui-icon-loading layui-icon-anim-rotate"></i>' +
|
||||||
|
'<p>请确保网络可访问 Windy API 服务</p>' +
|
||||||
|
'<p>正在获取天气预测数据...</p>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevBtn = document.getElementById('prevForecast');
|
||||||
|
var nextBtn = document.getElementById('nextForecast');
|
||||||
|
var timeDisplay = document.getElementById('forecastTimeDisplay');
|
||||||
|
|
||||||
|
if (prevBtn) prevBtn.disabled = true;
|
||||||
|
if (nextBtn) nextBtn.disabled = true;
|
||||||
|
if (timeDisplay) timeDisplay.textContent = '--:--';
|
||||||
|
|
||||||
|
fetchWeatherData(deviceInfo.latitude, deviceInfo.longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWeatherCard() {
|
||||||
|
var weatherCard = document.getElementById('weatherForecastCard');
|
||||||
|
if (weatherCard) {
|
||||||
|
weatherCard.style.display = 'none';
|
||||||
|
}
|
||||||
|
currentWeatherDevice = null;
|
||||||
|
weatherData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPrevForecast() {
|
||||||
|
if (!weatherData || currentForecastIndex <= 0) return;
|
||||||
|
|
||||||
|
currentForecastIndex--;
|
||||||
|
displayCurrentForecast();
|
||||||
|
updateForecastNavigation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNextForecast() {
|
||||||
|
if (!weatherData || !weatherData.ts || currentForecastIndex >= weatherData.ts.length - 1) return;
|
||||||
|
|
||||||
|
currentForecastIndex++;
|
||||||
|
displayCurrentForecast();
|
||||||
|
updateForecastNavigation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateForecastNavigation() {
|
||||||
|
if (!weatherData || !weatherData.ts) return;
|
||||||
|
|
||||||
|
var prevBtn = document.getElementById('prevForecast');
|
||||||
|
var nextBtn = document.getElementById('nextForecast');
|
||||||
|
var timeDisplay = document.getElementById('forecastTimeDisplay');
|
||||||
|
|
||||||
|
if (prevBtn) prevBtn.disabled = (currentForecastIndex <= 0);
|
||||||
|
if (nextBtn) nextBtn.disabled = (currentForecastIndex >= weatherData.ts.length - 1);
|
||||||
|
|
||||||
|
if (timeDisplay) {
|
||||||
|
var timestamp = weatherData.ts[currentForecastIndex];
|
||||||
|
var date = new Date(timestamp);
|
||||||
|
timeDisplay.textContent = formatDateTime(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchWeatherData(lat, lon) {
|
||||||
|
var requestBody = {
|
||||||
|
"lat": parseFloat(lat.toFixed(2)),
|
||||||
|
"lon": parseFloat(lon.toFixed(2)),
|
||||||
|
"model": "gfs",
|
||||||
|
"parameters": ["temp", "wind", "precip", "pressure", "rh", "windGust"],
|
||||||
|
"levels": ["surface"],
|
||||||
|
"key": weatherApiKey,
|
||||||
|
"hours": 72
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('https://api.windy.com/api/point-forecast/v2', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('网络响应状态: ' + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
weatherData = data;
|
||||||
|
|
||||||
|
var currentTime = new Date().getTime();
|
||||||
|
var closestIndex = 0;
|
||||||
|
var futureIndex = -1;
|
||||||
|
|
||||||
|
if (weatherData.ts && weatherData.ts.length > 0) {
|
||||||
|
for (var i = 0; i < weatherData.ts.length; i++) {
|
||||||
|
if (weatherData.ts[i] > currentTime) {
|
||||||
|
futureIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (futureIndex >= 0) {
|
||||||
|
closestIndex = futureIndex;
|
||||||
|
} else {
|
||||||
|
var smallestDiff = Number.MAX_VALUE;
|
||||||
|
for (var i = 0; i < weatherData.ts.length; i++) {
|
||||||
|
var diff = Math.abs(weatherData.ts[i] - currentTime);
|
||||||
|
if (diff < smallestDiff) {
|
||||||
|
smallestDiff = diff;
|
||||||
|
closestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentForecastIndex = closestIndex;
|
||||||
|
displayCurrentForecast();
|
||||||
|
updateForecastNavigation();
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('天气数据获取失败:', error);
|
||||||
|
displayWeatherError('获取天气数据失败: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayCurrentForecast() {
|
||||||
|
if (!weatherData || !weatherData.ts || weatherData.ts.length === 0) {
|
||||||
|
displayWeatherError('无可用的天气预测数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = currentForecastIndex;
|
||||||
|
var forecastHtml = '<div class="weather-forecast-item"><div class="weather-param-grid">';
|
||||||
|
|
||||||
|
if (weatherData['temp-surface'] && weatherData['temp-surface'][i] !== null) {
|
||||||
|
var temp = (weatherData['temp-surface'][i] - 273.15).toFixed(1);
|
||||||
|
forecastHtml += createWeatherParam('温度', temp + '°C');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weatherData['wind_u-surface'] && weatherData['wind_v-surface'] &&
|
||||||
|
weatherData['wind_u-surface'][i] !== null && weatherData['wind_v-surface'][i] !== null) {
|
||||||
|
var windU = weatherData['wind_u-surface'][i];
|
||||||
|
var windV = weatherData['wind_v-surface'][i];
|
||||||
|
var windSpeed = Math.sqrt(windU * windU + windV * windV).toFixed(1);
|
||||||
|
var windDir = getWindDirection(windU, windV);
|
||||||
|
forecastHtml += createWeatherParam('风速', windSpeed + ' m/s');
|
||||||
|
forecastHtml += createWeatherParam('风向', windDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weatherData['past3hprecip-surface'] && weatherData['past3hprecip-surface'][i] !== null) {
|
||||||
|
var precip = weatherData['past3hprecip-surface'][i].toFixed(1);
|
||||||
|
forecastHtml += createWeatherParam('降水', precip + ' mm');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weatherData['rh-surface'] && weatherData['rh-surface'][i] !== null) {
|
||||||
|
var humidity = weatherData['rh-surface'][i].toFixed(0);
|
||||||
|
forecastHtml += createWeatherParam('湿度', humidity + '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weatherData['pressure-surface'] && weatherData['pressure-surface'][i] !== null) {
|
||||||
|
var pressure = (weatherData['pressure-surface'][i] / 100).toFixed(0);
|
||||||
|
forecastHtml += createWeatherParam('气压', pressure + ' hPa');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weatherData['gust-surface'] && weatherData['gust-surface'][i] !== null) {
|
||||||
|
var gust = weatherData['gust-surface'][i].toFixed(1);
|
||||||
|
forecastHtml += createWeatherParam('阵风', gust + ' m/s');
|
||||||
|
}
|
||||||
|
|
||||||
|
forecastHtml += '</div></div>';
|
||||||
|
|
||||||
|
var contentElement = document.getElementById('weatherForecastContent');
|
||||||
|
if (contentElement) {
|
||||||
|
contentElement.innerHTML = forecastHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayWeatherError(message) {
|
||||||
|
var contentElement = document.getElementById('weatherForecastContent');
|
||||||
|
if (contentElement) {
|
||||||
|
contentElement.innerHTML =
|
||||||
|
'<div class="weather-error">' +
|
||||||
|
'<i class="layui-icon layui-icon-close"></i>' +
|
||||||
|
'<p>' + message + '</p>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWeatherParam(label, value) {
|
||||||
|
return '<div class="weather-param">' +
|
||||||
|
'<span class="weather-param-label">' + label + '</span>' +
|
||||||
|
'<span class="weather-param-value">' + value + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(date) {
|
||||||
|
var month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
var day = date.getDate().toString().padStart(2, '0');
|
||||||
|
var hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
var minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return month + '-' + day + ' ' + hours + ':' + minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWindDirection(u, v) {
|
||||||
|
var angle = Math.atan2(-u, -v) * 180 / Math.PI;
|
||||||
|
angle = (angle + 360) % 360;
|
||||||
|
|
||||||
|
var directions = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
|
||||||
|
var index = Math.round(angle / 45) % 8;
|
||||||
|
return directions[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabled() {
|
||||||
|
return weatherEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
toggleWeatherForecast: toggleWeatherForecast,
|
||||||
|
showWeatherForecast: showWeatherForecast,
|
||||||
|
closeWeatherCard: closeWeatherCard,
|
||||||
|
showPrevForecast: showPrevForecast,
|
||||||
|
showNextForecast: showNextForecast,
|
||||||
|
isEnabled: isEnabled
|
||||||
|
};
|
||||||
|
})();
|
||||||
File diff suppressed because it is too large
Load Diff
@ -144,6 +144,7 @@
|
|||||||
{field: 'group_id', title: '基本参数组', width: 60, sort: true},
|
{field: 'group_id', title: '基本参数组', width: 60, sort: true},
|
||||||
{field: 'calc_group_id', title: '解算参数组', width: 60, sort: true},
|
{field: 'calc_group_id', title: '解算参数组', width: 60, sort: true},
|
||||||
{field: 'parentid', title: '基站编号', width: 80, sort: true},
|
{field: 'parentid', title: '基站编号', width: 80, sort: true},
|
||||||
|
{field: 'parentid1', title: '备用基站', width: 80, sort: true},
|
||||||
{field: 'tenantname', title: '所属组织', width: 120},
|
{field: 'tenantname', title: '所属组织', width: 120},
|
||||||
{field: 'fwd_group_id', title: '推送组', width: 80},
|
{field: 'fwd_group_id', title: '推送组', width: 80},
|
||||||
{field: 'fwd_group_id2', title: '推送2', width: 80},
|
{field: 'fwd_group_id2', title: '推送2', width: 80},
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<label class="layui-form-label">设备号</label>
|
<label class="layui-form-label">设备号</label>
|
||||||
<div class="layui-input-inline">
|
<div class="layui-input-inline">
|
||||||
<input type="text" name="s_deviceid" autocomplete="off" class="layui-input">
|
<input type="text" name="s_deviceid" id="deviceid" autocomplete="off" class="layui-input">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-search-btn"><i class="layui-icon"></i> 搜 索</button>
|
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-search-btn"><i class="layui-icon"></i> 搜 索</button>
|
||||||
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-export-btn"><i class="layui-icon"></i>导出</button>
|
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-export-btn"><i class="layui-icon"></i>导出</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,17 +52,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<table class="layui-hide" id="currentTableId" lay-filter="currentTableFilter"></table>
|
<div class="layui-tab layui-tab-card" lay-filter="data-tab">
|
||||||
|
<ul class="layui-tab-title">
|
||||||
|
<li class="layui-this">数据表格</li>
|
||||||
|
<li>姿态曲线</li>
|
||||||
|
</ul>
|
||||||
|
<div class="layui-tab-content">
|
||||||
|
<div class="layui-tab-item layui-show">
|
||||||
|
<table class="layui-hide" id="currentTableId" lay-filter="currentTableFilter"></table>
|
||||||
|
</div>
|
||||||
|
<div class="layui-tab-item">
|
||||||
|
<div id="echarts-attitude" style="min-height:500px;padding: 10px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="../js/lay-module/echarts/echartsTheme.js" charset="utf-8"></script>
|
||||||
|
<script src="../js/lay-module/echarts/echarts.js" charset="utf-8"></script>
|
||||||
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
layui.use(['form', 'table', 'laydate'], function () {
|
layui.use(['form', 'table', 'laydate', 'element'], function () {
|
||||||
var $ = layui.$;
|
var $ = layui.$;
|
||||||
var form = layui.form,
|
var form = layui.form,
|
||||||
table = layui.table,
|
table = layui.table,
|
||||||
laydate = layui.laydate;
|
laydate = layui.laydate,
|
||||||
|
element = layui.element;
|
||||||
|
|
||||||
|
var echartsAttitude = echarts.init(document.getElementById('echarts-attitude'), 'walden');
|
||||||
|
var chartDataLoaded = false;
|
||||||
|
|
||||||
var cfg_cols = [
|
var cfg_cols = [
|
||||||
{field: 'deviceid', title: '设备号', width: 100},
|
{field: 'deviceid', title: '设备号', width: 100},
|
||||||
@ -78,9 +97,7 @@
|
|||||||
{field: 'temperature', title: '温度(°C)'},
|
{field: 'temperature', title: '温度(°C)'},
|
||||||
{field: 'humidity', title: '湿度(%)'}
|
{field: 'humidity', title: '湿度(%)'}
|
||||||
];
|
];
|
||||||
/**
|
|
||||||
* 初始化表单,要加上,不然刷新部分组件可能会不加载
|
|
||||||
*/
|
|
||||||
form.render();
|
form.render();
|
||||||
|
|
||||||
laydate.render({
|
laydate.render({
|
||||||
@ -95,7 +112,7 @@
|
|||||||
table.render({
|
table.render({
|
||||||
elem: '#currentTableId',
|
elem: '#currentTableId',
|
||||||
url: '/gnss/msg/status/list',
|
url: '/gnss/msg/status/list',
|
||||||
toolbar: '#toolbarDemo',//开启头部工具栏
|
toolbar: '#toolbarDemo',
|
||||||
defaultToolbar: ['filter'],
|
defaultToolbar: ['filter'],
|
||||||
cols: [
|
cols: [
|
||||||
cfg_cols
|
cfg_cols
|
||||||
@ -106,11 +123,9 @@
|
|||||||
skin: 'line'
|
skin: 'line'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听搜索操作
|
|
||||||
form.on('submit(data-search-btn)', function (data) {
|
form.on('submit(data-search-btn)', function (data) {
|
||||||
var result = JSON.stringify(data.field);
|
var result = JSON.stringify(data.field);
|
||||||
|
|
||||||
//执行搜索重载
|
|
||||||
table.reload('currentTableId', {
|
table.reload('currentTableId', {
|
||||||
page: {
|
page: {
|
||||||
curr: 1
|
curr: 1
|
||||||
@ -123,13 +138,179 @@
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听导出操作
|
|
||||||
form.on('submit(data-export-btn)', function (data) {
|
form.on('submit(data-export-btn)', function (data) {
|
||||||
var result = $('#searchFrm').serialize();
|
var result = $('#searchFrm').serialize();
|
||||||
var u = "/gnss/msg/status/export?" + result;
|
var u = "/gnss/msg/status/export?" + result;
|
||||||
window.open(u, "_blank");
|
window.open(u, "_blank");
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
element.on('tab(data-tab)', function(data){
|
||||||
|
if (data.index == 1) {
|
||||||
|
echartsAttitude.resize();
|
||||||
|
|
||||||
|
if (!chartDataLoaded) {
|
||||||
|
var deviceId = $('#deviceid').val();
|
||||||
|
if (deviceId) {
|
||||||
|
loadChartData(deviceId);
|
||||||
|
} else {
|
||||||
|
layer.msg('请先在搜索框中输入设备号');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadChartData(deviceId) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/gnss/msg/status/device_data',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
deviceId: deviceId
|
||||||
|
},
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
showChart(res.data, res.originalData);
|
||||||
|
chartDataLoaded = true;
|
||||||
|
} else {
|
||||||
|
layer.msg('获取数据失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
layer.msg('获取数据失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showChart(smoothedData, originalData) {
|
||||||
|
var t = [];
|
||||||
|
var roll = [];
|
||||||
|
var pitch = [];
|
||||||
|
var yaw = [];
|
||||||
|
var smoothRoll = [];
|
||||||
|
var smoothPitch = [];
|
||||||
|
var smoothYaw = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < originalData.length; i++) {
|
||||||
|
t.push(originalData[i].createtime);
|
||||||
|
roll.push(originalData[i].roll);
|
||||||
|
pitch.push(originalData[i].pitch);
|
||||||
|
yaw.push(originalData[i].yaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < smoothedData.length; i++) {
|
||||||
|
smoothRoll.push(smoothedData[i].roll);
|
||||||
|
smoothPitch.push(smoothedData[i].pitch);
|
||||||
|
smoothYaw.push(smoothedData[i].yaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
var option = {
|
||||||
|
title: {
|
||||||
|
text: '姿态数据曲线'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
label: {
|
||||||
|
backgroundColor: '#6a7985'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['Roll', 'Pitch', 'Yaw', '平滑Roll', '平滑Pitch', '平滑Yaw']
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: t,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: function(value) {
|
||||||
|
var date = new Date(value);
|
||||||
|
return date.getFullYear() + '-' +
|
||||||
|
(date.getMonth() + 1).toString().padStart(2, '0') + '-' +
|
||||||
|
date.getDate().toString().padStart(2, '0') + ' ' +
|
||||||
|
date.getHours().toString().padStart(2, '0') + ':' +
|
||||||
|
date.getMinutes().toString().padStart(2, '0') + ':' +
|
||||||
|
date.getSeconds().toString().padStart(2, '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Roll',
|
||||||
|
type: 'line',
|
||||||
|
data: roll,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pitch',
|
||||||
|
type: 'line',
|
||||||
|
data: pitch,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yaw',
|
||||||
|
type: 'line',
|
||||||
|
data: yaw,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '平滑Roll',
|
||||||
|
type: 'line',
|
||||||
|
data: smoothRoll,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '平滑Pitch',
|
||||||
|
type: 'line',
|
||||||
|
data: smoothPitch,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '平滑Yaw',
|
||||||
|
type: 'line',
|
||||||
|
data: smoothYaw,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
echartsAttitude.setOption(option);
|
||||||
|
|
||||||
|
window.onresize = function() {
|
||||||
|
echartsAttitude.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -89,6 +89,12 @@
|
|||||||
<input type="number" name="parentid" id="parentid" th:field="*{parentid}" placeholder="请输入关联基准站编号" value="" class="layui-input">
|
<input type="number" name="parentid" id="parentid" th:field="*{parentid}" placeholder="请输入关联基准站编号" value="" class="layui-input">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="layui-inline" >
|
||||||
|
<label class="layui-form-label">备选基站</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="number" name="parentid1" id="parentid1" th:field="*{parentid1}" placeholder="请输入备选基准站编号" value="" class="layui-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layui-form-item" id="ecef_div">
|
<div class="layui-form-item" id="ecef_div">
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
@ -320,7 +326,6 @@
|
|||||||
}
|
}
|
||||||
layui.form.render();
|
layui.form.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -3,8 +3,12 @@ package com.imdroid.ntripproxy.service;
|
|||||||
public class Ntrip2Channels {
|
public class Ntrip2Channels {
|
||||||
final private String localHost="127.0.0.1";
|
final private String localHost="127.0.0.1";
|
||||||
final private int localPort=9903;
|
final private int localPort=9903;
|
||||||
final private String remoteHost="47.107.50.52";
|
// 将远程主机改为本地,端口改为12000
|
||||||
final private int remotePort=9903;
|
final private String remoteHost="127.0.0.1";
|
||||||
|
//final private String remoteHost="100.91.37.6";
|
||||||
|
//final private String remoteHost="47.107.50.52";
|
||||||
|
//final private String remoteHost="8.134.185.53";
|
||||||
|
final private int remotePort=12000;
|
||||||
|
|
||||||
public static final Ntrip2Channels INSTANCE = new Ntrip2Channels();
|
public static final Ntrip2Channels INSTANCE = new Ntrip2Channels();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,185 @@
|
|||||||
|
package com.imdroid.ntripproxy.service;
|
||||||
|
|
||||||
|
import com.imdroid.common.util.DataTypeUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RTCM数据过滤服务器
|
||||||
|
* 监听UDP 12000端口,接收RTCM数据
|
||||||
|
* 开启TCP 12002端口,转发指定deviceId的RTCM数据
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RtcmFilterServer implements ApplicationRunner {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RtcmFilterServer.class);
|
||||||
|
|
||||||
|
// 配置参数
|
||||||
|
private static final int UDP_PORT = 12000;
|
||||||
|
private static final int TCP_PORT = 12002;
|
||||||
|
private static final String TARGET_DEVICE_ID = "3530795";
|
||||||
|
private static final int BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
// TCP客户端连接列表
|
||||||
|
private final List<SocketChannel> tcpClients = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
// 线程池
|
||||||
|
private final ExecutorService executorService = Executors.newFixedThreadPool(2);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
// 启动UDP监听服务
|
||||||
|
executorService.submit(this::startUdpServer);
|
||||||
|
|
||||||
|
// 启动TCP服务器
|
||||||
|
executorService.submit(this::startTcpServer);
|
||||||
|
|
||||||
|
logger.info("RTCM过滤服务已启动 - UDP监听端口:{}, TCP服务端口:{}, 目标设备ID:{}",
|
||||||
|
UDP_PORT, TCP_PORT, TARGET_DEVICE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动UDP服务器监听12000端口
|
||||||
|
*/
|
||||||
|
private void startUdpServer() {
|
||||||
|
try (DatagramSocket socket = new DatagramSocket(UDP_PORT)) {
|
||||||
|
logger.info("UDP服务已启动,监听端口: {}", UDP_PORT);
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||||
|
socket.receive(packet);
|
||||||
|
|
||||||
|
// 解析数据包
|
||||||
|
byte[] data = new byte[packet.getLength()];
|
||||||
|
System.arraycopy(packet.getData(), packet.getOffset(), data, 0, packet.getLength());
|
||||||
|
|
||||||
|
// 提取设备ID
|
||||||
|
String deviceId = extractDeviceId(data);
|
||||||
|
|
||||||
|
// 如果是目标设备ID,则转发到TCP客户端
|
||||||
|
if (TARGET_DEVICE_ID.equals(deviceId)) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("接收到目标设备 {} 的RTCM数据,长度: {}", deviceId, data.length);
|
||||||
|
}
|
||||||
|
forwardToTcpClients(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("UDP服务异常: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动TCP服务器监听12002端口
|
||||||
|
*/
|
||||||
|
private void startTcpServer() {
|
||||||
|
try {
|
||||||
|
Selector selector = Selector.open();
|
||||||
|
ServerSocketChannel serverChannel = ServerSocketChannel.open();
|
||||||
|
serverChannel.configureBlocking(false);
|
||||||
|
serverChannel.socket().bind(new InetSocketAddress(TCP_PORT));
|
||||||
|
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||||
|
|
||||||
|
logger.info("TCP服务已启动,监听端口: {}", TCP_PORT);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
selector.select();
|
||||||
|
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
|
||||||
|
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
SelectionKey key = keys.next();
|
||||||
|
keys.remove();
|
||||||
|
|
||||||
|
if (!key.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.isAcceptable()) {
|
||||||
|
acceptConnection(selector, serverChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("TCP服务异常: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接受新的TCP客户端连接
|
||||||
|
*/
|
||||||
|
private void acceptConnection(Selector selector, ServerSocketChannel serverChannel) throws IOException {
|
||||||
|
SocketChannel clientChannel = serverChannel.accept();
|
||||||
|
clientChannel.configureBlocking(false);
|
||||||
|
clientChannel.register(selector, SelectionKey.OP_READ);
|
||||||
|
|
||||||
|
tcpClients.add(clientChannel);
|
||||||
|
logger.info("新的TCP客户端已连接: {}", clientChannel.getRemoteAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据包中提取设备ID
|
||||||
|
*/
|
||||||
|
private String extractDeviceId(byte[] data) {
|
||||||
|
// 按照NtripMessage格式解析,设备ID从第4字节开始,4字节无符号整型
|
||||||
|
if (data.length >= 8) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(data, 4, 4);
|
||||||
|
long deviceId = buffer.getInt() & 0xFFFFFFFFL; // 转为无符号整型
|
||||||
|
return String.valueOf(deviceId);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据转发到所有TCP客户端
|
||||||
|
*/
|
||||||
|
private void forwardToTcpClients(byte[] data) {
|
||||||
|
if (tcpClients.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SocketChannel> disconnectedClients = new ArrayList<>();
|
||||||
|
|
||||||
|
for (SocketChannel client : tcpClients) {
|
||||||
|
try {
|
||||||
|
if (client.isOpen()) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||||
|
client.write(buffer);
|
||||||
|
} else {
|
||||||
|
disconnectedClients.add(client);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("向TCP客户端发送数据失败: {}", e.getMessage());
|
||||||
|
disconnectedClients.add(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除断开连接的客户端
|
||||||
|
for (SocketChannel client : disconnectedClients) {
|
||||||
|
tcpClients.remove(client);
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
logger.info("已关闭断开连接的TCP客户端");
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("关闭TCP客户端异常: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user