Compare commits

..

38 Commits

Author SHA1 Message Date
4a4def045a feat: rtksrv v2 test 2025-07-28 18:49:53 +08:00
0698640b26 feat: rtksrv v1 test 2025-07-28 18:39:21 +08:00
1ddfb78c41 Revert "feat: rtksrv v1 test"
This reverts commit 2d159953842abe29b9a0b228f5d18dc4189c27b9.
2025-07-28 18:26:27 +08:00
2d15995384 feat: rtksrv v1 test 2025-07-28 18:22:21 +08:00
weidong
e852b7c640 1、gxjs补推送断点 2025-07-27 18:50:47 +08:00
weidong
15b4cbbe3e 1、xyz显示到小数点后3位 2025-07-25 11:14:29 +08:00
weidong
032ab5c46b 1、增加手工添加补传记录 2025-07-23 17:07:53 +08:00
1875b5e848 feat: 新增多设备搜索 2025-07-22 18:56:29 +08:00
897507b97b fix: 调整D300转发间隔时间为 5s 2025-07-21 16:21:22 +08:00
weidong
40cee949a9 1、修改TCP的重连机制,只有在发送数据时才connect 2025-07-20 23:40:56 +08:00
weidong
f68d548a2c 1、修改TCP的重连机制,只有在发送数据时才connect 2025-07-20 22:53:32 +08:00
weidong
2fd12b9361 1、修改TCP的重连机制,只有在发送数据时才connect 2025-07-20 21:57:45 +08:00
weidong
e286c355ff 1、增加log查问题 2025-07-20 21:26:41 +08:00
weidong
cc5f3d0772 1、不在整点发 2025-07-20 20:12:26 +08:00
weidong
1e9e4b8680 1、不在整点发 2025-07-20 19:06:06 +08:00
weidong
6d0aa977fa 1、广西交设暂停推送历史数据 2025-07-20 15:37:25 +08:00
weidong
1045e33d1a 1、广西交设暂停推送历史数据 2025-07-20 15:37:02 +08:00
weidong
6d170eaed0 1、如果一个项目的设备数大于50,则拆包发送
2、广西交设、葛洲坝、广西路建继承同一个新发展类,基类不加Component注解
3、如果是对端强制中断TCP连接,则10秒后重连
2025-07-19 14:23:34 +08:00
weidong
bedb2e3ef5 1、增加TCP连接过程打印,以便查看连接失败原因 2025-07-19 10:46:43 +08:00
weidong
0acd7d31b8 1、增加TCP连接过程打印,以便查看连接失败原因 2025-07-19 10:27:51 +08:00
weidong
063716d539 1、增加TCP连接过程打印,以便查看连接失败原因 2025-07-19 10:01:50 +08:00
weidong
1996ef79d4 1、增加TCP连接过程打印,以便查看连接失败原因 2025-07-19 09:50:34 +08:00
weidong
87d6f6073d 1、增加TCP连接过程打印,以便查看连接失败原因 2025-07-19 09:49:26 +08:00
weidong
cef333b56b 1、取消周跳自动停止推送 2025-07-15 09:44:44 +08:00
weidong
21a6d2a8df 1、有设备长时间无d3f0但是有数据上来,增加device的统计 2025-07-14 09:24:48 +08:00
weidong
db9d27b3a0 1、接续数据增加接其他编号设备数据的功能,输入:id,datetime,如果不输入id,就是接本机数据
2、恢复周跳停止推送功能
2025-07-11 10:08:27 +08:00
weidong
d250bfc671 1、增加接续数据功能 2025-07-09 13:18:21 +08:00
7555c4c72b fix: 天气预测接口更替为开源的 Open-Meteo
- 默认情况下,将结合最适合的天气模型(auto)
- 预报 3天长度
2025-07-08 12:15:54 +08:00
d8456d165b fix: 修正地图中心计算逻辑,修正用户定位 2025-07-07 18:29:04 +08:00
fengyarnom
983efb2d6b feat: 页面优化、手机端适配、新增谷歌地图和测距功能
- 新增姿态平滑功能
- 优化页面显示效果
- 新增测距和比例尺功能
- 新增谷歌地图集成
- 适配手机端显示

# Conflicts:
#	sec-beidou/src/main/resources/templates/page/device_overview.html
2025-07-07 16:32:01 +08:00
1191c17c35 fix: 双通道 2025-07-04 09:41:08 +08:00
weidong
f9b85cd2d5 1、bug fix:进入变周期模式的设备会一直持续到模式结束,不会因为修改禁止变周期而结束 2025-07-02 22:51:49 +08:00
weidong
a6f711f18d 1、取消临时推送历史数据 2025-07-01 13:00:51 +08:00
weidong
e6ad5a0a3d 1、取消临时推送历史数据 2025-07-01 11:42:44 +08:00
weidong
ae28807818 1、取消临时推送历史数据 2025-06-30 23:26:21 +08:00
weidong
29af023a4f 1、临时推送历史数据 2025-06-30 23:07:11 +08:00
weidong
ee5bae1174 Merge remote-tracking branch 'origin/master' 2025-06-30 14:49:18 +08:00
weidong
893fe1ea78 1、把超过滤波窗口2/3的时间稳定改为1/2 2025-06-30 14:48:59 +08:00
39 changed files with 1347 additions and 1584 deletions

View File

@ -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,@RequestParam(name = "oldParentId1") String oldParentId1); HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId);
@PostMapping("/group_param_changed") @PostMapping("/group_param_changed")
HttpResp groupParamChanged(); HttpResp groupParamChanged();

View File

@ -81,8 +81,6 @@ 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");

View File

