新增单次解算缓存服务和页面控制类

- 新增 GnssSingleBufferService 服务类,用于持久化保存单次解算的结果
- 新增单次解算有关的页面控制类和前端页面
This commit is contained in:
fengyarnom 2024-11-05 09:37:08 +08:00
parent 27783443c2
commit 905796a7ea
8 changed files with 492 additions and 4 deletions

View File

@ -3,16 +3,14 @@ package com.imdroid.sideslope.calc;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.imdroid.common.util.DataTypeUtil;
import com.imdroid.common.util.ThreadManager;
import com.imdroid.secapi.dto.GnssCalcData;
import com.imdroid.secapi.dto.GnssCalcDataMapper;
import com.imdroid.secapi.dto.GnssGroupCalc;
import com.imdroid.secapi.dto.GnssGroupCalcMapper;
import com.imdroid.secapi.dto.*;
import com.imdroid.sideslope.bd.*;
import com.imdroid.sideslope.message.D341LocationMessage;
import com.imdroid.sideslope.sal.Device;
import com.imdroid.sideslope.sal.DeviceService;
import com.imdroid.sideslope.server.DeviceChannel;
import com.imdroid.sideslope.server.OnlineChannels;
import com.imdroid.sideslope.service.GnssSingleBufferService;
import com.imdroid.sideslope.service.WarningService;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@ -56,6 +54,9 @@ public class SingleLineGNSSCalcService implements GNSSDataCalcService {
@Autowired
GnssCalcDataMapper dataMapper;
@Autowired
private GnssSingleBufferService gnssSingleDataService;
// 非线程安全需加同步保护
List<GnssGroupCalc> groupCalcList;
@ -121,21 +122,27 @@ public class SingleLineGNSSCalcService implements GNSSDataCalcService {
// 读取惯导
Tilt tilt = message.getTilt();
if(tilt != null) {
focusCalculator.addTilt(tilt);
if (logger.isDebugEnabled()) {
logger.debug("测站{}惯导单次解析结果:{}", deviceId,tilt);
}
}
// 延迟
focusCalculator.addDelayMs(message.getPps());
// 单次b562
double[] doubles = message.getB562_loc();//unit: mm
if(doubles !=null) {
focusCalculator.addXyz(doubles, message.getCreateTime());
logger.info("测站{}的b562单次解析结果:{}", deviceId,Arrays.toString(doubles));
}
// 单次GGA
@ -144,11 +151,39 @@ public class SingleLineGNSSCalcService implements GNSSDataCalcService {
focusCalculator.addGGA(gga);
logger.info("测站{}的gga单次解析结果:{},{},{},{}",deviceId,
gga.getLongitude(), gga.getLatitude(), gga.getAltitude(), gga.getQuality());
if(groupCalc.getVer() == 7 && focusCalculator.isJump()){
logger.info("{}发生周跳",deviceId);
hardResetDevice(deviceId);
}
}
// 保存单次解析的原始数据 loggingmode 字段控制
GnssSingleData gnssSingleData = new GnssSingleData();
gnssSingleData.setDeviceid(device.getDeviceId());
gnssSingleData.setCreatetime(LocalDateTime.now());
gnssSingleData.setModel(device.getModel());
// 若是该设备开启了日志记录则保存单次解析的数据
if(device.getLoggingmode() == GnssDevice.LOGGING_MODE_FULL){
if((device.getModel() == GnssDevice.MODEL_G505) && (doubles !=null)){
gnssSingleData.setX(doubles[0]);
gnssSingleData.setY(doubles[1]);
gnssSingleData.setZ(doubles[2]);
gnssSingleData.setStatus((int) doubles[3]);
gnssSingleDataService.addData(gnssSingleData);
}
else if((device.getModel() == GnssDevice.MODEL_G510) && (gga !=null)){
gnssSingleData.setX(gga.getLatitude());
gnssSingleData.setY(gga.getLongitude());
gnssSingleData.setZ(gga.getAltitude());
gnssSingleData.setStatus(gga.getQuality());
gnssSingleDataService.addData(gnssSingleData);
}
}
}
@Override

View File

@ -9,6 +9,7 @@ import com.imdroid.sideslope.message.D3F2StopIndicationMessage;
import com.imdroid.sideslope.sal.Device;
import com.imdroid.sideslope.sal.DeviceService;
import com.imdroid.sideslope.service.DataPersistService;
import com.imdroid.sideslope.service.GnssSingleBufferServiceImpl;
import com.imdroid.sideslope.service.WarningService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -39,6 +40,9 @@ public class D3F2StopIndicationMessageExecutor implements Executor<D3F2StopIndic
@Autowired
WarningService warningService;
@Autowired
GnssSingleBufferServiceImpl gnssSingleBufferService;
@Override
public Void execute(D3F2StopIndicationMessage message) {
@ -63,6 +67,10 @@ public class D3F2StopIndicationMessageExecutor implements Executor<D3F2StopIndic
LocalDateTime uploadTime = multiLineGNSSCalcService.checkUploadTime(
device.getDeviceId(), device.getTenantId());
dataPersistService.saveDeviceTrxStat(message, (uploadTime!=null), device);
// 保存缓存中的 GNSS 单次解析结果
gnssSingleBufferService.flush();
// 通知beidou服务设备休眠
try {
beidouClient.onDeviceStop(deviceId, device.getTenantId());

View File

@ -30,6 +30,7 @@ public class DbDeviceServiceImpl implements DeviceService {
device.setTenantId(gnssDevice.getTenantid());
device.setDeviceId(gnssDevice.getDeviceid());
device.setDeviceType(gnssDevice.getDevicetype());
device.setModel(gnssDevice.getModel());
device.setParentId(gnssDevice.getParentid());
device.setName(gnssDevice.getName());
device.setProjectId(gnssDevice.getProject_id());

View File

@ -0,0 +1,10 @@
package com.imdroid.sideslope.service;
import com.imdroid.secapi.dto.GnssSingleData;
public interface GnssSingleBufferService {
void addData(GnssSingleData data);
void flush();
void clear();
int getBufferSize();
}

View File

@ -0,0 +1,84 @@
package com.imdroid.sideslope.service;
import com.imdroid.secapi.dto.GnssSingleData;
import com.imdroid.secapi.dto.GnssSingleDataMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class GnssSingleBufferServiceImpl implements GnssSingleBufferService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final List<GnssSingleData> buffer = new ArrayList<>();
// BUFFER_SIZE 暂定此大小
private static final int BUFFER_SIZE = 1024;
// 资源锁
private final Object lock = new Object();
@Autowired
private GnssSingleDataMapper gnssDataMapper;
@Autowired
private GnssSingleDataMapper gnssSingleDataMapper;
@Override
public void addData(GnssSingleData data) {
if (data == null) {
return;
}
synchronized (lock) {
buffer.add(data);
if (buffer.size() >= BUFFER_SIZE) {
// 溢出时直接保存
// 此处暂定但很有必要引入一个线程来保存避免阻塞主线程
flush();
}
}
}
@Override
public void flush() {
synchronized (lock) {
if (buffer.isEmpty()) {
return;
}
try {
List<GnssSingleData> batchList = new ArrayList<>(buffer);
buffer.clear();
// 由于每秒写数据库操作频繁这里采用批量保存的方式
saveBatch(batchList);
logger.debug("批量插入{}条数据成功", batchList.size());
} catch (Exception e) {
logger.error("批量插入数据失败", e);
throw e;
}
}
}
private void saveBatch(List<GnssSingleData> dataList) {
for (GnssSingleData data : dataList) {
gnssSingleDataMapper.insert(data);
}
}
@Override
public void clear() {
synchronized (lock) {
buffer.clear();
}
}
@Override
public int getBufferSize() {
synchronized(lock) {
return buffer.size();
}
}
}

View File

@ -0,0 +1,95 @@
package com.imdroid.beidou.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imdroid.beidou.service.CommonExcelService;
import com.imdroid.secapi.dto.GnssCalcData;
import com.imdroid.secapi.dto.GnssCalcDataMapper;
import com.imdroid.secapi.dto.GnssSingleData;
import com.imdroid.secapi.dto.GnssSingleDataMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Controller
public class GnssSingleDataController extends BasicController implements CommonExcelService<GnssSingleData, GnssSingleData> {
@Autowired
GnssSingleDataMapper gnssSingleDataMapper;
final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@RequestMapping("/page/gnss_single_data")
public String gnssSingleData(Model m, HttpSession session){
initModel(m, session);
return "/page/gnss_single_data";
}
@RequestMapping("/gnss/data/list_single_data")
@ResponseBody
public JSONObject listData(HttpSession session, Integer page, Integer limit, String searchParams) {
// 检查查询条件
JSONObject search = null;
if (searchParams != null) {
search = (JSONObject) JSONObject.parse(searchParams);
System.out.println(search);
String deviceId = search.getString("deviceid");
Integer freqency = search.getInteger("freqency");
String begin = search.getString("dgt_.createtime");
String end = search.getString("dlt_.createtime");
if(deviceId != null && !deviceId.isEmpty() && freqency!=0){
Page pageable = new Page<>(page == null ? 1 : page, limit == null ? 10 : limit);
// 缺省按1小时采样如果时间跨度较大则按最多300条记录的采样率采样
int sample = 6;
if(freqency == 2){
LocalDateTime endTime = LocalDateTime.now();
LocalDateTime beginTime = endTime.minusDays(30);
if(begin!=null && !begin.isEmpty()){
beginTime = LocalDateTime.parse(begin,df);
}
if(end!=null && !end.isEmpty()){
endTime = LocalDateTime.parse(end,df);
}
Duration duration = Duration.between(beginTime, endTime);
int hours = (int)duration.toHours();
if(hours > 300) sample = hours*6/300;
}
Page<GnssSingleData> calcDataList = gnssSingleDataMapper.queryByDeviceId(pageable, deviceId,begin,end,sample);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "");
jsonObject.put("count", calcDataList.getTotal());
jsonObject.put("data", calcDataList.getRecords());
return jsonObject;
}
}
return this.pageList(session, page, limit, searchParams);
}
@Override
public Class<GnssSingleData> getEntityClass() {
return GnssSingleData.class;
}
@Override
public BaseMapper<GnssSingleData> getMapper() {
return gnssSingleDataMapper;
}
}

View File

@ -53,6 +53,12 @@
"icon": "fa fa-calculator",
"target": "_self"
},
{
"title": "单次解算记录",
"href": "page/gnss_single_data",
"icon": "fa fa-clipboard",
"target": "_self"
},
{
"title": "配置管理",
"href": "",

View File

@ -0,0 +1,249 @@
<!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">
</head>
<body>
<div class="layuimini-container">
<div class="layuimini-main">
<fieldset class="table-search-fieldset">
<legend>搜索信息</legend>
<div style="margin: 10px 10px 10px 10px">
<form class="layui-form layui-form-pane" action="" id="searchFrm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">设备号</label>
<div class="layui-input-inline">
<input type="text" name="deviceid" id="input_deviceid" class="layui-input" style="position:absolute;z-index:2;width:85%;" value="" autocomplete="off">
<select id="deviceid" lay-search="" lay-filter="device" >
<option value="">全部</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">范围</label>
<div class="layui-input-inline">
<input type="text" name="dgt_createtime" autocomplete="off" id="ID-laydate-start-date" class="layui-input" placeholder="开始日期">
</div>
<div class="layui-input-inline">
<input type="text" name="dlt_createtime" autocomplete="off" id="ID-laydate-end-date" class="layui-input" placeholder="结束日期">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">粒度</label>
<div class="layui-input-inline">
<select name="freqency" id="freqency" lay-search="">
<option value="0"></option>
<option value="1">小时</option>
<option value="2">自动</option>
</select>
</div>
</div>
<div class="layui-inline">
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-search-btn"><i class="layui-icon">&#xe615;</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>
</form>
</div>
</fieldset>
<div class="layui-tab layui-tab-card" lay-filter="data-tab">
<ul class="layui-tab-title">
<li class="layui-this">数据表格</li>
<li>单个设备曲线</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<table class="layui-hide" id="currentTableId" lay-filter="currentTableFilter"></table>
</div>
<div class="layui-tab-item">
<div id="echarts-gnss" style="min-height:300px;padding: 10px"></div>
</div>
</div>
</div>
<script type="text/html" id="currentTableBar">
<a class="layui-btn layui-btn-normal layui-btn-xs data-count-edit" lay-event="cmd">命令行</a>
</script>
</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 th:inline="javascript">
layui.use(['form', 'table','laydate','element'], function () {
var $ = layui.$,
form = layui.form,
table = layui.table,
laydate = layui.laydate;
var searchDeviceId = false;
var echartsDevice = echarts.init(document.getElementById('echarts-gnss'), 'walden');
var data_cols = [
{field: 'deviceid', title: '设备号'},
{field: 'createtime', title: '产生时间', width:'18%', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'x', title: '东'},
{field: 'y', title: '北'},
{field: 'z', title: '天'},
{field: 'status', title: '状态'}
];
/**
* 初始化表单,要加上,不然刷新部分组件可能会不加载
*/
form.render();
laydate.render({
elem: '#ID-laydate-start-date',
type: 'datetime'
});
laydate.render({
elem: '#ID-laydate-end-date',
type: 'datetime'
});
table.render({
elem: '#currentTableId',
url: '/gnss/data/list_single_data',
toolbar: '#toolbarDemo', //开启头部工具栏
defaultToolbar: ['filter'],
cols: [
data_cols
],
limits: [20, 50, 100, 200, 300],
limit: 20,
page: true,
skin: 'line',
done: function (result, curr, count) {
if(searchDeviceId){
console.log(result.data);
showChart(result.data)
}
}
});
// 监听搜索操作
form.on('submit(data-search-btn)', function (data) {
var result = JSON.stringify(data.field);
var deviceId = $('#input_deviceid').val();
searchDeviceId = !isNaN(deviceId);
//执行搜索重载
table.reload('currentTableId', {
page: {
curr: 1
}
, where: {
searchParams: result
}
}, 'data');
return false;
});
// 监听导出操作
form.on('submit(data-export-btn)', function (data) {
var result = $('#searchFrm').serialize();
var u = "/gnss/data/calc/export?" + result;
window.open(u, "_blank");
return false;
});
form.on('select(device)', function (data) {
//对select和input进行监控渲染
if (data.value != "") {
$("#input_deviceid").val(data.value);
}
});
//监听Tab切换重新resize图表否则显示不出来
layui.element.on('tab(data-tab)', function(data){
if (data.index == 1) {
echartsDevice.resize();
}
});
function showChart(chartData){
var sortedData = chartData.sort((a, b) => new Date(a.createtime) - new Date(b.createtime));
// 处理数据
var times = sortedData.map(item => {
let date = new Date(item.createtime);
return date.toLocaleTimeString();
});
var xValues = sortedData.map(item => item.x);
var yValues = sortedData.map(item => item.y);
var zValues = sortedData.map(item => item.z);
// 图表配置
var option = {
title: {
text: 'GNSS 单次解记录',
left: 'center'
},
tooltip: {
trigger: 'axis',
formatter: function(params) {
let result = params[0].name + '<br/>';
params.forEach(param => {
result += param.seriesName + ': ' +
param.value.toFixed(2) + '<br/>';
});
return result;
}
},
legend: {
data: ['X坐标', 'Y坐标', 'Z坐标'],
top: '30px'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: times,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}'
}
},
series: [
{
name: 'X坐标',
type: 'line',
data: xValues,
smooth: true,
},
{
name: 'Y坐标',
type: 'line',
data: yValues,
smooth: true,
},
{
name: 'Z坐标',
type: 'line',
data: zValues,
smooth: true,
}
]
};
echartsDevice.setOption(option);
}
});
</script>
</body>
</html>