新增路径冲突检测功能,优化了数据采集和检测逻辑
This commit is contained in:
parent
5453291cd0
commit
f5d830b713
@ -1 +1 @@
|
||||
0.3.2
|
||||
0.3.4
|
||||
68
changelog.md
68
changelog.md
@ -5,6 +5,74 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
版本规范基于 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.3.4] - 2025-07-11
|
||||
|
||||
### 🚀 **核心功能增强:路径冲突与违规检测全面优化**
|
||||
|
||||
- **数据采集与违规检测频率解耦**:
|
||||
- **问题**:原系统将数据采集频率(250ms)与违规检测和消息推送频率强耦合,导致检测过于频繁。
|
||||
- **优化**:引入内存缓存 `activeMovingObjectsCache`。数据采集(无人车、航空器、机场车辆)以高频(例如 250ms)更新此缓存,确保及时获取最新数据。
|
||||
- **方案**:新增独立定时任务 `performPeriodicViolationDetection`,该任务按照 `websocketPushInterval` 配置的频率(例如 1000ms)从缓存中获取最新快照,批量进行路径冲突检测和实时违规检测,实现频率解耦。
|
||||
|
||||
- **无人车速度计算修复**:
|
||||
- **问题**:无人车速度在 `DataCollectorService` 中显示为 0,导致超速检测不触发。
|
||||
- **修复**:纠正了 `collectUnmannedVehicleData` 方法中的逻辑错误,恢复使用 `SpeedCalculationService.calculateRealtimeSpeed()` 基于位置变化来计算无人车速度,确保了速度数据的准确性。
|
||||
|
||||
- **前端车牌号显示修复**:
|
||||
- **问题**:前端 WebSocket 消息中,部分无人车车牌号显示不正确(例如“ID-6”)。
|
||||
- **修复**:修改了 `RuleEventWebSocketPublisher.java`,使其通过 `QuapDataAdapter` 查询数据库获取真实的 `SysVehicleInfo` 和车牌号 `licensePlate`,解决了前端显示错误车牌号的问题。
|
||||
|
||||
- **后台违规数量统计修复**:
|
||||
- **问题**:后台日志显示检测到违规事件,但最终统计报告为“0个违规”。
|
||||
- **修复**:在 `RealTimeViolationDetectorImpl.java` 中,通过引入 Spring `@EventListener` 机制和 `AtomicLong` 计数器 `currentBatchViolationsCount`,确保每次 `RuleExecutionEngineImpl` 发布 `RuleViolationEventOccurred` 事件时,都能正确地被监听并累加到统计中,实现了准确的违规数量统计。
|
||||
|
||||
### ✅ 验证结果
|
||||
|
||||
- **无人车速度准确**:日志显示无人车速度能正确计算并赋值,不再为 0。
|
||||
- **前端消息正常**:WebSocket 消息中的车牌号和速度信息正确,前端能正常接收违规事件。
|
||||
- **后台统计正确**:违规事件能够被正确统计,日志显示与实际违规数量一致。
|
||||
- **频率解耦生效**:数据采集和违规检测/推送频率已成功分离,系统运行更加合理。
|
||||
|
||||
### 📋 影响文件
|
||||
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java`:
|
||||
- 引入 `activeMovingObjectsCache`。
|
||||
- 修改 `collectAircraftData`、`collectVehicleData`、`collectUnmannedVehicleData` 以更新缓存并移除直接检测调用。
|
||||
- 恢复 `collectUnmannedVehicleData` 中无人车速度的正确计算逻辑。
|
||||
- 新增 `performPeriodicViolationDetection` 定时任务。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/RuleEventWebSocketPublisher.java`:
|
||||
- 修改 `getLicensePlateByVehicleId` 方法以通过数据库查询真实车牌号。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RealTimeViolationDetectorImpl.java`:
|
||||
- 引入 `@EventListener` 和 `AtomicLong` 计数器,修正违规统计逻辑。
|
||||
- `VERSION.md`:版本号更新为 `0.3.4`。
|
||||
|
||||
## [0.3.3] - 2025-07-10
|
||||
|
||||
### ✨ **新功能:实现基于路径的无人车与航空器冲突检测**
|
||||
|
||||
- **功能概述**:设计并实现了无人车与航空器之间的路径冲突检测功能,旨在提前预警和规避潜在的碰撞风险。该功能考虑了移动对象的路径、速度和距离,提供实时的冲突预警和告警。
|
||||
- **核心模块开发**:
|
||||
- **路径数据模型**:创建了新的数据库表结构(如 `transport_routes`、`object_route_assignments`、`conflict_alert_logs` 等,具体参考 `sql/create_path_conflict_detection_tables.sql`),用于存储和管理交通路径、对象与路径的分配关系以及冲突告警日志。
|
||||
- **冲突检测算法**:在 `qaup-collision/src/main/java/com/qaup/collision/pathconflict/` 包下实现了基于路径、速度和距离的冲突检测算法。
|
||||
- **预警告警系统**:创建了完善的预警和告警消息系统(200m 预警,100m 告警)。
|
||||
- **WebSocket广播**:实现了将路径冲突和违规告警消息实时广播到控制台的 WebSocket 机制。
|
||||
- **后端接口与集成**:增加了无人车位置信息API接口,并优化了现有后端模块与冲突检测功能的集成。
|
||||
- **数据库结构优化与清理**:
|
||||
- 优化了数据库表结构,删除了部分多余的字段和 SQL 脚本,以适应新的冲突检测模型。
|
||||
- **WebSocket消息格式优化**:
|
||||
- 优化了WebSocket消息格式,删除了多余的字段,并增加了限速值等必要字段,提高了数据传输效率和清晰度。
|
||||
|
||||
### 📋 影响文件
|
||||
|
||||
- `sql/create_path_conflict_detection_tables.sql`:新增数据库表结构。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/pathconflict/...`:涉及路径冲突检测核心代码。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/websocket/event/PathConflictAlertWebSocketEvent.java`:新增WebSocket事件。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/websocket/message/PathConflictAlertMessage.java`:新增WebSocket消息。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/RuleEventWebSocketPublisher.java`:更新WebSocket发布逻辑。
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java`:数据采集与检测逻辑的初步集成。
|
||||
- `VERSION.md`:版本号更新。
|
||||
- `changelog.md`:记录本次变更。
|
||||
|
||||
## [0.3.2] - 2025-07-10
|
||||
|
||||
- 增加无人车位置信息API接口,显示无人车的位置信息
|
||||
|
||||
@ -1,59 +1,30 @@
|
||||
package com.qaup.collision.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NoArgsConstructor; // 重新引入无参构造函数
|
||||
// import lombok.AllArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
// import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
/**
|
||||
* 航空器实体类
|
||||
* 航空器模型
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper=true)
|
||||
public class Aircraft extends MovingObject{
|
||||
/**
|
||||
* 航班号
|
||||
*/
|
||||
@NotBlank(message = "航班号不能为空")
|
||||
private String flightNo;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor // 重新引入无参构造函数
|
||||
// @AllArgsConstructor
|
||||
@SuperBuilder
|
||||
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未知的字段
|
||||
public class Aircraft extends MovingObject {
|
||||
|
||||
/**
|
||||
* 航迹号
|
||||
*/
|
||||
private Long trackNumber;
|
||||
// 根据用户反馈,移除 flightNo 和 time 字段。这些信息应从 sys_vehicle_info 表中查询。
|
||||
// 外部API的原始字段将通过 DataCollectorDao 中的内部 DTO 进行处理和映射。
|
||||
|
||||
@JsonCreator
|
||||
public Aircraft(
|
||||
@JsonProperty("latitude") double latitude,
|
||||
@JsonProperty("longitude") double longitude,
|
||||
@JsonProperty("altitude") double altitude,
|
||||
@JsonProperty("time") long timestamp
|
||||
) {
|
||||
this.currentPosition = new GeoPosition(longitude, latitude, altitude);
|
||||
this.velocity = new Velocity();
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Aircraft{" +
|
||||
"flightNo='" + flightNo + '\'' +
|
||||
", trackNumber=" + trackNumber +
|
||||
", currentPosition=" + currentPosition +
|
||||
", heading=" + heading +
|
||||
", velocity=" + velocity +
|
||||
", timestamp=" + timestamp +
|
||||
", stateHistory=" + stateHistory +
|
||||
", MAX_HISTORY=" + MAX_HISTORY +
|
||||
", maxSpeed=" + maxSpeed +
|
||||
", type=" + type +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,57 +1,31 @@
|
||||
package com.qaup.collision.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NoArgsConstructor; // 重新引入无参构造函数
|
||||
// import lombok.AllArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
// import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
/**
|
||||
* 机场车辆模型
|
||||
* 表示无人车以外的机场内车辆(如服务车、清洁车、加油车等)
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper=true)
|
||||
public class AirportVehicle extends MovingObject{
|
||||
/**
|
||||
* 车牌号(业务标识符)
|
||||
*/
|
||||
@NotBlank(message = "车牌号不能为空")
|
||||
@JsonProperty("vehicleNo")
|
||||
private String licensePlate;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor // 重新引入无参构造函数
|
||||
// @AllArgsConstructor
|
||||
@SuperBuilder
|
||||
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未知的字段
|
||||
public class AirportVehicle extends MovingObject {
|
||||
|
||||
public AirportVehicle(
|
||||
@JsonProperty("latitude") double latitude,
|
||||
@JsonProperty("longitude") double longitude,
|
||||
@JsonProperty("time") long timestamp,
|
||||
@JsonProperty("speed") double speed,
|
||||
@JsonProperty("direction") double heading
|
||||
) {
|
||||
this.currentPosition = new GeoPosition(longitude, latitude, 0);
|
||||
double radians = Math.toRadians(heading);
|
||||
double vx = speed *Math.sin(radians);
|
||||
double vy = speed *Math.cos(radians);
|
||||
// 对微小值归零(阈值可根据需求调整)
|
||||
vx = Math.abs(vx) < 1e-10 ? 0.0 : vx;
|
||||
vy = Math.abs(vy) < 1e-10 ? 0.0 : vy;
|
||||
this.velocity = new Velocity(0,0,0,vx,vy,0,0);
|
||||
this.heading = heading;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
// 根据用户反馈,移除 vehicleNo 和 time 字段。这些信息应从 sys_vehicle_info 表中查询。
|
||||
// 外部API的原始字段将通过 DataCollectorDao 中的内部 DTO 进行处理和映射。
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AirportVehicle{" +
|
||||
", licensePlate='" + licensePlate + '\'' +
|
||||
", currentPosition=" + currentPosition +
|
||||
", velocity=" + velocity +
|
||||
", heading=" + heading +
|
||||
", timestamp=" + timestamp +
|
||||
", stateHistory=" + stateHistory +
|
||||
", MAX_HISTORY=" + MAX_HISTORY +
|
||||
", maxSpeed=" + maxSpeed +
|
||||
", type=" + type +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,31 +3,73 @@ package com.qaup.collision.common.model;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
@Data
|
||||
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public abstract class MovingObject {
|
||||
public class MovingObject {
|
||||
|
||||
// 当前坐标
|
||||
public GeoPosition currentPosition;
|
||||
//速度
|
||||
public Velocity velocity;
|
||||
// 航向(度)
|
||||
public double heading;
|
||||
// 时间戳(毫秒)
|
||||
public long timestamp;
|
||||
// 历史状态队列
|
||||
public Deque<MovementState> stateHistory = new ArrayDeque<>();
|
||||
// 最大历史记录数
|
||||
public int MAX_HISTORY = 30; // 最大历史记录数,支持更长时间窗口的数据分析
|
||||
// 最大速度(子类初始化)
|
||||
public double maxSpeed;
|
||||
// 类型枚举
|
||||
public MovingObjectType type;
|
||||
/**
|
||||
* 对象唯一标识
|
||||
*/
|
||||
private String objectId;
|
||||
|
||||
/**
|
||||
* 对象类型
|
||||
*/
|
||||
private ObjectType objectType;
|
||||
|
||||
/**
|
||||
* 对象名称(车牌号或航班号)
|
||||
*/
|
||||
private String objectName;
|
||||
|
||||
/**
|
||||
* 当前位置
|
||||
*/
|
||||
private Point currentPosition;
|
||||
|
||||
/**
|
||||
* 当前速度 (km/h)
|
||||
*/
|
||||
private Double currentSpeed;
|
||||
|
||||
/**
|
||||
* 当前方向角 (度,0-360)
|
||||
*/
|
||||
private Double currentHeading;
|
||||
|
||||
/**
|
||||
* 高度(米)
|
||||
*/
|
||||
private Double altitude;
|
||||
|
||||
/**
|
||||
* 对象类型枚举
|
||||
*/
|
||||
public enum ObjectType {
|
||||
UNMANNED_VEHICLE, // 无人车
|
||||
AIRCRAFT, // 航空器
|
||||
SPECIAL_VEHICLE // 特种车辆(如机场车辆)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取速度(m/s)
|
||||
*/
|
||||
public double getSpeedInMetersPerSecond() {
|
||||
if (currentSpeed == null) {
|
||||
return 0.0;
|
||||
}
|
||||
return currentSpeed * 1000.0 / 3600.0; // km/h to m/s
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否在移动
|
||||
*/
|
||||
public boolean isMoving() {
|
||||
return currentSpeed != null && currentSpeed > 0.5; // 0.5 km/h作为阈值
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,63 +1,29 @@
|
||||
package com.qaup.collision.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NoArgsConstructor; // 重新引入无参构造函数
|
||||
// import lombok.AllArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
// import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* 车辆实体类
|
||||
* 无人车模型
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper=true)
|
||||
public class UnmannedVehicle extends MovingObject{
|
||||
/**
|
||||
* 消息唯一ID,消息的唯一标识符
|
||||
*/
|
||||
private String transId;
|
||||
|
||||
/**
|
||||
* 车牌号(业务标识符)
|
||||
*/
|
||||
@NotBlank(message = "车牌号不能为空")
|
||||
@JsonProperty("vehicleID")
|
||||
private String licensePlate;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor // 重新引入无参构造函数
|
||||
// @AllArgsConstructor
|
||||
@SuperBuilder
|
||||
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未知的字段
|
||||
public class UnmannedVehicle extends MovingObject {
|
||||
|
||||
public UnmannedVehicle(
|
||||
@JsonProperty("longitude") double longitude,
|
||||
@JsonProperty("latitude") double latitude,
|
||||
@JsonProperty("direction") double heading,
|
||||
@JsonProperty("speed") double speed
|
||||
) {
|
||||
this.currentPosition = new GeoPosition(longitude, latitude, 0);
|
||||
double vx = speed *Math.sin(heading);
|
||||
double vy = speed *Math.cos(heading);
|
||||
// 对微小值归零(阈值可根据需求调整)
|
||||
vx = Math.abs(vx) < 1e-10 ? 0.0 : vx;
|
||||
vy = Math.abs(vy) < 1e-10 ? 0.0 : vy;
|
||||
this.velocity = new Velocity(0,0,0,vx,vy,0,0);
|
||||
this.heading = heading;
|
||||
}
|
||||
// 根据用户反馈,移除 vehicleId, licensePlate, vehicleModel, usagePurpose 和 timestamp 字段。
|
||||
// 外部API的原始字段将通过 DataCollectorDao 中的内部 DTO 进行处理和映射。
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UnmannedVehicle{" +
|
||||
"transId='" + transId + '\'' +
|
||||
", licensePlate='" + licensePlate + '\'' +
|
||||
", currentPosition=" + currentPosition +
|
||||
", velocity=" + velocity +
|
||||
", heading=" + heading +
|
||||
", timestamp=" + timestamp +
|
||||
", stateHistory=" + stateHistory +
|
||||
", MAX_HISTORY=" + MAX_HISTORY +
|
||||
", maxSpeed=" + maxSpeed +
|
||||
", type=" + type +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,6 +196,7 @@ public class VehicleLocationService {
|
||||
vehicleLocation.setAltitude(altitude);
|
||||
vehicleLocation.setHeading(heading);
|
||||
vehicleLocation.setSpeed(speed);
|
||||
log.debug("在VehicleLocationService.createVehicleLocation方法中,设置速度后,vehicleLocation.speed为: {}", vehicleLocation.getSpeed());
|
||||
vehicleLocation.setTimestamp(LocalDateTime.now());
|
||||
vehicleLocation.setDataQuality("GOOD"); // 默认数据质量
|
||||
|
||||
@ -849,33 +850,35 @@ public class VehicleLocationService {
|
||||
* 构建车辆状态参数Map(用于规则执行)
|
||||
*/
|
||||
private Map<String, Object> buildVehicleStateMap(VehicleLocation vehicleLocation, SysVehicleInfo vehicleInfo, MovingObjectType vehicleType) {
|
||||
Map<String, Object> vehicleState = new HashMap<>();
|
||||
|
||||
// 基本位置信息
|
||||
vehicleState.put("vehicleId", vehicleLocation.getVehicleId());
|
||||
vehicleState.put("vehicleType", vehicleType);
|
||||
vehicleState.put("licensePlate", vehicleInfo.getLicensePlate());
|
||||
vehicleState.put("timestamp", vehicleLocation.getTimestamp());
|
||||
|
||||
// 位置坐标
|
||||
if (vehicleLocation.getLocation() != null) {
|
||||
vehicleState.put("longitude", vehicleLocation.getLocation().getX());
|
||||
vehicleState.put("latitude", vehicleLocation.getLocation().getY());
|
||||
Map<String, Object> stateMap = new HashMap<>();
|
||||
stateMap.put("vehicleId", vehicleLocation.getVehicleId());
|
||||
stateMap.put("licensePlate", vehicleLocation.getLicensePlate());
|
||||
stateMap.put("longitude", vehicleLocation.getLocation().getX());
|
||||
stateMap.put("latitude", vehicleLocation.getLocation().getY());
|
||||
stateMap.put("altitude", vehicleLocation.getAltitude());
|
||||
stateMap.put("speed", vehicleLocation.getSpeed());
|
||||
stateMap.put("heading", vehicleLocation.getHeading());
|
||||
stateMap.put("timestamp", vehicleLocation.getTimestamp());
|
||||
stateMap.put("vehicleType", vehicleType.name());
|
||||
|
||||
// 移除对 vehicleModel 和 usagePurpose 的引用,因为 SysVehicleInfo 中可能不包含这些字段
|
||||
// if (vehicleInfo != null) {
|
||||
// stateMap.put("vehicleModel", vehicleInfo.getVehicleModel());
|
||||
// stateMap.put("usagePurpose", vehicleInfo.getUsagePurpose());
|
||||
// }
|
||||
return stateMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计所有无人车数量
|
||||
*/
|
||||
public long countAllUnmannedVehicles() {
|
||||
try {
|
||||
// 假设VehicleLocationRepository主要存储无人车数据
|
||||
return vehicleLocationRepository.count();
|
||||
} catch (Exception e) {
|
||||
log.error("获取所有无人车数量失败", e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 可选属性
|
||||
if (vehicleLocation.getAltitude() != null) {
|
||||
vehicleState.put("altitude", vehicleLocation.getAltitude());
|
||||
}
|
||||
|
||||
if (vehicleLocation.getHeading() != null) {
|
||||
vehicleState.put("heading", vehicleLocation.getHeading());
|
||||
}
|
||||
|
||||
if (vehicleLocation.getSpeed() != null) {
|
||||
vehicleState.put("speed", vehicleLocation.getSpeed());
|
||||
}
|
||||
|
||||
return vehicleState;
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,17 @@ package com.qaup.collision.datacollector.dao;
|
||||
import com.qaup.collision.common.model.Aircraft;
|
||||
import com.qaup.collision.common.model.AirportVehicle;
|
||||
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.dto.Response;
|
||||
import com.qaup.collision.datacollector.service.AuthService;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
@ -17,9 +24,12 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@ -36,10 +46,13 @@ public class DataCollectorDao {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final AuthService authService;
|
||||
private final GeometryFactory geometryFactory;
|
||||
|
||||
@Autowired
|
||||
public DataCollectorDao(RestTemplate restTemplate, AuthService authService) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.authService = authService;
|
||||
this.geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84
|
||||
}
|
||||
|
||||
|
||||
@ -54,19 +67,35 @@ public class DataCollectorDao {
|
||||
headers.set("Authorization", authService.getToken());
|
||||
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<Response<List<Aircraft>>> response = restTemplate.exchange(
|
||||
ResponseEntity<Response<List<ExternalAircraftData>>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<Response<List<Aircraft>>>() {}
|
||||
new ParameterizedTypeReference<Response<List<ExternalAircraftData>>>() {}
|
||||
);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
Response<List<Aircraft>> responseBody = response.getBody();
|
||||
if (responseBody != null) {
|
||||
List<Aircraft> dataList = responseBody.getData();
|
||||
log.info("成功获取航空器数据,数量: {}", dataList.size());
|
||||
return dataList;
|
||||
Response<List<ExternalAircraftData>> responseBody = response.getBody();
|
||||
if (responseBody != null && responseBody.getData() != null) {
|
||||
List<Aircraft> processedAircrafts = responseBody.getData().stream().map(rawData -> {
|
||||
if (rawData.getFlightNo() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
|
||||
log.warn("原始航空器数据缺失必要字段 (flightNo, longitude, latitude),跳过处理: {}", rawData);
|
||||
return null;
|
||||
}
|
||||
Aircraft aircraft = new Aircraft();
|
||||
aircraft.setObjectId(rawData.getFlightNo());
|
||||
aircraft.setObjectName(rawData.getFlightNo()); // 使用 flightNo 作为 objectName
|
||||
aircraft.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
|
||||
// 外部API中没有直接的速度和方向,如果需要,可以在DataCollectorService中计算或使用默认值
|
||||
// 暂时设置为0.0,DataCollectorService会处理
|
||||
aircraft.setCurrentSpeed(0.0);
|
||||
aircraft.setCurrentHeading(0.0);
|
||||
aircraft.setAltitude(0.0); // 假设默认高度为0,如果API有提供可以设置
|
||||
return aircraft;
|
||||
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
log.info("成功获取航空器数据,数量: {}", processedAircrafts.size());
|
||||
return processedAircrafts;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -86,19 +115,33 @@ public class DataCollectorDao {
|
||||
headers.set("Authorization", authService.getToken());
|
||||
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<Response<List<AirportVehicle>>> response = restTemplate.exchange(
|
||||
ResponseEntity<Response<List<ExternalAirportVehicleData>>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<Response<List<AirportVehicle>>>() {}
|
||||
new ParameterizedTypeReference<Response<List<ExternalAirportVehicleData>>>() {}
|
||||
);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
Response<List<AirportVehicle>> responseBody = response.getBody();
|
||||
if (responseBody != null) {
|
||||
List<AirportVehicle> dataList = responseBody.getData();
|
||||
log.info("成功获取机场车辆数据,数量: {}", dataList.size());
|
||||
return dataList;
|
||||
Response<List<ExternalAirportVehicleData>> responseBody = response.getBody();
|
||||
if (responseBody != null && responseBody.getData() != null) {
|
||||
List<AirportVehicle> processedVehicles = responseBody.getData().stream().map(rawData -> {
|
||||
if (rawData.getVehicleNo() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
|
||||
log.warn("原始机场车辆数据缺失必要字段 (vehicleNo, longitude, latitude),跳过处理: {}", rawData);
|
||||
return null;
|
||||
}
|
||||
AirportVehicle vehicle = new AirportVehicle();
|
||||
vehicle.setObjectId(rawData.getVehicleNo());
|
||||
vehicle.setObjectName(rawData.getVehicleNo()); // 使用 vehicleNo 作为 objectName
|
||||
vehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
|
||||
vehicle.setCurrentSpeed(0.0); // 暂时设置为0.0,DataCollectorService会处理
|
||||
vehicle.setCurrentHeading(0.0);
|
||||
vehicle.setAltitude(0.0); // 假设默认高度为0,如果API有提供可以设置
|
||||
return vehicle;
|
||||
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
log.info("成功获取机场车辆数据,数量: {}", processedVehicles.size());
|
||||
return processedVehicles;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -126,18 +169,34 @@ public class DataCollectorDao {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<List<UnmannedVehicle>> response = restTemplate.exchange(
|
||||
ResponseEntity<List<ExternalUnmannedVehicleData>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<List<UnmannedVehicle>>() {}
|
||||
new ParameterizedTypeReference<List<ExternalUnmannedVehicleData>>() {}
|
||||
);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
List<UnmannedVehicle> responseBody = response.getBody();
|
||||
if (responseBody != null) {
|
||||
log.info("成功获取无人车定位信息,数量: {}", responseBody.size());
|
||||
return responseBody;
|
||||
List<ExternalUnmannedVehicleData> rawDataList = response.getBody();
|
||||
if (rawDataList != null) {
|
||||
List<UnmannedVehicle> processedUnmannedVehicles = rawDataList.stream().map(rawData -> {
|
||||
if (rawData.getVehicleID() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
|
||||
log.warn("原始无人车数据缺失必要字段 (vehicleID, longitude, latitude),跳过处理: {}", rawData);
|
||||
return null;
|
||||
}
|
||||
UnmannedVehicle unmannedVehicle = new UnmannedVehicle();
|
||||
unmannedVehicle.setObjectId(rawData.getVehicleID());
|
||||
unmannedVehicle.setObjectName(rawData.getVehicleID()); // 使用 vehicleID 作为 objectName
|
||||
unmannedVehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
|
||||
unmannedVehicle.setCurrentSpeed(rawData.getSpeed() != null ? rawData.getSpeed() * 3.6 : 0.0); // m/s to km/h
|
||||
unmannedVehicle.setCurrentHeading(rawData.getDirection() != null ? Math.toDegrees(rawData.getDirection()) : 0.0); // 弧度 to 角度
|
||||
unmannedVehicle.setAltitude(0.0); // 假设默认高度为0,如果API有提供可以设置
|
||||
unmannedVehicle.setObjectType(MovingObject.ObjectType.UNMANNED_VEHICLE); // 显式设置对象类型
|
||||
return unmannedVehicle;
|
||||
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
log.info("成功获取无人车定位信息,数量: {}", processedUnmannedVehicles.size());
|
||||
return processedUnmannedVehicles;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -147,4 +206,57 @@ public class DataCollectorDao {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 内部 DTOs,精确匹配外部 API 接口返回的 JSON 结构
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static class ExternalAircraftData {
|
||||
@JsonProperty("flightNo")
|
||||
private String flightNo;
|
||||
@JsonProperty("longitude")
|
||||
private Double longitude;
|
||||
@JsonProperty("latitude")
|
||||
private Double latitude;
|
||||
@JsonProperty("time")
|
||||
private Long time;
|
||||
// 其他可能存在的但我们不关心的字段将被 @JsonIgnoreProperties 忽略
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static class ExternalAirportVehicleData {
|
||||
@JsonProperty("vehicleNo")
|
||||
private String vehicleNo;
|
||||
@JsonProperty("longitude")
|
||||
private Double longitude;
|
||||
@JsonProperty("latitude")
|
||||
private Double latitude;
|
||||
@JsonProperty("time")
|
||||
private Long time;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static class ExternalUnmannedVehicleData {
|
||||
@JsonProperty("transId")
|
||||
private String transId;
|
||||
@JsonProperty("timestamp")
|
||||
private Long timestamp;
|
||||
@JsonProperty("vehicleID")
|
||||
private String vehicleID;
|
||||
@JsonProperty("latitude")
|
||||
private Double latitude;
|
||||
@JsonProperty("longitude")
|
||||
private Double longitude;
|
||||
@JsonProperty("speed")
|
||||
private Double speed;
|
||||
@JsonProperty("direction")
|
||||
private Double direction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
package com.qaup.collision.datacollector.service;
|
||||
|
||||
import com.qaup.collision.common.model.*;
|
||||
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
||||
import com.qaup.collision.common.service.VehicleLocationService;
|
||||
import com.qaup.collision.dataprocessing.service.SpeedCalculationService;
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
|
||||
import com.qaup.collision.rule.service.RealTimeViolationDetector;
|
||||
import com.qaup.collision.common.adapter.QuapDataAdapter;
|
||||
import com.qaup.system.domain.SysVehicleInfo;
|
||||
import com.qaup.collision.rule.event.RuleViolationEvent;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -16,9 +22,17 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
|
||||
/**
|
||||
* 数据采集服务 - 重构版本
|
||||
@ -65,12 +79,26 @@ public class DataCollectorService {
|
||||
@Autowired
|
||||
private SpeedCalculationService speedCalculationService;
|
||||
|
||||
@Autowired
|
||||
private PathConflictDetectionService pathConflictDetectionService;
|
||||
|
||||
@Autowired
|
||||
private RealTimeViolationDetector realTimeViolationDetector; // 注入 RealTimeViolationDetector
|
||||
|
||||
@Autowired
|
||||
private QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84
|
||||
|
||||
/**
|
||||
* WebSocket推送节流机制 - 跟踪每个对象的上次推送时间
|
||||
* key: objectId, value: lastPushTimestamp
|
||||
*/
|
||||
private final Map<String, Long> lastPushTimes = new ConcurrentHashMap<>();
|
||||
|
||||
// 用于缓存所有活跃的MovingObject的最新状态
|
||||
private final Map<String, MovingObject> activeMovingObjectsCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 定时采集航空器数据
|
||||
*
|
||||
@ -94,67 +122,96 @@ public class DataCollectorService {
|
||||
|
||||
log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size());
|
||||
|
||||
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
|
||||
|
||||
// 航空器数据仅用于实时处理,不存储到数据库
|
||||
// 数据处理完成后发布WebSocket事件进行实时推送
|
||||
for (Aircraft aircraft : newAircrafts) {
|
||||
try {
|
||||
// 计算速度和方向
|
||||
if (aircraft.getCurrentPosition() == null) {
|
||||
log.warn("航空器 {} 位置信息缺失,跳过处理。", aircraft.getObjectId());
|
||||
continue; // 跳过此航空器
|
||||
}
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
|
||||
aircraft.getCurrentPosition().getX(),
|
||||
aircraft.getCurrentPosition().getY()
|
||||
));
|
||||
|
||||
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
|
||||
aircraft.getFlightNo(),
|
||||
aircraft.getCurrentPosition().getLatitude(),
|
||||
aircraft.getCurrentPosition().getLongitude(),
|
||||
aircraft.getObjectId(), // Changed from getFlightNo()
|
||||
aircraft.getCurrentPosition().getY(),
|
||||
aircraft.getCurrentPosition().getX(),
|
||||
currentTime
|
||||
);
|
||||
Double calculatedDirection = speedCalculationService.getCurrentDirection(aircraft.getObjectId()); // Changed from getFlightNo()
|
||||
|
||||
Double calculatedDirection = speedCalculationService.getCurrentDirection(aircraft.getFlightNo());
|
||||
MovingObject movingObject = MovingObject.builder()
|
||||
.objectId(aircraft.getObjectId()) // Changed from getFlightNo()
|
||||
.objectType(MovingObject.ObjectType.AIRCRAFT)
|
||||
.objectName(aircraft.getObjectName()) // Changed from getFlightNo()
|
||||
.currentPosition(currentPosition)
|
||||
.currentSpeed(calculatedSpeed)
|
||||
.currentHeading(calculatedDirection)
|
||||
.altitude(aircraft.getAltitude())
|
||||
.build();
|
||||
|
||||
// activeMovingObjects.add(movingObject); // 移除此行
|
||||
// 将最新数据更新到缓存
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
// 如果没有计算出速度,尝试从velocity获取
|
||||
Double finalSpeed = calculatedSpeed;
|
||||
if (finalSpeed == null && aircraft.getVelocity() != null) {
|
||||
finalSpeed = aircraft.getVelocity().getSpeed();
|
||||
}
|
||||
|
||||
// 如果没有计算出方向,尝试从原有heading获取
|
||||
Double finalHeading = calculatedDirection;
|
||||
if (finalHeading == null) {
|
||||
finalHeading = aircraft.getHeading();
|
||||
}
|
||||
|
||||
// 创建位置更新消息负载
|
||||
PositionUpdatePayload.Position position = PositionUpdatePayload.Position.builder()
|
||||
.latitude(aircraft.getCurrentPosition().getLatitude())
|
||||
.longitude(aircraft.getCurrentPosition().getLongitude())
|
||||
// 创建位置更新消息负载(WebSocket推送)
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(currentPosition.getY())
|
||||
.longitude(currentPosition.getX())
|
||||
.build();
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(aircraft.getFlightNo())
|
||||
.objectType(MovingObjectType.AIRCRAFT.name())
|
||||
.position(position)
|
||||
.heading(finalHeading)
|
||||
.speed(finalSpeed)
|
||||
.timestamp(currentTime * 1000) // 微秒级时间戳
|
||||
.objectId(aircraft.getObjectId()) // Changed from getFlightNo()
|
||||
.objectType(MovingObject.ObjectType.AIRCRAFT.name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(movingObject.getCurrentSpeed())
|
||||
.timestamp(currentTime) // 更新为Long类型时间戳
|
||||
.build();
|
||||
|
||||
// 节流检查:只有达到推送间隔时才发布WebSocket事件
|
||||
if (shouldPushWebSocketMessage(aircraft.getFlightNo(), currentTime)) {
|
||||
if (shouldPushWebSocketMessage(aircraft.getObjectId(), currentTime)) { // Changed from getFlightNo()
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
|
||||
log.debug("处理航空器数据并发布事件: (航班号: {}, 位置: {}, {}, 速度: {})",
|
||||
aircraft.getFlightNo(),
|
||||
aircraft.getCurrentPosition().getLongitude(),
|
||||
aircraft.getCurrentPosition().getLatitude(),
|
||||
finalSpeed);
|
||||
aircraft.getObjectId(), // Changed from getFlightNo()
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
} else {
|
||||
log.trace("航空器数据采集但未推送(节流): {} (速度: {})",
|
||||
aircraft.getFlightNo(), finalSpeed);
|
||||
aircraft.getObjectId(), movingObject.getCurrentSpeed()); // Changed from getFlightNo()
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理航空器数据异常: flightNo={}", aircraft.getFlightNo(), e);
|
||||
log.error("处理航空器数据异常: objectId={}", aircraft.getObjectId(), e); // Changed from flightNo
|
||||
}
|
||||
}
|
||||
|
||||
// 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!activeMovingObjects.isEmpty()) {
|
||||
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
|
||||
// }
|
||||
|
||||
// 执行实时违规检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// for (MovingObject movingObject : activeMovingObjects) {
|
||||
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
|
||||
// if (tempVehicleLocation != null) {
|
||||
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
|
||||
// if (!violations.isEmpty()) {
|
||||
// log.warn("检测到航空器违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
|
||||
// // 如果需要,可以在这里进一步处理这些违规事件,例如持久化或发送告警
|
||||
// // realTimeViolationDetector.processBatchViolationEvents(violations);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
log.info("航空器数据处理和事件发布完成,处理数量: {}", newAircrafts.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -188,80 +245,112 @@ public class DataCollectorService {
|
||||
|
||||
log.info("采集到 {} 条机场车辆数据,用于实时处理", vehicles.size());
|
||||
|
||||
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
|
||||
|
||||
// 机场车辆数据仅用于实时处理,不存储到数据库
|
||||
// 数据处理完成后发布WebSocket事件进行实时推送
|
||||
for (AirportVehicle vehicle : vehicles) {
|
||||
try {
|
||||
// 计算速度和方向
|
||||
if (vehicle.getCurrentPosition() == null) {
|
||||
log.warn("机场车辆 {} 位置信息缺失,跳过处理。", vehicle.getObjectId());
|
||||
continue; // 跳过此机场车辆
|
||||
}
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
|
||||
vehicle.getCurrentPosition().getX(),
|
||||
vehicle.getCurrentPosition().getY()
|
||||
));
|
||||
|
||||
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
|
||||
vehicle.getLicensePlate(),
|
||||
vehicle.getCurrentPosition().getLatitude(),
|
||||
vehicle.getCurrentPosition().getLongitude(),
|
||||
vehicle.getObjectId(),
|
||||
vehicle.getCurrentPosition().getY(),
|
||||
vehicle.getCurrentPosition().getX(),
|
||||
currentTime
|
||||
);
|
||||
|
||||
Double calculatedDirection = speedCalculationService.getCurrentDirection(vehicle.getLicensePlate());
|
||||
|
||||
// 如果没有计算出速度,尝试从velocity获取
|
||||
Double finalSpeed = calculatedSpeed;
|
||||
if (finalSpeed == null && vehicle.getVelocity() != null) {
|
||||
finalSpeed = vehicle.getVelocity().getSpeed();
|
||||
}
|
||||
|
||||
// 如果没有计算出方向,尝试从原有heading获取
|
||||
Double finalHeading = calculatedDirection;
|
||||
if (finalHeading == null) {
|
||||
finalHeading = vehicle.getHeading();
|
||||
}
|
||||
|
||||
// 创建位置更新消息负载
|
||||
PositionUpdatePayload.Position position = PositionUpdatePayload.Position.builder()
|
||||
.latitude(vehicle.getCurrentPosition().getLatitude())
|
||||
.longitude(vehicle.getCurrentPosition().getLongitude())
|
||||
Double calculatedDirection = speedCalculationService.getCurrentDirection(vehicle.getObjectId());
|
||||
|
||||
MovingObject movingObject = MovingObject.builder()
|
||||
.objectId(vehicle.getObjectId())
|
||||
.objectType(MovingObject.ObjectType.SPECIAL_VEHICLE)
|
||||
.objectName(vehicle.getObjectName())
|
||||
.currentPosition(currentPosition)
|
||||
.currentSpeed(calculatedSpeed)
|
||||
.currentHeading(calculatedDirection)
|
||||
.altitude(vehicle.getAltitude())
|
||||
.build();
|
||||
|
||||
// activeMovingObjects.add(movingObject); // 移除此行
|
||||
// 将最新数据更新到缓存
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
// 创建位置更新消息负载(WebSocket推送)
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(currentPosition.getY())
|
||||
.longitude(currentPosition.getX())
|
||||
.build();
|
||||
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(vehicle.getLicensePlate()) // 使用车牌号作为标识符
|
||||
.objectType(MovingObjectType.AIRPORT_VEHICLE.name())
|
||||
.position(position)
|
||||
.heading(finalHeading)
|
||||
.speed(finalSpeed)
|
||||
.timestamp(currentTime * 1000) // 微秒级时间戳
|
||||
.objectId(vehicle.getObjectId())
|
||||
.objectType(MovingObject.ObjectType.SPECIAL_VEHICLE.name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(movingObject.getCurrentSpeed())
|
||||
.timestamp(currentTime)
|
||||
.build();
|
||||
|
||||
|
||||
// 节流检查:只有达到推送间隔时才发布WebSocket事件
|
||||
if (shouldPushWebSocketMessage(vehicle.getLicensePlate(), currentTime)) {
|
||||
if (shouldPushWebSocketMessage(vehicle.getObjectId(), currentTime)) {
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
|
||||
|
||||
log.debug("处理机场车辆数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
vehicle.getLicensePlate(),
|
||||
vehicle.getCurrentPosition().getLongitude(),
|
||||
vehicle.getCurrentPosition().getLatitude(),
|
||||
finalSpeed);
|
||||
vehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
} else {
|
||||
log.trace("机场车辆数据采集但未推送(节流): {} (速度: {})",
|
||||
vehicle.getLicensePlate(), finalSpeed);
|
||||
vehicle.getObjectId(), movingObject.getCurrentSpeed());
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理机场车辆数据异常: licensePlate={}", vehicle.getLicensePlate(), e);
|
||||
log.error("处理机场车辆数据异常: objectId={}", vehicle.getObjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!activeMovingObjects.isEmpty()) {
|
||||
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
|
||||
// }
|
||||
|
||||
// 执行实时违规检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// for (MovingObject movingObject : activeMovingObjects) {
|
||||
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
|
||||
// if (tempVehicleLocation != null) {
|
||||
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
|
||||
// if (!violations.isEmpty()) {
|
||||
// log.warn("检测到机场车辆违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
|
||||
// // 如果需要,可以在这里进一步处理这些违规事件,例如持久化或发送告警
|
||||
// // realTimeViolationDetector.processBatchViolationEvents(violations);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", vehicles.size());
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集机场车辆数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时采集无人车数据
|
||||
* 定时采集无人车数据 (外部API)
|
||||
*
|
||||
* 新增方法:支持无人车数据采集和选择性持久化
|
||||
* - 无人车位置数据:存储到数据库(用于轨迹回放和日志审计)
|
||||
* - 使用VehicleDataPersistenceService进行选择性存储
|
||||
* 数据来源:第2章 无人车位置上报 (/api/VehicleLocationInfo)
|
||||
* 说明:仅传递目前无人车平台已接入的无人车位置数据
|
||||
* 重构说明:
|
||||
* - 无人车数据仅用于实时处理,不存储到数据库
|
||||
* - 数据采集后直接用于碰撞检测等实时计算
|
||||
* - 不进行数据持久化
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
@Async // 异步执行
|
||||
@ -277,189 +366,272 @@ public class DataCollectorService {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始处理 {} 条无人车数据", unmannedVehicles.size());
|
||||
log.info("采集到 {} 条无人车数据", unmannedVehicles.size());
|
||||
|
||||
// 将采集到的无人车数据转换为MovingObject并添加到缓存
|
||||
// List<VehicleLocation> unmannedVehicleLocations = new ArrayList<>(); // 移除此行
|
||||
|
||||
// 先推送WebSocket位置更新事件
|
||||
for (UnmannedVehicle vehicle : unmannedVehicles) {
|
||||
for (UnmannedVehicle unmannedVehicle : unmannedVehicles) {
|
||||
try {
|
||||
// 计算速度和方向
|
||||
if (unmannedVehicle.getCurrentPosition() == null) {
|
||||
log.warn("无人车 {} 位置信息缺失,跳过处理。", unmannedVehicle.getObjectId());
|
||||
continue; // 跳过此无人车
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
|
||||
unmannedVehicle.getCurrentPosition().getX(),
|
||||
unmannedVehicle.getCurrentPosition().getY()
|
||||
));
|
||||
|
||||
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
|
||||
vehicle.getLicensePlate(),
|
||||
vehicle.getCurrentPosition().getLatitude(),
|
||||
vehicle.getCurrentPosition().getLongitude(),
|
||||
unmannedVehicle.getObjectId(),
|
||||
unmannedVehicle.getCurrentPosition().getY(),
|
||||
unmannedVehicle.getCurrentPosition().getX(),
|
||||
currentTime
|
||||
);
|
||||
log.debug("调用speedCalculationService后,无人车 {} 的速度为: {}", unmannedVehicle.getObjectId(), calculatedSpeed);
|
||||
|
||||
Double calculatedDirection = speedCalculationService.getCurrentDirection(unmannedVehicle.getObjectId());
|
||||
|
||||
MovingObject movingObject = MovingObject.builder()
|
||||
.objectId(unmannedVehicle.getObjectId()) // Changed from getLicensePlate()
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(unmannedVehicle.getObjectName()) // Changed from getLicensePlate()
|
||||
.currentPosition(currentPosition)
|
||||
.currentSpeed(calculatedSpeed)
|
||||
.currentHeading(calculatedDirection)
|
||||
.altitude(unmannedVehicle.getAltitude())
|
||||
.build();
|
||||
|
||||
log.debug("在DataCollectorService中,MovingObject的速度: {}", movingObject.getCurrentSpeed());
|
||||
|
||||
// unmannedVehicleLocations.add(createTemporaryVehicleLocationForDetection(movingObject)); // 移除此行
|
||||
// 将最新数据更新到缓存
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
Double calculatedDirection = speedCalculationService.getCurrentDirection(vehicle.getLicensePlate());
|
||||
|
||||
// 如果没有计算出速度,尝试从velocity获取
|
||||
Double finalSpeed = calculatedSpeed;
|
||||
if (finalSpeed == null && vehicle.getVelocity() != null) {
|
||||
finalSpeed = vehicle.getVelocity().getSpeed();
|
||||
}
|
||||
|
||||
// 如果没有计算出方向,尝试从原有heading获取
|
||||
Double finalHeading = calculatedDirection;
|
||||
if (finalHeading == null) {
|
||||
finalHeading = vehicle.getHeading();
|
||||
}
|
||||
|
||||
PositionUpdatePayload.Position position = PositionUpdatePayload.Position.builder()
|
||||
.latitude(vehicle.getCurrentPosition().getLatitude())
|
||||
.longitude(vehicle.getCurrentPosition().getLongitude())
|
||||
// 创建位置更新消息负载(WebSocket推送)
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(currentPosition.getY())
|
||||
.longitude(currentPosition.getX())
|
||||
.build();
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(vehicle.getLicensePlate()) // 将Long转换为String
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE.name())
|
||||
.position(position)
|
||||
.heading(finalHeading)
|
||||
.speed(finalSpeed)
|
||||
.timestamp(currentTime * 1000) // 微秒级时间戳
|
||||
.objectId(unmannedVehicle.getObjectId()) // Changed from getLicensePlate()
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE.name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(movingObject.getCurrentSpeed())
|
||||
.timestamp(currentTime)
|
||||
.build();
|
||||
|
||||
// 节流检查:只有达到推送间隔时才发布WebSocket事件
|
||||
if (shouldPushWebSocketMessage(vehicle.getLicensePlate(), currentTime)) {
|
||||
if (shouldPushWebSocketMessage(unmannedVehicle.getObjectId(), currentTime)) { // Changed from getLicensePlate()
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
log.debug("处理无人车数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
vehicle.getLicensePlate(),
|
||||
vehicle.getCurrentPosition().getLongitude(),
|
||||
vehicle.getCurrentPosition().getLatitude(),
|
||||
finalSpeed);
|
||||
|
||||
log.debug("处理无人车数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
unmannedVehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
} else {
|
||||
log.trace("无人车数据采集但未推送(节流): {} (速度: {})",
|
||||
vehicle.getLicensePlate(), finalSpeed);
|
||||
unmannedVehicle.getObjectId(), movingObject.getCurrentSpeed()); // Changed from getLicensePlate()
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理无人车数据异常: vehicleId={}", vehicle.getLicensePlate(), e);
|
||||
log.error("处理无人车数据异常: objectId={}", unmannedVehicle.getObjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为VehicleLocation对象列表
|
||||
List<com.qaup.collision.common.model.spatial.VehicleLocation> vehicleLocations =
|
||||
unmannedVehicles.stream()
|
||||
.map(this::convertToVehicleLocation)
|
||||
.filter(location -> location != null)
|
||||
.toList();
|
||||
|
||||
if (!vehicleLocations.isEmpty()) {
|
||||
// 使用VehicleDataPersistenceService进行批量保存
|
||||
List<com.qaup.collision.common.model.spatial.VehicleLocation> savedLocations =
|
||||
vehicleDataPersistenceService.batchSaveUnmannedVehicleLocations(vehicleLocations);
|
||||
|
||||
log.info("无人车数据采集完成,成功保存 {}/{} 条记录",
|
||||
savedLocations.size(), unmannedVehicles.size());
|
||||
} else {
|
||||
log.warn("无有效的无人车位置数据可保存");
|
||||
}
|
||||
// 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!unmannedVehicleLocations.isEmpty()) {
|
||||
// pathConflictDetectionService.detectPathConflicts(unmannedVehicleLocations.stream()
|
||||
// .map(v -> MovingObject.builder()
|
||||
// .objectId(String.valueOf(v.getVehicleId()))
|
||||
// .objectType(v.getVehicleType())
|
||||
// .objectName(v.getVehicleLicense())
|
||||
// .currentPosition(v.getLocation())
|
||||
// .currentSpeed(v.getSpeed())
|
||||
// .currentHeading(v.getHeading())
|
||||
// .altitude(v.getAltitude())
|
||||
// .build())
|
||||
// .collect(Collectors.toList()));
|
||||
// }
|
||||
|
||||
// 执行实时违规检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!unmannedVehicleLocations.isEmpty()) {
|
||||
// realTimeViolationDetector.detectBatchViolations(unmannedVehicleLocations);
|
||||
// }
|
||||
|
||||
log.info("无人车数据处理和事件发布完成,处理数量: {}", unmannedVehicles.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集无人车数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将UnmannedVehicle转换为VehicleLocation对象
|
||||
*
|
||||
* @param vehicle 无人车对象
|
||||
* @return VehicleLocation对象,转换失败时返回null
|
||||
* 定时执行路径冲突和实时违规检测
|
||||
* 这个任务将按照websocketPushInterval配置的频率运行,独立于数据采集频率。
|
||||
* 它会从activeMovingObjectsCache中获取所有最新的MovingObject数据,并进行批量检测。
|
||||
*/
|
||||
private com.qaup.collision.common.model.spatial.VehicleLocation convertToVehicleLocation(UnmannedVehicle vehicle) {
|
||||
try {
|
||||
String licensePlate = vehicle.getLicensePlate(); // 获取车牌号
|
||||
MovingObjectType vehicleType = MovingObjectType.UNMANNED_VEHICLE;
|
||||
double longitude = vehicle.getCurrentPosition().getLongitude();
|
||||
double latitude = vehicle.getCurrentPosition().getLatitude();
|
||||
Double altitude = vehicle.getCurrentPosition().getAltitude();
|
||||
Double heading = vehicle.getHeading();
|
||||
|
||||
// 使用SpeedCalculationService计算的速度(公里/小时),与WebSocket推送保持一致
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
|
||||
vehicle.getLicensePlate(),
|
||||
vehicle.getCurrentPosition().getLatitude(),
|
||||
vehicle.getCurrentPosition().getLongitude(),
|
||||
currentTime
|
||||
);
|
||||
|
||||
// 如果没有计算出速度,尝试从velocity获取并转换单位
|
||||
Double speed = calculatedSpeed;
|
||||
if (speed == null && vehicle.getVelocity() != null) {
|
||||
// Velocity.getSpeed()返回米/秒,需要转换为公里/小时
|
||||
speed = vehicle.getVelocity().getSpeed() * 3.6;
|
||||
}
|
||||
|
||||
// 使用VehicleLocationService创建VehicleLocation对象
|
||||
return vehicleLocationService.createVehicleLocationByLicensePlate(
|
||||
licensePlate, longitude, latitude,
|
||||
altitude, heading, speed
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换无人车数据失败: vehicleId={}", vehicle.getLicensePlate(), e);
|
||||
return null;
|
||||
@Scheduled(fixedRateString = "${data.collector.websocket.push-interval}")
|
||||
public void performPeriodicViolationDetection() {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeMovingObjectsCache.isEmpty()) {
|
||||
log.debug("活跃对象缓存为空,跳过周期性违规检测和冲突检测。");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始周期性违规检测和路径冲突检测,活跃对象数量: {}", activeMovingObjectsCache.size());
|
||||
|
||||
// 获取所有活跃的MovingObject的快照
|
||||
List<MovingObject> currentActiveObjects = new ArrayList<>(activeMovingObjectsCache.values());
|
||||
|
||||
// 执行路径冲突检测
|
||||
pathConflictDetectionService.detectPathConflicts(currentActiveObjects);
|
||||
|
||||
// 将MovingObject列表转换为VehicleLocation列表进行批量检测
|
||||
List<VehicleLocation> detectionVehicleLocations = currentActiveObjects.stream()
|
||||
.map(this::createTemporaryVehicleLocationForDetection)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 执行实时违规检测 (超速、区域访问等)
|
||||
if (!detectionVehicleLocations.isEmpty()) {
|
||||
realTimeViolationDetector.detectBatchViolations(detectionVehicleLocations);
|
||||
} else {
|
||||
log.debug("没有可用于实时违规检测的车辆位置数据。");
|
||||
}
|
||||
|
||||
log.info("周期性违规检测和路径冲突检测完成。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据采集统计信息
|
||||
*
|
||||
* 提供数据采集的运行状态和统计信息
|
||||
* 统计当前数据采集服务的健康状态和关键指标
|
||||
* @return 统计信息字符串
|
||||
*/
|
||||
public String getCollectionStats() {
|
||||
if (collectorDisabled) {
|
||||
return "数据采集服务已禁用";
|
||||
}
|
||||
long totalAircraft = 0; // 由于不持久化,这里只是示例
|
||||
long totalVehicles = 0; // 同上
|
||||
// 实际上可以统计已处理的事件数量等
|
||||
|
||||
StringBuilder stats = new StringBuilder();
|
||||
stats.append("数据采集服务状态: 运行中\n");
|
||||
stats.append("数据源配置:\n");
|
||||
stats.append(" - 航空器API: ").append(airportBaseUrl).append(airportAircraftEndpoint).append(" (仅实时处理)\n");
|
||||
stats.append(" - 机场车辆API: ").append(airportBaseUrl).append(airportVehicleEndpoint).append(" (仅实时处理)\n");
|
||||
stats.append(" - 无人车API: 用于数据持久化和轨迹回放\n");
|
||||
|
||||
// 添加数据持久化统计信息
|
||||
try {
|
||||
String persistenceStats = vehicleDataPersistenceService.getPersistenceStatistics();
|
||||
stats.append("\n").append(persistenceStats);
|
||||
} catch (Exception e) {
|
||||
stats.append("\n数据持久化统计获取失败: ").append(e.getMessage()).append("\n");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取数据库中的无人车数据统计(只有无人车数据会存储)
|
||||
long unmannedCount = vehicleLocationService.getActiveVehiclesByType(MovingObjectType.UNMANNED_VEHICLE, 60).size();
|
||||
|
||||
stats.append("数据持久化统计:\n");
|
||||
stats.append(" - 航空器: 仅实时处理,不存储\n");
|
||||
stats.append(" - 机场车辆: 仅实时处理,不存储\n");
|
||||
stats.append(" - 无人车: ").append(unmannedCount).append(" 条记录(最近1小时)\n");
|
||||
|
||||
} catch (Exception e) {
|
||||
stats.append("获取统计信息失败: ").append(e.getMessage()).append("\n");
|
||||
}
|
||||
|
||||
return stats.toString();
|
||||
return String.format("DataCollectorService Stats: " +
|
||||
"Total Aircraft Processed: %d, " +
|
||||
"Total Airport Vehicles Processed: %d, " +
|
||||
"Total Unmanned Vehicles Processed: %d",
|
||||
totalAircraft, totalVehicles, vehicleLocationService.countAllUnmannedVehicles());
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.info("正在关闭数据采集服务...");
|
||||
// 重构说明:移除内存缓存清理,因为已经不使用内存存储
|
||||
log.info("数据采集服务已关闭 - PostGIS存储模式");
|
||||
log.info("DataCollectorService 正在关闭...");
|
||||
// 清理资源,如果需要
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查是否应该推送WebSocket消息(节流机制)
|
||||
* @param objectId 对象ID
|
||||
* @param currentTime 当前时间
|
||||
* @return 是否应该推送
|
||||
* 检查是否应该推送WebSocket消息(节流)
|
||||
*/
|
||||
private boolean shouldPushWebSocketMessage(String objectId, long currentTime) {
|
||||
Long lastPushTime = lastPushTimes.get(objectId);
|
||||
if (lastPushTime == null || (currentTime - lastPushTime) >= websocketPushInterval) {
|
||||
Long lastPushTime = lastPushTimes.getOrDefault(objectId, 0L);
|
||||
if (currentTime - lastPushTime >= websocketPushInterval) {
|
||||
lastPushTimes.put(objectId, currentTime);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 UnmannedVehicle 转换为 VehicleLocation 实体用于持久化
|
||||
*/
|
||||
private com.qaup.collision.common.model.spatial.VehicleLocation convertToVehicleLocation(UnmannedVehicle vehicle) {
|
||||
if (vehicle == null || vehicle.getObjectId() == null || vehicle.getObjectId().trim().isEmpty()) {
|
||||
log.warn("无法转换无人车位置数据:对象或其 ID 为空或无效。对象: {}", vehicle);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 根据无人车的 objectId (车牌号/外部ID) 查询其在 sys_vehicle_info 表中的 Long 类型 vehicleId
|
||||
Optional<SysVehicleInfo> vehicleInfoOptional = quapDataAdapter.findVehicleByLicensePlate(vehicle.getObjectId());
|
||||
|
||||
if (vehicleInfoOptional.isEmpty()) {
|
||||
log.warn("未找到无人车 {} 对应的系统车辆信息,跳过持久化。这可能意味着该无人车未在系统中注册。", vehicle.getObjectId());
|
||||
return null;
|
||||
}
|
||||
|
||||
SysVehicleInfo sysVehicleInfo = vehicleInfoOptional.get();
|
||||
|
||||
return com.qaup.collision.common.model.spatial.VehicleLocation.builder()
|
||||
.vehicleId(sysVehicleInfo.getVehicleId()) // 使用从 sys_vehicle_info 获取的 Long 类型 vehicleId
|
||||
.location(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(vehicle.getCurrentPosition().getX(), vehicle.getCurrentPosition().getY())))
|
||||
.altitude(vehicle.getAltitude())
|
||||
.speed(vehicle.getCurrentSpeed())
|
||||
.heading(vehicle.getCurrentHeading())
|
||||
.timestamp(java.time.LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, java.time.ZoneOffset.UTC))
|
||||
.licensePlate(sysVehicleInfo.getLicensePlate()) // 设置运行时属性 licensePlate
|
||||
.vehicleType(com.qaup.collision.common.model.MovingObjectType.UNMANNED_VEHICLE) // 从无人车接口获取的数据,直接设置为无人车类型
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("转换无人车位置数据时发生未知错误: objectId={}", vehicle.getObjectId(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时VehicleLocation用于违规检测(航空器和机场车辆)
|
||||
* 对于不持久化的MovingObject,生成一个临时的VehicleLocation用于RealTimeViolationDetector
|
||||
*/
|
||||
private com.qaup.collision.common.model.spatial.VehicleLocation createTemporaryVehicleLocationForDetection(MovingObject movingObject) {
|
||||
if (movingObject == null || movingObject.getObjectId() == null || movingObject.getObjectId().trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Long vehicleIdForDetection;
|
||||
String licensePlateForDetection;
|
||||
com.qaup.collision.common.model.MovingObjectType objectTypeForDetection = com.qaup.collision.common.model.MovingObjectType.valueOf(movingObject.getObjectType().name());
|
||||
|
||||
if (MovingObject.ObjectType.UNMANNED_VEHICLE.equals(movingObject.getObjectType())) {
|
||||
// 对于无人车,其objectId就是车牌号,可以从sys_vehicle_info中查到vehicleId
|
||||
Optional<SysVehicleInfo> vehicleInfoOptional = quapDataAdapter.findVehicleByLicensePlate(movingObject.getObjectId());
|
||||
if (vehicleInfoOptional.isEmpty()) {
|
||||
log.warn("未找到无人车 {} 对应的系统车辆信息,跳过违规检测。这可能意味着该无人车未在系统中注册。", movingObject.getObjectId());
|
||||
return null;
|
||||
}
|
||||
SysVehicleInfo sysVehicleInfo = vehicleInfoOptional.get();
|
||||
vehicleIdForDetection = sysVehicleInfo.getVehicleId();
|
||||
licensePlateForDetection = sysVehicleInfo.getLicensePlate();
|
||||
} else {
|
||||
// 对于航空器和机场车辆(包括特勤车),其objectId是flightNo或vehicleNo,
|
||||
// 生成一个临时的、一致的Long类型ID,用于内部检测和日志记录
|
||||
vehicleIdForDetection = (long) movingObject.getObjectId().hashCode(); // 使用hashCode生成一个伪ID
|
||||
licensePlateForDetection = movingObject.getObjectId(); // 用objectId作为车牌号
|
||||
}
|
||||
|
||||
Point currentPosition = movingObject.getCurrentPosition();
|
||||
if (currentPosition == null) {
|
||||
log.warn("对象 {} 位置信息缺失,跳过违规检测。", movingObject.getObjectId());
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("在createTemporaryVehicleLocationForDetection方法中,准备创建VehicleLocation,MovingObject的速度为: {}", movingObject.getCurrentSpeed());
|
||||
|
||||
return com.qaup.collision.common.model.spatial.VehicleLocation.builder()
|
||||
.vehicleId(vehicleIdForDetection)
|
||||
.location(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(currentPosition.getX(), currentPosition.getY())))
|
||||
.altitude(movingObject.getAltitude() != null ? movingObject.getAltitude() : 0.0)
|
||||
.speed(movingObject.getCurrentSpeed() != null ? movingObject.getCurrentSpeed() : 0.0)
|
||||
.heading(movingObject.getCurrentHeading() != null ? movingObject.getCurrentHeading() : 0.0)
|
||||
.timestamp(java.time.LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, java.time.ZoneOffset.UTC)) // 使用当前时间作为检测时间戳
|
||||
.licensePlate(licensePlateForDetection)
|
||||
.vehicleType(objectTypeForDetection)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("创建临时VehicleLocation用于违规检测失败: objectId={}", movingObject.getObjectId(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
package com.qaup.collision.dataprocessing.service;
|
||||
|
||||
import com.qaup.collision.common.model.GeoPosition;
|
||||
import com.qaup.collision.common.model.MovementState;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.Velocity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@ -29,10 +23,6 @@ public class SpeedCalculationService {
|
||||
private static final double MAX_POSITION_JUMP = 100.0; // 米
|
||||
private static final double MAX_TIME_JUMP = 5000.0; // 毫秒
|
||||
|
||||
/**
|
||||
* 数据预处理
|
||||
*/
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* 实时速度计算相关 - 历史位置信息缓存
|
||||
@ -48,171 +38,6 @@ public class SpeedCalculationService {
|
||||
@Value("${data.collector.websocket.push-interval:1000}")
|
||||
private long websocketPushInterval;
|
||||
|
||||
public void preprocessData(List<MovingObject> dataList) {
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
for (MovingObject obj : dataList) {
|
||||
|
||||
// 如果历史状态队列为空,使用当前状态初始化
|
||||
if (obj.getStateHistory().isEmpty()) {
|
||||
MovementState state = new MovementState();
|
||||
state.setPosition(obj.getCurrentPosition());
|
||||
state.setVelocity(obj.getVelocity());
|
||||
state.setTimestamp(obj.getTimestamp());
|
||||
|
||||
// 默认设置数据质量为GOOD
|
||||
state.setDataQuality(MovementState.DataQuality.GOOD);
|
||||
obj.getStateHistory().add(state);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取最近的历史状态
|
||||
MovementState lastState = obj.getStateHistory().getFirst();
|
||||
|
||||
// 异常值检测
|
||||
double distance = calculateDistance(obj.getVelocity(), lastState.getVelocity());
|
||||
long timeDiff = obj.getTimestamp() - lastState.getTimestamp();
|
||||
|
||||
// 检查位置跳变
|
||||
if (distance > MAX_POSITION_JUMP) {
|
||||
obj.getStateHistory().getFirst().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
// state.setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
}
|
||||
|
||||
// 检查时间异常
|
||||
if (timeDiff <= 0 || timeDiff > MAX_TIME_JUMP) {
|
||||
obj.getStateHistory().getFirst().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
// state.setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
}
|
||||
|
||||
// 检查速度异常
|
||||
double speed = distance / (timeDiff / 1000.0);
|
||||
if (speed > obj.getMaxSpeed()) {
|
||||
obj.getStateHistory().getFirst().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
// state.setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
}
|
||||
|
||||
// 只对质量良好的数据进行卡尔曼滤波
|
||||
// if (object.getStateHistory().getLast().getDataQuality() == MovementState.DataQuality.GOOD) {
|
||||
// applyKalmanFilter(object.getStateHistory().getLast(), object.getStateHistory());
|
||||
// }
|
||||
|
||||
// object.getStateHistory().add(state);
|
||||
}
|
||||
}finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常值检测
|
||||
*/
|
||||
private boolean isDataAnomaly(MovingObject object, GeoPosition newPosition, long timestamp) {
|
||||
if (object.getStateHistory().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MovementState lastState = object.getStateHistory().getLast();
|
||||
|
||||
// 检查位置跳变
|
||||
double distance = calculateDistance(lastState.getVelocity(), object.getVelocity());
|
||||
if (distance > MAX_POSITION_JUMP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查时间异常
|
||||
long timeDiff = timestamp - lastState.getTimestamp();
|
||||
if (timeDiff <= 0 || timeDiff > MAX_TIME_JUMP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查速度异常
|
||||
double speed = distance / (timeDiff / 1000.0);
|
||||
return speed > object.getMaxSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡尔曼滤波
|
||||
*/
|
||||
private void applyKalmanFilter(MovementState state, Deque<MovementState> history) {
|
||||
// TODO: 实现卡尔曼滤波算法
|
||||
// 状态向量:[x, y, vx, vy]
|
||||
// 观测向量:[x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算速度
|
||||
*/
|
||||
public void calculateSpeed(MovingObject object, MovementState newState) {
|
||||
// Deque<MovementState> history = object.getStateHistory();
|
||||
// if (history.isEmpty()) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 确定窗口大小
|
||||
// int windowSize = determineWindowSize(object);
|
||||
//
|
||||
// // 计算速度
|
||||
// Velocity velocity = calculateVelocityComponents(object, object.getStateHistory().getLast());
|
||||
//
|
||||
// // 应用加速度约束
|
||||
// applyAccelerationConstraints(velocity, object);
|
||||
//
|
||||
// newState.setVelocity(velocity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从滑动窗口计算速度
|
||||
*/
|
||||
private void calculateVelocityComponents(MovingObject object, MovementState lastState) {
|
||||
|
||||
Velocity velocity = object.getVelocity();
|
||||
|
||||
long currentTime = object.getTimestamp();
|
||||
long lastTime = lastState.getTimestamp();
|
||||
long deltaTime = currentTime - lastTime;
|
||||
|
||||
if (deltaTime <= 0) {
|
||||
object.getStateHistory().getLast().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
|
||||
return;
|
||||
}
|
||||
|
||||
double deltaX = object.getVelocity().getX() - lastState.getVelocity().getX();
|
||||
double deltaY = object.getVelocity().getY() - lastState.getVelocity().getY();
|
||||
double deltaZ = object.getVelocity().getZ() - lastState.getVelocity().getZ();
|
||||
|
||||
object.getVelocity().setVx(deltaX / (deltaTime / 1000.0));
|
||||
object.getVelocity().setVy(deltaY / (deltaTime / 1000.0));
|
||||
object.getVelocity().setVz(deltaZ / (deltaTime / 1000.0));
|
||||
|
||||
// 缓存瞬时加速度
|
||||
double lastSpeed = Math.sqrt(
|
||||
Math.pow(lastState.getVelocity().getVx(), 2) +
|
||||
Math.pow(lastState.getVelocity().getVy(), 2) +
|
||||
Math.pow(lastState.getVelocity().getVz(), 2)
|
||||
);
|
||||
double currentSpeed = Math.sqrt(Math.pow(velocity.getVx(), 2) + Math.pow(velocity.getVy(), 2));
|
||||
object.getVelocity().setCachedAcceleration((currentSpeed - lastSpeed) / (deltaTime / 1000.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个向量间的距离
|
||||
* @param currentVelocity 当前向量
|
||||
* @param historicalVelocity 历史向量
|
||||
* @return 距离
|
||||
*/
|
||||
private double calculateDistance(Velocity currentVelocity, Velocity historicalVelocity) {
|
||||
|
||||
double deltaX = currentVelocity.getX() - historicalVelocity.getX();
|
||||
double deltaY = currentVelocity.getY() - historicalVelocity.getY();
|
||||
double deltaZ = currentVelocity.getZ() - historicalVelocity.getZ();
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ);
|
||||
}
|
||||
|
||||
// ==================== 实时速度计算功能 ====================
|
||||
|
||||
@ -322,41 +147,6 @@ public class SpeedCalculationService {
|
||||
return roundedDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带有计算速度的Velocity对象(用于复杂分析)
|
||||
* @param objectId 对象ID
|
||||
* @param latitude 当前纬度
|
||||
* @param longitude 当前经度
|
||||
* @param timestamp 当前时间戳(毫秒)
|
||||
* @param heading 航向角(度,可为null)
|
||||
* @return Velocity对象,如果无法计算速度则返回null
|
||||
*/
|
||||
public Velocity createRealtimeVelocity(String objectId, double latitude, double longitude, long timestamp, Double heading) {
|
||||
Double speed = calculateRealtimeSpeed(objectId, latitude, longitude, timestamp);
|
||||
|
||||
if (speed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Velocity velocity = new Velocity();
|
||||
|
||||
// 如果有航向角,计算速度分量
|
||||
if (heading != null) {
|
||||
double headingRad = Math.toRadians(heading);
|
||||
velocity.setVx(speed * Math.cos(headingRad)); // 东向速度
|
||||
velocity.setVy(speed * Math.sin(headingRad)); // 北向速度
|
||||
} else {
|
||||
// 没有航向角时,假设都是东向移动
|
||||
velocity.setVx(speed);
|
||||
velocity.setVy(0);
|
||||
}
|
||||
|
||||
velocity.setVz(0); // 垂直速度为0
|
||||
velocity.setConfidence(0.8); // 设置置信度
|
||||
|
||||
return velocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的当前速度(不更新历史记录)
|
||||
* @param objectId 对象ID
|
||||
|
||||
@ -0,0 +1,156 @@
|
||||
package com.qaup.collision.pathconflict.event;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.pathconflict.service.VehicleCommandService;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 冲突告警事件监听器
|
||||
* 负责处理路径冲突告警事件,发送WebSocket消息和车辆控制指令
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ConflictAlertEventListener {
|
||||
|
||||
private final RuleEventWebSocketPublisher webSocketPublisher;
|
||||
private final VehicleCommandService vehicleCommandService;
|
||||
private final ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
/**
|
||||
* 处理冲突告警事件
|
||||
*/
|
||||
@Async
|
||||
@EventListener
|
||||
public void handleConflictAlert(ConflictAlertEvent event) {
|
||||
log.info("处理路径冲突告警事件: conflictId={}, alertType={}",
|
||||
event.getConflictId(), event.getAlertType());
|
||||
|
||||
try {
|
||||
// 发送WebSocket消息到控制台
|
||||
sendWebSocketAlert(event);
|
||||
|
||||
// 向涉及的无人车发送控制指令
|
||||
sendVehicleCommands(event);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理冲突告警事件失败: conflictId={}", event.getConflictId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送WebSocket告警消息到控制台
|
||||
*/
|
||||
private void sendWebSocketAlert(ConflictAlertEvent event) {
|
||||
try {
|
||||
PathConflictAlertMessage message = PathConflictAlertMessage.builder()
|
||||
.conflictId(event.getConflictId())
|
||||
.alertType(event.getAlertType().name()) // 使用枚举的name()
|
||||
.alertLevel(event.getAlertLevel().name()) // 使用枚举的name()
|
||||
.message(event.getMessage())
|
||||
.object1(PathConflictAlertMessage.ConflictObject.builder()
|
||||
.objectId(event.getObject1Id())
|
||||
.build()) // 移除 objectType
|
||||
.object2(PathConflictAlertMessage.ConflictObject.builder()
|
||||
.objectId(event.getObject2Id())
|
||||
.build()) // 移除 objectType
|
||||
.position(event.getConflictPointLatitude() != null && event.getConflictPointLongitude() != null ?
|
||||
PathConflictAlertMessage.Position.builder()
|
||||
.latitude(event.getConflictPointLatitude())
|
||||
.longitude(event.getConflictPointLongitude())
|
||||
.build() : null)
|
||||
.object1Distance(event.getObject1Distance()) // 新增
|
||||
.object2Distance(event.getObject2Distance()) // 新增
|
||||
.timeToConflict1(event.getEstimatedTimeToConflictObj1()) // 新增
|
||||
.timeToConflict2(event.getEstimatedTimeToConflictObj2()) // 新增
|
||||
.timeGap(event.getTimeGapSeconds()) // 新增
|
||||
.eventTime(event.getEventTime())
|
||||
.build();
|
||||
|
||||
// 发送WebSocket消息
|
||||
webSocketPublisher.publishPathConflictAlert(message);
|
||||
|
||||
log.info("WebSocket告警消息已发送: conflictId={}", event.getConflictId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket告警消息失败: conflictId={}", event.getConflictId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向涉及的无人车发送控制指令
|
||||
*/
|
||||
private void sendVehicleCommands(ConflictAlertEvent event) {
|
||||
try {
|
||||
// 向对象1发送指令(如果是无人车)
|
||||
objectRouteAssignmentRepository.findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc(
|
||||
event.getObject1Id(), ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.ifPresent(assignment -> sendVehicleCommand(event.getObject1Id(), event));
|
||||
|
||||
// 向对象2发送指令(如果是无人车)
|
||||
objectRouteAssignmentRepository.findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc(
|
||||
event.getObject2Id(), ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.ifPresent(assignment -> sendVehicleCommand(event.getObject2Id(), event));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送车辆控制指令失败: conflictId={}", event.getConflictId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定无人车发送控制指令
|
||||
*/
|
||||
private void sendVehicleCommand(String vehicleId, ConflictAlertEvent event) {
|
||||
try {
|
||||
String commandType;
|
||||
String commandMessage;
|
||||
|
||||
// 根据告警级别确定指令类型
|
||||
if (event.isEmergencyAlert()) {
|
||||
commandType = "EMERGENCY_STOP";
|
||||
commandMessage = String.format("紧急告警:检测到路径冲突,立即停车!对象1距离冲突点:%.1f米,对象2距离冲突点:%.1f米,时间差:%.1f秒",
|
||||
event.getObject1Distance(), event.getObject2Distance(), event.getTimeGapSeconds());
|
||||
} else if (event.isAlert()) {
|
||||
commandType = "SLOW_DOWN";
|
||||
commandMessage = String.format("路径冲突告警:前方对象1预计%d秒,对象2预计%d秒到达冲突点,请减速行驶,时间差:%.1f秒",
|
||||
event.getEstimatedTimeToConflictObj1(), event.getEstimatedTimeToConflictObj2(), event.getTimeGapSeconds());
|
||||
} else if (event.isWarning()) {
|
||||
commandType = "CAUTION";
|
||||
commandMessage = String.format("路径冲突预警:前方检测到潜在冲突,请注意观察。对象1距离冲突点:%.1f米,对象2距离冲突点:%.1f米",
|
||||
event.getObject1Distance(), event.getObject2Distance());
|
||||
} else {
|
||||
commandType = "INFO";
|
||||
commandMessage = "路径冲突信息提醒";
|
||||
}
|
||||
|
||||
// 发送指令到无人车
|
||||
boolean success = vehicleCommandService.sendCommand(
|
||||
vehicleId, commandType, commandMessage, event.getConflictId());
|
||||
|
||||
if (success) {
|
||||
log.info("车辆控制指令已发送: vehicleId={}, commandType={}, conflictId={}",
|
||||
vehicleId, commandType, event.getConflictId());
|
||||
} else {
|
||||
log.warn("车辆控制指令发送失败: vehicleId={}, conflictId={}",
|
||||
vehicleId, event.getConflictId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送车辆控制指令时出错: vehicleId={}, conflictId={}",
|
||||
vehicleId, event.getConflictId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.qaup.collision.pathconflict.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 冲突告警事件DTO
|
||||
* 用于在系统内部传递冲突告警信息
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ConflictAlertEvent {
|
||||
|
||||
/**
|
||||
* 冲突ID
|
||||
*/
|
||||
private String conflictId;
|
||||
|
||||
/**
|
||||
* 告警类型:WARNING, ALERT, EMERGENCY
|
||||
*/
|
||||
private ConflictAlertLog.AlertType alertType;
|
||||
|
||||
/**
|
||||
* 告警级别:INFO, WARNING, CRITICAL, EMERGENCY
|
||||
*/
|
||||
private ConflictAlertLog.AlertLevel alertLevel;
|
||||
|
||||
/**
|
||||
* 对象1 ID
|
||||
*/
|
||||
private String object1Id;
|
||||
|
||||
/**
|
||||
* 对象2 ID
|
||||
*/
|
||||
private String object2Id;
|
||||
|
||||
/**
|
||||
* 冲突点位置
|
||||
*/
|
||||
private Point conflictPoint;
|
||||
|
||||
/**
|
||||
* 告警消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 对象1距离冲突点距离 (米)
|
||||
*/
|
||||
private Double object1Distance;
|
||||
|
||||
/**
|
||||
* 对象2距离冲突点距离 (米)
|
||||
*/
|
||||
private Double object2Distance;
|
||||
|
||||
/**
|
||||
* 预计到达冲突点时间1 (秒)
|
||||
*/
|
||||
private Integer estimatedTimeToConflictObj1;
|
||||
|
||||
/**
|
||||
* 预计到达冲突点时间2 (秒)
|
||||
*/
|
||||
private Integer estimatedTimeToConflictObj2;
|
||||
|
||||
/**
|
||||
* 时间差 (秒)
|
||||
*/
|
||||
private Double timeGapSeconds;
|
||||
|
||||
/**
|
||||
* 事件发生时间
|
||||
*/
|
||||
@Builder.Default
|
||||
private LocalDateTime eventTime = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 事件来源
|
||||
*/
|
||||
@Builder.Default
|
||||
private String eventSource = "PATH_CONFLICT_DETECTION";
|
||||
|
||||
/**
|
||||
* 判断是否为紧急告警
|
||||
*/
|
||||
public boolean isEmergencyAlert() {
|
||||
return ConflictAlertLog.AlertType.EMERGENCY.equals(alertType) || ConflictAlertLog.AlertLevel.CRITICAL.equals(alertLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为预警
|
||||
*/
|
||||
public boolean isWarning() {
|
||||
return ConflictAlertLog.AlertType.WARNING.equals(alertType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为告警
|
||||
*/
|
||||
public boolean isAlert() {
|
||||
return ConflictAlertLog.AlertType.ALERT.equals(alertType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冲突点经度
|
||||
*/
|
||||
public Double getConflictPointLongitude() {
|
||||
return conflictPoint != null ? conflictPoint.getX() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冲突点纬度
|
||||
*/
|
||||
public Double getConflictPointLatitude() {
|
||||
return conflictPoint != null ? conflictPoint.getY() : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.qaup.collision.pathconflict.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 冲突预警告警记录实体类
|
||||
* 对应 conflict_alert_logs 表,记录发送的所有预警和告警消息
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "conflict_alert_logs", indexes = {
|
||||
@Index(name = "idx_conflict_alerts_conflict_id", columnList = "conflict_id"),
|
||||
@Index(name = "idx_conflict_alerts_type", columnList = "alert_type"),
|
||||
@Index(name = "idx_conflict_alerts_level", columnList = "alert_level"),
|
||||
@Index(name = "idx_conflict_alerts_time", columnList = "alert_time DESC")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ConflictAlertLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 冲突的唯一标识,可能来自实时计算或是一个事件ID
|
||||
*/
|
||||
@Column(name = "conflict_id", nullable = false, length = 50)
|
||||
private String conflictId;
|
||||
|
||||
/**
|
||||
* 告警类型:WARNING, ALERT, EMERGENCY
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "alert_type", nullable = false, length = 20)
|
||||
private AlertType alertType;
|
||||
|
||||
/**
|
||||
* 告警级别:INFO, WARNING, CRITICAL, EMERGENCY
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "alert_level", nullable = false, length = 20)
|
||||
private AlertLevel alertLevel;
|
||||
|
||||
/**
|
||||
* 告警消息内容
|
||||
*/
|
||||
@Column(name = "alert_message", nullable = false, columnDefinition = "TEXT")
|
||||
private String alertMessage;
|
||||
|
||||
/**
|
||||
* 对象1距离冲突点距离 (米)
|
||||
*/
|
||||
@Column(name = "object1_distance")
|
||||
private Double object1Distance;
|
||||
|
||||
/**
|
||||
* 对象2距离冲突点距离 (米)
|
||||
*/
|
||||
@Column(name = "object2_distance")
|
||||
private Double object2Distance;
|
||||
|
||||
/**
|
||||
* 两对象之间最小距离 (米)
|
||||
*/
|
||||
@Column(name = "minimum_distance")
|
||||
private Double minimumDistance;
|
||||
|
||||
/**
|
||||
* 告警时间
|
||||
*/
|
||||
@Column(name = "alert_time")
|
||||
private LocalDateTime alertTime;
|
||||
|
||||
/**
|
||||
* 告警类型枚举
|
||||
*/
|
||||
public enum AlertType {
|
||||
WARNING, // 预警
|
||||
ALERT, // 告警
|
||||
EMERGENCY // 紧急
|
||||
}
|
||||
|
||||
/**
|
||||
* 告警级别枚举
|
||||
*/
|
||||
public enum AlertLevel {
|
||||
INFO, // 信息
|
||||
WARNING, // 警告
|
||||
CRITICAL, // 严重
|
||||
EMERGENCY // 紧急
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
alertTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.qaup.collision.pathconflict.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 实时对象路径分配实体类
|
||||
* 对应 object_route_assignments 表,跟踪对象当前使用的路径
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "object_route_assignments", indexes = {
|
||||
@Index(name = "idx_object_route_assignments_object", columnList = "object_id,object_type"),
|
||||
@Index(name = "idx_object_route_assignments_route", columnList = "assigned_route_id"),
|
||||
@Index(name = "idx_object_route_assignments_assigned_at", columnList = "assigned_at DESC")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ObjectRouteAssignment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 对象ID:无人车ID或航空器标识
|
||||
*/
|
||||
@Column(name = "object_id", nullable = false, length = 50)
|
||||
private String objectId;
|
||||
|
||||
/**
|
||||
* 对象类型:UNMANNED_VEHICLE, AIRCRAFT, SPECIAL_VEHICLE
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "object_type", nullable = false, length = 20)
|
||||
private ObjectType objectType;
|
||||
|
||||
/**
|
||||
* 对象名称:车牌号或航班号
|
||||
*/
|
||||
@Column(name = "object_name", length = 100)
|
||||
private String objectName;
|
||||
|
||||
/**
|
||||
* 分配的路径ID
|
||||
*/
|
||||
@Column(name = "assigned_route_id", nullable = false, length = 50)
|
||||
private String assignedRouteId;
|
||||
|
||||
/**
|
||||
* 分配时间
|
||||
*/
|
||||
@Column(name = "assigned_at")
|
||||
private LocalDateTime assignedAt;
|
||||
|
||||
/**
|
||||
* 对象类型枚举
|
||||
*/
|
||||
public enum ObjectType {
|
||||
UNMANNED_VEHICLE, // 无人车
|
||||
AIRCRAFT, // 航空器
|
||||
SPECIAL_VEHICLE // 特种车辆
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
assignedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
package com.qaup.collision.pathconflict.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 交通路径实体类
|
||||
* 对应 transport_routes 表,存储航空器和无人车的预定路径
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "transport_routes", indexes = {
|
||||
@Index(name = "idx_transport_routes_route_id", columnList = "route_id"),
|
||||
@Index(name = "idx_transport_routes_type", columnList = "route_type"),
|
||||
@Index(name = "idx_transport_routes_status", columnList = "status")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class TransportRoute {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 路径唯一标识符
|
||||
*/
|
||||
@Column(name = "route_id", nullable = false, unique = true, length = 50)
|
||||
private String routeId;
|
||||
|
||||
/**
|
||||
* 路径名称
|
||||
*/
|
||||
@Column(name = "route_name", nullable = false, length = 100)
|
||||
private String routeName;
|
||||
|
||||
/**
|
||||
* 路径类型:AIRCRAFT, UNMANNED_VEHICLE, SPECIAL_VEHICLE
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "route_type", nullable = false, length = 20)
|
||||
private RouteType routeType;
|
||||
|
||||
/**
|
||||
* 路径描述
|
||||
*/
|
||||
@Column(name = "description", length = 500)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 路径几何形状 (LineString)
|
||||
*/
|
||||
@Column(name = "route_geometry", nullable = false, columnDefinition = "geometry(LineString,4326)")
|
||||
private LineString routeGeometry;
|
||||
|
||||
/**
|
||||
* 最大速度 (km/h)
|
||||
*/
|
||||
@Column(name = "max_speed_kph")
|
||||
private Double maxSpeedKph;
|
||||
|
||||
/**
|
||||
* 典型速度 (km/h)
|
||||
*/
|
||||
@Column(name = "typical_speed_kph")
|
||||
private Double typicalSpeedKph;
|
||||
|
||||
/**
|
||||
* 活跃时间开始
|
||||
*/
|
||||
@Column(name = "active_time_start")
|
||||
private LocalTime activeTimeStart;
|
||||
|
||||
/**
|
||||
* 活跃时间结束
|
||||
*/
|
||||
@Column(name = "active_time_end")
|
||||
private LocalTime activeTimeEnd;
|
||||
|
||||
/**
|
||||
* 路径状态
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", length = 20)
|
||||
@Builder.Default
|
||||
private RouteStatus status = RouteStatus.ACTIVE;
|
||||
|
||||
/**
|
||||
* 是否双向
|
||||
*/
|
||||
@Column(name = "is_bidirectional")
|
||||
@Builder.Default
|
||||
private Boolean isBidirectional = false;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@Column(name = "created_by", length = 50)
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@Column(name = "updated_by", length = 50)
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 路径类型枚举
|
||||
*/
|
||||
public enum RouteType {
|
||||
AIRCRAFT, // 航空器路径
|
||||
UNMANNED_VEHICLE, // 无人车路径
|
||||
SPECIAL_VEHICLE // 特种车辆路径
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径状态枚举
|
||||
*/
|
||||
public enum RouteStatus {
|
||||
ACTIVE, // 活跃
|
||||
INACTIVE, // 非活跃
|
||||
MAINTENANCE // 维护中
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.qaup.collision.pathconflict.repository;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 冲突预警告警记录Repository
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Repository
|
||||
public interface ConflictAlertLogRepository extends JpaRepository<ConflictAlertLog, Long> {
|
||||
|
||||
/**
|
||||
* 根据冲突ID查找告警记录
|
||||
*/
|
||||
List<ConflictAlertLog> findByConflictId(String conflictId);
|
||||
|
||||
/**
|
||||
* 根据告警类型和级别查找告警记录
|
||||
*/
|
||||
List<ConflictAlertLog> findByAlertTypeAndAlertLevel(
|
||||
ConflictAlertLog.AlertType alertType,
|
||||
ConflictAlertLog.AlertLevel alertLevel
|
||||
);
|
||||
|
||||
/**
|
||||
* 查找最新的一条告警记录
|
||||
*/
|
||||
Optional<ConflictAlertLog> findFirstByOrderByAlertTimeDesc();
|
||||
|
||||
/**
|
||||
* 查找某个冲突ID下最新的告警记录
|
||||
*/
|
||||
Optional<ConflictAlertLog> findFirstByConflictIdOrderByAlertTimeDesc(String conflictId);
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.qaup.collision.pathconflict.repository;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 实时对象路径分配Repository
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Repository
|
||||
public interface ObjectRouteAssignmentRepository extends JpaRepository<ObjectRouteAssignment, Long> {
|
||||
|
||||
/**
|
||||
* 根据对象ID和对象类型查找最新的路径分配记录
|
||||
*/
|
||||
Optional<ObjectRouteAssignment> findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc(
|
||||
String objectId,
|
||||
ObjectRouteAssignment.ObjectType objectType
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据路径ID查找所有分配到该路径的对象
|
||||
*/
|
||||
List<ObjectRouteAssignment> findByAssignedRouteId(String assignedRouteId);
|
||||
|
||||
/**
|
||||
* 根据对象ID查找所有分配到该对象的路径记录
|
||||
*/
|
||||
List<ObjectRouteAssignment> findByObjectId(String objectId);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.qaup.collision.pathconflict.repository;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 交通路径Repository
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Repository
|
||||
public interface TransportRouteRepository extends JpaRepository<TransportRoute, Long> {
|
||||
|
||||
/**
|
||||
* 根据路径ID查找路径
|
||||
*/
|
||||
Optional<TransportRoute> findByRouteId(String routeId);
|
||||
|
||||
/**
|
||||
* 根据路径类型和状态查找路径
|
||||
*/
|
||||
List<TransportRoute> findByRouteTypeAndStatus(
|
||||
TransportRoute.RouteType routeType,
|
||||
TransportRoute.RouteStatus status);
|
||||
|
||||
/**
|
||||
* 查找指定范围内的路径
|
||||
*/
|
||||
@Query(value = "SELECT * FROM transport_routes r WHERE " +
|
||||
"ST_DWithin(r.route_geometry, ST_GeomFromText(:point, 4326), :radiusMeters) " +
|
||||
"AND r.status = :status", nativeQuery = true)
|
||||
List<TransportRoute> findRoutesWithinRadius(
|
||||
@Param("point") String pointWKT,
|
||||
@Param("radiusMeters") double radiusMeters,
|
||||
@Param("status") String status);
|
||||
|
||||
/**
|
||||
* 查找与指定路径相交的路径
|
||||
*/
|
||||
@Query(value = "SELECT r2.* FROM transport_routes r1, transport_routes r2 WHERE " +
|
||||
"r1.route_id = :routeId AND r2.route_id != :routeId " +
|
||||
"AND ST_Intersects(r1.route_geometry, r2.route_geometry) " +
|
||||
"AND r2.status = 'ACTIVE'", nativeQuery = true)
|
||||
List<TransportRoute> findIntersectingRoutes(@Param("routeId") String routeId);
|
||||
|
||||
/**
|
||||
* 查找最近的路径
|
||||
*/
|
||||
@Query(value = "SELECT *, ST_Distance(route_geometry, ST_GeomFromText(:point, 4326)) as distance " +
|
||||
"FROM transport_routes WHERE route_type = :routeType AND status = :status " +
|
||||
"ORDER BY distance LIMIT :limit", nativeQuery = true)
|
||||
List<TransportRoute> findNearestRoutes(
|
||||
@Param("point") String pointWKT,
|
||||
@Param("routeType") String routeType,
|
||||
@Param("status") String status,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
@ -0,0 +1,415 @@
|
||||
package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.*;
|
||||
import org.locationtech.jts.operation.distance.DistanceOp;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 路径冲突检测服务
|
||||
* 基于路径、速度和距离的冲突检测算法
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PathConflictDetectionService {
|
||||
|
||||
private final TransportRouteRepository routeRepository;
|
||||
private final ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
private final ConflictAlertLogRepository conflictAlertLogRepository;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
// 预警距离阈值(米)
|
||||
private static final double WARNING_DISTANCE_THRESHOLD = 200.0;
|
||||
// 告警距离阈值(米)
|
||||
private static final double ALERT_DISTANCE_THRESHOLD = 100.0;
|
||||
// 最大检测时间范围(秒)
|
||||
private static final int MAX_PREDICTION_TIME_SECONDS = 300; // 5分钟
|
||||
// 最小时间间隔阈值(秒)
|
||||
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
||||
|
||||
/**
|
||||
* 执行路径冲突检测
|
||||
*
|
||||
* @param activeObjects 当前活跃的移动对象(无人车和航空器)
|
||||
*/
|
||||
public void detectPathConflicts(List<MovingObject> activeObjects) {
|
||||
log.debug("开始路径冲突检测,活跃对象数量: {}", activeObjects.size());
|
||||
|
||||
List<ConflictAlertEvent> detectedAlertEvents = new ArrayList<>();
|
||||
|
||||
// 生成所有对象组合
|
||||
for (int i = 0; i < activeObjects.size(); i++) {
|
||||
for (int j = i + 1; j < activeObjects.size(); j++) {
|
||||
MovingObject obj1 = activeObjects.get(i);
|
||||
MovingObject obj2 = activeObjects.get(j);
|
||||
|
||||
// 检测两个对象之间的潜在冲突
|
||||
Optional<ConflictAlertEvent> alertEventOptional = detectConflictBetweenObjects(obj1, obj2);
|
||||
alertEventOptional.ifPresent(detectedAlertEvents::add);
|
||||
}
|
||||
}
|
||||
|
||||
// 发布告警事件
|
||||
for (ConflictAlertEvent event : detectedAlertEvents) {
|
||||
eventPublisher.publishEvent(event);
|
||||
log.info("发布冲突告警事件: conflictId={}, alertType={}, alertLevel={}",
|
||||
event.getConflictId(), event.getAlertType(), event.getAlertLevel());
|
||||
}
|
||||
|
||||
log.info("路径冲突检测完成,检测到并发布 {} 个潜在冲突告警事件", detectedAlertEvents.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测两个对象之间的路径冲突
|
||||
*/
|
||||
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
|
||||
try {
|
||||
// 获取对象的路径
|
||||
TransportRoute route1 = getObjectRoute(obj1);
|
||||
TransportRoute route2 = getObjectRoute(obj2);
|
||||
|
||||
if (route1 == null || route2 == null) {
|
||||
log.debug("对象 {} 或 {} 没有分配路径,跳过冲突检测", obj1.getObjectId(), obj2.getObjectId());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// 计算路径交点
|
||||
List<Point> intersectionPoints = calculateRouteIntersections(route1, route2);
|
||||
|
||||
for (Point intersectionPoint : intersectionPoints) {
|
||||
// 计算每个对象到交点的距离和时间
|
||||
ConflictCalculationResult result = calculateConflictDetails(obj1, obj2, route1, route2, intersectionPoint);
|
||||
|
||||
if (result != null && isSignificantConflict(result)) {
|
||||
String conflictId = generateConflictId(obj1.getObjectId(), obj2.getObjectId());
|
||||
String description = generateConflictDescription(obj1, obj2, route1, route2);
|
||||
|
||||
ConflictAlertLog alertLog = ConflictAlertLog.builder()
|
||||
.conflictId(conflictId)
|
||||
.alertType(result.getAlertType())
|
||||
.alertLevel(result.getAlertLevel())
|
||||
.alertMessage(description)
|
||||
.object1Distance(result.getDistance1())
|
||||
.object2Distance(result.getDistance2())
|
||||
.minimumDistance(Math.min(result.getDistance1(), result.getDistance2())) // 简化处理,可根据需要调整
|
||||
.build();
|
||||
|
||||
conflictAlertLogRepository.save(alertLog);
|
||||
log.info("保存冲突告警日志: {}", alertLog);
|
||||
|
||||
return Optional.of(ConflictAlertEvent.builder()
|
||||
.conflictId(conflictId)
|
||||
.alertType(result.getAlertType())
|
||||
.alertLevel(result.getAlertLevel())
|
||||
.message(description)
|
||||
.object1Id(obj1.getObjectId())
|
||||
.object2Id(obj2.getObjectId())
|
||||
.conflictPoint(intersectionPoint)
|
||||
.object1Distance(result.getDistance1())
|
||||
.object2Distance(result.getDistance2())
|
||||
.estimatedTimeToConflictObj1(result.getTimeToConflict1())
|
||||
.estimatedTimeToConflictObj2(result.getTimeToConflict2())
|
||||
.timeGapSeconds(result.getTimeGap())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("检测对象 {} 和 {} 之间的冲突时发生异常", obj1.getObjectId(), obj2.getObjectId(), e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象当前使用的路径
|
||||
*/
|
||||
private TransportRoute getObjectRoute(MovingObject obj) {
|
||||
Optional<ObjectRouteAssignment> assignmentOptional = objectRouteAssignmentRepository.findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc(
|
||||
obj.getObjectId(),
|
||||
ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name())
|
||||
);
|
||||
|
||||
return assignmentOptional.map(assignment -> routeRepository.findByRouteId(assignment.getAssignedRouteId()).orElse(null))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找距离指定位置最近的路径 (此方法可能不再需要,因为我们现在通过分配表获取路径)
|
||||
* 或者可以用于处理未分配路径的对象,但当前逻辑主要依赖于已分配路径
|
||||
*/
|
||||
private TransportRoute findNearestRoute(Point position, List<TransportRoute> routes) {
|
||||
TransportRoute nearestRoute = null;
|
||||
double minDistance = Double.MAX_VALUE;
|
||||
|
||||
for (TransportRoute route : routes) {
|
||||
try {
|
||||
double distance = position.distance(route.getRouteGeometry());
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestRoute = route;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("计算到路径 {} 的距离时出错", route.getRouteId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return nearestRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两条路径的交点
|
||||
*/
|
||||
private List<Point> calculateRouteIntersections(TransportRoute route1, TransportRoute route2) {
|
||||
List<Point> intersections = new ArrayList<>();
|
||||
|
||||
try {
|
||||
LineString line1 = route1.getRouteGeometry();
|
||||
LineString line2 = route2.getRouteGeometry();
|
||||
|
||||
Geometry intersection = line1.intersection(line2);
|
||||
|
||||
if (intersection instanceof Point) {
|
||||
intersections.add((Point) intersection);
|
||||
} else if (intersection instanceof MultiPoint) {
|
||||
MultiPoint multiPoint = (MultiPoint) intersection;
|
||||
for (int i = 0; i < multiPoint.getNumGeometries(); i++) {
|
||||
intersections.add((Point) multiPoint.getGeometryN(i));
|
||||
}
|
||||
} else if (intersection instanceof LineString) {
|
||||
// 如果是线段重叠,取线段的中点
|
||||
LineString overlapLine = (LineString) intersection;
|
||||
Point midPoint = overlapLine.getInteriorPoint();
|
||||
intersections.add(midPoint);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算路径 {} 和 {} 的交点时出错", route1.getRouteId(), route2.getRouteId(), e);
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算冲突详细信息
|
||||
*/
|
||||
private ConflictCalculationResult calculateConflictDetails(
|
||||
MovingObject obj1, MovingObject obj2,
|
||||
TransportRoute route1, TransportRoute route2,
|
||||
Point intersectionPoint) {
|
||||
|
||||
try {
|
||||
// 计算对象到交点的距离
|
||||
double distance1 = calculateDistanceAlongRoute(obj1.getCurrentPosition(), intersectionPoint, route1);
|
||||
double distance2 = calculateDistanceAlongRoute(obj2.getCurrentPosition(), intersectionPoint, route2);
|
||||
|
||||
// 计算到达交点的时间(基于当前速度)
|
||||
double speed1 = Math.max(obj1.getCurrentSpeed(), 1.0); // 避免除零,最小速度1 km/h
|
||||
double speed2 = Math.max(obj2.getCurrentSpeed(), 1.0);
|
||||
|
||||
// 转换为m/s
|
||||
double speed1_ms = speed1 * 1000.0 / 3600.0;
|
||||
double speed2_ms = speed2 * 1000.0 / 3600.0;
|
||||
|
||||
int timeToConflict1 = (int) (distance1 / speed1_ms);
|
||||
int timeToConflict2 = (int) (distance2 / speed2_ms);
|
||||
|
||||
// 计算时间差
|
||||
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
|
||||
|
||||
// 评估冲突告警级别和类型
|
||||
ConflictAlertLog.AlertLevel alertLevel = evaluateAlertLevel(distance1, distance2, timeGap);
|
||||
ConflictAlertLog.AlertType alertType = evaluateAlertType(alertLevel);
|
||||
|
||||
return new ConflictCalculationResult(
|
||||
distance1, distance2, timeToConflict1, timeToConflict2, timeGap, alertType, alertLevel);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算冲突详细信息时出错", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估冲突的告警级别
|
||||
*/
|
||||
private ConflictAlertLog.AlertLevel evaluateAlertLevel(
|
||||
double distance1, double distance2, double timeGap) {
|
||||
|
||||
double minDistance = Math.min(distance1, distance2);
|
||||
|
||||
if (minDistance <= ALERT_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS) {
|
||||
return ConflictAlertLog.AlertLevel.CRITICAL;
|
||||
} else if (minDistance <= WARNING_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
|
||||
return ConflictAlertLog.AlertLevel.WARNING;
|
||||
} else if (minDistance <= WARNING_DISTANCE_THRESHOLD * 2 && timeGap <= MAX_PREDICTION_TIME_SECONDS) {
|
||||
return ConflictAlertLog.AlertLevel.INFO;
|
||||
} else {
|
||||
return ConflictAlertLog.AlertLevel.INFO; // 默认信息级别
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据告警级别评估告警类型
|
||||
*/
|
||||
private ConflictAlertLog.AlertType evaluateAlertType(ConflictAlertLog.AlertLevel level) {
|
||||
switch (level) {
|
||||
case CRITICAL:
|
||||
case EMERGENCY:
|
||||
return ConflictAlertLog.AlertType.ALERT; // 达到告警级别
|
||||
case WARNING:
|
||||
return ConflictAlertLog.AlertType.WARNING; // 预警级别
|
||||
case INFO:
|
||||
default:
|
||||
return ConflictAlertLog.AlertType.WARNING; // 默认预警
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为显著冲突
|
||||
*/
|
||||
private boolean isSignificantConflict(ConflictCalculationResult result) {
|
||||
return result.getAlertType() == ConflictAlertLog.AlertType.ALERT ||
|
||||
result.getAlertType() == ConflictAlertLog.AlertType.WARNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算沿路径的距离
|
||||
*/
|
||||
private double calculateDistanceAlongRoute(Point currentPos, Point targetPos, TransportRoute route) {
|
||||
try {
|
||||
LineString routeLine = route.getRouteGeometry();
|
||||
|
||||
// 找到当前位置在路径上的最近点
|
||||
DistanceOp distOp1 = new DistanceOp(currentPos, routeLine);
|
||||
Coordinate[] nearestPoints1 = distOp1.nearestPoints();
|
||||
Point nearestOnRoute1 = currentPos.getFactory().createPoint(nearestPoints1[1]);
|
||||
|
||||
// 找到目标位置在路径上的最近点
|
||||
DistanceOp distOp2 = new DistanceOp(targetPos, routeLine);
|
||||
Coordinate[] nearestPoints2 = distOp2.nearestPoints();
|
||||
Point nearestOnRoute2 = currentPos.getFactory().createPoint(nearestPoints2[1]);
|
||||
|
||||
// 计算沿路径的距离
|
||||
double distanceToCurrentOnRoute = getLinearLengthToPoint(routeLine, nearestOnRoute1);
|
||||
double distanceToTargetOnRoute = getLinearLengthToPoint(routeLine, nearestOnRoute2);
|
||||
|
||||
return Math.abs(distanceToTargetOnRoute - distanceToCurrentOnRoute);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算沿路径距离时出错: currentPos={}, targetPos={}, routeId={}",
|
||||
currentPos, targetPos, route.getRouteId(), e);
|
||||
return -1.0; // 表示错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成冲突ID
|
||||
*/
|
||||
private String generateConflictId(String obj1Id, String obj2Id) {
|
||||
// 确保ID的生成是确定性的,与对象的顺序无关
|
||||
String[] ids = {obj1Id, obj2Id};
|
||||
Arrays.sort(ids);
|
||||
return "CONFLICT-" + ids[0] + "-" + ids[1] + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成冲突描述
|
||||
*/
|
||||
private String generateConflictDescription(
|
||||
MovingObject obj1, MovingObject obj2,
|
||||
TransportRoute route1, TransportRoute route2) {
|
||||
return String.format("对象 %s (%s) 在路径 %s 与对象 %s (%s) 在路径 %s 上可能发生冲突。",
|
||||
obj1.getObjectId(), obj1.getObjectType(), route1.getRouteId(),
|
||||
obj2.getObjectId(), obj2.getObjectType(), route2.getRouteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算沿LineString从起点到给定点的距离。
|
||||
* 假定point已经投影到line上。
|
||||
* 如果点不在line上,结果可能不准确。
|
||||
*/
|
||||
private double getLinearLengthToPoint(LineString line, Point point) {
|
||||
double length = 0.0;
|
||||
Coordinate[] coords = line.getCoordinates();
|
||||
|
||||
for (int i = 0; i < coords.length - 1; i++) {
|
||||
Point p1 = line.getFactory().createPoint(coords[i]);
|
||||
Point p2 = line.getFactory().createPoint(coords[i+1]);
|
||||
|
||||
// 如果点在线段 (p1, p2) 上,则计算到p1的距离,然后加上点到p1的距离
|
||||
if (point.equals(p1)) {
|
||||
return length;
|
||||
}
|
||||
|
||||
double segmentLength = p1.distance(p2);
|
||||
// 检查点是否在当前线段上 (使用一个小的容差)
|
||||
if (point.distance(p1) + point.distance(p2) - segmentLength < 1e-6) { // 容差检查
|
||||
return length + p1.distance(point);
|
||||
}
|
||||
length += segmentLength;
|
||||
}
|
||||
// 如果点是最后一个点
|
||||
if (point.equals(line.getEndPoint())) {
|
||||
return line.getLength();
|
||||
}
|
||||
// 如果没有找到匹配,返回-1或者抛异常,这里返回线长表示无法准确计算
|
||||
return -1.0; // 表示未找到精确位置或计算错误
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突计算结果内部类
|
||||
*/
|
||||
private static class ConflictCalculationResult {
|
||||
private final double distance1;
|
||||
private final double distance2;
|
||||
private final int timeToConflict1;
|
||||
private final int timeToConflict2;
|
||||
private final double timeGap;
|
||||
private final ConflictAlertLog.AlertType alertType;
|
||||
private final ConflictAlertLog.AlertLevel alertLevel;
|
||||
|
||||
public ConflictCalculationResult(double distance1, double distance2,
|
||||
int timeToConflict1, int timeToConflict2,
|
||||
double timeGap,
|
||||
ConflictAlertLog.AlertType alertType,
|
||||
ConflictAlertLog.AlertLevel alertLevel) {
|
||||
this.distance1 = distance1;
|
||||
this.distance2 = distance2;
|
||||
this.timeToConflict1 = timeToConflict1;
|
||||
this.timeToConflict2 = timeToConflict2;
|
||||
this.timeGap = timeGap;
|
||||
this.alertType = alertType;
|
||||
this.alertLevel = alertLevel;
|
||||
}
|
||||
|
||||
public double getDistance1() { return distance1; }
|
||||
public double getDistance2() { return distance2; }
|
||||
public int getTimeToConflict1() { return timeToConflict1; }
|
||||
public int getTimeToConflict2() { return timeToConflict2; }
|
||||
public double getTimeGap() { return timeGap; }
|
||||
public ConflictAlertLog.AlertType getAlertType() { return alertType; }
|
||||
public ConflictAlertLog.AlertLevel getAlertLevel() { return alertLevel; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 车辆指令服务
|
||||
* 负责向无人车发送各种控制指令
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleCommandService {
|
||||
|
||||
/**
|
||||
* 向指定车辆发送控制指令
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @param commandType 指令类型 (EMERGENCY_STOP, SLOW_DOWN, CAUTION, INFO)
|
||||
* @param message 指令消息
|
||||
* @param conflictId 关联的冲突ID
|
||||
* @return 发送成功返回true
|
||||
*/
|
||||
public boolean sendCommand(String vehicleId, String commandType, String message, String conflictId) {
|
||||
try {
|
||||
log.info("向车辆发送指令: vehicleId={}, commandType={}, message={}, conflictId={}",
|
||||
vehicleId, commandType, message, conflictId);
|
||||
|
||||
// TODO: 实际实现时需要根据车辆通信协议发送指令
|
||||
// 这里可能包括:
|
||||
// 1. HTTP接口调用
|
||||
// 2. MQTT消息发送
|
||||
// 3. WebSocket推送
|
||||
// 4. CAN总线通信
|
||||
// 5. 其他车联网协议
|
||||
|
||||
VehicleCommand command = VehicleCommand.builder()
|
||||
.vehicleId(vehicleId)
|
||||
.commandType(commandType)
|
||||
.message(message)
|
||||
.conflictId(conflictId)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.priority(getCommandPriority(commandType))
|
||||
.build();
|
||||
|
||||
// 模拟发送指令的过程
|
||||
boolean success = sendCommandToVehicle(command);
|
||||
|
||||
if (success) {
|
||||
log.info("车辆指令发送成功: vehicleId={}, commandType={}", vehicleId, commandType);
|
||||
// 可以在这里记录指令发送日志到数据库
|
||||
recordCommandLog(command, true, null);
|
||||
} else {
|
||||
log.warn("车辆指令发送失败: vehicleId={}, commandType={}", vehicleId, commandType);
|
||||
recordCommandLog(command, false, "发送失败");
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送车辆指令时出错: vehicleId={}, commandType={}", vehicleId, commandType, e);
|
||||
recordCommandLog(VehicleCommand.builder()
|
||||
.vehicleId(vehicleId)
|
||||
.commandType(commandType)
|
||||
.message(message)
|
||||
.conflictId(conflictId)
|
||||
.build(), false, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际发送指令到车辆
|
||||
* TODO: 这里需要根据实际的车辆通信方式实现
|
||||
*/
|
||||
private boolean sendCommandToVehicle(VehicleCommand command) {
|
||||
// 根据指令类型选择不同的发送策略
|
||||
switch (command.getCommandType()) {
|
||||
case "EMERGENCY_STOP":
|
||||
return sendEmergencyStopCommand(command);
|
||||
case "SLOW_DOWN":
|
||||
return sendSlowDownCommand(command);
|
||||
case "CAUTION":
|
||||
return sendCautionCommand(command);
|
||||
case "INFO":
|
||||
return sendInfoCommand(command);
|
||||
default:
|
||||
log.warn("未知的指令类型: {}", command.getCommandType());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送紧急停车指令
|
||||
*/
|
||||
private boolean sendEmergencyStopCommand(VehicleCommand command) {
|
||||
// TODO: 实现紧急停车指令发送
|
||||
// 这通常需要最高优先级和最快的通信方式
|
||||
log.info("发送紧急停车指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送减速指令
|
||||
*/
|
||||
private boolean sendSlowDownCommand(VehicleCommand command) {
|
||||
// TODO: 实现减速指令发送
|
||||
log.info("发送减速指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注意指令
|
||||
*/
|
||||
private boolean sendCautionCommand(VehicleCommand command) {
|
||||
// TODO: 实现注意指令发送
|
||||
log.info("发送注意指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息指令
|
||||
*/
|
||||
private boolean sendInfoCommand(VehicleCommand command) {
|
||||
// TODO: 实现信息指令发送
|
||||
log.info("发送信息指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指令优先级
|
||||
*/
|
||||
private int getCommandPriority(String commandType) {
|
||||
switch (commandType) {
|
||||
case "EMERGENCY_STOP":
|
||||
return 1; // 最高优先级
|
||||
case "SLOW_DOWN":
|
||||
return 2;
|
||||
case "CAUTION":
|
||||
return 3;
|
||||
case "INFO":
|
||||
return 4; // 最低优先级
|
||||
default:
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录指令发送日志
|
||||
* TODO: 实际实现时可以保存到数据库
|
||||
*/
|
||||
private void recordCommandLog(VehicleCommand command, boolean success, String errorMessage) {
|
||||
log.debug("记录指令日志: vehicleId={}, commandType={}, success={}, error={}",
|
||||
command.getVehicleId(), command.getCommandType(), success, errorMessage);
|
||||
|
||||
// 这里可以保存到数据库中的指令日志表
|
||||
// 包括发送时间、成功状态、错误信息等
|
||||
}
|
||||
|
||||
/**
|
||||
* 车辆指令内部类
|
||||
*/
|
||||
@lombok.Data
|
||||
@lombok.Builder
|
||||
@lombok.NoArgsConstructor
|
||||
@lombok.AllArgsConstructor
|
||||
public static class VehicleCommand {
|
||||
private String vehicleId;
|
||||
private String commandType;
|
||||
private String message;
|
||||
private String conflictId;
|
||||
private LocalDateTime timestamp;
|
||||
private int priority;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import com.qaup.collision.common.model.MovingObjectType;
|
||||
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
||||
import com.qaup.collision.geofence.model.enums.GeofenceAlertLevel;
|
||||
import com.qaup.collision.rule.event.RuleViolationEvent;
|
||||
import com.qaup.collision.rule.event.RuleViolationEventOccurred; // 导入RuleViolationEventOccurred
|
||||
import com.qaup.collision.rule.model.entity.SpatialRule;
|
||||
import com.qaup.collision.rule.model.enums.RuleCategory;
|
||||
import com.qaup.collision.rule.model.enums.ViolationType;
|
||||
@ -18,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.context.event.EventListener; // 导入EventListener
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@ -53,9 +55,18 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
|
||||
private final AtomicLong totalDetections = new AtomicLong(0);
|
||||
private final AtomicLong totalViolations = new AtomicLong(0);
|
||||
private final AtomicLong totalProcessingTime = new AtomicLong(0);
|
||||
|
||||
// 用于统计本次批处理中实际检测到的违规数量
|
||||
private final AtomicLong currentBatchViolationsCount = new AtomicLong(0);
|
||||
|
||||
// ========== 核心违规检测方法 ==========
|
||||
|
||||
@EventListener
|
||||
public void handleRuleViolationEventOccurred(RuleViolationEventOccurred event) {
|
||||
currentBatchViolationsCount.incrementAndGet();
|
||||
log.debug("事件监听器收到 RuleViolationEventOccurred,当前批次违规数: {}", currentBatchViolationsCount.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RuleViolationEvent> detectViolations(VehicleLocation vehicleLocation) {
|
||||
if (!isRealTimeDetectionEnabled()) {
|
||||
@ -82,12 +93,12 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
|
||||
}
|
||||
|
||||
// 检测违规
|
||||
List<RuleViolationEvent> violations = new ArrayList<>();
|
||||
// RuleViolationEvent将通过事件监听器处理和统计,此处不再收集列表
|
||||
Map<String, Object> vehicleState = buildVehicleStateMap(vehicleLocation);
|
||||
|
||||
for (SpatialRule rule : applicableRules) {
|
||||
try {
|
||||
RuleViolationEvent violation = ruleExecutionEngine.detectViolation(
|
||||
ruleExecutionEngine.detectViolation(
|
||||
rule,
|
||||
String.valueOf(vehicleLocation.getVehicleId()), // 将Long转换为String
|
||||
vehicleLocation.getVehicleType(),
|
||||
@ -96,31 +107,26 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
|
||||
vehicleLocation.getTimestamp()
|
||||
);
|
||||
|
||||
if (violation != null) {
|
||||
// 设置检测上下文信息
|
||||
violations.add(violation);
|
||||
|
||||
log.info("检测到违规: vehicleId={}, ruleId={}, violationType={}",
|
||||
vehicleLocation.getVehicleId(), rule.getRuleId(), violation.getViolationType());
|
||||
}
|
||||
// RuleViolationEventOccurred事件由 RuleExecutionEngineImpl 内部发布,
|
||||
// 并由本类的 @EventListener 方法负责计数和处理。
|
||||
// 因此,此处不再需要 `violations.add(violation);`
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("规则违规检测失败: vehicleId={}, ruleId={}",
|
||||
vehicleLocation.getVehicleId(), rule.getRuleId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步处理检测到的违规事件(简化架构)
|
||||
if (!violations.isEmpty()) {
|
||||
processBatchViolationEvents(violations);
|
||||
}
|
||||
// 违规事件将由 @EventListener 监听器自动处理,这里不需要手动调用 processBatchViolationEvents
|
||||
// processBatchViolationEvents(violations);
|
||||
|
||||
// 更新性能指标
|
||||
updateMetrics(startTime, violations.size());
|
||||
updateMetrics(startTime, 0); // 这里的violations.size()将不再有效,但totalViolations由EventListener更新
|
||||
|
||||
log.debug("违规检测完成: vehicleId={}, 检测到{}个违规",
|
||||
vehicleLocation.getVehicleId(), violations.size());
|
||||
log.debug("违规检测完成: vehicleId={}",
|
||||
vehicleLocation.getVehicleId());
|
||||
|
||||
return violations;
|
||||
return new ArrayList<>(); // 返回空列表,因为实际违规通过事件监听器处理
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("实时违规检测失败: vehicleId={}, detectionId={}",
|
||||
@ -136,14 +142,19 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 在每次批量检测开始前,重置计数器
|
||||
currentBatchViolationsCount.set(0);
|
||||
|
||||
log.info("开始批量违规检测: 车辆数量={}", vehicleLocations.size());
|
||||
|
||||
List<RuleViolationEvent> allViolations = new ArrayList<>();
|
||||
// 不再需要 allViolations 列表,通过事件监听器进行统计
|
||||
// List<RuleViolationEvent> allViolations = new ArrayList<>();
|
||||
|
||||
for (VehicleLocation vehicleLocation : vehicleLocations) {
|
||||
try {
|
||||
List<RuleViolationEvent> violations = detectViolations(vehicleLocation);
|
||||
allViolations.addAll(violations);
|
||||
// 调用 detectViolations,它会发布 RuleViolationEventOccurred 事件
|
||||
detectViolations(vehicleLocation);
|
||||
// allViolations.addAll(violations); // 不再需要收集列表
|
||||
} catch (Exception e) {
|
||||
log.error("批量违规检测中单个车辆处理失败: vehicleId={}",
|
||||
vehicleLocation.getVehicleId(), e);
|
||||
@ -151,9 +162,10 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
|
||||
}
|
||||
|
||||
log.info("批量违规检测完成: 处理车辆{}个,检测到违规{}个",
|
||||
vehicleLocations.size(), allViolations.size());
|
||||
vehicleLocations.size(), currentBatchViolationsCount.get()); // 使用监听器统计的违规数
|
||||
log.debug("RealTimeViolationDetectorImpl - 批量检测结束,最终计数: {}", currentBatchViolationsCount.get());
|
||||
|
||||
return allViolations;
|
||||
return new ArrayList<>(); // 返回空列表,因为实际违规通过事件监听器处理
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -9,9 +9,13 @@ import com.qaup.collision.rule.model.enums.RuleExecutionResult;
|
||||
import com.qaup.collision.websocket.event.RuleExecutionStatusWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
|
||||
import com.qaup.collision.websocket.message.RuleStateChangePayload;
|
||||
import com.qaup.collision.websocket.message.RuleViolationPayload;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import com.qaup.collision.common.adapter.QuapDataAdapter; // 导入 QuapDataAdapter
|
||||
import com.qaup.system.domain.SysVehicleInfo; // 导入 SysVehicleInfo
|
||||
import org.locationtech.jts.io.WKTWriter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -22,6 +26,7 @@ import org.springframework.stereotype.Service;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional; // 导入 Optional
|
||||
|
||||
/**
|
||||
* 规则事件WebSocket发布服务(简化版)
|
||||
@ -37,10 +42,12 @@ public class RuleEventWebSocketPublisher {
|
||||
private static final Logger logger = LoggerFactory.getLogger(RuleEventWebSocketPublisher.class);
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
|
||||
|
||||
// 构造函数中初始化ObjectMapper
|
||||
public RuleEventWebSocketPublisher(ApplicationEventPublisher eventPublisher) {
|
||||
public RuleEventWebSocketPublisher(ApplicationEventPublisher eventPublisher, QuapDataAdapter quapDataAdapter) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.quapDataAdapter = quapDataAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -292,6 +299,26 @@ public class RuleEventWebSocketPublisher {
|
||||
return "LOW";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布路径冲突告警WebSocket消息
|
||||
*
|
||||
* @param message 路径冲突告警消息
|
||||
*/
|
||||
public void publishPathConflictAlert(PathConflictAlertMessage message) {
|
||||
try {
|
||||
// 创建路径冲突告警WebSocket事件
|
||||
PathConflictAlertWebSocketEvent webSocketEvent = PathConflictAlertWebSocketEvent.create(message);
|
||||
eventPublisher.publishEvent(webSocketEvent);
|
||||
|
||||
logger.info("Published path conflict alert WebSocket event: conflictId={}, alertType={}",
|
||||
message.getConflictId(), message.getAlertType());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to publish path conflict alert WebSocket event: conflictId={}",
|
||||
message.getConflictId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过vehicle_id动态查询车牌号
|
||||
*
|
||||
@ -304,19 +331,16 @@ public class RuleEventWebSocketPublisher {
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里可以注入相应的服务来查询车牌号
|
||||
// 暂时使用硬编码的逻辑,实际应该通过服务查询
|
||||
// TODO: 注入 VehicleService 或 QuapDataAdapter 来查询
|
||||
|
||||
// 简化实现:根据vehicleId返回对应的车牌号
|
||||
switch (vehicleId.intValue()) {
|
||||
case 5: return "鲁B567";
|
||||
case 1: return "京A123";
|
||||
case 2: return "沪B456";
|
||||
default: return "ID-" + vehicleId;
|
||||
// 通过注入的QuapDataAdapter查询车辆信息
|
||||
Optional<SysVehicleInfo> vehicleInfoOpt = quapDataAdapter.findVehicleById(vehicleId);
|
||||
if (vehicleInfoOpt.isPresent()) {
|
||||
return vehicleInfoOpt.get().getLicensePlate();
|
||||
} else {
|
||||
logger.warn("未找到vehicleId: {} 对应的车辆信息,使用默认车牌号。", vehicleId);
|
||||
return "ID-" + vehicleId; // 如果未找到,返回一个标识性的ID
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to query license plate for vehicleId: {}, error: {}", vehicleId, e.getMessage());
|
||||
logger.error("查询vehicleId: {} 对应的车牌号失败: {}", vehicleId, e.getMessage());
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||
import com.qaup.collision.websocket.event.RuleExecutionStatusWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.TrafficLightStatusEvent;
|
||||
import com.qaup.collision.websocket.event.VehicleCommandEvent;
|
||||
import com.qaup.collision.websocket.message.CollisionWarningPayload;
|
||||
@ -17,6 +18,7 @@ import com.qaup.collision.websocket.message.RuleViolationPayload;
|
||||
import com.qaup.collision.websocket.message.TrafficLightStatusPayload;
|
||||
import com.qaup.collision.websocket.message.VehicleCommandPayload;
|
||||
import com.qaup.collision.websocket.message.UniversalMessage;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -159,6 +161,22 @@ public class WebSocketMessageBroadcaster {
|
||||
broadcastMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理路径冲突告警事件
|
||||
* @param event 路径冲突告警事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handlePathConflictAlert(PathConflictAlertWebSocketEvent event) {
|
||||
UniversalMessage<PathConflictAlertMessage> message = UniversalMessage.<PathConflictAlertMessage>builder()
|
||||
.type(MessageTypeConstants.PATH_CONFLICT_ALERT)
|
||||
.timestamp(event.getTimestamp())
|
||||
.messageId(generateMessageId())
|
||||
.payload((PathConflictAlertMessage) event.getPayload())
|
||||
.build();
|
||||
|
||||
broadcastMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一消息广播方法 - 使用原生WebSocket发送JSON消息
|
||||
* @param message 要广播的消息
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package com.qaup.collision.websocket.event;
|
||||
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 路径冲突告警WebSocket事件
|
||||
* 用于通过WebSocket向前端发送路径冲突告警信息
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class PathConflictAlertWebSocketEvent implements WebSocketEvent {
|
||||
|
||||
/**
|
||||
* 事件唯一标识
|
||||
*/
|
||||
private final String eventId;
|
||||
|
||||
/**
|
||||
* 事件时间戳(微秒级)
|
||||
*/
|
||||
private final long timestamp;
|
||||
|
||||
/**
|
||||
* 路径冲突告警消息载荷
|
||||
*/
|
||||
private final PathConflictAlertMessage payload;
|
||||
|
||||
/**
|
||||
* 创建路径冲突告警WebSocket事件的工厂方法
|
||||
*
|
||||
* @param payload 路径冲突告警消息载荷
|
||||
* @return 路径冲突告警WebSocket事件实例
|
||||
*/
|
||||
public static PathConflictAlertWebSocketEvent create(PathConflictAlertMessage payload) {
|
||||
return PathConflictAlertWebSocketEvent.builder()
|
||||
.eventId(UUID.randomUUID().toString())
|
||||
.timestamp(java.time.Instant.now().toEpochMilli() * 1000) // 微秒级绝对时间戳
|
||||
.payload(payload)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType() {
|
||||
return "PATH_CONFLICT_ALERT"; // 定义事件类型
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冲突ID
|
||||
*/
|
||||
public String getConflictId() {
|
||||
return getPayload() != null ? ((PathConflictAlertMessage) getPayload()).getConflictId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取告警类型
|
||||
*/
|
||||
public String getAlertType() {
|
||||
return getPayload() != null ? ((PathConflictAlertMessage) getPayload()).getAlertType() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取告警级别
|
||||
*/
|
||||
public String getAlertLevel() {
|
||||
return getPayload() != null ? ((PathConflictAlertMessage) getPayload()).getAlertLevel() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为紧急告警
|
||||
*/
|
||||
public boolean isEmergencyAlert() {
|
||||
return getPayload() != null && ((PathConflictAlertMessage) getPayload()).isEmergencyAlert();
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,11 @@ public final class MessageTypeConstants {
|
||||
*/
|
||||
public static final String RULE_ALERT = "rule_alert";
|
||||
|
||||
/**
|
||||
* 路径冲突告警消息
|
||||
*/
|
||||
public static final String PATH_CONFLICT_ALERT = "path_conflict_alert";
|
||||
|
||||
// 私有构造函数,防止实例化
|
||||
private MessageTypeConstants() {
|
||||
throw new UnsupportedOperationException("常量类不允许实例化");
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
package com.qaup.collision.websocket.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 路径冲突告警WebSocket消息
|
||||
* 用于向前端控制台发送路径冲突告警信息
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PathConflictAlertMessage {
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
@Builder.Default
|
||||
private String messageType = "PATH_CONFLICT_ALERT";
|
||||
|
||||
/**
|
||||
* 冲突ID
|
||||
*/
|
||||
private String conflictId;
|
||||
|
||||
/**
|
||||
* 告警类型:WARNING, ALERT, EMERGENCY
|
||||
*/
|
||||
private String alertType;
|
||||
|
||||
/**
|
||||
* 告警级别:INFO, WARNING, CRITICAL, EMERGENCY
|
||||
*/
|
||||
private String alertLevel;
|
||||
|
||||
/**
|
||||
* 告警消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 冲突对象1
|
||||
*/
|
||||
private ConflictObject object1;
|
||||
|
||||
/**
|
||||
* 冲突对象2
|
||||
*/
|
||||
private ConflictObject object2;
|
||||
|
||||
/**
|
||||
* 冲突位置
|
||||
*/
|
||||
private Position position;
|
||||
|
||||
/**
|
||||
* 对象1距离冲突点距离 (米)
|
||||
*/
|
||||
private Double object1Distance;
|
||||
|
||||
/**
|
||||
* 对象2距离冲突点距离 (米)
|
||||
*/
|
||||
private Double object2Distance;
|
||||
|
||||
/**
|
||||
* 预计到达冲突点时间1 (秒)
|
||||
*/
|
||||
private Integer timeToConflict1;
|
||||
|
||||
/**
|
||||
* 预计到达冲突点时间2 (秒)
|
||||
*/
|
||||
private Integer timeToConflict2;
|
||||
|
||||
/**
|
||||
* 时间差 (秒)
|
||||
*/
|
||||
private Double timeGap;
|
||||
|
||||
/**
|
||||
* 事件发生时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime eventTime;
|
||||
|
||||
/**
|
||||
* 冲突对象信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ConflictObject {
|
||||
|
||||
/**
|
||||
* 对象ID
|
||||
*/
|
||||
private String objectId;
|
||||
|
||||
/**
|
||||
* 对象类型:UNMANNED_VEHICLE, AIRCRAFT, SPECIAL_VEHICLE (新增SPECIAL_VEHICLE)
|
||||
*/
|
||||
private String objectType;
|
||||
|
||||
/**
|
||||
* 对象名称(车牌号或航班号)
|
||||
*/
|
||||
private String objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Position {
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 海拔(可选)
|
||||
*/
|
||||
private Double altitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为紧急告警
|
||||
*/
|
||||
public boolean isEmergencyAlert() {
|
||||
return "EMERGENCY".equals(alertType) || "CRITICAL".equals(alertLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为预警
|
||||
*/
|
||||
public boolean isWarning() {
|
||||
return "WARNING".equals(alertType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为告警
|
||||
*/
|
||||
public boolean isAlert() {
|
||||
return "ALERT".equals(alertType);
|
||||
}
|
||||
}
|
||||
147
sql/create_path_conflict_detection_tables.sql
Normal file
147
sql/create_path_conflict_detection_tables.sql
Normal file
@ -0,0 +1,147 @@
|
||||
-- ============================================
|
||||
-- 基于路径的冲突检测系统数据库表
|
||||
-- 创建时间: 2025-01-17
|
||||
-- 版本: 1.0
|
||||
-- ============================================
|
||||
|
||||
-- 启用PostGIS扩展(如果尚未启用)
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 路径定义表 (transport_routes)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS transport_routes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- 路径基本信息
|
||||
route_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
route_name VARCHAR(100) NOT NULL,
|
||||
route_type VARCHAR(20) NOT NULL CHECK (route_type IN ('AIRCRAFT', 'UNMANNED_VEHICLE', 'SPECIAL_VEHICLE')),
|
||||
|
||||
-- 路径描述
|
||||
description VARCHAR(500),
|
||||
|
||||
-- 空间路径 (LineString)
|
||||
route_geometry GEOMETRY(LINESTRING, 4326) NOT NULL,
|
||||
|
||||
-- 路径属性
|
||||
max_speed_kph DOUBLE PRECISION CHECK (max_speed_kph > 0),
|
||||
typical_speed_kph DOUBLE PRECISION CHECK (typical_speed_kph > 0),
|
||||
|
||||
-- 使用条件
|
||||
active_time_start TIME,
|
||||
active_time_end TIME,
|
||||
|
||||
-- 状态控制
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'MAINTENANCE')),
|
||||
is_bidirectional BOOLEAN DEFAULT false,
|
||||
|
||||
-- 审计字段
|
||||
created_by VARCHAR(50),
|
||||
updated_by VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 路径表索引
|
||||
CREATE INDEX idx_transport_routes_route_id ON transport_routes(route_id);
|
||||
CREATE INDEX idx_transport_routes_type ON transport_routes(route_type);
|
||||
CREATE INDEX idx_transport_routes_status ON transport_routes(status);
|
||||
CREATE INDEX idx_transport_routes_geometry_gist ON transport_routes USING GIST(route_geometry);
|
||||
|
||||
-- ============================================
|
||||
-- 4. 实时对象路径分配表 (object_route_assignments)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS object_route_assignments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- 对象信息
|
||||
object_id VARCHAR(50) NOT NULL, -- 车辆ID或航空器标识
|
||||
object_type VARCHAR(20) NOT NULL CHECK (object_type IN ('UNMANNED_VEHICLE', 'AIRCRAFT', 'SPECIAL_VEHICLE')),
|
||||
object_name VARCHAR(100), -- 车牌号或航班号
|
||||
|
||||
-- 路径分配
|
||||
assigned_route_id VARCHAR(50) NOT NULL,
|
||||
|
||||
-- 时间信息
|
||||
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 外键约束
|
||||
FOREIGN KEY (assigned_route_id) REFERENCES transport_routes(route_id)
|
||||
);
|
||||
|
||||
-- 对象路径分配表索引
|
||||
CREATE INDEX idx_object_route_assignments_object ON object_route_assignments(object_id, object_type);
|
||||
CREATE INDEX idx_object_route_assignments_route ON object_route_assignments(assigned_route_id);
|
||||
CREATE INDEX idx_object_route_assignments_assigned_at ON object_route_assignments(assigned_at DESC);
|
||||
|
||||
-- ============================================
|
||||
-- 5. 预警/告警记录表 (conflict_alert_logs)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS conflict_alert_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- 关联冲突 (这里假设冲突ID仍然由算法生成并关联,即使path_conflict_detections表被删除)
|
||||
conflict_id VARCHAR(50) NOT NULL, -- 冲突的唯一标识,可能来自实时计算或是一个事件ID
|
||||
|
||||
-- 告警信息
|
||||
alert_type VARCHAR(20) NOT NULL CHECK (alert_type IN ('WARNING', 'ALERT', 'EMERGENCY')),
|
||||
alert_level VARCHAR(20) NOT NULL CHECK (alert_level IN ('INFO', 'WARNING', 'CRITICAL', 'EMERGENCY')),
|
||||
alert_message TEXT NOT NULL,
|
||||
|
||||
-- 距离信息
|
||||
object1_distance DOUBLE PRECISION, -- 对象1距离冲突点距离
|
||||
object2_distance DOUBLE PRECISION, -- 对象2距离冲突点距离
|
||||
minimum_distance DOUBLE PRECISION, -- 两对象之间最小距离
|
||||
|
||||
-- 时间信息
|
||||
alert_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 告警记录表索引
|
||||
CREATE INDEX idx_conflict_alerts_conflict_id ON conflict_alert_logs(conflict_id);
|
||||
CREATE INDEX idx_conflict_alerts_type ON conflict_alert_logs(alert_type);
|
||||
CREATE INDEX idx_conflict_alerts_level ON conflict_alert_logs(alert_level);
|
||||
CREATE INDEX idx_conflict_alerts_time ON conflict_alert_logs(alert_time DESC);
|
||||
|
||||
-- ============================================
|
||||
-- 6. 创建触发器自动更新时间戳
|
||||
-- ============================================
|
||||
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 为相关表创建触发器
|
||||
CREATE TRIGGER trigger_transport_routes_updated_at
|
||||
BEFORE UPDATE ON transport_routes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_timestamp_column();
|
||||
|
||||
-- ============================================
|
||||
-- 7. 添加表注释
|
||||
-- ============================================
|
||||
COMMENT ON TABLE transport_routes IS '交通路径定义表 - 存储航空器和无人车的预定路径';
|
||||
COMMENT ON TABLE object_route_assignments IS '实时对象路径分配表 - 跟踪对象当前使用的路径';
|
||||
COMMENT ON TABLE conflict_alert_logs IS '冲突预警告警记录表 - 记录发送的所有预警和告警消息';
|
||||
|
||||
-- ============================================
|
||||
-- 8. 插入示例数据
|
||||
-- ============================================
|
||||
|
||||
-- 示例航空器滑行路径
|
||||
INSERT INTO transport_routes (route_id, route_name, route_type, description, route_geometry, max_speed_kph, typical_speed_kph) VALUES
|
||||
('TAXI_01', '1号滑行道路径', 'AIRCRAFT', '从停机坪到跑道的标准滑行路径',
|
||||
ST_GeomFromText('LINESTRING(120.084174 36.367162, 120.085 36.368, 120.086 36.369)', 4326),
|
||||
30, 20),
|
||||
('SERVICE_ROAD_A', 'A区服务道路', 'UNMANNED_VEHICLE', '无人车服务区域A的标准路径',
|
||||
ST_GeomFromText('LINESTRING(120.084 36.367, 120.085 36.368, 120.086 36.369, 120.087 36.370)', 4326),
|
||||
25, 15);
|
||||
|
||||
-- 示例路径分配
|
||||
INSERT INTO object_route_assignments (object_id, object_type, object_name, assigned_route_id) VALUES
|
||||
('UAV001', 'UNMANNED_VEHICLE', '无人车-001', 'SERVICE_ROAD_A'),
|
||||
('AIRCRAFT001', 'AIRCRAFT', '航班CZ345', 'TAXI_01');
|
||||
Loading…
Reference in New Issue
Block a user