@ -19,7 +19,8 @@ public class OpLogManager {
public final static Short OP_OBJ_GROUP = 6; public final static Short OP_OBJ_GROUP = 6;
public final static Short OP_OBJ_WARNING = 7; public final static Short OP_OBJ_WARNING = 7;
public final static Short OP_OBJ_SYS = 8; public final static Short OP_OBJ_SYS = 8;
public final static Short OP_OBJ_DEVICE_MSG = 9; public final static Short OP_OBJ_FWD_RECORD = 9;
@Autowired @Autowired
OpLogMapper opLogMapper; OpLogMapper opLogMapper;

View File

@ -1,6 +1,5 @@
package com.imdroid.beidou_fwd.service; package com.imdroid.beidou_fwd.service;
import com.imdroid.common.util.ThreadManager;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
@ -13,7 +12,6 @@ import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class TCPClient { public class TCPClient {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
@ -29,7 +27,7 @@ public class TCPClient {
TCPListener listener; TCPListener listener;
public void start() { public void start() {
new Thread(this::connect, "forwarder tcp-client").start(); //new Thread(this::connect, host+":"+port+" forwarder tcp-client").start();
} }
public void init(String dest_addr, int dest_port, TCPListener listener) { public void init(String dest_addr, int dest_port, TCPListener listener) {
@ -54,7 +52,7 @@ public class TCPClient {
} }
public void connect() { public void connect() {
logger.info("netty client starting"); logger.info("{}:{} tcp connecting...",host,port);
//启动客户端去连接服务器端 //启动客户端去连接服务器端
try { try {
ChannelFuture cf = bootstrap.connect(host, port); ChannelFuture cf = bootstrap.connect(host, port);
@ -62,17 +60,22 @@ public class TCPClient {
@Override @Override
public void operationComplete(ChannelFuture future) throws Exception { public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) { if (!future.isSuccess()) {
logger.info("{}:{} tcp connect failed. {}",host,port,future.cause().toString());
//重连交给后端线程执行 //重连交给后端线程执行
future.channel().eventLoop().schedule(() -> { /*future.channel().eventLoop().schedule(() -> {
logger.info("tcp client reconnect"); logger.info("{}:{} tcp client reconnect",host,port);
try { try {
connect(); connect();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
}, 5000, TimeUnit.MILLISECONDS); }, 5000, TimeUnit.MILLISECONDS);*/
} else { } else {
logger.info("tcp client start success!"); /*future.channel().config().setWriteBufferWaterMark(new WriteBufferWaterMark(
1024 * 1024, // low
4 *1024*1024 // high
));*/
logger.info("{}:{} tcp client start success!",host,port);
} }
} }
}); });
@ -80,18 +83,35 @@ public class TCPClient {
this.channel = cf.channel(); this.channel = cf.channel();
this.channel.closeFuture().sync(); this.channel.closeFuture().sync();
} catch (Exception e) { } catch (Exception e) {
logger.error("netty client error:", e); logger.error(host+":"+port+" tcp connect error:", e);
} }
} }
boolean tryReconnect() throws Exception{
new Thread(this::connect, host+":"+port+" forwarder tcp-client").start();
for(int i=0; i<20; i++){
Thread.sleep(50);
if(channel!=null && channel.isActive()) return true;
}
return false;
}
public void writeAndFlush(String json) { public void writeAndFlush(String json) {
ByteBuf sendBuffer = Unpooled.buffer(); ByteBuf sendBuffer = Unpooled.buffer();
sendBuffer.writeBytes(json.getBytes(StandardCharsets.UTF_8)); sendBuffer.writeBytes(json.getBytes(StandardCharsets.UTF_8));
//logger.info("send to {}: {}",host,json);
if(channel==null || !channel.isActive()){
try {
if(!tryReconnect()) return;
} catch (Exception e) {
logger.error(e.toString());
}
}
channel.writeAndFlush(sendBuffer).addListener(future -> { channel.writeAndFlush(sendBuffer).addListener(future -> {
if (future.isSuccess()) { if (future.isSuccess()) {
logger.info("send to tcp:"+host+" succeed."); logger.info("send to tcp:"+host+" succeed.");
} else { } else {
logger.info("send to tcp:"+host+" failed."); logger.info("send to tcp: {} failed. {}",host,future.cause().toString());
if(listener!=null){ if(listener!=null){
listener.onMessage("failed"); listener.onMessage("failed");
} }
@ -103,8 +123,8 @@ public class TCPClient {
connectTime = LocalDateTime.now(); connectTime = LocalDateTime.now();
} }
public void onDisconnect(){ public void onDisconnect(boolean isIdle){
if(connectTime.isBefore(LocalDateTime.now().minusMinutes(1))) { /*if(connectTime.isBefore(LocalDateTime.now().minusMinutes(1))) {
connect(); connect();
} }
else{ else{
@ -114,8 +134,8 @@ public class TCPClient {
} catch (Exception e) { } catch (Exception e) {
logger.error(e.toString()); logger.error(e.toString());
} }
},60, TimeUnit.SECONDS); },isIdle?30:10, TimeUnit.SECONDS);
} }*/
} }
public void onMessage(String msg){ public void onMessage(String msg){

View File

@ -40,13 +40,14 @@ public class TcpMessageHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("tcp channel inactive"); logger.info("tcp channel inactive");
tcpClient.onDisconnect(); tcpClient.onDisconnect(true);
} }
@Override @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("TcpMessageHandler error: {}", cause.toString()); logger.error("TcpMessageHandler error: {}", cause.toString());
ctx.close(); ctx.close();
tcpClient.onDisconnect(false);
} }
} }

View File

@ -37,17 +37,17 @@ public class Forwarder {
static boolean isFwdTableInit = true;//false; static boolean isFwdTableInit = true;//false;
@Autowired @Autowired
private GnssDeviceMapper deviceMapper; GnssDeviceMapper deviceMapper;
@Autowired @Autowired
private GnssCalcDataMapper gnssDataMapper; GnssCalcDataMapper gnssDataMapper;
@Autowired @Autowired
private FwdRecordMapper fwdRecordsMapper; FwdRecordMapper fwdRecordsMapper;
@Autowired @Autowired
private ResendRecordMapper resendRecordMapper; ResendRecordMapper resendRecordMapper;
@Autowired @Autowired
private GnssStatusMapper gnssStatusMapper; GnssStatusMapper gnssStatusMapper;
@Autowired @Autowired
GnssGroupFwdMapper fwdMapper; GnssGroupFwdMapper fwdMapper;

View File

@ -49,7 +49,7 @@ public class GXJKForwarder extends Forwarder {
/** /**
* 每半小时转发GNSS解算结果 * 每半小时转发GNSS解算结果
*/ */
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.debug("gxjk forwardGnss"); logger.debug("gxjk forwardGnss");
if(mqttClient.isConnected()) forwardCurrentGnss(); if(mqttClient.isConnected()) forwardCurrentGnss();

View File

@ -0,0 +1,75 @@
package com.imdroid.beidou_fwd.task;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.imdroid.beidou_fwd.service.TCPClient;
import com.imdroid.secapi.dto.ResendRecord;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.List;
@Component
@Configuration
@EnableScheduling
public class GXJSForwarder extends GXXfzForwarder{
private final String FORWARDER_NAME = "广西新发展";
@Value("${xfz.server.host}")
private String host;
@Value("${xfz.server.port}")
private int port;
private boolean enabled=true;
@PostConstruct
void registerMe(){
init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30);
xfzTcpClient = new TCPClient();
xfzTcpClient.init(host, port,listener);
if(!enabled) return;
xfzTcpClient.start();
}
/**
* 每半小时转发GNSS解算结果
*/
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() {
if(!enabled) return;
logger.debug("gxjs forwardGnss");
forwardCurrentGnss();
}
@Override
void forwardHistoryGnss(){
// 1.从转发记录表里检索待补传记录时间表含设备Id时间段
QueryWrapper<ResendRecord> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("fwd_group_id",fwdGroupId);
queryWrapper.eq("state",ResendRecord.STATE_BREAK_POINT);
queryWrapper.ge("createtime", LocalDateTime.now().minusDays(30));
List<ResendRecord> resendRecordsList = resendRecordMapper.selectList(queryWrapper);
if(resendRecordsList!=null && resendRecordsList.size()>0){
//修改状态
UpdateWrapper<ResendRecord> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("fwd_group_id",fwdGroupId);
updateWrapper.eq("state",ResendRecord.STATE_BREAK_POINT);
updateWrapper.ge("createtime", LocalDateTime.now().minusDays(30));
updateWrapper.set("state",ResendRecord.STATE_FWDING);
int updateNum = resendRecordMapper.update(null, updateWrapper);
logger.debug("{} forward history records: {}, update {}",fwdGroupId, resendRecordsList.size(),updateNum);
// 2.检索这个这个时间段的解算结果如果有数据则单个终端转发标志记录为已补传
for(ResendRecord record:resendRecordsList){
if(record.getProjectid()!=null)
logger.debug("{} forward history {}",fwdGroupId, record.getProjectid());
forwardBatchGnssRecords(record);
}
}
}
}

View File

@ -4,6 +4,7 @@ import com.imdroid.beidou_fwd.service.TCPClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -11,20 +12,31 @@ import javax.annotation.PostConstruct;
@Component @Component
@Configuration @Configuration
@EnableScheduling @EnableScheduling
public class GXXfz2Forwarder extends GXXfzForwarder{ public class GXLJForwarder extends GXXfzForwarder{
private String FORWARDER_NAME = "广西路建"; private final String FORWARDER_NAME = "广西路建";
@Value("${gxlj.server.host}") @Value("${gxlj.server.host}")
private String host; private String host;
@Value("${gxlj.server.port}") @Value("${gxlj.server.port}")
private int port; private int port;
private boolean enabled=true;
@PostConstruct @PostConstruct
@Override
void registerMe(){ void registerMe(){
init(FORWARDER_NAME, "TCP "+host+":"+port,6,FWD_DEVICE_ID,30); init(FORWARDER_NAME, "TCP "+host+":"+port,6,FWD_DEVICE_ID,30);
xfzTcpClient = new TCPClient(); xfzTcpClient = new TCPClient();
xfzTcpClient.init(host, port,listener); xfzTcpClient.init(host, port,listener);
if(!enabled) return;
xfzTcpClient.start(); xfzTcpClient.start();
} }
/**
* 每半小时转发GNSS解算结果
*/
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() {
if(!enabled) return;
logger.debug("gxlj forwardGnss");
forwardCurrentGnss();
}
} }

View File

@ -6,31 +6,15 @@ import com.imdroid.beidou_fwd.service.TCPListener;
import com.imdroid.common.util.GsonUtil; import com.imdroid.common.util.GsonUtil;
import com.imdroid.common.util.NumberUtils; import com.imdroid.common.util.NumberUtils;
import com.imdroid.secapi.dto.GnssCalcData; import com.imdroid.secapi.dto.GnssCalcData;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Component
@Configuration
@EnableScheduling
public class GXXfzForwarder extends Forwarder{ public class GXXfzForwarder extends Forwarder{
private final String FORWARDER_NAME = "广西新发展";
@Value("${xfz.server.host}")
private String host;
@Value("${xfz.server.port}")
private int port;
TCPClient xfzTcpClient; TCPClient xfzTcpClient;
static class XFZTCPListener implements TCPListener{ class XFZTCPListener implements TCPListener{
public static final int STATE_NO_ACK = 0; public static final int STATE_NO_ACK = 0;
public static final int STATE_OK = 1; public static final int STATE_OK = 1;
public static final int STATE_FAILED = 2; public static final int STATE_FAILED = 2;
@ -51,36 +35,16 @@ public class GXXfzForwarder extends Forwarder{
@Override @Override
public void onMessage(String msg) { public void onMessage(String msg) {
logger.info("{} client rx: {}",fwdGroupId, msg);
if(msg.contains("succeed")) state = STATE_OK; if(msg.contains("succeed")) state = STATE_OK;
else state = STATE_FAILED; else state = STATE_FAILED;
} }
} }
XFZTCPListener listener = new XFZTCPListener(); XFZTCPListener listener = new XFZTCPListener();
@PostConstruct
void registerMe(){
init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30);
xfzTcpClient = new TCPClient();
xfzTcpClient.init(host, port,listener);
xfzTcpClient.start();
}
/**
* 每半小时转发GNSS解算结果
*/
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() {
logger.debug("xfz forwardGnss");
forwardCurrentGnss();
}
/*
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
private void checkDevice() {
//logger.debug("zny checkDevice");
checkOfflineDevice("2345053","2350106","2350124");
}*/
@Override @Override
int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime){ int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime){
int batchNum = 0;
int sendNum = 0; int sendNum = 0;
if(records.size() == 0) return 0; if(records.size() == 0) return 0;
@ -104,27 +68,48 @@ public class GXXfzForwarder extends Forwarder{
// 经纬度 // 经纬度
data.setDevLng(locationRecord.getR9250e()); data.setDevLng(locationRecord.getR9250e());
data.setDevLat(locationRecord.getR9250n()); data.setDevLat(locationRecord.getR9250n());
sendNum++; // 发送
batchNum++;
if(batchNum==20){
String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!";
//logger.debug("project {}: forwad {} gnss records to {}",projectId, dataList.size(),fwdGroupId);
//logger.debug(json);
try {
listener.clear();
xfzTcpClient.writeAndFlush(json);
//等待应答
if(checkResult()) sendNum += batchNum;
} catch (Exception e1) {
logger.error(e1.toString());
}
batchNum = 0;
dataList.clear();
}
} }
String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!";
logger.debug("project " + projectId + ": push calculation result to XFZ"); if(batchNum>0){
logger.debug(json); String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!";
try { logger.debug("project {}: forwad {} gnss records to {}",projectId, dataList.size(),fwdGroupId);
listener.clear(); logger.debug(json);
xfzTcpClient.writeAndFlush(json); try {
//等待应答 listener.clear();
if(!checkResult()) sendNum = 0; xfzTcpClient.writeAndFlush(json);
} catch (Exception e1) { //等待应答
sendNum = 0; if(checkResult()) sendNum += batchNum;
e1.printStackTrace(); } catch (Exception e1) {
logger.error(e1.toString());
}
dataList.clear();
} }
return sendNum; return sendNum;
} }
boolean checkResult() throws InterruptedException { boolean checkResult() throws InterruptedException {
// 等待应答最多等1s // 等待应答最多等500ms
for(int i=0; i<10; i++){ for(int i=0; i<10; i++){
Thread.sleep(100); Thread.sleep(50);
if(listener.state == XFZTCPListener.STATE_OK) return true; if(listener.state == XFZTCPListener.STATE_OK) return true;
else if(listener.state == XFZTCPListener.STATE_FAILED) return false; else if(listener.state == XFZTCPListener.STATE_FAILED) return false;
} }

View File

@ -4,6 +4,7 @@ import com.imdroid.beidou_fwd.service.TCPClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -12,20 +13,30 @@ import javax.annotation.PostConstruct;
@Configuration @Configuration
@EnableScheduling @EnableScheduling
public class GZBForwarder extends GXXfzForwarder{ public class GZBForwarder extends GXXfzForwarder{
private String FORWARDER_NAME = "葛洲坝"; private final String FORWARDER_NAME = "葛洲坝";
@Value("${gzb.server.host}") @Value("${gzb.server.host}")
private String host; private String host;
@Value("${gzb.server.port}") @Value("${gzb.server.port}")
private int port; private int port;
private boolean enabled=true;
@PostConstruct @PostConstruct
@Override
void registerMe(){ void registerMe(){
init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30); init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30);
xfzTcpClient = new TCPClient(); xfzTcpClient = new TCPClient();
xfzTcpClient.init(host, port,listener); xfzTcpClient.init(host, port,listener);
if(!enabled) return;
xfzTcpClient.start(); xfzTcpClient.start();
} }
/**
* 每半小时转发GNSS解算结果
*/
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() {
if(!enabled) return;
logger.debug("gzb forwardGnss");
forwardCurrentGnss();
}
} }

View File

@ -28,17 +28,18 @@ import java.util.List;
@EnableScheduling @EnableScheduling
public class GZYMQTTForwarder extends Forwarder { public class GZYMQTTForwarder extends Forwarder {
static final String FORWARDER_NAME = "贵州交勘院MQTT"; static final String FORWARDER_NAME = "贵州交勘院MQTT";
@Value("${mqtt.server.brokerUrl}") @Value("${gzymqtt.server.brokerUrl}")
private String brokerUrl; private String brokerUrl;
@Value("${mqtt.server.username}") @Value("${gzymqtt.server.username}")
private String username; private String username;
@Value("${mqtt.server.password}") @Value("${gzymqtt.server.password}")
private String password; private String password;
@Value("${mqtt.server.clientid}") @Value("${gzymqtt.server.clientid}")
private String clientid; private String clientid;
@Value("${mqtt.server.topic}") @Value("${gzymqtt.server.topic}")
private String topic; private String topic;
//@Value("${gzymqtt.server.enabled}")
private boolean enabled=true;
@Autowired @Autowired
GnssStatusMsgMapper statusMsgMapper; GnssStatusMsgMapper statusMsgMapper;
MQTTClient mqttClient; MQTTClient mqttClient;
@ -47,6 +48,7 @@ public class GZYMQTTForwarder extends Forwarder {
void registerMe() throws MqttException { void registerMe() throws MqttException {
init(FORWARDER_NAME, "MQTT "+brokerUrl,2,FWD_DEVICE_ALIAS_NAME,30); init(FORWARDER_NAME, "MQTT "+brokerUrl,2,FWD_DEVICE_ALIAS_NAME,30);
mqttClient = new MQTTClient(brokerUrl, username, password,clientid); mqttClient = new MQTTClient(brokerUrl, username, password,clientid);
if(!enabled) return;
try{ try{
mqttClient.connect(); mqttClient.connect();
} }
@ -61,6 +63,7 @@ public class GZYMQTTForwarder extends Forwarder {
@Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次 @Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
//@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次 //@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
if(!enabled) return;
logger.debug("gzy mqtt forwardGnss"); logger.debug("gzy mqtt forwardGnss");
if(mqttClient.isConnected()) { if(mqttClient.isConnected()) {
forwardCurrentGnss(); forwardCurrentGnss();
@ -75,6 +78,11 @@ public class GZYMQTTForwarder extends Forwarder {
} }
} }
} }
/*
@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次
void forwardHistoryGnss() {
super.forwardHistoryGnss();
}*/
@Override @Override
int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime) { int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime) {

View File

@ -44,11 +44,14 @@ public class GNSSCalcFilterService {
Integer xyfilterCycle = groupCalc.getFilter_hour(); Integer xyfilterCycle = groupCalc.getFilter_hour();
Integer zfilterCycle = groupCalc.getZfilter_hour(); Integer zfilterCycle = groupCalc.getZfilter_hour();
if(null == zfilterCycle) zfilterCycle = xyfilterCycle; if(null == zfilterCycle) zfilterCycle = xyfilterCycle;
VaryFilterCycle varyCycle = autoCycleDevices.get(deviceId); if(groupCalc.getAuto_filter()) {
if(varyCycle!=null){ VaryFilterCycle varyCycle = autoCycleDevices.get(deviceId);
xyfilterCycle = varyCycle.filterCycleHour; if (varyCycle != null) {
zfilterCycle = varyCycle.filterCycleHour; xyfilterCycle = varyCycle.filterCycleHour;
zfilterCycle = varyCycle.filterCycleHour;
}
} }
else autoCycleDevices.remove(deviceId);
// 平滑处理 // 平滑处理
calcFilterLocation(newRecord, referPos, xyfilterCycle, zfilterCycle, groupCalc.getFilter_min_hour(), calcFilterLocation(newRecord, referPos, xyfilterCycle, zfilterCycle, groupCalc.getFilter_min_hour(),
@ -166,7 +169,7 @@ public class GNSSCalcFilterService {
newRecord.setAuxn(NumberUtils.scaleTwo(msumN / minCount)); newRecord.setAuxn(NumberUtils.scaleTwo(msumN / minCount));
newRecord.setAuxd(NumberUtils.scaleTwo(msumD / minCount)); newRecord.setAuxd(NumberUtils.scaleTwo(msumD / minCount));
//滤波窗口里的时间跨度超过滤波时间的2/3才认为稳定 //滤波窗口里的时间跨度超过滤波时间的2/3才认为稳定
newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 2 / 3))); newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 1 / 2)));
return true; return true;
} }
} }
@ -246,7 +249,7 @@ public class GNSSCalcFilterService {
newRecord.setAuxn(NumberUtils.scaleTwo(msumN / minCount)); newRecord.setAuxn(NumberUtils.scaleTwo(msumN / minCount));
newRecord.setAuxd(NumberUtils.scaleTwo(msumD / minCount)); newRecord.setAuxd(NumberUtils.scaleTwo(msumD / minCount));
//滤波窗口里的时间跨度超过滤波时间的2/3才认为稳定 //滤波窗口里的时间跨度超过滤波时间的2/3才认为稳定
newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 2 / 3))); newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 1 / 2)));
return true; return true;
} }
} }

