From f5d830b7134554f2354622bbc71d9ce4f7dcba3c Mon Sep 17 00:00:00 2001 From: Tian jianyong <11429339@qq.com> Date: Fri, 11 Jul 2025 14:32:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B7=AF=E5=BE=84=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E6=A3=80=E6=B5=8B=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BA=86=E6=95=B0=E6=8D=AE=E9=87=87=E9=9B=86=E5=92=8C?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.md | 2 +- changelog.md | 68 ++ .../qaup/collision/common/model/Aircraft.java | 67 +- .../common/model/AirportVehicle.java | 70 +- .../collision/common/model/MovingObject.java | 82 ++- .../common/model/UnmannedVehicle.java | 70 +- .../service/VehicleLocationService.java | 57 +- .../datacollector/dao/DataCollectorDao.java | 152 ++++- .../service/DataCollectorService.java | 610 +++++++++++------- .../service/SpeedCalculationService.java | 210 ------ .../event/ConflictAlertEventListener.java | 156 +++++ .../model/dto/ConflictAlertEvent.java | 132 ++++ .../model/entity/ConflictAlertLog.java | 109 ++++ .../model/entity/ObjectRouteAssignment.java | 79 +++ .../model/entity/TransportRoute.java | 159 +++++ .../ConflictAlertLogRepository.java | 42 ++ .../ObjectRouteAssignmentRepository.java | 37 ++ .../repository/TransportRouteRepository.java | 65 ++ .../service/PathConflictDetectionService.java | 415 ++++++++++++ .../service/VehicleCommandService.java | 190 ++++++ .../impl/RealTimeViolationDetectorImpl.java | 56 +- .../RuleEventWebSocketPublisher.java | 48 +- .../WebSocketMessageBroadcaster.java | 18 + .../PathConflictAlertWebSocketEvent.java | 97 +++ .../message/MessageTypeConstants.java | 5 + .../message/PathConflictAlertMessage.java | 167 +++++ sql/create_path_conflict_detection_tables.sql | 147 +++++ 27 files changed, 2631 insertions(+), 679 deletions(-) create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/event/ConflictAlertEventListener.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/dto/ConflictAlertEvent.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ConflictAlertLog.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ObjectRouteAssignment.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/TransportRoute.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ConflictAlertLogRepository.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ObjectRouteAssignmentRepository.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/TransportRouteRepository.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/websocket/event/PathConflictAlertWebSocketEvent.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/websocket/message/PathConflictAlertMessage.java create mode 100644 sql/create_path_conflict_detection_tables.sql diff --git a/VERSION.md b/VERSION.md index 9fc80f93..448a0fa1 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -0.3.2 \ No newline at end of file +0.3.4 \ No newline at end of file diff --git a/changelog.md b/changelog.md index 54be2f43..e02065a4 100644 --- a/changelog.md +++ b/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接口,显示无人车的位置信息 diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/Aircraft.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/Aircraft.java index 2ee1ad6a..51ee28de 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/model/Aircraft.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/Aircraft.java @@ -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 + - '}'; - } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/AirportVehicle.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/AirportVehicle.java index 78a70663..4b8ea799 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/model/AirportVehicle.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/AirportVehicle.java @@ -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 + - '}'; - } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java index 3fad42da..6dfe65c1 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java @@ -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 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作为阈值 + } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java index 3be29a60..568029b1 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java @@ -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 + - '}'; - } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java b/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java index d434c23a..5b5cc877 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java @@ -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 buildVehicleStateMap(VehicleLocation vehicleLocation, SysVehicleInfo vehicleInfo, MovingObjectType vehicleType) { - Map 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 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; } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java index 62fb58ba..ae4c43f9 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java @@ -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 requestEntity = new HttpEntity<>(headers); - ResponseEntity>> response = restTemplate.exchange( + ResponseEntity>> response = restTemplate.exchange( url, HttpMethod.GET, requestEntity, - new ParameterizedTypeReference>>() {} + new ParameterizedTypeReference>>() {} ); if (response.getStatusCode().is2xxSuccessful()) { - Response> responseBody = response.getBody(); - if (responseBody != null) { - List dataList = responseBody.getData(); - log.info("成功获取航空器数据,数量: {}", dataList.size()); - return dataList; + Response> responseBody = response.getBody(); + if (responseBody != null && responseBody.getData() != null) { + List 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 requestEntity = new HttpEntity<>(headers); - ResponseEntity>> response = restTemplate.exchange( + ResponseEntity>> response = restTemplate.exchange( url, HttpMethod.GET, requestEntity, - new ParameterizedTypeReference>>() {} + new ParameterizedTypeReference>>() {} ); if (response.getStatusCode().is2xxSuccessful()) { - Response> responseBody = response.getBody(); - if (responseBody != null) { - List dataList = responseBody.getData(); - log.info("成功获取机场车辆数据,数量: {}", dataList.size()); - return dataList; + Response> responseBody = response.getBody(); + if (responseBody != null && responseBody.getData() != null) { + List 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 requestEntity = new HttpEntity<>(headers); - ResponseEntity> response = restTemplate.exchange( + ResponseEntity> response = restTemplate.exchange( url, HttpMethod.GET, requestEntity, - new ParameterizedTypeReference>() {} + new ParameterizedTypeReference>() {} ); if (response.getStatusCode().is2xxSuccessful()) { - List responseBody = response.getBody(); - if (responseBody != null) { - log.info("成功获取无人车定位信息,数量: {}", responseBody.size()); - return responseBody; + List rawDataList = response.getBody(); + if (rawDataList != null) { + List 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; + } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java index b705e572..39168165 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java @@ -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 lastPushTimes = new ConcurrentHashMap<>(); + // 用于缓存所有活跃的MovingObject的最新状态 + private final Map activeMovingObjectsCache = new ConcurrentHashMap<>(); + /** * 定时采集航空器数据 * @@ -94,67 +122,96 @@ public class DataCollectorService { log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size()); + // List 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 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 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 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 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 vehicleLocations = - unmannedVehicles.stream() - .map(this::convertToVehicleLocation) - .filter(location -> location != null) - .toList(); - - if (!vehicleLocations.isEmpty()) { - // 使用VehicleDataPersistenceService进行批量保存 - List 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 currentActiveObjects = new ArrayList<>(activeMovingObjectsCache.values()); + + // 执行路径冲突检测 + pathConflictDetectionService.detectPathConflicts(currentActiveObjects); + + // 将MovingObject列表转换为VehicleLocation列表进行批量检测 + List 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 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 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; + } + } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java index cb2f903b..d6e7ce1d 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java @@ -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 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 history) { - // TODO: 实现卡尔曼滤波算法 - // 状态向量:[x, y, vx, vy] - // 观测向量:[x, y] - } - - /** - * 计算速度 - */ - public void calculateSpeed(MovingObject object, MovementState newState) { -// Deque 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 diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/event/ConflictAlertEventListener.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/event/ConflictAlertEventListener.java new file mode 100644 index 00000000..1f3e14f6 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/event/ConflictAlertEventListener.java @@ -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); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/dto/ConflictAlertEvent.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/dto/ConflictAlertEvent.java new file mode 100644 index 00000000..c04b522f --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/dto/ConflictAlertEvent.java @@ -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; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ConflictAlertLog.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ConflictAlertLog.java new file mode 100644 index 00000000..b6dc8a45 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ConflictAlertLog.java @@ -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(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ObjectRouteAssignment.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ObjectRouteAssignment.java new file mode 100644 index 00000000..df81a5ae --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/ObjectRouteAssignment.java @@ -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(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/TransportRoute.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/TransportRoute.java new file mode 100644 index 00000000..4dc27c45 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/model/entity/TransportRoute.java @@ -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(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ConflictAlertLogRepository.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ConflictAlertLogRepository.java new file mode 100644 index 00000000..6ccbca5b --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ConflictAlertLogRepository.java @@ -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 { + + /** + * 根据冲突ID查找告警记录 + */ + List findByConflictId(String conflictId); + + /** + * 根据告警类型和级别查找告警记录 + */ + List findByAlertTypeAndAlertLevel( + ConflictAlertLog.AlertType alertType, + ConflictAlertLog.AlertLevel alertLevel + ); + + /** + * 查找最新的一条告警记录 + */ + Optional findFirstByOrderByAlertTimeDesc(); + + /** + * 查找某个冲突ID下最新的告警记录 + */ + Optional findFirstByConflictIdOrderByAlertTimeDesc(String conflictId); +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ObjectRouteAssignmentRepository.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ObjectRouteAssignmentRepository.java new file mode 100644 index 00000000..a3c8df4b --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/ObjectRouteAssignmentRepository.java @@ -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 { + + /** + * 根据对象ID和对象类型查找最新的路径分配记录 + */ + Optional findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc( + String objectId, + ObjectRouteAssignment.ObjectType objectType + ); + + /** + * 根据路径ID查找所有分配到该路径的对象 + */ + List findByAssignedRouteId(String assignedRouteId); + + /** + * 根据对象ID查找所有分配到该对象的路径记录 + */ + List findByObjectId(String objectId); +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/TransportRouteRepository.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/TransportRouteRepository.java new file mode 100644 index 00000000..d59b7d6e --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/repository/TransportRouteRepository.java @@ -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 { + + /** + * 根据路径ID查找路径 + */ + Optional findByRouteId(String routeId); + + /** + * 根据路径类型和状态查找路径 + */ + List 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 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 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 findNearestRoutes( + @Param("point") String pointWKT, + @Param("routeType") String routeType, + @Param("status") String status, + @Param("limit") int limit); +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java new file mode 100644 index 00000000..7c7dc144 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java @@ -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 activeObjects) { + log.debug("开始路径冲突检测,活跃对象数量: {}", activeObjects.size()); + + List 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 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 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 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 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 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 calculateRouteIntersections(TransportRoute route1, TransportRoute route2) { + List 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; } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java new file mode 100644 index 00000000..bb831a0c --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java @@ -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; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RealTimeViolationDetectorImpl.java b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RealTimeViolationDetectorImpl.java index 8a7856b2..80fa12a3 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RealTimeViolationDetectorImpl.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RealTimeViolationDetectorImpl.java @@ -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 detectViolations(VehicleLocation vehicleLocation) { if (!isRealTimeDetectionEnabled()) { @@ -82,12 +93,12 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector } // 检测违规 - List violations = new ArrayList<>(); + // RuleViolationEvent将通过事件监听器处理和统计,此处不再收集列表 Map 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 allViolations = new ArrayList<>(); + // 不再需要 allViolations 列表,通过事件监听器进行统计 + // List allViolations = new ArrayList<>(); for (VehicleLocation vehicleLocation : vehicleLocations) { try { - List 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 diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/RuleEventWebSocketPublisher.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/RuleEventWebSocketPublisher.java index 8c5f42cd..2d83eed4 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/RuleEventWebSocketPublisher.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/RuleEventWebSocketPublisher.java @@ -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 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"; } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java index cda67b50..af468746 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java @@ -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 message = UniversalMessage.builder() + .type(MessageTypeConstants.PATH_CONFLICT_ALERT) + .timestamp(event.getTimestamp()) + .messageId(generateMessageId()) + .payload((PathConflictAlertMessage) event.getPayload()) + .build(); + + broadcastMessage(message); + } + /** * 统一消息广播方法 - 使用原生WebSocket发送JSON消息 * @param message 要广播的消息 diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/event/PathConflictAlertWebSocketEvent.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/event/PathConflictAlertWebSocketEvent.java new file mode 100644 index 00000000..9ac653d3 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/event/PathConflictAlertWebSocketEvent.java @@ -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(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java index e6cfd66a..d1a29b31 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java @@ -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("常量类不允许实例化"); diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/PathConflictAlertMessage.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/PathConflictAlertMessage.java new file mode 100644 index 00000000..23314911 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/PathConflictAlertMessage.java @@ -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); + } +} \ No newline at end of file diff --git a/sql/create_path_conflict_detection_tables.sql b/sql/create_path_conflict_detection_tables.sql new file mode 100644 index 00000000..2082e3c9 --- /dev/null +++ b/sql/create_path_conflict_detection_tables.sql @@ -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'); \ No newline at end of file