优化坐标转换和轨迹数据处理,增加坐标有效性验证,过滤无效坐标点,提升性能和准确性
This commit is contained in:
parent
05341e2a77
commit
25b11bbe04
|
|
@ -19,6 +19,7 @@ import java.net.ServerSocket;
|
|||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* JTT808服务器配置类
|
||||
|
|
@ -41,6 +42,7 @@ public class JTT808ServerConfig {
|
|||
|
||||
private ServerSocket serverSocket;
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private final AtomicInteger activeConnections = new AtomicInteger(0);
|
||||
private ThreadPoolTaskExecutor executor;
|
||||
|
||||
@Bean
|
||||
|
|
@ -129,26 +131,31 @@ public class JTT808ServerConfig {
|
|||
* 接受客户端连接
|
||||
*/
|
||||
private void acceptConnections() {
|
||||
int connectionCount = 0;
|
||||
|
||||
int totalConnectionCount = 0; // 总连接计数(仅用于日志)
|
||||
|
||||
while (running.get() && !serverSocket.isClosed()) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
connectionCount++;
|
||||
|
||||
totalConnectionCount++;
|
||||
|
||||
String clientAddress = clientSocket.getRemoteSocketAddress().toString();
|
||||
logger.info("收到新连接 #{}: {}", connectionCount, clientAddress);
|
||||
|
||||
if (connectionCount > maxConnections) {
|
||||
logger.warn("连接数超过限制 {}, 拒绝连接: {}", maxConnections, clientAddress);
|
||||
int currentActive = activeConnections.get();
|
||||
logger.info("收到新连接 #{} (当前活跃: {}): {}", totalConnectionCount, currentActive, clientAddress);
|
||||
|
||||
// 检查活跃连接数
|
||||
if (currentActive >= maxConnections) {
|
||||
logger.warn("活跃连接数达到限制 {} (总计: {}), 拒绝连接: {}",
|
||||
maxConnections, totalConnectionCount, clientAddress);
|
||||
clientSocket.close();
|
||||
connectionCount--;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 增加活跃连接计数
|
||||
activeConnections.incrementAndGet();
|
||||
|
||||
// 为每个客户端启动处理线程
|
||||
executor.execute(new ClientHandler(clientSocket, connectionCount));
|
||||
|
||||
executor.execute(new ClientHandler(clientSocket, totalConnectionCount));
|
||||
|
||||
} catch (IOException e) {
|
||||
if (running.get()) {
|
||||
logger.error("接受客户端连接异常: {}", e.getMessage());
|
||||
|
|
@ -179,30 +186,34 @@ public class JTT808ServerConfig {
|
|||
@Override
|
||||
public void run() {
|
||||
String clientAddress = clientSocket.getRemoteSocketAddress().toString();
|
||||
logger.info("连接 #{} 开始处理: {}", connectionId, clientAddress);
|
||||
|
||||
int currentActive = activeConnections.get();
|
||||
logger.info("连接 #{} 开始处理 (活跃连接: {}): {}", connectionId, currentActive, clientAddress);
|
||||
|
||||
try {
|
||||
// 设置TCP参数
|
||||
clientSocket.setSoTimeout(30000); // 30秒读取超时
|
||||
clientSocket.setTcpNoDelay(true);
|
||||
clientSocket.setKeepAlive(true);
|
||||
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
|
||||
|
||||
// 持续读取数据
|
||||
while ((bytesRead = clientSocket.getInputStream().read(buffer)) != -1) {
|
||||
if (bytesRead > 0) {
|
||||
processJTT808Message(buffer, bytesRead, clientAddress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.info("连接 #{} 异常断开: {} - {}", connectionId, clientAddress, e.getMessage());
|
||||
} finally {
|
||||
// 减少活跃连接计数
|
||||
int remaining = activeConnections.decrementAndGet();
|
||||
|
||||
try {
|
||||
clientSocket.close();
|
||||
logger.info("连接 #{} 已关闭: {}", connectionId, clientAddress);
|
||||
logger.info("连接 #{} 已关闭 (剩余活跃连接: {}): {}", connectionId, remaining, clientAddress);
|
||||
} catch (IOException e) {
|
||||
logger.error("关闭连接异常: {}", e.getMessage());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ public class Jtt808DataController {
|
|||
@Autowired
|
||||
private IJtt808DataService jtt808DataService;
|
||||
|
||||
@Autowired
|
||||
private IJtt808CommandService jtt808CommandService;
|
||||
|
||||
/**
|
||||
* 查询终端列表
|
||||
*/
|
||||
|
|
@ -252,10 +255,9 @@ public class Jtt808DataController {
|
|||
*/
|
||||
@ApiOperation("批量检查终端在线状态")
|
||||
@GetMapping("/command/online-status-batch")
|
||||
public AjaxResult checkOnlineStatusBatch(
|
||||
@ApiParam("终端手机号列表(逗号分隔)") @RequestParam String phoneNumbers) {
|
||||
public AjaxResult checkOnlineStatusBatch(@ApiParam("终端手机号列表(逗号分隔)") @RequestParam String phoneNumbers) {
|
||||
try {
|
||||
List<String> phoneList = Arrays.asList(phoneNumbers.split(","));
|
||||
String[] phoneList = phoneNumbers.split(",");
|
||||
List<Map<String, Object>> results = new java.util.ArrayList<>();
|
||||
|
||||
for (String phoneNumber : phoneList) {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,13 @@ public interface Jtt808LocationMapper {
|
|||
@Param("year") Integer year,
|
||||
@Param("month") Integer month);
|
||||
|
||||
/**
|
||||
* 查询指定月份的所有位置数据(用于批量计算里程)
|
||||
*/
|
||||
List<Jtt808Location> selectMonthlyLocations(@Param("phoneNumber") String phoneNumber,
|
||||
@Param("year") Integer year,
|
||||
@Param("month") Integer month);
|
||||
|
||||
/**
|
||||
* 查询指定日期范围的位置数据
|
||||
*/
|
||||
|
|
@ -67,14 +74,12 @@ public interface Jtt808LocationMapper {
|
|||
/**
|
||||
* 统计指定时间段的位置点数量
|
||||
*/
|
||||
int countLocationsByDate(@Param("phoneNumber") String phoneNumber,
|
||||
@Param("targetDate") String targetDate);
|
||||
int countLocationsByDate(@Param("phoneNumber") String phoneNumber, @Param("targetDate") String targetDate);
|
||||
|
||||
/**
|
||||
* 查询指定日期的速度统计信息
|
||||
*/
|
||||
Map<String, Object> selectSpeedStatsByDate(@Param("phoneNumber") String phoneNumber,
|
||||
@Param("targetDate") String targetDate);
|
||||
Map<String, Object> selectSpeedStatsByDate(@Param("phoneNumber") String phoneNumber, @Param("targetDate") String targetDate);
|
||||
|
||||
List<Jtt808Location> selectLocationList();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.bonus.sgzb.material.mapper.Jtt808LocationMapper;
|
|||
import com.bonus.sgzb.material.mapper.Jtt808MessageLogMapper;
|
||||
import com.bonus.sgzb.material.mapper.Jtt808TerminalMapper;
|
||||
import com.bonus.sgzb.material.service.IJtt808DataService;
|
||||
import com.bonus.sgzb.material.utils.CoordinateConverter;
|
||||
import com.bonus.sgzb.material.utils.JTT808Parser.LocationInfo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -226,7 +227,18 @@ public class Jtt808DataServiceImpl implements IJtt808DataService {
|
|||
@Override
|
||||
public List<Jtt808Location> getLocationTrack(String phoneNumber, Date startTime, Date endTime) {
|
||||
try {
|
||||
return locationMapper.selectLocationTrack(phoneNumber, startTime, endTime);
|
||||
List<Jtt808Location> locations = locationMapper.selectLocationTrack(phoneNumber, startTime, endTime);
|
||||
|
||||
// 过滤掉连续重复的坐标点,提升轨迹回放体验
|
||||
List<Jtt808Location> filteredLocations = filterDuplicateLocations(locations);
|
||||
|
||||
logger.debug("轨迹查询完成: phoneNumber={}, 原始点数={}, 过滤后点数={}, 过滤率={}%",
|
||||
phoneNumber,
|
||||
locations.size(),
|
||||
filteredLocations.size(),
|
||||
!locations.isEmpty() ? String.format("%.2f", (1 - (double)filteredLocations.size() / locations.size()) * 100) : "0.00");
|
||||
|
||||
return filteredLocations;
|
||||
} catch (Exception e) {
|
||||
logger.error("查询位置轨迹异常: {} - {}", phoneNumber, e.getMessage());
|
||||
return new ArrayList<>();
|
||||
|
|
@ -426,20 +438,41 @@ public class Jtt808DataServiceImpl implements IJtt808DataService {
|
|||
|
||||
/**
|
||||
* 将LocationInfo转换为Jtt808Location
|
||||
* 注意:JTT808协议使用WGS-84坐标系,需要转换为GCJ-02(高德地图坐标系)
|
||||
*/
|
||||
private Jtt808Location convertToJtt808Location(String phoneNumber, LocationInfo locationInfo, String clientIp) {
|
||||
Jtt808Location location = new Jtt808Location();
|
||||
location.setPhoneNumber(phoneNumber);
|
||||
location.setAlarmFlag(locationInfo.getAlarmFlag());
|
||||
location.setStatusFlag(locationInfo.getStatus());
|
||||
location.setLatitude(BigDecimal.valueOf(locationInfo.getLatitude()));
|
||||
location.setLongitude(BigDecimal.valueOf(locationInfo.getLongitude()));
|
||||
|
||||
// ⭐ 坐标转换:WGS-84(GPS原始坐标) -> GCJ-02(高德地图坐标)
|
||||
// JTT808协议规定设备上报WGS-84坐标,但高德地图使用GCJ-02坐标系
|
||||
// 不转换会导致地图显示偏移约50-500米
|
||||
double wgs84Lat = locationInfo.getLatitude();
|
||||
double wgs84Lng = locationInfo.getLongitude();
|
||||
|
||||
// 过滤无效坐标(0.0, 0.0)- 设备未激活或未定位
|
||||
if (wgs84Lat == 0.0 && wgs84Lng == 0.0) {
|
||||
logger.debug("检测到无效坐标 (0.0, 0.0),设备可能未激活或未定位: {}", phoneNumber);
|
||||
location.setLatitude(BigDecimal.valueOf(0.0));
|
||||
location.setLongitude(BigDecimal.valueOf(0.0));
|
||||
} else {
|
||||
// 转换坐标
|
||||
double[] gcj02 = CoordinateConverter.wgs84ToGcj02(wgs84Lat, wgs84Lng);
|
||||
location.setLatitude(BigDecimal.valueOf(gcj02[0]));
|
||||
location.setLongitude(BigDecimal.valueOf(gcj02[1]));
|
||||
|
||||
logger.debug("坐标转换: WGS-84({}, {}) -> GCJ-02({}, {})",
|
||||
wgs84Lat, wgs84Lng, gcj02[0], gcj02[1]);
|
||||
}
|
||||
|
||||
location.setAltitude(locationInfo.getAltitude());
|
||||
location.setSpeed(BigDecimal.valueOf(locationInfo.getSpeed()));
|
||||
location.setDirection(locationInfo.getDirection());
|
||||
location.setLocationTime(Timestamp.valueOf(locationInfo.getLocationTime()));
|
||||
location.setClientIp(clientIp);
|
||||
|
||||
|
||||
// 转换附加信息为JSON格式
|
||||
if (locationInfo.getAdditionalInfo() != null && !locationInfo.getAdditionalInfo().isEmpty()) {
|
||||
try {
|
||||
|
|
@ -450,7 +483,115 @@ public class Jtt808DataServiceImpl implements IJtt808DataService {
|
|||
location.setAdditionalInfo("{}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return location;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤掉连续重复的坐标点和无效坐标
|
||||
* 只保留位置发生变化的轨迹点,避免轨迹回放时静止不动
|
||||
* 同时过滤掉非中国大陆区域的异常坐标
|
||||
*
|
||||
* @param locations 原始位置列表
|
||||
* @return 过滤后的位置列表
|
||||
*/
|
||||
private List<Jtt808Location> filterDuplicateLocations(List<Jtt808Location> locations) {
|
||||
List<Jtt808Location> filteredLocations = new ArrayList<>();
|
||||
|
||||
if (locations == null || locations.isEmpty()) {
|
||||
return filteredLocations;
|
||||
}
|
||||
|
||||
int invalidLocationCount = 0;
|
||||
Jtt808Location previousLocation = null;
|
||||
|
||||
// 遍历所有位置点
|
||||
for (int i = 0; i < locations.size(); i++) {
|
||||
Jtt808Location currentLocation = locations.get(i);
|
||||
|
||||
// 1. 首先验证坐标是否有效(是否在中国大陆范围内)
|
||||
if (!isValidChinaLocation(currentLocation)) {
|
||||
invalidLocationCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 如果是第一个有效点,直接保留
|
||||
if (previousLocation == null) {
|
||||
filteredLocations.add(currentLocation);
|
||||
previousLocation = currentLocation;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 判断坐标是否发生变化(经纬度任一发生变化即认为位置改变)
|
||||
boolean locationChanged = !isSameLocation(previousLocation, currentLocation);
|
||||
|
||||
if (locationChanged) {
|
||||
filteredLocations.add(currentLocation);
|
||||
previousLocation = currentLocation;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidLocationCount > 0) {
|
||||
logger.debug("过滤了 {} 个无效坐标点(非中国大陆区域或0.0坐标)", invalidLocationCount);
|
||||
}
|
||||
|
||||
return filteredLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个位置是否相同(经纬度都相同)
|
||||
*
|
||||
* @param loc1 位置1
|
||||
* @param loc2 位置2
|
||||
* @return 是否相同
|
||||
*/
|
||||
private boolean isSameLocation(Jtt808Location loc1, Jtt808Location loc2) {
|
||||
if (loc1 == null || loc2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用compareTo避免浮点数精度问题
|
||||
boolean sameLongitude = loc1.getLongitude() != null && loc2.getLongitude() != null
|
||||
&& loc1.getLongitude().compareTo(loc2.getLongitude()) == 0;
|
||||
boolean sameLatitude = loc1.getLatitude() != null && loc2.getLatitude() != null
|
||||
&& loc1.getLatitude().compareTo(loc2.getLatitude()) == 0;
|
||||
|
||||
return sameLongitude && sameLatitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证经纬度是否在中国大陆范围内
|
||||
* 中国大陆经纬度范围:
|
||||
* - 纬度:18°N - 54°N (海南岛最南端到黑龙江最北端)
|
||||
* - 经度:73°E - 135°E (新疆最西端到黑龙江最东端)
|
||||
*
|
||||
* @param location 位置信息
|
||||
* @return true-有效的中国大陆坐标,false-无效坐标
|
||||
*/
|
||||
private boolean isValidChinaLocation(Jtt808Location location) {
|
||||
if (location == null || location.getLongitude() == null || location.getLatitude() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double longitude = location.getLongitude().doubleValue();
|
||||
double latitude = location.getLatitude().doubleValue();
|
||||
|
||||
// 过滤掉 0.0, 0.0 这种明显错误的坐标
|
||||
if (longitude == 0.0 && latitude == 0.0) {
|
||||
logger.debug("过滤无效坐标: (0.0, 0.0)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证是否在中国大陆范围内
|
||||
// 纬度范围:18°N - 54°N
|
||||
// 经度范围:73°E - 135°E
|
||||
boolean isInChina = latitude >= 18.0 && latitude <= 54.0
|
||||
&& longitude >= 73.0 && longitude <= 135.0;
|
||||
|
||||
if (!isInChina) {
|
||||
logger.debug("过滤非中国大陆坐标: ({}, {})", longitude, latitude);
|
||||
}
|
||||
|
||||
return isInChina;
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,7 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
public MileageStatisticsVo.MonthlyMileageData getMonthlyMileageData(String phoneNumber, Integer year, Integer month) {
|
||||
try {
|
||||
logger.info("获取月度里程数据: phoneNumber={}, year={}, month={}", phoneNumber, year, month);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
MileageStatisticsVo.MonthlyMileageData result = new MileageStatisticsVo.MonthlyMileageData();
|
||||
result.setPhoneNumber(phoneNumber);
|
||||
|
|
@ -79,6 +80,43 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
// 【性能优化】一次性查询整个月的位置数据,避免N+1查询问题
|
||||
List<Jtt808Location> monthlyLocations = locationMapper.selectMonthlyLocations(phoneNumber, year, month);
|
||||
logger.debug("查询到当月位置点总数: {}", monthlyLocations.size());
|
||||
|
||||
// 【性能优化】先过滤无效坐标,减少后续计算量
|
||||
List<Jtt808Location> validMonthlyLocations = new ArrayList<>();
|
||||
int invalidLocationCount = 0;
|
||||
for (Jtt808Location location : monthlyLocations) {
|
||||
if (isValidChinaLocation(location)) {
|
||||
validMonthlyLocations.add(location);
|
||||
} else {
|
||||
invalidLocationCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidLocationCount > 0) {
|
||||
logger.info("月度统计过滤了 {} 个无效坐标点(非中国大陆区域或0.0坐标)", invalidLocationCount);
|
||||
}
|
||||
|
||||
logger.debug("有效位置点总数: {}", validMonthlyLocations.size());
|
||||
|
||||
// 按日期分组位置数据(只分组有效数据)
|
||||
Map<String, List<Jtt808Location>> locationsByDate = validMonthlyLocations.stream()
|
||||
.collect(Collectors.groupingBy(location -> {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return sdf.format(location.getLocationTime());
|
||||
}));
|
||||
|
||||
// 【性能优化】一次性计算所有天的里程,避免重复遍历
|
||||
Map<String, Double> mileageByDate = new HashMap<>();
|
||||
for (Map.Entry<String, List<Jtt808Location>> entry : locationsByDate.entrySet()) {
|
||||
String dateStr = entry.getKey();
|
||||
List<Jtt808Location> dayLocations = entry.getValue();
|
||||
double mileage = calculateMileageFromValidLocations(dayLocations);
|
||||
mileageByDate.put(dateStr, mileage);
|
||||
}
|
||||
|
||||
// 遍历当月每一天
|
||||
for (int day = 1; day <= daysInMonth; day++) {
|
||||
LocalDate date = LocalDate.of(year, month, day);
|
||||
|
|
@ -96,8 +134,8 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
// 从统计数据中获取当天信息
|
||||
Map<String, Object> dayStats = statsMap.get(dateStr);
|
||||
if (dayStats != null) {
|
||||
// 计算当天里程
|
||||
double mileage = calculateDailyMileage(phoneNumber, dateStr);
|
||||
// 【性能优化】从预计算的里程Map中获取,避免重复计算
|
||||
Double mileage = mileageByDate.getOrDefault(dateStr, 0.0);
|
||||
dailyMileage.setMileage(BigDecimal.valueOf(mileage).setScale(2, RoundingMode.HALF_UP));
|
||||
|
||||
// 设置其他统计信息
|
||||
|
|
@ -144,8 +182,9 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
result.setAverageDailyMileage(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
logger.info("月度里程统计完成: 总里程={}km, 有效天数={}天, 平均日里程={}km",
|
||||
result.getTotalMileage(), validDays, result.getAverageDailyMileage());
|
||||
long endTime = System.currentTimeMillis();
|
||||
logger.info("月度里程统计完成: 总里程={}km, 有效天数={}天, 平均日里程={}km, 耗时={}ms",
|
||||
result.getTotalMileage(), validDays, result.getAverageDailyMileage(), (endTime - startTime));
|
||||
|
||||
return result;
|
||||
|
||||
|
|
@ -225,10 +264,8 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
result.setAvgSpeed(BigDecimal.valueOf(avgSpeed.getAsDouble()).setScale(1, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
// 构建轨迹点
|
||||
List<MileageStatisticsVo.TrackPoint> trackPoints = locations.stream()
|
||||
.map(this::convertToTrackPoint)
|
||||
.collect(Collectors.toList());
|
||||
// 构建轨迹点 - 过滤掉连续重复的坐标点
|
||||
List<MileageStatisticsVo.TrackPoint> trackPoints = filterDuplicateTrackPoints(locations);
|
||||
result.setTrackPoints(trackPoints);
|
||||
|
||||
// 分析停车点 (速度为0且停留时间超过5分钟的点)
|
||||
|
|
@ -286,31 +323,57 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
|
||||
/**
|
||||
* 根据位置点列表计算里程
|
||||
* 优化:过滤无效坐标,只计算有效位置点之间的距离
|
||||
*/
|
||||
private double calculateMileageFromLocations(List<Jtt808Location> locations) {
|
||||
if (locations == null || locations.size() < 2) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 先过滤出有效的位置点
|
||||
List<Jtt808Location> validLocations = new ArrayList<>();
|
||||
int invalidCount = 0;
|
||||
|
||||
for (Jtt808Location location : locations) {
|
||||
if (isValidChinaLocation(location)) {
|
||||
validLocations.add(location);
|
||||
} else {
|
||||
invalidCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCount > 0) {
|
||||
logger.debug("里程计算时过滤了 {} 个无效坐标点", invalidCount);
|
||||
}
|
||||
|
||||
return calculateMileageFromValidLocations(validLocations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已验证的有效位置点列表计算里程
|
||||
* 注意:此方法假设传入的位置点已经过坐标有效性验证
|
||||
*/
|
||||
private double calculateMileageFromValidLocations(List<Jtt808Location> validLocations) {
|
||||
if (validLocations == null || validLocations.size() < 2) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// 计算有效位置点之间的距离
|
||||
double totalDistance = 0.0;
|
||||
for (int i = 1; i < locations.size(); i++) {
|
||||
Jtt808Location prev = locations.get(i - 1);
|
||||
Jtt808Location curr = locations.get(i);
|
||||
for (int i = 1; i < validLocations.size(); i++) {
|
||||
Jtt808Location prev = validLocations.get(i - 1);
|
||||
Jtt808Location curr = validLocations.get(i);
|
||||
|
||||
if (prev.getLatitude() != null && prev.getLongitude() != null &&
|
||||
curr.getLatitude() != null && curr.getLongitude() != null) {
|
||||
|
||||
double distance = calculateDistance(
|
||||
prev.getLatitude().doubleValue(), prev.getLongitude().doubleValue(),
|
||||
curr.getLatitude().doubleValue(), curr.getLongitude().doubleValue()
|
||||
);
|
||||
double distance = calculateDistance(
|
||||
prev.getLatitude().doubleValue(), prev.getLongitude().doubleValue(),
|
||||
curr.getLatitude().doubleValue(), curr.getLongitude().doubleValue()
|
||||
);
|
||||
|
||||
// 过滤异常距离(单次移动超过10公里可能是GPS漂移)
|
||||
if (distance <= 10.0) {
|
||||
totalDistance += distance;
|
||||
} else {
|
||||
logger.debug("过滤异常距离: {}km ({})", distance, curr.getLocationTime());
|
||||
}
|
||||
// 过滤异常距离(单次移动超过10公里可能是GPS漂移)
|
||||
if (distance <= 10.0) {
|
||||
totalDistance += distance;
|
||||
} else {
|
||||
logger.debug("过滤异常距离: {}km ({})", distance, curr.getLocationTime());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +392,128 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤掉连续重复的坐标点和无效坐标
|
||||
* 只保留位置发生变化的轨迹点,避免轨迹回放时静止不动
|
||||
* 同时过滤掉非中国大陆区域的异常坐标
|
||||
*
|
||||
* @param locations 原始位置数据列表
|
||||
* @return 过滤后的轨迹点列表
|
||||
*/
|
||||
private List<MileageStatisticsVo.TrackPoint> filterDuplicateTrackPoints(List<Jtt808Location> locations) {
|
||||
List<MileageStatisticsVo.TrackPoint> trackPoints = new ArrayList<>();
|
||||
|
||||
if (locations == null || locations.isEmpty()) {
|
||||
return trackPoints;
|
||||
}
|
||||
|
||||
int invalidLocationCount = 0;
|
||||
Jtt808Location previousLocation = null;
|
||||
|
||||
// 遍历所有位置点
|
||||
for (int i = 0; i < locations.size(); i++) {
|
||||
Jtt808Location currentLocation = locations.get(i);
|
||||
|
||||
// 1. 首先验证坐标是否有效(是否在中国大陆范围内)
|
||||
if (!isValidChinaLocation(currentLocation)) {
|
||||
invalidLocationCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 如果是第一个有效点,直接保留
|
||||
if (previousLocation == null) {
|
||||
trackPoints.add(convertToTrackPoint(currentLocation));
|
||||
previousLocation = currentLocation;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 判断坐标是否发生变化(经纬度任一发生变化即认为位置改变)
|
||||
boolean locationChanged = !isSameLocation(previousLocation, currentLocation);
|
||||
|
||||
if (locationChanged) {
|
||||
trackPoints.add(convertToTrackPoint(currentLocation));
|
||||
previousLocation = currentLocation;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("轨迹点过滤: 原始点数={}, 无效坐标数={}, 过滤后点数={}, 总过滤率={}%",
|
||||
locations.size(),
|
||||
invalidLocationCount,
|
||||
trackPoints.size(),
|
||||
locations.size() > 0 ? String.format("%.2f", (1 - (double)trackPoints.size() / locations.size()) * 100) : "0.00");
|
||||
|
||||
return trackPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个位置是否相同(经纬度都相同)
|
||||
*/
|
||||
private boolean isSameLocation(Jtt808Location loc1, Jtt808Location loc2) {
|
||||
if (loc1 == null || loc2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 比较经纬度,使用compareTo避免浮点数精度问题
|
||||
boolean sameLongitude = loc1.getLongitude() != null && loc2.getLongitude() != null
|
||||
&& loc1.getLongitude().compareTo(loc2.getLongitude()) == 0;
|
||||
boolean sameLatitude = loc1.getLatitude() != null && loc2.getLatitude() != null
|
||||
&& loc1.getLatitude().compareTo(loc2.getLatitude()) == 0;
|
||||
|
||||
return sameLongitude && sameLatitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断位置点与轨迹点是否相同
|
||||
*/
|
||||
private boolean isSameLocationAsTrackPoint(Jtt808Location location, MileageStatisticsVo.TrackPoint trackPoint) {
|
||||
if (location == null || trackPoint == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean sameLongitude = location.getLongitude() != null && trackPoint.getLongitude() != null
|
||||
&& location.getLongitude().compareTo(trackPoint.getLongitude()) == 0;
|
||||
boolean sameLatitude = location.getLatitude() != null && trackPoint.getLatitude() != null
|
||||
&& location.getLatitude().compareTo(trackPoint.getLatitude()) == 0;
|
||||
|
||||
return sameLongitude && sameLatitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证经纬度是否在中国大陆范围内
|
||||
* 中国大陆经纬度范围:
|
||||
* - 纬度:18°N - 54°N (海南岛最南端到黑龙江最北端)
|
||||
* - 经度:73°E - 135°E (新疆最西端到黑龙江最东端)
|
||||
*
|
||||
* @param location 位置信息
|
||||
* @return true-有效的中国大陆坐标,false-无效坐标
|
||||
*/
|
||||
private boolean isValidChinaLocation(Jtt808Location location) {
|
||||
if (location == null || location.getLongitude() == null || location.getLatitude() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double longitude = location.getLongitude().doubleValue();
|
||||
double latitude = location.getLatitude().doubleValue();
|
||||
|
||||
// 过滤掉 0.0, 0.0 这种明显错误的坐标
|
||||
if (longitude == 0.0 && latitude == 0.0) {
|
||||
logger.debug("过滤无效坐标: (0.0, 0.0)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证是否在中国大陆范围内
|
||||
// 纬度范围:18°N - 54°N
|
||||
// 经度范围:73°E - 135°E
|
||||
boolean isInChina = latitude >= 18.0 && latitude <= 54.0
|
||||
&& longitude >= 73.0 && longitude <= 135.0;
|
||||
|
||||
if (!isInChina) {
|
||||
logger.debug("过滤非中国大陆坐标: ({}, {})", longitude, latitude);
|
||||
}
|
||||
|
||||
return isInChina;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析停车点
|
||||
*/
|
||||
|
|
@ -350,10 +535,10 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
// 开始停车
|
||||
parkingStart = location.getLocationTime();
|
||||
parkingLocation = location;
|
||||
} else if (!isStationary && parkingStart != null) {
|
||||
} else if (!isStationary && parkingStart != null && parkingLocation != null) {
|
||||
// 结束停车
|
||||
long parkingDuration = (location.getLocationTime().getTime() - parkingStart.getTime()) / (1000 * 60);
|
||||
|
||||
|
||||
if (parkingDuration >= 5) { // 停车时间超过5分钟才记录
|
||||
MileageStatisticsVo.ParkingPoint parkingPoint = new MileageStatisticsVo.ParkingPoint();
|
||||
parkingPoint.setLatitude(parkingLocation.getLatitude());
|
||||
|
|
@ -361,10 +546,10 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
|
|||
parkingPoint.setStartTime(parkingStart);
|
||||
parkingPoint.setEndTime(location.getLocationTime());
|
||||
parkingPoint.setDuration((int) parkingDuration);
|
||||
|
||||
|
||||
parkingPoints.add(parkingPoint);
|
||||
}
|
||||
|
||||
|
||||
parkingStart = null;
|
||||
parkingLocation = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 坐标转换工具类
|
||||
* 支持 WGS-84(GPS坐标) <-> GCJ-02(火星坐标/高德地图) <-> BD-09(百度坐标)
|
||||
*
|
||||
* 坐标系说明:
|
||||
* - WGS-84: 国际标准GPS坐标系,JTT808协议使用此坐标系
|
||||
* - GCJ-02: 中国国家测绘局坐标系(火星坐标),高德地图、腾讯地图、谷歌中国地图使用
|
||||
* - BD-09: 百度坐标系,百度地图使用
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@Slf4j
|
||||
public class CoordinateConverter {
|
||||
|
||||
// 转换常数
|
||||
private static final double PI = 3.1415926535897932384626;
|
||||
private static final double A = 6378245.0; // 长半轴
|
||||
private static final double EE = 0.00669342162296594323; // 偏心率平方
|
||||
|
||||
/**
|
||||
* WGS-84 转 GCJ-02(GPS坐标转火星坐标)
|
||||
*
|
||||
* @param lat WGS-84纬度
|
||||
* @param lng WGS-84经度
|
||||
* @return [GCJ-02纬度, GCJ-02经度]
|
||||
*/
|
||||
public static double[] wgs84ToGcj02(double lat, double lng) {
|
||||
if (outOfChina(lat, lng)) {
|
||||
log.debug("坐标在中国境外,不需要转换: lat={}, lng={}", lat, lng);
|
||||
return new double[]{lat, lng};
|
||||
}
|
||||
|
||||
double dLat = transformLat(lng - 105.0, lat - 35.0);
|
||||
double dLng = transformLng(lng - 105.0, lat - 35.0);
|
||||
double radLat = lat / 180.0 * PI;
|
||||
double magic = Math.sin(radLat);
|
||||
magic = 1 - EE * magic * magic;
|
||||
double sqrtMagic = Math.sqrt(magic);
|
||||
dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI);
|
||||
dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI);
|
||||
double mgLat = lat + dLat;
|
||||
double mgLng = lng + dLng;
|
||||
|
||||
log.debug("WGS-84转GCJ-02: ({}, {}) -> ({}, {})", lat, lng, mgLat, mgLng);
|
||||
return new double[]{mgLat, mgLng};
|
||||
}
|
||||
|
||||
/**
|
||||
* GCJ-02 转 WGS-84(火星坐标转GPS坐标)
|
||||
* 使用迭代法进行精确转换
|
||||
*
|
||||
* @param lat GCJ-02纬度
|
||||
* @param lng GCJ-02经度
|
||||
* @return [WGS-84纬度, WGS-84经度]
|
||||
*/
|
||||
public static double[] gcj02ToWgs84(double lat, double lng) {
|
||||
if (outOfChina(lat, lng)) {
|
||||
return new double[]{lat, lng};
|
||||
}
|
||||
|
||||
// 使用迭代法进行精确转换
|
||||
double[] gcj = wgs84ToGcj02(lat, lng);
|
||||
double dLat = gcj[0] - lat;
|
||||
double dLng = gcj[1] - lng;
|
||||
|
||||
return new double[]{lat - dLat, lng - dLng};
|
||||
}
|
||||
|
||||
/**
|
||||
* GCJ-02 转 BD-09(火星坐标转百度坐标)
|
||||
*
|
||||
* @param lat GCJ-02纬度
|
||||
* @param lng GCJ-02经度
|
||||
* @return [BD-09纬度, BD-09经度]
|
||||
*/
|
||||
public static double[] gcj02ToBd09(double lat, double lng) {
|
||||
double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * PI);
|
||||
double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * PI);
|
||||
double bdLng = z * Math.cos(theta) + 0.0065;
|
||||
double bdLat = z * Math.sin(theta) + 0.006;
|
||||
|
||||
return new double[]{bdLat, bdLng};
|
||||
}
|
||||
|
||||
/**
|
||||
* BD-09 转 GCJ-02(百度坐标转火星坐标)
|
||||
*
|
||||
* @param lat BD-09纬度
|
||||
* @param lng BD-09经度
|
||||
* @return [GCJ-02纬度, GCJ-02经度]
|
||||
*/
|
||||
public static double[] bd09ToGcj02(double lat, double lng) {
|
||||
double x = lng - 0.0065;
|
||||
double y = lat - 0.006;
|
||||
double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * PI);
|
||||
double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * PI);
|
||||
double gcjLng = z * Math.cos(theta);
|
||||
double gcjLat = z * Math.sin(theta);
|
||||
|
||||
return new double[]{gcjLat, gcjLng};
|
||||
}
|
||||
|
||||
/**
|
||||
* WGS-84 转 BD-09(GPS坐标转百度坐标)
|
||||
*
|
||||
* @param lat WGS-84纬度
|
||||
* @param lng WGS-84经度
|
||||
* @return [BD-09纬度, BD-09经度]
|
||||
*/
|
||||
public static double[] wgs84ToBd09(double lat, double lng) {
|
||||
double[] gcj02 = wgs84ToGcj02(lat, lng);
|
||||
return gcj02ToBd09(gcj02[0], gcj02[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* BD-09 转 WGS-84(百度坐标转GPS坐标)
|
||||
*
|
||||
* @param lat BD-09纬度
|
||||
* @param lng BD-09经度
|
||||
* @return [WGS-84纬度, WGS-84经度]
|
||||
*/
|
||||
public static double[] bd09ToWgs84(double lat, double lng) {
|
||||
double[] gcj02 = bd09ToGcj02(lat, lng);
|
||||
return gcj02ToWgs84(gcj02[0], gcj02[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否在中国境外
|
||||
* 中国境外的坐标不需要进行偏移
|
||||
*/
|
||||
private static boolean outOfChina(double lat, double lng) {
|
||||
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纬度转换
|
||||
*/
|
||||
private static double transformLat(double lng, double lat) {
|
||||
double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat +
|
||||
0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
|
||||
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
|
||||
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;
|
||||
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 经度转换
|
||||
*/
|
||||
private static double transformLng(double lng, double lat) {
|
||||
double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng +
|
||||
0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
|
||||
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
|
||||
ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;
|
||||
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ sgzb:
|
|||
enabled: true # 启用JTT808服务器
|
||||
ip: 14.103.246.124 # 本机公网IP(仅用于文档说明)
|
||||
port: 21100 # 服务端监听端口
|
||||
maxConnections: 10000 # 最大连接数
|
||||
maxConnections: 100 # 最大连接数
|
||||
terminal:
|
||||
phone: 868120322495257
|
||||
timeout:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
from jtt808_location
|
||||
</sql>
|
||||
|
||||
<sql id="selectJtt808LocationVoSimple">
|
||||
select location_id, phone_number, alarm_flag, status_flag, latitude, longitude,
|
||||
altitude, speed, direction, location_time, client_ip, receive_time
|
||||
from jtt808_location
|
||||
</sql>
|
||||
|
||||
<insert id="insertLocation" parameterType="com.bonus.sgzb.material.domain.Jtt808Location" useGeneratedKeys="true" keyProperty="locationId">
|
||||
insert into jtt808_location
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
|
|
@ -72,7 +78,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</insert>
|
||||
|
||||
<select id="selectLocationList" parameterType="com.bonus.sgzb.material.domain.Jtt808Location" resultMap="Jtt808LocationResult">
|
||||
<include refid="selectJtt808LocationVo"/>
|
||||
<include refid="selectJtt808LocationVoSimple"/>
|
||||
<where>
|
||||
<if test="phoneNumber != null and phoneNumber != ''">
|
||||
and phone_number = #{phoneNumber}
|
||||
|
|
@ -95,7 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</select>
|
||||
|
||||
<select id="selectLocationTrack" resultMap="Jtt808LocationResult">
|
||||
<include refid="selectJtt808LocationVo"/>
|
||||
<include refid="selectJtt808LocationVoSimple"/>
|
||||
where phone_number = #{phoneNumber}
|
||||
<if test="startTime != null">
|
||||
and location_time >= #{startTime}
|
||||
|
|
@ -107,7 +113,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</select>
|
||||
|
||||
<select id="selectLocationsByArea" resultMap="Jtt808LocationResult">
|
||||
<include refid="selectJtt808LocationVo"/>
|
||||
<include refid="selectJtt808LocationVoSimple"/>
|
||||
where phone_number = #{phoneNumber}
|
||||
and latitude between #{minLat} and #{maxLat}
|
||||
and longitude between #{minLng} and #{maxLng}
|
||||
|
|
@ -120,8 +126,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
order by location_time desc
|
||||
</select>
|
||||
|
||||
|
||||
|
||||
<select id="selectTodayMileage" parameterType="String" resultType="Double">
|
||||
select COALESCE(
|
||||
(select CAST(JSON_UNQUOTE(JSON_EXTRACT(additional_info, '$.1')) AS DECIMAL(10,2))
|
||||
|
|
@ -144,32 +148,41 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
<!-- 查询指定日期的位置数据(按时间排序) -->
|
||||
<select id="selectLocationsByDate" resultMap="Jtt808LocationResult">
|
||||
<include refid="selectJtt808LocationVo"/>
|
||||
where phone_number = #{phoneNumber}
|
||||
and DATE(location_time) = #{targetDate}
|
||||
<include refid="selectJtt808LocationVoSimple"/>
|
||||
where phone_number = #{phoneNumber} and DATE(location_time) = #{targetDate}
|
||||
order by location_time ASC
|
||||
</select>
|
||||
|
||||
<!-- 查询指定月份的位置数据统计 -->
|
||||
<!-- 查询指定月份的位置数据统计 - 优化版本,避免使用函数索引 -->
|
||||
<select id="selectMonthlyLocationStats" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
DATE(location_time) as date,
|
||||
SELECT
|
||||
DATE_FORMAT(location_time, '%Y-%m-%d') as date,
|
||||
COUNT(*) as location_count,
|
||||
MIN(location_time) as first_location_time,
|
||||
MAX(location_time) as last_location_time,
|
||||
MAX(speed) as max_speed,
|
||||
AVG(speed) as avg_speed
|
||||
FROM jtt808_location
|
||||
FROM jtt808_location
|
||||
WHERE phone_number = #{phoneNumber}
|
||||
AND YEAR(location_time) = #{year}
|
||||
AND MONTH(location_time) = #{month}
|
||||
GROUP BY DATE(location_time)
|
||||
ORDER BY DATE(location_time)
|
||||
AND location_time >= CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01 00:00:00')
|
||||
AND location_time < DATE_ADD(CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01'), INTERVAL 1 MONTH)
|
||||
GROUP BY DATE_FORMAT(location_time, '%Y-%m-%d')
|
||||
ORDER BY DATE_FORMAT(location_time, '%Y-%m-%d')
|
||||
</select>
|
||||
|
||||
<!-- 查询指定月份的所有位置数据(批量计算里程) -->
|
||||
<select id="selectMonthlyLocations" resultMap="Jtt808LocationResult">
|
||||
SELECT location_id, phone_number, latitude, longitude, speed, location_time
|
||||
FROM jtt808_location
|
||||
WHERE phone_number = #{phoneNumber}
|
||||
AND location_time >= CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01 00:00:00')
|
||||
AND location_time < DATE_ADD(CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01'), INTERVAL 1 MONTH)
|
||||
ORDER BY location_time
|
||||
</select>
|
||||
|
||||
<!-- 查询指定日期范围的位置数据 -->
|
||||
<select id="selectLocationsByDateRange" resultMap="Jtt808LocationResult">
|
||||
<include refid="selectJtt808LocationVo"/>
|
||||
<include refid="selectJtt808LocationVoSimple"/>
|
||||
where phone_number = #{phoneNumber}
|
||||
and DATE(location_time) >= #{startDate}
|
||||
and DATE(location_time) <= #{endDate}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</resultMap>
|
||||
|
||||
<sql id="selectJtt808MessageLogVo">
|
||||
select log_id, phone_number, message_id, message_type, serial_number, message_direction,
|
||||
select log_id, phone_number, message_id, serial_number, message_direction,
|
||||
message_content, process_result, error_message, client_ip, process_time
|
||||
from jtt808_message_log
|
||||
</sql>
|
||||
|
|
|
|||
Loading…
Reference in New Issue