View File

@ -0,0 +1,65 @@
package com.imdroid.sideslope.ntrip;
import com.imdroid.common.util.ByteUtil;
import com.imdroid.common.util.ThreadManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.List;
/**
* 将RTCM数据转发到指定UDP端口
*/
@Service
public class RtcmUdpForwarder {
private final Logger logger = LoggerFactory.getLogger(RtcmUdpForwarder.class);
@Value("${rtcm.forward.port:12001}")
private Integer forwardPort;
private DatagramSocket socket;
private DatagramPacket outPacket;
/**
* 转发RTCM数据到UDP端口
* @param mount 挂载点名称
* @param hexRtcm 差分信息16进制字符串
*/
public void forward(String mount, String hexRtcm) {
ThreadManager.getSingleThreadPool(RtcmUdpForwarder.class.getName()).execute(() -> {
try {
if (socket == null) socket = new DatagramSocket();
if (outPacket == null) outPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName("localhost"), forwardPort);
// 转发原始RTCM数据不添加挂载点信息
byte[] rtcmData = ByteUtil.hexStringTobyte(hexRtcm);
outPacket.setData(rtcmData);
socket.send(outPacket);
logger.debug("Forwarded RTCM data from mountpoint {} to UDP port {}, data length: {}",
mount, forwardPort, rtcmData.length);
} catch (Exception e) {
socket = null;
outPacket = null;
logger.error("Failed to forward RTCM data: {}", e.getMessage());
}
});
}
/**
* 转发多条RTCM数据到UDP端口
* @param mount 挂载点名称
* @param hexRtcmList 差分信息16进制字符串列表
*/
public void forward(String mount, List<String> hexRtcmList) {
logger.debug("Forwarding {} RTCM messages from mountpoint {}", hexRtcmList.size(), mount);
for (String hexRtcm : hexRtcmList) {
forward(mount, hexRtcm);
}
}
}

View File

@ -0,0 +1,110 @@
package com.imdroid.sideslope.server.tcp;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 特定设备RTCM数据TCP服务器
*/
@Component
public class RtcmSpecificDeviceTcpServer implements ApplicationRunner {
private final Logger logger = LoggerFactory.getLogger(RtcmSpecificDeviceTcpServer.class);
@Value("${rtcm.specific.device.port:12002}")
private Integer port;
@Value("${rtcm.specific.device.id:3530795}")
private String targetDeviceId;
// 存储所有连接的客户端通道
private final CopyOnWriteArrayList<SocketChannel> clientChannels = new CopyOnWriteArrayList<>();
@Override
public void run(ApplicationArguments args) throws Exception {
new Thread(this::start0, "specific-device-tcp-server").start();
}
private void start0() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 添加到客户端列表
clientChannels.add(ch);
// 当连接关闭时从列表中移除
ch.closeFuture().addListener(future -> clientChannels.remove(ch));
logger.info("New client connected: {}", ch.remoteAddress());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
logger.info("Specific device TCP server started on port {} for device {}", port, targetDeviceId);
future.channel().closeFuture().sync();
} catch (Exception e) {
logger.error("Error starting Specific device TCP server at port {}", port, e);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
/**
* 向所有连接的客户端发送RTCM数据
* @param rtcmData RTCM数据字节数组
*/
public void broadcastRtcmData(byte[] rtcmData) {
if (clientChannels.isEmpty()) {
return;
}
for (SocketChannel channel : clientChannels) {
if (channel.isActive()) {
channel.writeAndFlush(io.netty.buffer.Unpooled.wrappedBuffer(rtcmData));
}
}
logger.debug("Broadcasted RTCM data to {} clients, data length: {}", clientChannels.size(), rtcmData.length);
}
/**
* 获取目标设备ID
* @return 目标设备ID
*/
public String getTargetDeviceId() {
return targetDeviceId;
}
/**
* 获取服务器端口
* @return 服务器端口
*/
public Integer getPort() {
return port;
}
}

View File

@ -74,6 +74,7 @@ public class Device {
private BigDecimal used; private BigDecimal used;
LocalDateTime lastRxTime; LocalDateTime lastRxTime;
LocalDateTime lastD3f0f2Time;
LocalDateTime lastD3f2Time; LocalDateTime lastD3f2Time;
short noFixedAndFloatResult=0; short noFixedAndFloatResult=0;
@ -89,15 +90,14 @@ public class Device {
short abnormalD341Num = 0; short abnormalD341Num = 0;
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
int lasQuality = 0;
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;
switch (head){ switch (head){
case 0xd3f0: case 0xd3f0:
//lastD3f0f2Time = LocalDateTime.now(); lastD3f0f2Time = LocalDateTime.now();
//clearStat(); //clearStat();
break; break;
case 0xd3f2: case 0xd3f2:
@ -130,6 +130,7 @@ public class Device {
} }
public void updateCalcQuality(int quality){ public void updateCalcQuality(int quality){
lasQuality = quality;
if(b562AsCalc) { if(b562AsCalc) {
if (quality == UBXUtil.FIX_RESULT) fixedNum++; if (quality == UBXUtil.FIX_RESULT) fixedNum++;
else if (quality == UBXUtil.FLOAT_RESULT) floatNum++; else if (quality == UBXUtil.FLOAT_RESULT) floatNum++;

View File

@ -11,6 +11,4 @@ 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);
} }

View File

@ -51,7 +51,6 @@ 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;
} }
@ -66,19 +65,6 @@ 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);
@ -93,43 +79,23 @@ public class LocalDeviceServiceImpl implements DeviceService {
@Override @Override
public List<Device> findByParentId(String parentId) { public List<Device> findByParentId(String parentId) {
String cacheKey = "PARENTID_" + parentId; List<Device> devices = subDeviceCache.getIfPresent(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(cacheKey, devices); subDeviceCache.put(parentId, devices);
} }
} }
return devices; return devices;
} }
@Override public void refresh(String deviceId, String oldParentId){
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("PARENTID_" + device.getParentId()); subDeviceCache.invalidate(device.getParentId());
if(oldParentId!=null && !oldParentId.equals(device.getParentId())){ if(oldParentId!=null && !oldParentId.equals(device.getParentId())){
subDeviceCache.invalidate("PARENTID_" + oldParentId); subDeviceCache.invalidate(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);

View File

@ -154,9 +154,9 @@ public class ApiController {
} }
@PostMapping("/device_param_changed") @PostMapping("/device_param_changed")
public HttpResp deviceParamChanged(String deviceId, String oldParentId, String oldParentId1) { public HttpResp deviceParamChanged(String deviceId, String oldParentId) {
// 更新设备缓存 // 更新设备缓存
localDeviceService.refresh(deviceId, oldParentId, oldParentId1); localDeviceService.refresh(deviceId, oldParentId);
calcService.refreshDeviceCalc(deviceId); calcService.refreshDeviceCalc(deviceId);
HttpResp resp = new HttpResp(); HttpResp resp = new HttpResp();

View File

@ -287,7 +287,7 @@ public class GnssDeviceController extends BasicController{
} else { } else {
// 更新组参数的关联个数 // 更新组参数的关联个数
updateBasicGroupAssociatedNum(device,old_device); updateBasicGroupAssociatedNum(device,old_device);
rtcmClient.deviceParamChanged(device.getDeviceid(), old_device != null ? old_device.getParentid() : null, old_device != null ? old_device.getParentid1() : null); rtcmClient.deviceParamChanged(device.getDeviceid(), device.getParentid());
return HttpResult.ok(); return HttpResult.ok();
} }
} }
@ -341,6 +341,65 @@ public class GnssDeviceController extends BasicController{
return null; return null;
} }
@PostMapping("/gnss/device/cont_loc")
@ResponseBody
public JSONObject contLoc(String deviceid,String createtime){
JSONObject jsonObject = new JSONObject();
//old data
String[] paras = createtime.split(",");
String oldDeviceId;
if(paras.length==2){
oldDeviceId = paras[0];
createtime = paras[1];
}
else {
oldDeviceId = deviceid;
createtime = paras[0];
}
QueryWrapper<GnssCalcData> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deviceid",oldDeviceId);
queryWrapper.eq("enabled",1);
queryWrapper.eq("createtime",createtime);
queryWrapper.last("limit 1");
queryWrapper.isNotNull("rpose");
GnssCalcData calcOldData = gnssCalcDataMapper.selectOne(queryWrapper);
if(calcOldData==null) {
jsonObject.put("code",1);
jsonObject.put("msg","该时间无有效解算结果");
return jsonObject;
}
// new data
QueryWrapper<GnssCalcData> queryWrapper1 = new QueryWrapper<>();
queryWrapper1.eq("deviceid",deviceid);
queryWrapper1.eq("enabled",1);
queryWrapper1.orderByDesc("createtime");
queryWrapper1.last("limit 1");
queryWrapper1.isNotNull("rpose");
GnssCalcData calcNewData = gnssCalcDataMapper.selectOne(queryWrapper1);
if(calcNewData==null) {
jsonObject.put("code",1);
jsonObject.put("msg","无有效解算结果");
return jsonObject;
}
// 原来的初始值
QueryWrapper<GnssDevice> deviceQueryWrapper = new QueryWrapper<>();
deviceQueryWrapper.eq("deviceid",oldDeviceId);
deviceQueryWrapper.last("limit 1");
GnssDevice device = gnssDeviceMapper.selectOne(deviceQueryWrapper);
if(device.getIpose()==null) device.setIpose(0.0);
if(device.getIposn()==null) device.setIposn(0.0);
if(device.getIposd()==null) device.setIposd(0.0);
jsonObject.put("code",0);
jsonObject.put("ipose", NumberUtils.scaleTwo(calcNewData.getRpose()-calcOldData.getRpose()+device.getIpose()));
jsonObject.put("iposn",NumberUtils.scaleTwo(calcNewData.getRposn()-calcOldData.getRposn()+device.getIposn()));
jsonObject.put("iposd",NumberUtils.scaleTwo(calcNewData.getRposd()-calcOldData.getRposd()+device.getIposd()));
return jsonObject;
}
void updateBasicGroupAssociatedNum(GnssDevice newCfg, GnssDevice oldCfg){ void updateBasicGroupAssociatedNum(GnssDevice newCfg, GnssDevice oldCfg){
updateBasicGroupAssociatedNum(newCfg.getGroup_id()); updateBasicGroupAssociatedNum(newCfg.getGroup_id());
updateCalcGroupAssociatedNum(newCfg.getCalc_group_id()); updateCalcGroupAssociatedNum(newCfg.getCalc_group_id());
@ -409,7 +468,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, null); rtcmClient.deviceParamChanged(del_id, null);
return HttpResult.ok(); return HttpResult.ok();
} }
} }

View File

@ -1,7 +1,6 @@
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;
@ -12,13 +11,12 @@ 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.*; import java.util.List;
/** /**
* 状态消息 控制器 * 状态消息 控制器
@ -34,10 +32,6 @@ 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);
@ -60,118 +54,6 @@ 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
* *

View File

@ -2,18 +2,16 @@ package com.imdroid.beidou.controller;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imdroid.beidou.common.HttpResult;
import com.imdroid.beidou.service.CommonExcelService; import com.imdroid.beidou.service.CommonExcelService;
import com.imdroid.secapi.dto.GnssGroupFwd; import com.imdroid.secapi.dto.*;
import com.imdroid.secapi.dto.GnssGroupFwdMapper;
import com.imdroid.secapi.dto.ResendRecord;
import com.imdroid.secapi.dto.ResendRecordMapper;
import org.springframework.beans.factory.annotation.Autowired; 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.*;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@Controller @Controller
@ -22,6 +20,10 @@ public class GnssResendController extends BasicController implements CommonExcel
GnssGroupFwdMapper gnssGroupFwdMapper; GnssGroupFwdMapper gnssGroupFwdMapper;
@Autowired @Autowired
ResendRecordMapper resendRecordMapper; ResendRecordMapper resendRecordMapper;
@Autowired
TenantMapper tenantMapper;
@Autowired
OpLogManager opLogManager;
/********* 推送页面 *********/ /********* 推送页面 *********/
@RequestMapping("/page/resend_records") @RequestMapping("/page/resend_records")
@ -33,6 +35,19 @@ public class GnssResendController extends BasicController implements CommonExcel
return "/page/resend_records"; return "/page/resend_records";
} }
@RequestMapping("/page/table/resend_record_add")
public String gnssAddDev(Model m, HttpSession session) {
initModel(m, session);
//以下用于下拉框数据
List<Tenant> tenants = tenantMapper.selectList(null);
List<GnssGroupFwd> gnssGroupFwds = gnssGroupFwdMapper.selectList(null);
m.addAttribute("tenant_list", tenants);
m.addAttribute("gnss_group_fwd_list", gnssGroupFwds);
m.addAttribute("device", new GnssDevice());
return "/page/table/resend_record_add";
}
/********* 推送数据 *********/ /********* 推送数据 *********/
@RequestMapping("/fwd/resend_records") @RequestMapping("/fwd/resend_records")
@ -41,6 +56,40 @@ public class GnssResendController extends BasicController implements CommonExcel
return this.pageList(session, page, limit, searchParams); return this.pageList(session, page, limit, searchParams);
} }
@PostMapping("/fwd/resend_records/add")
@ResponseBody
public String update(HttpSession session, @RequestBody JSONObject object) throws Exception {
// 从请求参数中创建对象
ResendRecord resendRecord = JSONObject.toJavaObject(object,ResendRecord.class);
resendRecord.setCreatetime(LocalDateTime.now());
resendRecord.setState(ResendRecord.STATE_BREAK_POINT);
int num = resendRecordMapper.insert(resendRecord);
if (num == 0) {
return HttpResult.failed();
} else {
opLogManager.addLog(getLoginUser(session),getTenantId(session),
OpLogManager.OP_TYPE_ADD,
OpLogManager.OP_OBJ_FWD_RECORD,
"create new resend record");
return HttpResult.ok();
}
}
@PostMapping("/fwd/resend_records/delete")
@ResponseBody
public String delete(HttpSession session, @RequestParam String del_id) throws Exception {
int num = resendRecordMapper.deleteById(del_id);
opLogManager.addLog(getLoginUser(session),getTenantId(session),
OpLogManager.OP_TYPE_DEL,
OpLogManager.OP_OBJ_FWD_RECORD,
del_id + " deleted");
if (num == 0) {
return HttpResult.failed();
} else{
return HttpResult.ok();
}
}
/** /**
* 获取实体类的class * 获取实体类的class
* *

View File

@ -1,7 +1,3 @@
/**
* 坐标转换工具模块
* 提供WGS84和GCJ-02坐标系之间的转换功能
*/
var CoordinateUtils = (function() { var CoordinateUtils = (function() {
'use strict'; 'use strict';

View File

@ -1,7 +1,3 @@
/**
* 设备标记管理模块
* 负责设备标记的创建分类和显示管理
*/
var DeviceMarkers = (function() { var DeviceMarkers = (function() {
'use strict'; 'use strict';
@ -281,11 +277,11 @@ var DeviceMarkers = (function() {
vectorSource.addFeature(myLocationFeature); vectorSource.addFeature(myLocationFeature);
map.getView().animate({ // map.getView().animate({
center: coordinates, // center: coordinates,
zoom: 15, // zoom: 15,
duration: 1000 // duration: 1000
}); // });
}, },
function(error) { function(error) {
console.error('获取位置失败:', error); console.error('获取位置失败:', error);
@ -294,6 +290,50 @@ var DeviceMarkers = (function() {
} }
} }
function locateMyLocation() {
if (myLocationFeature) {
var geometry = myLocationFeature.getGeometry();
var coordinates = geometry.getCoordinates();
map.getView().animate({
center: coordinates,
zoom: 15,
duration: 1000
});
return true;
} else {
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
});
},
);
}
}
return false;
}
function startLocationUpdates() { function startLocationUpdates() {
if (navigator.geolocation) { if (navigator.geolocation) {
@ -354,6 +394,7 @@ var DeviceMarkers = (function() {
findDeviceById: findDeviceById, findDeviceById: findDeviceById,
locateDevice: locateDevice, locateDevice: locateDevice,
getMyLocation: getMyLocation, getMyLocation: getMyLocation,
locateMyLocation: locateMyLocation,
startLocationUpdates: startLocationUpdates, startLocationUpdates: startLocationUpdates,
stopLocationUpdates: stopLocationUpdates, stopLocationUpdates: stopLocationUpdates,
updateMyLocationForMapType: updateMyLocationForMapType, updateMyLocationForMapType: updateMyLocationForMapType,

View File

@ -1,196 +1,126 @@
/**
* 设备总览主入口文件
* 负责整个设备总览模块的初始化和全局函数暴露
*/
var DeviceOverview = (function() { var DeviceOverview = (function() {
'use strict'; 'use strict';
// 全局变量从原HTML中提取
var deviceList = []; var deviceList = [];
/**
* 初始化设备总览模块
* @param {Array} devices - 设备列表数据
* @param {Object} options - 配置选项
*/
function init(devices, options) { function init(devices, options) {
deviceList = devices || []; deviceList = devices || [];
// 设置全局变量供其他模块使用
window.deviceList = deviceList; window.deviceList = deviceList;
window.userRole = options && options.role ? options.role : 'USER'; window.userRole = options && options.role ? options.role : 'USER';
// 等待layui加载完成后初始化
layui.use(['form'], function(){ layui.use(['form'], function(){
var form = layui.form; var form = layui.form;
// 绑定表单事件
form.on('select(mapTypeNew)', function(data){ form.on('select(mapTypeNew)', function(data){
MapCore.switchMapType(data.value); MapCore.switchMapType(data.value);
}); });
// 初始化地图核心
MapCore.initialize(deviceList); MapCore.initialize(deviceList);
// 默认显示所有设备
document.getElementById('warningFilter').value = 'all'; document.getElementById('warningFilter').value = 'all';
SearchFilter.showAllDevices(); SearchFilter.showAllDevices();
}); });
} }
// 暴露给HTML使用的全局函数
/**
* 地图类型变化处理
*/
function onMapTypeChange() { function onMapTypeChange() {
return MapCore.onMapTypeChange(); return MapCore.onMapTypeChange();
} }
/**
* 搜索设备
*/
function searchDeviceNew() { function searchDeviceNew() {
return MapCore.searchDeviceNew(); return MapCore.searchDeviceNew();
} }
/**
* 告警过滤变化处理
*/
function onWarningFilterChange() { function onWarningFilterChange() {
return MapCore.onWarningFilterChange(); return MapCore.onWarningFilterChange();
} }
/**
* 切换地图功能菜单
*/
function toggleMapFunctionsMenu() { function toggleMapFunctionsMenu() {
return MapCore.toggleMapFunctionsMenu(); return MapCore.toggleMapFunctionsMenu();
} }
/**
* 切换设备ID显示
*/
function toggleDeviceId() { function toggleDeviceId() {
return MapCore.toggleDeviceId(); return MapCore.toggleDeviceId();
} }
/**
* 切换集群显示
*/
function toggleCluster() { function toggleCluster() {
return MapCore.toggleCluster(); return MapCore.toggleCluster();
} }
/**
* 切换测距功能
*/
function toggleMeasureDistance() { function toggleMeasureDistance() {
return MeasureTools.toggleMeasureDistance(); return MeasureTools.toggleMeasureDistance();
} }
/**
* 完成测量
*/
function finishMeasuring() { function finishMeasuring() {
return MeasureTools.finishMeasuring(); return MeasureTools.finishMeasuring();
} }
/**
* 清除测距
*/
function clearMeasure() { function clearMeasure() {
return MeasureTools.clearMeasure(); return MeasureTools.clearMeasure();
} }
/**
* 切换天气预报功能
*/
function toggleWeatherForecast() { function toggleWeatherForecast() {
return WeatherForecast.toggleWeatherForecast(); return WeatherForecast.toggleWeatherForecast();
} }
/**
* 关闭天气卡片
*/
function closeWeatherCard() { function closeWeatherCard() {
return WeatherForecast.closeWeatherCard(); return WeatherForecast.closeWeatherCard();
} }
/**
* 显示上一个天气预报
*/
function showPrevForecast() { function showPrevForecast() {
return WeatherForecast.showPrevForecast(); return WeatherForecast.showPrevForecast();
} }
/**
* 显示下一个天气预报
*/
function showNextForecast() { function showNextForecast() {
return WeatherForecast.showNextForecast(); return WeatherForecast.showNextForecast();
} }
/**
* 查询设备
* @param {string} statusType - 状态类型
*/
function queryDevices(statusType) { function queryDevices(statusType) {
return SearchFilter.queryDevices(statusType); return SearchFilter.queryDevices(statusType);
} }
/**
* 定位设备到地图
* @param {string} deviceId - 设备ID
* @param {number} latitude - 纬度
* @param {number} longitude - 经度
*/
function locateDeviceOnMap(deviceId, latitude, longitude) { function locateDeviceOnMap(deviceId, latitude, longitude) {
return SearchFilter.locateDeviceOnMap(deviceId, latitude, longitude); return SearchFilter.locateDeviceOnMap(deviceId, latitude, longitude);
} }
/**
* 直接定位设备
* @param {string} deviceId - 设备ID
*/
function locateDeviceDirectly(deviceId) { function locateDeviceDirectly(deviceId) {
return SearchFilter.locateDeviceDirectly(deviceId); return SearchFilter.locateDeviceDirectly(deviceId);
} }
/**
* 切换地图类型
* @param {string} mapType - 地图类型
*/
function switchMapType(mapType) { function switchMapType(mapType) {
return MapCore.switchMapType(mapType); return MapCore.switchMapType(mapType);
} }
// 公开API function locateMyLocation() {
return DeviceMarkers.locateMyLocation();
}
function clearDeviceFilter() {
return SearchFilter.clearDeviceFilter();
}
return { return {
init: init, init: init,
// 地图相关
onMapTypeChange: onMapTypeChange, onMapTypeChange: onMapTypeChange,
switchMapType: switchMapType, switchMapType: switchMapType,
// 搜索和过滤
searchDeviceNew: searchDeviceNew, searchDeviceNew: searchDeviceNew,
onWarningFilterChange: onWarningFilterChange, onWarningFilterChange: onWarningFilterChange,
queryDevices: queryDevices, queryDevices: queryDevices,
locateDeviceOnMap: locateDeviceOnMap, locateDeviceOnMap: locateDeviceOnMap,
locateDeviceDirectly: locateDeviceDirectly, locateDeviceDirectly: locateDeviceDirectly,
locateMyLocation: locateMyLocation,
clearDeviceFilter: clearDeviceFilter,
// 地图功能
toggleMapFunctionsMenu: toggleMapFunctionsMenu, toggleMapFunctionsMenu: toggleMapFunctionsMenu,
toggleDeviceId: toggleDeviceId, toggleDeviceId: toggleDeviceId,
toggleCluster: toggleCluster, toggleCluster: toggleCluster,
// 测距工具
toggleMeasureDistance: toggleMeasureDistance, toggleMeasureDistance: toggleMeasureDistance,
finishMeasuring: finishMeasuring, finishMeasuring: finishMeasuring,
clearMeasure: clearMeasure, clearMeasure: clearMeasure,
// 天气预报
toggleWeatherForecast: toggleWeatherForecast, toggleWeatherForecast: toggleWeatherForecast,
closeWeatherCard: closeWeatherCard, closeWeatherCard: closeWeatherCard,
showPrevForecast: showPrevForecast, showPrevForecast: showPrevForecast,
@ -198,7 +128,6 @@ var DeviceOverview = (function() {
}; };
})(); })();
// 将主要函数暴露到全局作用域供HTML中的onclick等使用
window.DeviceOverview = DeviceOverview; window.DeviceOverview = DeviceOverview;
window.onMapTypeChange = DeviceOverview.onMapTypeChange; window.onMapTypeChange = DeviceOverview.onMapTypeChange;
window.searchDeviceNew = DeviceOverview.searchDeviceNew; window.searchDeviceNew = DeviceOverview.searchDeviceNew;
@ -216,3 +145,5 @@ window.showNextForecast = DeviceOverview.showNextForecast;
window.queryDevices = DeviceOverview.queryDevices; window.queryDevices = DeviceOverview.queryDevices;
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap; window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly; window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;
window.locateMyLocation = DeviceOverview.locateMyLocation;
window.clearDeviceFilter = DeviceOverview.clearDeviceFilter;

View File

@ -1,7 +1,3 @@
/**
* 地图核心模块
* 负责地图初始化事件处理和核心功能管理
*/
var MapCore = (function() { var MapCore = (function() {
'use strict'; 'use strict';
@ -92,10 +88,9 @@ var MapCore = (function() {
if (deviceList && deviceList.length > 0) { if (deviceList && deviceList.length > 0) {
setCenterFromDevices(deviceList); setCenterFromDevices(deviceList);
DeviceMarkers.addDeviceMarkers(deviceList);
} }
DeviceMarkers.addDeviceMarkers(deviceList);
DeviceMarkers.getMyLocation(); DeviceMarkers.getMyLocation();
DeviceMarkers.startLocationUpdates(); DeviceMarkers.startLocationUpdates();
@ -216,21 +211,13 @@ var MapCore = (function() {
function setCenterFromDevices(deviceList) { function setCenterFromDevices(deviceList) {
if (!deviceList || deviceList.length === 0) return; if (!deviceList || deviceList.length === 0) return;
var minLat = deviceList[0].latitude; var sumLat = 0, sumLon = 0;
var maxLat = deviceList[0].latitude; for (var i = 0; i < deviceList.length; i++) {
var minLon = deviceList[0].longitude; sumLat += deviceList[i].latitude;
var maxLon = deviceList[0].longitude; sumLon += deviceList[i].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 = sumLat / deviceList.length;
var centerLat = (minLat + maxLat) / 2; var centerLon = sumLon / deviceList.length;
var centerLon = (minLon + maxLon) / 2;
map.getView().setCenter(ol.proj.fromLonLat([centerLon, centerLat])); map.getView().setCenter(ol.proj.fromLonLat([centerLon, centerLat]));
} }
@ -241,15 +228,22 @@ var MapCore = (function() {
return; return;
} }
// 保存当前视图状态
var currentCenter = map.getView().getCenter();
var currentZoom = map.getView().getZoom();
map.removeLayer(currentBaseLayer); map.removeLayer(currentBaseLayer);
currentBaseLayer = MapLayers.getLayer(mapType); currentBaseLayer = MapLayers.getLayer(mapType);
map.getLayers().insertAt(0, currentBaseLayer); map.getLayers().insertAt(0, currentBaseLayer);
DeviceMarkers.updateMyLocationForMapType(mapType); DeviceMarkers.updateMyLocationForMapType(mapType);
// 重新获取设备列表并添加标记
var deviceList = window.deviceList || []; var deviceList = window.deviceList || [];
DeviceMarkers.addDeviceMarkers(deviceList); DeviceMarkers.addDeviceMarkers(deviceList);
// 恢复之前的视图状态
map.getView().setCenter(currentCenter);
map.getView().setZoom(currentZoom);
} }
function toggleDeviceId() { function toggleDeviceId() {
@ -281,10 +275,6 @@ var MapCore = (function() {
} }
} }
/**
* 关闭地图功能菜单
* @param {Event} event - 点击事件
*/
function closeMapFunctionsMenu(event) { function closeMapFunctionsMenu(event) {
var dropdown = document.getElementById('mapFunctionsMenu'); var dropdown = document.getElementById('mapFunctionsMenu');
var toggleBtn = document.querySelector('.dropdown-toggle'); var toggleBtn = document.querySelector('.dropdown-toggle');
@ -295,9 +285,6 @@ var MapCore = (function() {
} }
} }
/**
* 地图类型变化处理
*/
function onMapTypeChange() { function onMapTypeChange() {
var mapType = document.getElementById('mapTypeSelectNew').value; var mapType = document.getElementById('mapTypeSelectNew').value;
switchMapType(mapType); switchMapType(mapType);

View File

@ -1,11 +1,8 @@
/**
* 地图图层管理模块
* 管理不同类型的地图图层天地图高德谷歌等
*/
var MapLayers = (function() { var MapLayers = (function() {
'use strict'; 'use strict';
// 天地图 API 密钥 fengyarnom@gmail.com // 天地图 API 密钥 fengyarnom@gmail.com
// 天地图的英文就是 TIANDITU 所以这里用拼音指代
var TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac'; var TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac';
function createTiandituTileLoadFunction() { function createTiandituTileLoadFunction() {

View File

@ -1,7 +1,3 @@
/**
* 测距工具模块
* 提供地图测距功能
*/
var MeasureTools = (function() { var MeasureTools = (function() {
'use strict'; 'use strict';

View File

@ -1,12 +1,10 @@
/**
* 搜索和过滤功能模块
* 负责设备搜索过滤和查询功能
*/
var SearchFilter = (function() { var SearchFilter = (function() {
'use strict'; 'use strict';
var currentSearchedDevice = null; var currentSearchedDevice = null;
var markerState = 3; // 1:all; 2:orange; 3:red var markerState = 3; // 1:all; 2:orange; 3:red
var filteredDeviceIds = [];
var isFilterActive = false;
function searchDevice(deviceId) { function searchDevice(deviceId) {
if (!deviceId || !deviceId.trim()) { if (!deviceId || !deviceId.trim()) {
@ -15,6 +13,13 @@ var SearchFilter = (function() {
} }
deviceId = deviceId.trim(); deviceId = deviceId.trim();
// 多设备
if (deviceId.indexOf(',') !== -1) {
return handleMultiDeviceSearch(deviceId);
}
// 单设备
currentSearchedDevice = deviceId; currentSearchedDevice = deviceId;
var success = DeviceMarkers.locateDevice(deviceId); var success = DeviceMarkers.locateDevice(deviceId);
@ -41,6 +46,126 @@ var SearchFilter = (function() {
return success; return success;
} }
function handleMultiDeviceSearch(deviceIdsString) {
var deviceIds = deviceIdsString.split(',').map(function(id) {
return id.trim();
}).filter(function(id) {
return id !== '';
});
if (deviceIds.length === 0) {
clearSearch();
return false;
}
filteredDeviceIds = deviceIds;
var foundDevices = filterDevicesByIds(deviceIds);
if (foundDevices.length > 0) {
showFilterStatus(deviceIds.length);
isFilterActive = true;
var firstDevice = foundDevices[0];
var geometry = firstDevice.getGeometry();
var coordinates = geometry.getCoordinates();
if (MapCore.getMap()) {
MapCore.getMap().getView().animate({
center: coordinates,
zoom: 12,
duration: 1000
});
}
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('已过滤显示 ' + foundDevices.length + ' 个设备');
}
return true;
} else {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('未找到任何匹配的设备');
}
return false;
}
}
function filterDevicesByIds(deviceIds) {
if (!DeviceMarkers.getAllFeatures) return [];
var allFeatures = DeviceMarkers.getAllFeatures();
var foundDevices = [];
var vectorSource = MapCore.getVectorSource();
if (!vectorSource) return [];
var myLocationFeature = DeviceMarkers.getMyLocationFeature();
vectorSource.clear();
if (myLocationFeature) {
vectorSource.addFeature(myLocationFeature);
}
for (var i = 0; i < allFeatures.length; i++) {
var feature = allFeatures[i];
var deviceInfo = feature.get('deviceInfo');
if (deviceInfo) {
var deviceId = String(deviceInfo.deviceid).trim();
for (var j = 0; j < deviceIds.length; j++) {
var searchId = String(deviceIds[j]).trim();
if (deviceId === searchId ||
deviceId.indexOf(searchId) !== -1 ||
deviceId.replace(/\s+/g, '') === searchId.replace(/\s+/g, '')) {
vectorSource.addFeature(feature);
foundDevices.push(feature);
break;
}
}
}
}
return foundDevices;
}
function showFilterStatus(count) {
var filterStatus = document.getElementById('filterStatus');
if (filterStatus) {
filterStatus.innerHTML =
'<span style="color: #1aa094; font-weight: bold; font-size: 12px;">定位中</span>' +
'<button class="toolbar-btn" onclick="clearDeviceFilter()" style="background: #f56565; margin-left: 6px;">' +
'<i class="layui-icon layui-icon-close" style="font-size: 12px;"></i>' +
'</button>';
filterStatus.style.display = 'flex';
}
}
function hideFilterStatus() {
var filterStatus = document.getElementById('filterStatus');
if (filterStatus) {
filterStatus.style.display = 'none';
}
}
function clearDeviceFilter() {
isFilterActive = false;
filteredDeviceIds = [];
hideFilterStatus();
applyCurrentFilter();
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('已清除设备过滤');
}
}
function clearSearch() { function clearSearch() {
currentSearchedDevice = null; currentSearchedDevice = null;
var searchInput = document.getElementById('deviceSearchNew'); var searchInput = document.getElementById('deviceSearchNew');
@ -48,8 +173,12 @@ var SearchFilter = (function() {
searchInput.value = ''; searchInput.value = '';
} }
// 恢复到当前的过滤状态 if (isFilterActive) {
applyCurrentFilter(); clearDeviceFilter();
} else {
// 恢复到当前的过滤状态
applyCurrentFilter();
}
} }
function applyCurrentFilter() { function applyCurrentFilter() {
@ -170,6 +299,14 @@ var SearchFilter = (function() {
markerState = state; markerState = state;
} }
function isFilterModeActive() {
return isFilterActive;
}
function getFilteredDeviceIds() {
return filteredDeviceIds;
}
return { return {
searchDevice: searchDevice, searchDevice: searchDevice,
clearSearch: clearSearch, clearSearch: clearSearch,
@ -182,6 +319,9 @@ var SearchFilter = (function() {
locateDeviceDirectly: locateDeviceDirectly, locateDeviceDirectly: locateDeviceDirectly,
getCurrentSearchedDevice: getCurrentSearchedDevice, getCurrentSearchedDevice: getCurrentSearchedDevice,
getMarkerState: getMarkerState, getMarkerState: getMarkerState,
setMarkerState: setMarkerState setMarkerState: setMarkerState,
clearDeviceFilter: clearDeviceFilter,
isFilterModeActive: isFilterModeActive,
getFilteredDeviceIds: getFilteredDeviceIds
}; };
})(); })();

View File

@ -1,7 +1,6 @@
var WeatherForecast = (function() { var WeatherForecast = (function() {
'use strict'; 'use strict';
var weatherApiKey = 'Uxh4IdMuAvhSiBnsf4UUDVGF4e3YAp2B';
var weatherEnabled = false; var weatherEnabled = false;
var weatherData = null; var weatherData = null;
var currentForecastIndex = 0; var currentForecastIndex = 0;
@ -81,7 +80,6 @@ var WeatherForecast = (function() {
contentElement.innerHTML = contentElement.innerHTML =
'<div class="weather-loading">' + '<div class="weather-loading">' +
'<i class="layui-icon layui-icon-loading layui-icon-anim-rotate"></i>' + '<i class="layui-icon layui-icon-loading layui-icon-anim-rotate"></i>' +
'<p>请确保网络可访问 Windy API 服务</p>' +
'<p>正在获取天气预测数据...</p>' + '<p>正在获取天气预测数据...</p>' +
'</div>'; '</div>';
} }
@ -140,23 +138,19 @@ var WeatherForecast = (function() {
} }
function fetchWeatherData(lat, lon) { function fetchWeatherData(lat, lon) {
var requestBody = { var url = 'https://api.open-meteo.com/v1/forecast?' +
"lat": parseFloat(lat.toFixed(2)), 'latitude=' + lat.toFixed(4) +
"lon": parseFloat(lon.toFixed(2)), '&longitude=' + lon.toFixed(4) +
"model": "gfs", '&current=temperature_2m,wind_speed_10m,wind_direction_10m,relative_humidity_2m,surface_pressure' +
"parameters": ["temp", "wind", "precip", "pressure", "rh", "windGust"], '&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,wind_direction_10m,precipitation,surface_pressure,wind_gusts_10m' +
"levels": ["surface"], '&forecast_days=3' +
"key": weatherApiKey, '&timezone=auto';
"hours": 72
};
fetch('https://api.windy.com/api/point-forecast/v2', { fetch(url, {
method: 'POST', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json'
}, }
body: JSON.stringify(requestBody)
}) })
.then(function(response) { .then(function(response) {
if (!response.ok) { if (!response.ok) {
@ -165,7 +159,7 @@ var WeatherForecast = (function() {
return response.json(); return response.json();
}) })
.then(function(data) { .then(function(data) {
weatherData = data; weatherData = transformOpenMeteoData(data);
var currentTime = new Date().getTime(); var currentTime = new Date().getTime();
var closestIndex = 0; var closestIndex = 0;
@ -203,6 +197,40 @@ var WeatherForecast = (function() {
}); });
} }
function transformOpenMeteoData(data) {
var transformed = {
ts: [],
'temp-surface': [],
'wind-speed': [],
'wind-direction': [],
'precipitation': [],
'rh-surface': [],
'pressure-surface': [],
'gust-surface': []
};
if (!data.hourly || !data.hourly.time) {
return transformed;
}
for (var i = 0; i < data.hourly.time.length; i++) {
transformed.ts.push(new Date(data.hourly.time[i]).getTime());
transformed['temp-surface'].push(data.hourly.temperature_2m[i]);
var windSpeedMs = data.hourly.wind_speed_10m[i] ? data.hourly.wind_speed_10m[i] / 3.6 : null;
transformed['wind-speed'].push(windSpeedMs);
transformed['wind-direction'].push(data.hourly.wind_direction_10m[i]);
transformed['precipitation'].push(data.hourly.precipitation[i]);
transformed['rh-surface'].push(data.hourly.relative_humidity_2m[i]);
transformed['pressure-surface'].push(data.hourly.surface_pressure[i]);
var gustMs = data.hourly.wind_gusts_10m[i] ? data.hourly.wind_gusts_10m[i] / 3.6 : null;
transformed['gust-surface'].push(gustMs);
}
return transformed;
}
function displayCurrentForecast() { function displayCurrentForecast() {
if (!weatherData || !weatherData.ts || weatherData.ts.length === 0) { if (!weatherData || !weatherData.ts || weatherData.ts.length === 0) {
displayWeatherError('无可用的天气预测数据'); displayWeatherError('无可用的天气预测数据');
@ -213,22 +241,22 @@ var WeatherForecast = (function() {
var forecastHtml = '<div class="weather-forecast-item"><div class="weather-param-grid">'; var forecastHtml = '<div class="weather-forecast-item"><div class="weather-param-grid">';
if (weatherData['temp-surface'] && weatherData['temp-surface'][i] !== null) { if (weatherData['temp-surface'] && weatherData['temp-surface'][i] !== null) {
var temp = (weatherData['temp-surface'][i] - 273.15).toFixed(1); var temp = weatherData['temp-surface'][i].toFixed(1);
forecastHtml += createWeatherParam('温度', temp + '°C'); forecastHtml += createWeatherParam('温度', temp + '°C');
} }
if (weatherData['wind_u-surface'] && weatherData['wind_v-surface'] && if (weatherData['wind-speed'] && weatherData['wind-speed'][i] !== null) {
weatherData['wind_u-surface'][i] !== null && weatherData['wind_v-surface'][i] !== null) { var windSpeed = weatherData['wind-speed'][i].toFixed(1);
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('风速', windSpeed + ' m/s');
}
if (weatherData['wind-direction'] && weatherData['wind-direction'][i] !== null) {
var windDir = getWindDirectionFromDegrees(weatherData['wind-direction'][i]);
forecastHtml += createWeatherParam('风向', windDir); forecastHtml += createWeatherParam('风向', windDir);
} }
if (weatherData['past3hprecip-surface'] && weatherData['past3hprecip-surface'][i] !== null) { if (weatherData['precipitation'] && weatherData['precipitation'][i] !== null) {
var precip = weatherData['past3hprecip-surface'][i].toFixed(1); var precip = weatherData['precipitation'][i].toFixed(1);
forecastHtml += createWeatherParam('降水', precip + ' mm'); forecastHtml += createWeatherParam('降水', precip + ' mm');
} }
@ -238,7 +266,7 @@ var WeatherForecast = (function() {
} }
if (weatherData['pressure-surface'] && weatherData['pressure-surface'][i] !== null) { if (weatherData['pressure-surface'] && weatherData['pressure-surface'][i] !== null) {
var pressure = (weatherData['pressure-surface'][i] / 100).toFixed(0); var pressure = weatherData['pressure-surface'][i].toFixed(0);
forecastHtml += createWeatherParam('气压', pressure + ' hPa'); forecastHtml += createWeatherParam('气压', pressure + ' hPa');
} }
@ -291,6 +319,14 @@ var WeatherForecast = (function() {
return directions[index]; return directions[index];
} }
function getWindDirectionFromDegrees(degrees) {
if (degrees === null || degrees === undefined) return '无风';
var directions = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
var index = Math.round(degrees / 45) % 8;
return directions[index];
}
function isEnabled() { function isEnabled() {
return weatherEnabled; return weatherEnabled;
} }

View File

@ -1,3 +1,6 @@
<!--由于需要一些特定的页面效果,
导致无法使用layui提供的样式所以这里我还是自己写css页面主要的 js 在 static/js/device-overview 库中它是通过暴露方法让main JS 获得方法的-->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -12,6 +15,7 @@
<link rel="stylesheet" href="../css/public.css" media="all"> <link rel="stylesheet" href="../css/public.css" media="all">
<link rel="stylesheet" href="../css/ol.css"> <link rel="stylesheet" href="../css/ol.css">
<!-- 谷歌地图的API接口这个访问没有天地图的次数多所以这边限制只有管理员能使用 --> <!-- 谷歌地图的API接口这个访问没有天地图的次数多所以这边限制只有管理员能使用 -->
<!-- 账户是 fengyarnom@gmail.com-->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBEBxB5n9vikPHbfNMzo_dzneXJi77YqjE" async defer></script> <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBEBxB5n9vikPHbfNMzo_dzneXJi77YqjE" async defer></script>
<style> <style>
.top-panel { .top-panel {
@ -522,11 +526,9 @@
/* 测量状态指示器样式 */ /* 测量状态指示器样式 */
#measureStatus { #measureStatus {
background: rgba(255, 255, 255, 0.98);
border: 1px solid rgba(72, 187, 120, 0.3); border: 1px solid rgba(72, 187, 120, 0.3);
border-radius: 8px; border-radius: 8px;
padding: 6px 12px; padding: 6px 12px;
box-shadow: 0 2px 8px rgba(72, 187, 120, 0.2);
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
@ -537,6 +539,20 @@
min-width: 24px; min-width: 24px;
} }
#filterStatus {
border: 1px solid rgba(26, 160, 148, 0.3);
border-radius: 8px;
padding: 6px 12px;
align-items: center;
gap: 8px;
}
#filterStatus .toolbar-btn {
padding: 2px 6px;
height: 24px;
min-width: 24px;
}
.weather-forecast-card { .weather-forecast-card {
position: absolute; position: absolute;
top: 55%; top: 55%;
@ -788,6 +804,14 @@
justify-content: center; justify-content: center;
padding: 6px 0px; padding: 6px 0px;
} }
#filterStatus {
display: flex;
justify-content: center;
padding: 6px 0px;
}
} }
</style> </style>
@ -847,7 +871,7 @@
<div id="weatherForecastCard" class="weather-forecast-card" style="display: none;" th:if="${role=='SUPER_ADMIN'}"> <div id="weatherForecastCard" class="weather-forecast-card" style="display: none;" th:if="${role=='SUPER_ADMIN'}">
<div class="weather-card-header"> <div class="weather-card-header">
<h3 class="weather-card-title"> <h3 class="weather-card-title">
Windy 天气预测 Open-Meteo 天气预测
</h3> </h3>
<button class="weather-close-btn" onclick="closeWeatherCard()">×</button> <button class="weather-close-btn" onclick="closeWeatherCard()">×</button>
</div> </div>
@ -892,7 +916,7 @@
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<div class="toolbar-item"> <div class="toolbar-item">
<input type="text" id="deviceSearchNew" class="toolbar-input" placeholder="搜索设备编号"> <input type="text" id="deviceSearchNew" class="toolbar-input" placeholder="设备编号">
<button class="toolbar-btn" onclick="searchDeviceNew()">搜索</button> <button class="toolbar-btn" onclick="searchDeviceNew()">搜索</button>
</div> </div>
@ -931,20 +955,23 @@
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div class="dropdown-group"> <div class="dropdown-group">
<div class="dropdown-group-title">测量工具</div> <div class="dropdown-group-title">定位工具</div>
<div class="dropdown-item" onclick="toggleMeasureDistance()"> <div class="dropdown-item" onclick="toggleMeasureDistance()">
测距 测距
</div> </div>
<div class="dropdown-item" onclick="clearMeasure()"> <div class="dropdown-item" onclick="clearMeasure()">
清除测距 清除测距
</div> </div>
<div class="dropdown-item" onclick="locateMyLocation()">
当前位置
</div>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div class="dropdown-group" th:if="${role=='SUPER_ADMIN'}"> <div class="dropdown-group" th:if="${role=='SUPER_ADMIN'}">
<div class="dropdown-group-title">Windy 天气预测</div> <div class="dropdown-group-title">Open-Meteo 天气预测</div>
<div class="dropdown-item"> <div class="dropdown-item">
<label class="switch-label"> <label class="switch-label">
<span>启用天气预测(需网络被代理)</span> <span>启用天气预测</span>
<input type="checkbox" id="enableWeatherSwitch" onchange="toggleWeatherForecast()"> <input type="checkbox" id="enableWeatherSwitch" onchange="toggleWeatherForecast()">
<span class="switch-slider"></span> <span class="switch-slider"></span>
</label> </label>
@ -955,11 +982,14 @@
</div> </div>
<div id="measureStatus" class="toolbar-item" style="display: none;"> <div id="measureStatus" class="toolbar-item" style="display: none;">
<span style="color: #48bb78; font-weight: bold; font-size: 12px;">测量中</span> <span style="color: #1aa094; font-weight: bold; font-size: 12px;">测量中</span>
<button class="toolbar-btn" onclick="finishMeasuring()" style="background: #f56565; margin-left: 6px;"> <button class="toolbar-btn" onclick="finishMeasuring()" style="background: #f56565; margin-left: 6px;">
<i class="layui-icon layui-icon-close" style="font-size: 12px;"></i> <i class="layui-icon layui-icon-close" style="font-size: 12px;"></i>
</button> </button>
</div> </div>
<div id="filterStatus" class="toolbar-item" style="display: none;">
</div>
</div> </div>
@ -970,12 +1000,10 @@
</div> </div>
<!-- JavaScript 依赖 -->
<script src="../js/ol.js"></script> <script src="../js/ol.js"></script>
<script src="../lib/jquery-3.4.1/jquery-3.4.1.min.js" charset="utf-8"></script> <script src="../lib/jquery-3.4.1/jquery-3.4.1.min.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>
<!-- 设备总览模块化JavaScript文件 -->
<script src="../js/device-overview-module/coordinate-utils.js"></script> <script src="../js/device-overview-module/coordinate-utils.js"></script>
<script src="../js/device-overview-module/map-layers.js"></script> <script src="../js/device-overview-module/map-layers.js"></script>
<script src="../js/device-overview-module/device-markers.js"></script> <script src="../js/device-overview-module/device-markers.js"></script>
@ -998,15 +1026,12 @@
/*[/]*/ /*[/]*/
]; ];
// 获取用户角色
var userRole = /*[[${role}]]*/ 'USER'; var userRole = /*[[${role}]]*/ 'USER';
// 初始化设备总览模块
DeviceOverview.init(deviceList, { DeviceOverview.init(deviceList, {
role: userRole role: userRole
}); });
// 暴露全局函数供HTML调用
window.onMapTypeChange = DeviceOverview.onMapTypeChange; window.onMapTypeChange = DeviceOverview.onMapTypeChange;
window.searchDeviceNew = DeviceOverview.searchDeviceNew; window.searchDeviceNew = DeviceOverview.searchDeviceNew;
window.onWarningFilterChange = DeviceOverview.onWarningFilterChange; window.onWarningFilterChange = DeviceOverview.onWarningFilterChange;
@ -1024,6 +1049,7 @@
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap; window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly; window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;
window.switchMapType = DeviceOverview.switchMapType; window.switchMapType = DeviceOverview.switchMapType;
window.locateMyLocation = DeviceOverview.locateMyLocation;
</script> </script>
</body> </body>

View File

@ -144,7 +144,6 @@
{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},

View File

@ -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" id="deviceid" autocomplete="off" class="layui-input"> <input type="text" name="s_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">&#xe67d;</i>导出</button> <button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-export-btn"><i class="layui-icon">&#xe67d;</i>导出</button>
</div> </div>
</div> </div>
@ -52,43 +52,24 @@
</div> </div>
</fieldset> </fieldset>
<div class="layui-tab layui-tab-card" lay-filter="data-tab"> <table class="layui-hide" id="currentTableId" lay-filter="currentTableFilter"></table>
<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', 'element'], function () { layui.use(['form', 'table', 'laydate'], 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},
{field: 'createtime', title: '上报时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"}, {field: 'createtime', title: '上报时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'roll', title: 'roll', templet: "<div>{{d.roll.toFixed(2)}}</div>"}, {field: 'roll', title: 'roll', templet: "<div>{{d.roll.toFixed(3)}}</div>"},
{field: 'pitch', title: 'pitch', templet: "<div>{{d.pitch.toFixed(2)}}</div>"}, {field: 'pitch', title: 'pitch', templet: "<div>{{d.pitch.toFixed(3)}}</div>"},
{field: 'yaw', title: 'yaw', templet: "<div>{{d.yaw.toFixed(2)}}</div>"}, {field: 'yaw', title: 'yaw', templet: "<div>{{d.yaw.toFixed(3)}}</div>"},
{field: 'rssi', title: '信号强度'}, {field: 'rssi', title: '信号强度'},
{field: 'voltage', title: '电压(mV)'}, {field: 'voltage', title: '电压(mV)'},
{field: 'solarvoltage', title: '光伏电压(mV)'}, {field: 'solarvoltage', title: '光伏电压(mV)'},
@ -97,7 +78,9 @@
{field: 'temperature', title: '温度(°C)'}, {field: 'temperature', title: '温度(°C)'},
{field: 'humidity', title: '湿度(%)'} {field: 'humidity', title: '湿度(%)'}
]; ];
/**
* 初始化表单,要加上,不然刷新部分组件可能会不加载
*/
form.render(); form.render();
laydate.render({ laydate.render({
@ -112,7 +95,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
@ -123,9 +106,11 @@
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
@ -138,6 +123,7 @@
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;
@ -145,173 +131,6 @@
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>

View File

@ -53,16 +53,38 @@
</fieldset> </fieldset>
<table class="layui-hide" id="forwardParaTableId" lay-filter="forwardParaTableFilter"></table> <table class="layui-hide" id="forwardParaTableId" lay-filter="forwardParaTableFilter"></table>
<script type="text/html" id="currentTableBar">
<a class="layui-btn layui-btn-xs layui-btn-danger data-count-delete" lay-event="delete">删除</a>
</script>
<script type="text/html" id="toolbarTop">
<div class="layui-btn-container" th:if="${role=='SUPER_ADMIN'}">
<button class="layui-btn layui-btn-normal layui-btn-sm data-add-btn" lay-event="add">添加</button>
</div>
</script>
</div> </div>
</div> </div>
<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="none"> <script th:inline="javascript">
layui.use(['form', 'table'], function () { layui.use(['form', 'table'], function () {
var $ = layui.$, var $ = layui.$,
form = layui.form, form = layui.form,
table = layui.table, table = layui.table,
laydate = layui.laydate; laydate = layui.laydate;
var cfg_cols = [
{field: 'projectid', title: '项目号', sort: true},
{field: 'deviceid', title: '设备号'},
{field: 'createtime', title: '推送时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'starttime', title: '数据开始时间', templet: "<div>{{layui.util.toDateString(d.starttime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'endtime', title: '数据结束时间', templet: "<div>{{layui.util.toDateString(d.endtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'fwd_group_id', title: '推送组'},
{field: 'state', title: '状态',templet: '#stateTrans'},
{title: '操作', toolbar: '#currentTableBar', fixed: "right", width: 120}
];
if([[${role}]] != "SUPER_ADMIN") {
cfg_cols[7].hide = true;
}
laydate.render({ laydate.render({
elem: '#ID-laydate-start-date1', elem: '#ID-laydate-start-date1',
type: 'datetime' type: 'datetime'
@ -85,17 +107,11 @@
table.render({ table.render({
elem: '#forwardParaTableId', elem: '#forwardParaTableId',
url: '/fwd/resend_records', url: '/fwd/resend_records',
toolbar: '#toolbarTable', toolbar: '#toolbarTop',
defaultToolbar: ['filter'], defaultToolbar: ['filter'],
cols: [[ cols: [
{field: 'projectid', title: '项目号', sort: true}, cfg_cols
{field: 'deviceid', title: '设备号'}, ],
{field: 'createtime', title: '推送时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'starttime', title: '数据开始时间', templet: "<div>{{layui.util.toDateString(d.starttime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'endtime', title: '数据结束时间', templet: "<div>{{layui.util.toDateString(d.endtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'fwd_group_id', title: '推送组'},
{field: 'state', title: '状态',templet: '#stateTrans'}
]],
limits: [10, 20, 50, 100, 150], limits: [10, 20, 50, 100, 150],
limit: 10, limit: 10,
page: true, page: true,
@ -119,8 +135,50 @@
return false; return false;
}); });
}); table.on('toolbar(forwardParaTableFilter)', function (obj) {
if (obj.event === 'add') { // 监听添加操作
var index = layer.open({
title: '添加补传记录',
type: 2,
shade: 0.2,
maxmin:true,
shadeClose: true,
area: ['100%', '100%'],
content: '../page/table/resend_record_add'
});
$(window).on("resize", function () {
layer.full(index);
});
}
});
table.on('tool(forwardParaTableFilter)', function (obj) {
var data = obj.data;
if (obj.event === 'delete') {
layer.confirm('确定删除?', function(index){
$.ajax({
type:"POST",
url:"/fwd/resend_records/delete",
data:{
'del_id':data.id
},
success: function (data) {
table.reload('forwardParaTableId');
},
error: function () {
console.log("ajax error");
}
});
layer.close(index);
});
}
});
});
function onUpdated(){
layui.table.reload('forwardParaTableId');
}
</script> </script>
<script type="text/html" id="stateTrans"> <script type="text/html" id="stateTrans">
@ -129,9 +187,9 @@
{{# } else if(d.state == 1){ }} {{# } else if(d.state == 1){ }}
<span class="layui-badge layui-bg-red">推送失败</span> <span class="layui-badge layui-bg-red">推送失败</span>
{{# } else if(d.state == 2){ }} {{# } else if(d.state == 2){ }}
<span class="layui-badge layui-bg-red">断点补传</span> <span class="layui-badge layui-bg-orange">断点补传</span>
{{# } else { }} {{# } else { }}
<span class="layui-badge layui-bg-orange">推送中</span> <span class="layui-badge layui-bg-blue">推送中</span>
{{# } }} {{# } }}
</script> </script>

View File

@ -89,12 +89,6 @@
<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">
@ -236,8 +230,9 @@
<hr th:if="${role=='SUPER_ADMIN'}"> <hr th:if="${role=='SUPER_ADMIN'}">
<div class="layui-form-item"> <div class="layui-form-item">
<div class="layui-input-block" style="float:right" > <div class="layui-input-block" style="float:right" >
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="saveBtn">确认保存</button> <button class="layui-btn layui-btn-normal" lay-submit lay-filter="saveBtn">保存</button>
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="initLocBtn">取初值</button> <button class="layui-btn layui-btn-normal" lay-submit lay-filter="initLocBtn">取初值</button>
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="contBtn">续数据</button>
</div> </div>
</div> </div>
</div> </div>
@ -292,6 +287,47 @@
return false; return false;
}); });
form.on('submit(contBtn)', function (data) {
// 弹出输入框
layer.prompt({
title: '请输入接续的设备编号和日期时间,用逗号隔开', // 弹窗标题
formType: 0, // 0-文本输入框
maxlength: 50, // 最大输入长度
value: '', // 初始值
btn: ['确定', '取消'] // 按钮组
}, function(value, index) { // 确定按钮回调
// 处理输入结果
if(value) {
$.ajax({
type:"POST",
url:"/gnss/device/cont_loc",
data:{
'deviceid':$('#deviceid').val(),
'createtime':value
},
success: function (result) {
if(result.code == 0) {
$('#ipose').val(result.ipose);
$('#iposn').val(result.iposn);
$('#iposd').val(result.iposd);
}
else{
layer.alert(result.msg);
}
},
error: function () {
console.log("ajax error");
}
});
} else {
layer.msg('输入内容不能为空!', {icon: 2});
}
// 关闭当前弹窗
layer.close(index);
});
return false;
});
form.on('select(device_type)', function (data) { form.on('select(device_type)', function (data) {
setEcefEditor(); setEcefEditor();
}); });
@ -326,6 +362,7 @@
} }
layui.form.render(); layui.form.render();
} }
</script> </script>
</body> </body>

View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>补传数据</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="../../lib/layui-v2.6.3/css/layui.css" media="all">
<link rel="stylesheet" href="../../css/public.css" media="all">
<style>
body {
background-color: #ffffff;
}
</style>
</head>
<body>
<div class="layui-form layuimini-form">
<input type="hidden" name="id" id="id">
<div class="layui-form-item">
<label class="layui-form-label">所属部门</label>
<div class="layui-input-inline">
<select name="tenantid" id="tenantid" lay-search="" lay-filter="tenant">
<option value="">全部</option>
<option th:each="item : ${tenant_list}" th:text="${item.name}" th:value="${item.id}"></option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">所属项目</label>
<div class="layui-input-inline">
<select name="project_id" id="projectid" lay-search="" lay-filter="project">
<option value="">全部</option>
<option th:each="item : ${project_list}" th:text="${item.project_id}" th:value="${item.project_id}"></option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">设备编号</label>
<div class="layui-input-inline">
<input type="text" name="deviceid" id="deviceid" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label required">推送组</label>
<div class="layui-input-inline">
<select name="fwd_group_id" id="fwd_group_id" lay-verify="required" lay-reqtext="不能为空" lay-search="">
<option th:each="item : ${gnss_group_fwd_list}" th:text="${item.name}" th:value="${item.name}"></option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label required">时间段</label>
<div class="layui-input-inline">
<input type="text" name="starttime" autocomplete="off" lay-verify="required" lay-reqtext="不能为空" id="ID-laydate-start-date" class="layui-input" placeholder="开始日期">
</div>
<div class="layui-input-inline">
<input type="text" name="endtime" autocomplete="off" lay-verify="required" lay-reqtext="不能为空" id="ID-laydate-end-date" class="layui-input" placeholder="结束日期">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="saveBtn">确认保存</button>
</div>
</div>
</div>
<script src="../../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script>
layui.use(['form'], function () {
var form = layui.form,
$ = layui.$,
laydate = layui.laydate;
var iframeIndex = parent.layer.getFrameIndex(window.name);
laydate.render({
elem: '#ID-laydate-start-date',
type: 'datetime'
});
laydate.render({
elem: '#ID-laydate-end-date',
type: 'datetime'
});
// 所属部门下拉框改变,修改项目下拉框
form.on('select(tenant)', function (data) {
console.log(data.value);
$.ajax({
type:"GET",
url:"/gnss/device/q_project",
data:{
'tenantid':data.value
},
success: function(result) {
$('#projectid').empty();
$('#projectid').append(new Option("全部", ""));
$('#deviceid').empty();
//console.log(result);
$.each(result.data, function (index, item) {
$('#projectid')
.append(new Option(item.project_id, item.project_id));
});
layui.form.render("select");
},
error: function () {
console.log("ajax error");
}
});
});
//监听提交
form.on('submit(saveBtn)', function (data) {
$.ajax({
type:"POST",
url:"/fwd/resend_records/add",
contentType: "application/json;charset=UTF-8",
data: JSON.stringify(data.field),
success: function (result) {
parent.onUpdated();
parent.layer.close(iframeIndex);
},
error: function () {
console.log("ajax error");
parent.layer.close(iframeIndex);
}
});
return false;
});
});
</script>
</body>
</html>

View File

@ -3,12 +3,10 @@ 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;
// 将远程主机改为本地端口改为12000 final private String remoteHost="8.134.185.53";
final private String remoteHost="127.0.0.1";
//final private String remoteHost="100.91.37.6"; //final private String remoteHost="100.91.37.6";
//final private String remoteHost="47.107.50.52"; //final private String remoteHost="47.107.50.52";
//final private String remoteHost="8.134.185.53"; final private int remotePort=9903;
final private int remotePort=12000;
public static final Ntrip2Channels INSTANCE = new Ntrip2Channels(); public static final Ntrip2Channels INSTANCE = new Ntrip2Channels();

View File

@ -1,185 +0,0 @@
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());
}
}
}
}