新增路径冲突检测功能,优化了数据采集和检测逻辑

This commit is contained in:
Tian jianyong 2025-07-11 14:32:16 +08:00
parent 5453291cd0
commit f5d830b713
27 changed files with 2631 additions and 679 deletions

View File

@ -1 +1 @@
0.3.2
0.3.4

View File

@ -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接口显示无人车的位置信息

View File

@ -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 +
'}';
}
}

View File

@ -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 +
'}';
}
}

View File

@ -3,31 +3,73 @@ package com.qaup.collision.common.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayDeque;
import java.util.Deque;
import lombok.experimental.SuperBuilder;
import org.locationtech.jts.geom.Point;
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class MovingObject {
public class MovingObject {
// 当前坐标
public GeoPosition currentPosition;
//速度
public Velocity velocity;
// 航向
public double heading;
// 时间戳毫秒
public long timestamp;
// 历史状态队列
public Deque<MovementState> stateHistory = new ArrayDeque<>();
// 最大历史记录数
public int MAX_HISTORY = 30; // 最大历史记录数支持更长时间窗口的数据分析
// 最大速度子类初始化
public double maxSpeed;
// 类型枚举
public MovingObjectType type;
/**
* 对象唯一标识
*/
private String objectId;
/**
* 对象类型
*/
private ObjectType objectType;
/**
* 对象名称车牌号或航班号
*/
private String objectName;
/**
* 当前位置
*/
private Point currentPosition;
/**
* 当前速度 (km/h)
*/
private Double currentSpeed;
/**
* 当前方向角 (0-360)
*/
private Double currentHeading;
/**
* 高度
*/
private Double altitude;
/**
* 对象类型枚举
*/
public enum ObjectType {
UNMANNED_VEHICLE, // 无人车
AIRCRAFT, // 航空器
SPECIAL_VEHICLE // 特种车辆如机场车辆
}
/**
* 获取速度m/s
*/
public double getSpeedInMetersPerSecond() {
if (currentSpeed == null) {
return 0.0;
}
return currentSpeed * 1000.0 / 3600.0; // km/h to m/s
}
/**
* 判断是否在移动
*/
public boolean isMoving() {
return currentSpeed != null && currentSpeed > 0.5; // 0.5 km/h作为阈值
}
}

View File

@ -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 +
'}';
}
}

View File

@ -196,6 +196,7 @@ public class VehicleLocationService {
vehicleLocation.setAltitude(altitude);
vehicleLocation.setHeading(heading);
vehicleLocation.setSpeed(speed);
log.debug("在VehicleLocationService.createVehicleLocation方法中设置速度后vehicleLocation.speed为: {}", vehicleLocation.getSpeed());
vehicleLocation.setTimestamp(LocalDateTime.now());
vehicleLocation.setDataQuality("GOOD"); // 默认数据质量
@ -849,33 +850,35 @@ public class VehicleLocationService {
* 构建车辆状态参数Map用于规则执行
*/
private Map<String, Object> buildVehicleStateMap(VehicleLocation vehicleLocation, SysVehicleInfo vehicleInfo, MovingObjectType vehicleType) {
Map<String, Object> vehicleState = new HashMap<>();
// 基本位置信息
vehicleState.put("vehicleId", vehicleLocation.getVehicleId());
vehicleState.put("vehicleType", vehicleType);
vehicleState.put("licensePlate", vehicleInfo.getLicensePlate());
vehicleState.put("timestamp", vehicleLocation.getTimestamp());
// 位置坐标
if (vehicleLocation.getLocation() != null) {
vehicleState.put("longitude", vehicleLocation.getLocation().getX());
vehicleState.put("latitude", vehicleLocation.getLocation().getY());
Map<String, Object> stateMap = new HashMap<>();
stateMap.put("vehicleId", vehicleLocation.getVehicleId());
stateMap.put("licensePlate", vehicleLocation.getLicensePlate());
stateMap.put("longitude", vehicleLocation.getLocation().getX());
stateMap.put("latitude", vehicleLocation.getLocation().getY());
stateMap.put("altitude", vehicleLocation.getAltitude());
stateMap.put("speed", vehicleLocation.getSpeed());
stateMap.put("heading", vehicleLocation.getHeading());
stateMap.put("timestamp", vehicleLocation.getTimestamp());
stateMap.put("vehicleType", vehicleType.name());
// 移除对 vehicleModel usagePurpose 的引用因为 SysVehicleInfo 中可能不包含这些字段
// if (vehicleInfo != null) {
// stateMap.put("vehicleModel", vehicleInfo.getVehicleModel());
// stateMap.put("usagePurpose", vehicleInfo.getUsagePurpose());
// }
return stateMap;
}
/**
* 统计所有无人车数量
*/
public long countAllUnmannedVehicles() {
try {
// 假设VehicleLocationRepository主要存储无人车数据
return vehicleLocationRepository.count();
} catch (Exception e) {
log.error("获取所有无人车数量失败", e);
return 0;
}
// 可选属性
if (vehicleLocation.getAltitude() != null) {
vehicleState.put("altitude", vehicleLocation.getAltitude());
}
if (vehicleLocation.getHeading() != null) {
vehicleState.put("heading", vehicleLocation.getHeading());
}
if (vehicleLocation.getSpeed() != null) {
vehicleState.put("speed", vehicleLocation.getSpeed());
}
return vehicleState;
}
}

View File

@ -4,10 +4,17 @@ package com.qaup.collision.datacollector.dao;
import com.qaup.collision.common.model.Aircraft;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.collision.common.model.UnmannedVehicle;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.dto.Response;
import com.qaup.collision.datacollector.service.AuthService;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
@ -17,9 +24,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@ -36,10 +46,13 @@ public class DataCollectorDao {
private final RestTemplate restTemplate;
private final AuthService authService;
private final GeometryFactory geometryFactory;
@Autowired
public DataCollectorDao(RestTemplate restTemplate, AuthService authService) {
this.restTemplate = restTemplate;
this.authService = authService;
this.geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84
}
@ -54,19 +67,35 @@ public class DataCollectorDao {
headers.set("Authorization", authService.getToken());
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Response<List<Aircraft>>> response = restTemplate.exchange(
ResponseEntity<Response<List<ExternalAircraftData>>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<Response<List<Aircraft>>>() {}
new ParameterizedTypeReference<Response<List<ExternalAircraftData>>>() {}
);
if (response.getStatusCode().is2xxSuccessful()) {
Response<List<Aircraft>> responseBody = response.getBody();
if (responseBody != null) {
List<Aircraft> dataList = responseBody.getData();
log.info("成功获取航空器数据,数量: {}", dataList.size());
return dataList;
Response<List<ExternalAircraftData>> responseBody = response.getBody();
if (responseBody != null && responseBody.getData() != null) {
List<Aircraft> processedAircrafts = responseBody.getData().stream().map(rawData -> {
if (rawData.getFlightNo() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
log.warn("原始航空器数据缺失必要字段 (flightNo, longitude, latitude),跳过处理: {}", rawData);
return null;
}
Aircraft aircraft = new Aircraft();
aircraft.setObjectId(rawData.getFlightNo());
aircraft.setObjectName(rawData.getFlightNo()); // 使用 flightNo 作为 objectName
aircraft.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
// 外部API中没有直接的速度和方向如果需要可以在DataCollectorService中计算或使用默认值
// 暂时设置为0.0DataCollectorService会处理
aircraft.setCurrentSpeed(0.0);
aircraft.setCurrentHeading(0.0);
aircraft.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
return aircraft;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
log.info("成功获取航空器数据,数量: {}", processedAircrafts.size());
return processedAircrafts;
}
}
} catch (Exception e) {
@ -86,19 +115,33 @@ public class DataCollectorDao {
headers.set("Authorization", authService.getToken());
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Response<List<AirportVehicle>>> response = restTemplate.exchange(
ResponseEntity<Response<List<ExternalAirportVehicleData>>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<Response<List<AirportVehicle>>>() {}
new ParameterizedTypeReference<Response<List<ExternalAirportVehicleData>>>() {}
);
if (response.getStatusCode().is2xxSuccessful()) {
Response<List<AirportVehicle>> responseBody = response.getBody();
if (responseBody != null) {
List<AirportVehicle> dataList = responseBody.getData();
log.info("成功获取机场车辆数据,数量: {}", dataList.size());
return dataList;
Response<List<ExternalAirportVehicleData>> responseBody = response.getBody();
if (responseBody != null && responseBody.getData() != null) {
List<AirportVehicle> processedVehicles = responseBody.getData().stream().map(rawData -> {
if (rawData.getVehicleNo() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
log.warn("原始机场车辆数据缺失必要字段 (vehicleNo, longitude, latitude),跳过处理: {}", rawData);
return null;
}
AirportVehicle vehicle = new AirportVehicle();
vehicle.setObjectId(rawData.getVehicleNo());
vehicle.setObjectName(rawData.getVehicleNo()); // 使用 vehicleNo 作为 objectName
vehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
vehicle.setCurrentSpeed(0.0); // 暂时设置为0.0DataCollectorService会处理
vehicle.setCurrentHeading(0.0);
vehicle.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
return vehicle;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
log.info("成功获取机场车辆数据,数量: {}", processedVehicles.size());
return processedVehicles;
}
}
} catch (Exception e) {
@ -126,18 +169,34 @@ public class DataCollectorDao {
HttpHeaders headers = new HttpHeaders();
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<List<UnmannedVehicle>> response = restTemplate.exchange(
ResponseEntity<List<ExternalUnmannedVehicleData>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<List<UnmannedVehicle>>() {}
new ParameterizedTypeReference<List<ExternalUnmannedVehicleData>>() {}
);
if (response.getStatusCode().is2xxSuccessful()) {
List<UnmannedVehicle> responseBody = response.getBody();
if (responseBody != null) {
log.info("成功获取无人车定位信息,数量: {}", responseBody.size());
return responseBody;
List<ExternalUnmannedVehicleData> rawDataList = response.getBody();
if (rawDataList != null) {
List<UnmannedVehicle> processedUnmannedVehicles = rawDataList.stream().map(rawData -> {
if (rawData.getVehicleID() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
log.warn("原始无人车数据缺失必要字段 (vehicleID, longitude, latitude),跳过处理: {}", rawData);
return null;
}
UnmannedVehicle unmannedVehicle = new UnmannedVehicle();
unmannedVehicle.setObjectId(rawData.getVehicleID());
unmannedVehicle.setObjectName(rawData.getVehicleID()); // 使用 vehicleID 作为 objectName
unmannedVehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
unmannedVehicle.setCurrentSpeed(rawData.getSpeed() != null ? rawData.getSpeed() * 3.6 : 0.0); // m/s to km/h
unmannedVehicle.setCurrentHeading(rawData.getDirection() != null ? Math.toDegrees(rawData.getDirection()) : 0.0); // 弧度 to 角度
unmannedVehicle.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
unmannedVehicle.setObjectType(MovingObject.ObjectType.UNMANNED_VEHICLE); // 显式设置对象类型
return unmannedVehicle;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
log.info("成功获取无人车定位信息,数量: {}", processedUnmannedVehicles.size());
return processedUnmannedVehicles;
}
}
} catch (Exception e) {
@ -147,4 +206,57 @@ public class DataCollectorDao {
return Collections.emptyList();
}
// 内部 DTOs精确匹配外部 API 接口返回的 JSON 结构
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
private static class ExternalAircraftData {
@JsonProperty("flightNo")
private String flightNo;
@JsonProperty("longitude")
private Double longitude;
@JsonProperty("latitude")
private Double latitude;
@JsonProperty("time")
private Long time;
// 其他可能存在的但我们不关心的字段将被 @JsonIgnoreProperties 忽略
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
private static class ExternalAirportVehicleData {
@JsonProperty("vehicleNo")
private String vehicleNo;
@JsonProperty("longitude")
private Double longitude;
@JsonProperty("latitude")
private Double latitude;
@JsonProperty("time")
private Long time;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
private static class ExternalUnmannedVehicleData {
@JsonProperty("transId")
private String transId;
@JsonProperty("timestamp")
private Long timestamp;
@JsonProperty("vehicleID")
private String vehicleID;
@JsonProperty("latitude")
private Double latitude;
@JsonProperty("longitude")
private Double longitude;
@JsonProperty("speed")
private Double speed;
@JsonProperty("direction")
private Double direction;
}
}

View File

@ -1,11 +1,17 @@
package com.qaup.collision.datacollector.service;
import com.qaup.collision.common.model.*;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.service.VehicleLocationService;
import com.qaup.collision.dataprocessing.service.SpeedCalculationService;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import com.qaup.collision.websocket.event.PositionUpdateEvent;
import com.qaup.collision.websocket.message.PositionUpdatePayload;
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
import com.qaup.collision.rule.service.RealTimeViolationDetector;
import com.qaup.collision.common.adapter.QuapDataAdapter;
import com.qaup.system.domain.SysVehicleInfo;
import com.qaup.collision.rule.event.RuleViolationEvent;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
@ -16,9 +22,17 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationEventPublisher;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.PrecisionModel;
/**
* 数据采集服务 - 重构版本
@ -65,12 +79,26 @@ public class DataCollectorService {
@Autowired
private SpeedCalculationService speedCalculationService;
@Autowired
private PathConflictDetectionService pathConflictDetectionService;
@Autowired
private RealTimeViolationDetector realTimeViolationDetector; // 注入 RealTimeViolationDetector
@Autowired
private QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84
/**
* WebSocket推送节流机制 - 跟踪每个对象的上次推送时间
* key: objectId, value: lastPushTimestamp
*/
private final Map<String, Long> lastPushTimes = new ConcurrentHashMap<>();
// 用于缓存所有活跃的MovingObject的最新状态
private final Map<String, MovingObject> activeMovingObjectsCache = new ConcurrentHashMap<>();
/**
* 定时采集航空器数据
*
@ -94,67 +122,96 @@ public class DataCollectorService {
log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size());
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
// 航空器数据仅用于实时处理不存储到数据库
// 数据处理完成后发布WebSocket事件进行实时推送
for (Aircraft aircraft : newAircrafts) {
try {
// 计算速度和方向
if (aircraft.getCurrentPosition() == null) {
log.warn("航空器 {} 位置信息缺失,跳过处理。", aircraft.getObjectId());
continue; // 跳过此航空器
}
long currentTime = System.currentTimeMillis();
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
aircraft.getCurrentPosition().getX(),
aircraft.getCurrentPosition().getY()
));
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
aircraft.getFlightNo(),
aircraft.getCurrentPosition().getLatitude(),
aircraft.getCurrentPosition().getLongitude(),
aircraft.getObjectId(), // Changed from getFlightNo()
aircraft.getCurrentPosition().getY(),
aircraft.getCurrentPosition().getX(),
currentTime
);
Double calculatedDirection = speedCalculationService.getCurrentDirection(aircraft.getObjectId()); // Changed from getFlightNo()
Double calculatedDirection = speedCalculationService.getCurrentDirection(aircraft.getFlightNo());
MovingObject movingObject = MovingObject.builder()
.objectId(aircraft.getObjectId()) // Changed from getFlightNo()
.objectType(MovingObject.ObjectType.AIRCRAFT)
.objectName(aircraft.getObjectName()) // Changed from getFlightNo()
.currentPosition(currentPosition)
.currentSpeed(calculatedSpeed)
.currentHeading(calculatedDirection)
.altitude(aircraft.getAltitude())
.build();
// activeMovingObjects.add(movingObject); // 移除此行
// 将最新数据更新到缓存
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
// 如果没有计算出速度尝试从velocity获取
Double finalSpeed = calculatedSpeed;
if (finalSpeed == null && aircraft.getVelocity() != null) {
finalSpeed = aircraft.getVelocity().getSpeed();
}
// 如果没有计算出方向尝试从原有heading获取
Double finalHeading = calculatedDirection;
if (finalHeading == null) {
finalHeading = aircraft.getHeading();
}
// 创建位置更新消息负载
PositionUpdatePayload.Position position = PositionUpdatePayload.Position.builder()
.latitude(aircraft.getCurrentPosition().getLatitude())
.longitude(aircraft.getCurrentPosition().getLongitude())
// 创建位置更新消息负载WebSocket推送
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
.latitude(currentPosition.getY())
.longitude(currentPosition.getX())
.build();
PositionUpdatePayload payload = PositionUpdatePayload.builder()
.objectId(aircraft.getFlightNo())
.objectType(MovingObjectType.AIRCRAFT.name())
.position(position)
.heading(finalHeading)
.speed(finalSpeed)
.timestamp(currentTime * 1000) // 微秒级时间戳
.objectId(aircraft.getObjectId()) // Changed from getFlightNo()
.objectType(MovingObject.ObjectType.AIRCRAFT.name())
.position(positionPayload)
.heading(movingObject.getCurrentHeading())
.speed(movingObject.getCurrentSpeed())
.timestamp(currentTime) // 更新为Long类型时间戳
.build();
// 节流检查只有达到推送间隔时才发布WebSocket事件
if (shouldPushWebSocketMessage(aircraft.getFlightNo(), currentTime)) {
if (shouldPushWebSocketMessage(aircraft.getObjectId(), currentTime)) { // Changed from getFlightNo()
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
log.debug("处理航空器数据并发布事件: (航班号: {}, 位置: {}, {}, 速度: {})",
aircraft.getFlightNo(),
aircraft.getCurrentPosition().getLongitude(),
aircraft.getCurrentPosition().getLatitude(),
finalSpeed);
aircraft.getObjectId(), // Changed from getFlightNo()
currentPosition.getX(),
currentPosition.getY(),
movingObject.getCurrentSpeed());
} else {
log.trace("航空器数据采集但未推送(节流): {} (速度: {})",
aircraft.getFlightNo(), finalSpeed);
aircraft.getObjectId(), movingObject.getCurrentSpeed()); // Changed from getFlightNo()
}
} catch (Exception e) {
log.error("处理航空器数据异常: flightNo={}", aircraft.getFlightNo(), e);
log.error("处理航空器数据异常: objectId={}", aircraft.getObjectId(), e); // Changed from flightNo
}
}
// 执行路径冲突检测 (临时移除将在新的定时任务中统一处理)
// if (!activeMovingObjects.isEmpty()) {
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
// }
// 执行实时违规检测 (临时移除将在新的定时任务中统一处理)
// for (MovingObject movingObject : activeMovingObjects) {
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
// if (tempVehicleLocation != null) {
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
// if (!violations.isEmpty()) {
// log.warn("检测到航空器违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
// // 如果需要可以在这里进一步处理这些违规事件例如持久化或发送告警
// // realTimeViolationDetector.processBatchViolationEvents(violations);
// }
// }
// }
log.info("航空器数据处理和事件发布完成,处理数量: {}", newAircrafts.size());
} catch (Exception e) {
@ -188,80 +245,112 @@ public class DataCollectorService {
log.info("采集到 {} 条机场车辆数据,用于实时处理", vehicles.size());
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
// 机场车辆数据仅用于实时处理不存储到数据库
// 数据处理完成后发布WebSocket事件进行实时推送
for (AirportVehicle vehicle : vehicles) {
try {
// 计算速度和方向
if (vehicle.getCurrentPosition() == null) {
log.warn("机场车辆 {} 位置信息缺失,跳过处理。", vehicle.getObjectId());
continue; // 跳过此机场车辆
}
long currentTime = System.currentTimeMillis();
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
vehicle.getCurrentPosition().getX(),
vehicle.getCurrentPosition().getY()
));
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
vehicle.getLicensePlate(),
vehicle.getCurrentPosition().getLatitude(),
vehicle.getCurrentPosition().getLongitude(),
vehicle.getObjectId(),
vehicle.getCurrentPosition().getY(),
vehicle.getCurrentPosition().getX(),
currentTime
);
Double calculatedDirection = speedCalculationService.getCurrentDirection(vehicle.getLicensePlate());
// 如果没有计算出速度尝试从velocity获取
Double finalSpeed = calculatedSpeed;
if (finalSpeed == null && vehicle.getVelocity() != null) {
finalSpeed = vehicle.getVelocity().getSpeed();
}
// 如果没有计算出方向尝试从原有heading获取
Double finalHeading = calculatedDirection;
if (finalHeading == null) {
finalHeading = vehicle.getHeading();
}
// 创建位置更新消息负载
PositionUpdatePayload.Position position = PositionUpdatePayload.Position.builder()
.latitude(vehicle.getCurrentPosition().getLatitude())
.longitude(vehicle.getCurrentPosition().getLongitude())
Double calculatedDirection = speedCalculationService.getCurrentDirection(vehicle.getObjectId());
MovingObject movingObject = MovingObject.builder()
.objectId(vehicle.getObjectId())
.objectType(MovingObject.ObjectType.SPECIAL_VEHICLE)
.objectName(vehicle.getObjectName())
.currentPosition(currentPosition)
.currentSpeed(calculatedSpeed)
.currentHeading(calculatedDirection)
.altitude(vehicle.getAltitude())
.build();
// activeMovingObjects.add(movingObject); // 移除此行
// 将最新数据更新到缓存
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
// 创建位置更新消息负载WebSocket推送
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
.latitude(currentPosition.getY())
.longitude(currentPosition.getX())
.build();
PositionUpdatePayload payload = PositionUpdatePayload.builder()
.objectId(vehicle.getLicensePlate()) // 使用车牌号作为标识符
.objectType(MovingObjectType.AIRPORT_VEHICLE.name())
.position(position)
.heading(finalHeading)
.speed(finalSpeed)
.timestamp(currentTime * 1000) // 微秒级时间戳
.objectId(vehicle.getObjectId())
.objectType(MovingObject.ObjectType.SPECIAL_VEHICLE.name())
.position(positionPayload)
.heading(movingObject.getCurrentHeading())
.speed(movingObject.getCurrentSpeed())
.timestamp(currentTime)
.build();
// 节流检查只有达到推送间隔时才发布WebSocket事件
if (shouldPushWebSocketMessage(vehicle.getLicensePlate(), currentTime)) {
if (shouldPushWebSocketMessage(vehicle.getObjectId(), currentTime)) {
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
log.debug("处理机场车辆数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
vehicle.getLicensePlate(),
vehicle.getCurrentPosition().getLongitude(),
vehicle.getCurrentPosition().getLatitude(),
finalSpeed);
vehicle.getObjectId(),
currentPosition.getX(),
currentPosition.getY(),
movingObject.getCurrentSpeed());
} else {
log.trace("机场车辆数据采集但未推送(节流): {} (速度: {})",
vehicle.getLicensePlate(), finalSpeed);
vehicle.getObjectId(), movingObject.getCurrentSpeed());
}
} catch (Exception e) {
log.error("处理机场车辆数据异常: licensePlate={}", vehicle.getLicensePlate(), e);
log.error("处理机场车辆数据异常: objectId={}", vehicle.getObjectId(), e);
}
}
// 执行路径冲突检测 (临时移除将在新的定时任务中统一处理)
// if (!activeMovingObjects.isEmpty()) {
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
// }
// 执行实时违规检测 (临时移除将在新的定时任务中统一处理)
// for (MovingObject movingObject : activeMovingObjects) {
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
// if (tempVehicleLocation != null) {
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
// if (!violations.isEmpty()) {
// log.warn("检测到机场车辆违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
// // 如果需要可以在这里进一步处理这些违规事件例如持久化或发送告警
// // realTimeViolationDetector.processBatchViolationEvents(violations);
// }
// }
// }
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", vehicles.size());
} catch (Exception e) {
log.error("采集机场车辆数据异常", e);
}
}
/**
* 定时采集无人车数据
* 定时采集无人车数据 (外部API)
*
* 新增方法支持无人车数据采集和选择性持久化
* - 无人车位置数据存储到数据库用于轨迹回放和日志审计
* - 使用VehicleDataPersistenceService进行选择性存储
* 数据来源第2章 无人车位置上报 (/api/VehicleLocationInfo)
* 说明仅传递目前无人车平台已接入的无人车位置数据
* 重构说明
* - 无人车数据仅用于实时处理不存储到数据库
* - 数据采集后直接用于碰撞检测等实时计算
* - 不进行数据持久化
*/
@Scheduled(fixedRateString = "${data.collector.interval}")
@Async // 异步执行
@ -277,189 +366,272 @@ public class DataCollectorService {
return;
}
log.info("开始处理 {} 条无人车数据", unmannedVehicles.size());
log.info("采集到 {} 条无人车数据", unmannedVehicles.size());
// 将采集到的无人车数据转换为MovingObject并添加到缓存
// List<VehicleLocation> unmannedVehicleLocations = new ArrayList<>(); // 移除此行
// 先推送WebSocket位置更新事件
for (UnmannedVehicle vehicle : unmannedVehicles) {
for (UnmannedVehicle unmannedVehicle : unmannedVehicles) {
try {
// 计算速度和方向
if (unmannedVehicle.getCurrentPosition() == null) {
log.warn("无人车 {} 位置信息缺失,跳过处理。", unmannedVehicle.getObjectId());
continue; // 跳过此无人车
}
long currentTime = System.currentTimeMillis();
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
unmannedVehicle.getCurrentPosition().getX(),
unmannedVehicle.getCurrentPosition().getY()
));
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
vehicle.getLicensePlate(),
vehicle.getCurrentPosition().getLatitude(),
vehicle.getCurrentPosition().getLongitude(),
unmannedVehicle.getObjectId(),
unmannedVehicle.getCurrentPosition().getY(),
unmannedVehicle.getCurrentPosition().getX(),
currentTime
);
log.debug("调用speedCalculationService后无人车 {} 的速度为: {}", unmannedVehicle.getObjectId(), calculatedSpeed);
Double calculatedDirection = speedCalculationService.getCurrentDirection(unmannedVehicle.getObjectId());
MovingObject movingObject = MovingObject.builder()
.objectId(unmannedVehicle.getObjectId()) // Changed from getLicensePlate()
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectName(unmannedVehicle.getObjectName()) // Changed from getLicensePlate()
.currentPosition(currentPosition)
.currentSpeed(calculatedSpeed)
.currentHeading(calculatedDirection)
.altitude(unmannedVehicle.getAltitude())
.build();
log.debug("在DataCollectorService中MovingObject的速度: {}", movingObject.getCurrentSpeed());
// unmannedVehicleLocations.add(createTemporaryVehicleLocationForDetection(movingObject)); // 移除此行
// 将最新数据更新到缓存
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
Double calculatedDirection = speedCalculationService.getCurrentDirection(vehicle.getLicensePlate());
// 如果没有计算出速度尝试从velocity获取
Double finalSpeed = calculatedSpeed;
if (finalSpeed == null && vehicle.getVelocity() != null) {
finalSpeed = vehicle.getVelocity().getSpeed();
}
// 如果没有计算出方向尝试从原有heading获取
Double finalHeading = calculatedDirection;
if (finalHeading == null) {
finalHeading = vehicle.getHeading();
}
PositionUpdatePayload.Position position = PositionUpdatePayload.Position.builder()
.latitude(vehicle.getCurrentPosition().getLatitude())
.longitude(vehicle.getCurrentPosition().getLongitude())
// 创建位置更新消息负载WebSocket推送
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
.latitude(currentPosition.getY())
.longitude(currentPosition.getX())
.build();
PositionUpdatePayload payload = PositionUpdatePayload.builder()
.objectId(vehicle.getLicensePlate()) // 将Long转换为String
.objectType(MovingObjectType.UNMANNED_VEHICLE.name())
.position(position)
.heading(finalHeading)
.speed(finalSpeed)
.timestamp(currentTime * 1000) // 微秒级时间戳
.objectId(unmannedVehicle.getObjectId()) // Changed from getLicensePlate()
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE.name())
.position(positionPayload)
.heading(movingObject.getCurrentHeading())
.speed(movingObject.getCurrentSpeed())
.timestamp(currentTime)
.build();
// 节流检查只有达到推送间隔时才发布WebSocket事件
if (shouldPushWebSocketMessage(vehicle.getLicensePlate(), currentTime)) {
if (shouldPushWebSocketMessage(unmannedVehicle.getObjectId(), currentTime)) { // Changed from getLicensePlate()
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
log.debug("处理无人车数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
vehicle.getLicensePlate(),
vehicle.getCurrentPosition().getLongitude(),
vehicle.getCurrentPosition().getLatitude(),
finalSpeed);
log.debug("处理无人车数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
unmannedVehicle.getObjectId(),
currentPosition.getX(),
currentPosition.getY(),
movingObject.getCurrentSpeed());
} else {
log.trace("无人车数据采集但未推送(节流): {} (速度: {})",
vehicle.getLicensePlate(), finalSpeed);
unmannedVehicle.getObjectId(), movingObject.getCurrentSpeed()); // Changed from getLicensePlate()
}
} catch (Exception e) {
log.error("处理无人车数据异常: vehicleId={}", vehicle.getLicensePlate(), e);
log.error("处理无人车数据异常: objectId={}", unmannedVehicle.getObjectId(), e);
}
}
// 转换为VehicleLocation对象列表
List<com.qaup.collision.common.model.spatial.VehicleLocation> vehicleLocations =
unmannedVehicles.stream()
.map(this::convertToVehicleLocation)
.filter(location -> location != null)
.toList();
if (!vehicleLocations.isEmpty()) {
// 使用VehicleDataPersistenceService进行批量保存
List<com.qaup.collision.common.model.spatial.VehicleLocation> savedLocations =
vehicleDataPersistenceService.batchSaveUnmannedVehicleLocations(vehicleLocations);
log.info("无人车数据采集完成,成功保存 {}/{} 条记录",
savedLocations.size(), unmannedVehicles.size());
} else {
log.warn("无有效的无人车位置数据可保存");
}
// 执行路径冲突检测 (临时移除将在新的定时任务中统一处理)
// if (!unmannedVehicleLocations.isEmpty()) {
// pathConflictDetectionService.detectPathConflicts(unmannedVehicleLocations.stream()
// .map(v -> MovingObject.builder()
// .objectId(String.valueOf(v.getVehicleId()))
// .objectType(v.getVehicleType())
// .objectName(v.getVehicleLicense())
// .currentPosition(v.getLocation())
// .currentSpeed(v.getSpeed())
// .currentHeading(v.getHeading())
// .altitude(v.getAltitude())
// .build())
// .collect(Collectors.toList()));
// }
// 执行实时违规检测 (临时移除将在新的定时任务中统一处理)
// if (!unmannedVehicleLocations.isEmpty()) {
// realTimeViolationDetector.detectBatchViolations(unmannedVehicleLocations);
// }
log.info("无人车数据处理和事件发布完成,处理数量: {}", unmannedVehicles.size());
} catch (Exception e) {
log.error("采集无人车数据异常", e);
}
}
/**
* 将UnmannedVehicle转换为VehicleLocation对象
*
* @param vehicle 无人车对象
* @return VehicleLocation对象转换失败时返回null
* 定时执行路径冲突和实时违规检测
* 这个任务将按照websocketPushInterval配置的频率运行独立于数据采集频率
* 它会从activeMovingObjectsCache中获取所有最新的MovingObject数据并进行批量检测
*/
private com.qaup.collision.common.model.spatial.VehicleLocation convertToVehicleLocation(UnmannedVehicle vehicle) {
try {
String licensePlate = vehicle.getLicensePlate(); // 获取车牌号
MovingObjectType vehicleType = MovingObjectType.UNMANNED_VEHICLE;
double longitude = vehicle.getCurrentPosition().getLongitude();
double latitude = vehicle.getCurrentPosition().getLatitude();
Double altitude = vehicle.getCurrentPosition().getAltitude();
Double heading = vehicle.getHeading();
// 使用SpeedCalculationService计算的速度公里/小时与WebSocket推送保持一致
long currentTime = System.currentTimeMillis();
Double calculatedSpeed = speedCalculationService.calculateRealtimeSpeed(
vehicle.getLicensePlate(),
vehicle.getCurrentPosition().getLatitude(),
vehicle.getCurrentPosition().getLongitude(),
currentTime
);
// 如果没有计算出速度尝试从velocity获取并转换单位
Double speed = calculatedSpeed;
if (speed == null && vehicle.getVelocity() != null) {
// Velocity.getSpeed()返回米/需要转换为公里/小时
speed = vehicle.getVelocity().getSpeed() * 3.6;
}
// 使用VehicleLocationService创建VehicleLocation对象
return vehicleLocationService.createVehicleLocationByLicensePlate(
licensePlate, longitude, latitude,
altitude, heading, speed
);
} catch (Exception e) {
log.error("转换无人车数据失败: vehicleId={}", vehicle.getLicensePlate(), e);
return null;
@Scheduled(fixedRateString = "${data.collector.websocket.push-interval}")
public void performPeriodicViolationDetection() {
if (collectorDisabled) {
return;
}
if (activeMovingObjectsCache.isEmpty()) {
log.debug("活跃对象缓存为空,跳过周期性违规检测和冲突检测。");
return;
}
log.info("开始周期性违规检测和路径冲突检测,活跃对象数量: {}", activeMovingObjectsCache.size());
// 获取所有活跃的MovingObject的快照
List<MovingObject> currentActiveObjects = new ArrayList<>(activeMovingObjectsCache.values());
// 执行路径冲突检测
pathConflictDetectionService.detectPathConflicts(currentActiveObjects);
// 将MovingObject列表转换为VehicleLocation列表进行批量检测
List<VehicleLocation> detectionVehicleLocations = currentActiveObjects.stream()
.map(this::createTemporaryVehicleLocationForDetection)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 执行实时违规检测 (超速区域访问等)
if (!detectionVehicleLocations.isEmpty()) {
realTimeViolationDetector.detectBatchViolations(detectionVehicleLocations);
} else {
log.debug("没有可用于实时违规检测的车辆位置数据。");
}
log.info("周期性违规检测和路径冲突检测完成。");
}
/**
* 获取数据采集统计信息
*
* 提供数据采集的运行状态和统计信息
* 统计当前数据采集服务的健康状态和关键指标
* @return 统计信息字符串
*/
public String getCollectionStats() {
if (collectorDisabled) {
return "数据采集服务已禁用";
}
long totalAircraft = 0; // 由于不持久化这里只是示例
long totalVehicles = 0; // 同上
// 实际上可以统计已处理的事件数量等
StringBuilder stats = new StringBuilder();
stats.append("数据采集服务状态: 运行中\n");
stats.append("数据源配置:\n");
stats.append(" - 航空器API: ").append(airportBaseUrl).append(airportAircraftEndpoint).append(" (仅实时处理)\n");
stats.append(" - 机场车辆API: ").append(airportBaseUrl).append(airportVehicleEndpoint).append(" (仅实时处理)\n");
stats.append(" - 无人车API: 用于数据持久化和轨迹回放\n");
// 添加数据持久化统计信息
try {
String persistenceStats = vehicleDataPersistenceService.getPersistenceStatistics();
stats.append("\n").append(persistenceStats);
} catch (Exception e) {
stats.append("\n数据持久化统计获取失败: ").append(e.getMessage()).append("\n");
}
try {
// 获取数据库中的无人车数据统计只有无人车数据会存储
long unmannedCount = vehicleLocationService.getActiveVehiclesByType(MovingObjectType.UNMANNED_VEHICLE, 60).size();
stats.append("数据持久化统计:\n");
stats.append(" - 航空器: 仅实时处理,不存储\n");
stats.append(" - 机场车辆: 仅实时处理,不存储\n");
stats.append(" - 无人车: ").append(unmannedCount).append(" 条记录最近1小时\n");
} catch (Exception e) {
stats.append("获取统计信息失败: ").append(e.getMessage()).append("\n");
}
return stats.toString();
return String.format("DataCollectorService Stats: " +
"Total Aircraft Processed: %d, " +
"Total Airport Vehicles Processed: %d, " +
"Total Unmanned Vehicles Processed: %d",
totalAircraft, totalVehicles, vehicleLocationService.countAllUnmannedVehicles());
}
@PreDestroy
public void shutdown() {
log.info("正在关闭数据采集服务...");
// 重构说明移除内存缓存清理因为已经不使用内存存储
log.info("数据采集服务已关闭 - PostGIS存储模式");
log.info("DataCollectorService 正在关闭...");
// 清理资源如果需要
}
/**
* 检查是否应该推送WebSocket消息节流机制
* @param objectId 对象ID
* @param currentTime 当前时间
* @return 是否应该推送
* 检查是否应该推送WebSocket消息节流
*/
private boolean shouldPushWebSocketMessage(String objectId, long currentTime) {
Long lastPushTime = lastPushTimes.get(objectId);
if (lastPushTime == null || (currentTime - lastPushTime) >= websocketPushInterval) {
Long lastPushTime = lastPushTimes.getOrDefault(objectId, 0L);
if (currentTime - lastPushTime >= websocketPushInterval) {
lastPushTimes.put(objectId, currentTime);
return true;
}
return false;
}
/**
* UnmannedVehicle 转换为 VehicleLocation 实体用于持久化
*/
private com.qaup.collision.common.model.spatial.VehicleLocation convertToVehicleLocation(UnmannedVehicle vehicle) {
if (vehicle == null || vehicle.getObjectId() == null || vehicle.getObjectId().trim().isEmpty()) {
log.warn("无法转换无人车位置数据:对象或其 ID 为空或无效。对象: {}", vehicle);
return null;
}
try {
// 根据无人车的 objectId (车牌号/外部ID) 查询其在 sys_vehicle_info 表中的 Long 类型 vehicleId
Optional<SysVehicleInfo> vehicleInfoOptional = quapDataAdapter.findVehicleByLicensePlate(vehicle.getObjectId());
if (vehicleInfoOptional.isEmpty()) {
log.warn("未找到无人车 {} 对应的系统车辆信息,跳过持久化。这可能意味着该无人车未在系统中注册。", vehicle.getObjectId());
return null;
}
SysVehicleInfo sysVehicleInfo = vehicleInfoOptional.get();
return com.qaup.collision.common.model.spatial.VehicleLocation.builder()
.vehicleId(sysVehicleInfo.getVehicleId()) // 使用从 sys_vehicle_info 获取的 Long 类型 vehicleId
.location(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(vehicle.getCurrentPosition().getX(), vehicle.getCurrentPosition().getY())))
.altitude(vehicle.getAltitude())
.speed(vehicle.getCurrentSpeed())
.heading(vehicle.getCurrentHeading())
.timestamp(java.time.LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, java.time.ZoneOffset.UTC))
.licensePlate(sysVehicleInfo.getLicensePlate()) // 设置运行时属性 licensePlate
.vehicleType(com.qaup.collision.common.model.MovingObjectType.UNMANNED_VEHICLE) // 从无人车接口获取的数据直接设置为无人车类型
.build();
} catch (Exception e) {
log.error("转换无人车位置数据时发生未知错误: objectId={}", vehicle.getObjectId(), e);
return null;
}
}
/**
* 创建临时VehicleLocation用于违规检测航空器和机场车辆
* 对于不持久化的MovingObject生成一个临时的VehicleLocation用于RealTimeViolationDetector
*/
private com.qaup.collision.common.model.spatial.VehicleLocation createTemporaryVehicleLocationForDetection(MovingObject movingObject) {
if (movingObject == null || movingObject.getObjectId() == null || movingObject.getObjectId().trim().isEmpty()) {
return null;
}
try {
Long vehicleIdForDetection;
String licensePlateForDetection;
com.qaup.collision.common.model.MovingObjectType objectTypeForDetection = com.qaup.collision.common.model.MovingObjectType.valueOf(movingObject.getObjectType().name());
if (MovingObject.ObjectType.UNMANNED_VEHICLE.equals(movingObject.getObjectType())) {
// 对于无人车其objectId就是车牌号可以从sys_vehicle_info中查到vehicleId
Optional<SysVehicleInfo> vehicleInfoOptional = quapDataAdapter.findVehicleByLicensePlate(movingObject.getObjectId());
if (vehicleInfoOptional.isEmpty()) {
log.warn("未找到无人车 {} 对应的系统车辆信息,跳过违规检测。这可能意味着该无人车未在系统中注册。", movingObject.getObjectId());
return null;
}
SysVehicleInfo sysVehicleInfo = vehicleInfoOptional.get();
vehicleIdForDetection = sysVehicleInfo.getVehicleId();
licensePlateForDetection = sysVehicleInfo.getLicensePlate();
} else {
// 对于航空器和机场车辆包括特勤车其objectId是flightNo或vehicleNo
// 生成一个临时的一致的Long类型ID用于内部检测和日志记录
vehicleIdForDetection = (long) movingObject.getObjectId().hashCode(); // 使用hashCode生成一个伪ID
licensePlateForDetection = movingObject.getObjectId(); // 用objectId作为车牌号
}
Point currentPosition = movingObject.getCurrentPosition();
if (currentPosition == null) {
log.warn("对象 {} 位置信息缺失,跳过违规检测。", movingObject.getObjectId());
return null;
}
log.debug("在createTemporaryVehicleLocationForDetection方法中准备创建VehicleLocationMovingObject的速度为: {}", 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;
}
}
}

View File

@ -1,13 +1,7 @@
package com.qaup.collision.dataprocessing.service;
import com.qaup.collision.common.model.GeoPosition;
import com.qaup.collision.common.model.MovementState;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.Velocity;
import org.springframework.stereotype.Service;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@ -29,10 +23,6 @@ public class SpeedCalculationService {
private static final double MAX_POSITION_JUMP = 100.0; //
private static final double MAX_TIME_JUMP = 5000.0; // 毫秒
/**
* 数据预处理
*/
private final ReentrantLock lock = new ReentrantLock();
/**
* 实时速度计算相关 - 历史位置信息缓存
@ -48,171 +38,6 @@ public class SpeedCalculationService {
@Value("${data.collector.websocket.push-interval:1000}")
private long websocketPushInterval;
public void preprocessData(List<MovingObject> dataList) {
if (dataList == null || dataList.isEmpty()) {
return;
}
lock.lock();
try {
for (MovingObject obj : dataList) {
// 如果历史状态队列为空使用当前状态初始化
if (obj.getStateHistory().isEmpty()) {
MovementState state = new MovementState();
state.setPosition(obj.getCurrentPosition());
state.setVelocity(obj.getVelocity());
state.setTimestamp(obj.getTimestamp());
// 默认设置数据质量为GOOD
state.setDataQuality(MovementState.DataQuality.GOOD);
obj.getStateHistory().add(state);
continue;
}
// 获取最近的历史状态
MovementState lastState = obj.getStateHistory().getFirst();
// 异常值检测
double distance = calculateDistance(obj.getVelocity(), lastState.getVelocity());
long timeDiff = obj.getTimestamp() - lastState.getTimestamp();
// 检查位置跳变
if (distance > MAX_POSITION_JUMP) {
obj.getStateHistory().getFirst().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
// state.setDataQuality(MovementState.DataQuality.SUSPICIOUS);
}
// 检查时间异常
if (timeDiff <= 0 || timeDiff > MAX_TIME_JUMP) {
obj.getStateHistory().getFirst().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
// state.setDataQuality(MovementState.DataQuality.SUSPICIOUS);
}
// 检查速度异常
double speed = distance / (timeDiff / 1000.0);
if (speed > obj.getMaxSpeed()) {
obj.getStateHistory().getFirst().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
// state.setDataQuality(MovementState.DataQuality.SUSPICIOUS);
}
// 只对质量良好的数据进行卡尔曼滤波
// if (object.getStateHistory().getLast().getDataQuality() == MovementState.DataQuality.GOOD) {
// applyKalmanFilter(object.getStateHistory().getLast(), object.getStateHistory());
// }
// object.getStateHistory().add(state);
}
}finally {
lock.unlock();
}
}
/**
* 异常值检测
*/
private boolean isDataAnomaly(MovingObject object, GeoPosition newPosition, long timestamp) {
if (object.getStateHistory().isEmpty()) {
return false;
}
MovementState lastState = object.getStateHistory().getLast();
// 检查位置跳变
double distance = calculateDistance(lastState.getVelocity(), object.getVelocity());
if (distance > MAX_POSITION_JUMP) {
return true;
}
// 检查时间异常
long timeDiff = timestamp - lastState.getTimestamp();
if (timeDiff <= 0 || timeDiff > MAX_TIME_JUMP) {
return true;
}
// 检查速度异常
double speed = distance / (timeDiff / 1000.0);
return speed > object.getMaxSpeed();
}
/**
* 卡尔曼滤波
*/
private void applyKalmanFilter(MovementState state, Deque<MovementState> history) {
// TODO: 实现卡尔曼滤波算法
// 状态向量[x, y, vx, vy]
// 观测向量[x, y]
}
/**
* 计算速度
*/
public void calculateSpeed(MovingObject object, MovementState newState) {
// Deque<MovementState> history = object.getStateHistory();
// if (history.isEmpty()) {
// return;
// }
//
// // 确定窗口大小
// int windowSize = determineWindowSize(object);
//
// // 计算速度
// Velocity velocity = calculateVelocityComponents(object, object.getStateHistory().getLast());
//
// // 应用加速度约束
// applyAccelerationConstraints(velocity, object);
//
// newState.setVelocity(velocity);
}
/**
* 从滑动窗口计算速度
*/
private void calculateVelocityComponents(MovingObject object, MovementState lastState) {
Velocity velocity = object.getVelocity();
long currentTime = object.getTimestamp();
long lastTime = lastState.getTimestamp();
long deltaTime = currentTime - lastTime;
if (deltaTime <= 0) {
object.getStateHistory().getLast().setDataQuality(MovementState.DataQuality.SUSPICIOUS);
return;
}
double deltaX = object.getVelocity().getX() - lastState.getVelocity().getX();
double deltaY = object.getVelocity().getY() - lastState.getVelocity().getY();
double deltaZ = object.getVelocity().getZ() - lastState.getVelocity().getZ();
object.getVelocity().setVx(deltaX / (deltaTime / 1000.0));
object.getVelocity().setVy(deltaY / (deltaTime / 1000.0));
object.getVelocity().setVz(deltaZ / (deltaTime / 1000.0));
// 缓存瞬时加速度
double lastSpeed = Math.sqrt(
Math.pow(lastState.getVelocity().getVx(), 2) +
Math.pow(lastState.getVelocity().getVy(), 2) +
Math.pow(lastState.getVelocity().getVz(), 2)
);
double currentSpeed = Math.sqrt(Math.pow(velocity.getVx(), 2) + Math.pow(velocity.getVy(), 2));
object.getVelocity().setCachedAcceleration((currentSpeed - lastSpeed) / (deltaTime / 1000.0));
}
/**
* 计算两个向量间的距离
* @param currentVelocity 当前向量
* @param historicalVelocity 历史向量
* @return 距离
*/
private double calculateDistance(Velocity currentVelocity, Velocity historicalVelocity) {
double deltaX = currentVelocity.getX() - historicalVelocity.getX();
double deltaY = currentVelocity.getY() - historicalVelocity.getY();
double deltaZ = currentVelocity.getZ() - historicalVelocity.getZ();
return Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ);
}
// ==================== 实时速度计算功能 ====================
@ -322,41 +147,6 @@ public class SpeedCalculationService {
return roundedDirection;
}
/**
* 创建带有计算速度的Velocity对象用于复杂分析
* @param objectId 对象ID
* @param latitude 当前纬度
* @param longitude 当前经度
* @param timestamp 当前时间戳毫秒
* @param heading 航向角可为null
* @return Velocity对象如果无法计算速度则返回null
*/
public Velocity createRealtimeVelocity(String objectId, double latitude, double longitude, long timestamp, Double heading) {
Double speed = calculateRealtimeSpeed(objectId, latitude, longitude, timestamp);
if (speed == null) {
return null;
}
Velocity velocity = new Velocity();
// 如果有航向角计算速度分量
if (heading != null) {
double headingRad = Math.toRadians(heading);
velocity.setVx(speed * Math.cos(headingRad)); // 东向速度
velocity.setVy(speed * Math.sin(headingRad)); // 北向速度
} else {
// 没有航向角时假设都是东向移动
velocity.setVx(speed);
velocity.setVy(0);
}
velocity.setVz(0); // 垂直速度为0
velocity.setConfidence(0.8); // 设置置信度
return velocity;
}
/**
* 获取对象的当前速度不更新历史记录
* @param objectId 对象ID

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,42 @@
package com.qaup.collision.pathconflict.repository;
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 冲突预警告警记录Repository
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Repository
public interface ConflictAlertLogRepository extends JpaRepository<ConflictAlertLog, Long> {
/**
* 根据冲突ID查找告警记录
*/
List<ConflictAlertLog> findByConflictId(String conflictId);
/**
* 根据告警类型和级别查找告警记录
*/
List<ConflictAlertLog> findByAlertTypeAndAlertLevel(
ConflictAlertLog.AlertType alertType,
ConflictAlertLog.AlertLevel alertLevel
);
/**
* 查找最新的一条告警记录
*/
Optional<ConflictAlertLog> findFirstByOrderByAlertTimeDesc();
/**
* 查找某个冲突ID下最新的告警记录
*/
Optional<ConflictAlertLog> findFirstByConflictIdOrderByAlertTimeDesc(String conflictId);
}

View File

@ -0,0 +1,37 @@
package com.qaup.collision.pathconflict.repository;
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 实时对象路径分配Repository
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Repository
public interface ObjectRouteAssignmentRepository extends JpaRepository<ObjectRouteAssignment, Long> {
/**
* 根据对象ID和对象类型查找最新的路径分配记录
*/
Optional<ObjectRouteAssignment> findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc(
String objectId,
ObjectRouteAssignment.ObjectType objectType
);
/**
* 根据路径ID查找所有分配到该路径的对象
*/
List<ObjectRouteAssignment> findByAssignedRouteId(String assignedRouteId);
/**
* 根据对象ID查找所有分配到该对象的路径记录
*/
List<ObjectRouteAssignment> findByObjectId(String objectId);
}

View File

@ -0,0 +1,65 @@
package com.qaup.collision.pathconflict.repository;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 交通路径Repository
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Repository
public interface TransportRouteRepository extends JpaRepository<TransportRoute, Long> {
/**
* 根据路径ID查找路径
*/
Optional<TransportRoute> findByRouteId(String routeId);
/**
* 根据路径类型和状态查找路径
*/
List<TransportRoute> findByRouteTypeAndStatus(
TransportRoute.RouteType routeType,
TransportRoute.RouteStatus status);
/**
* 查找指定范围内的路径
*/
@Query(value = "SELECT * FROM transport_routes r WHERE " +
"ST_DWithin(r.route_geometry, ST_GeomFromText(:point, 4326), :radiusMeters) " +
"AND r.status = :status", nativeQuery = true)
List<TransportRoute> findRoutesWithinRadius(
@Param("point") String pointWKT,
@Param("radiusMeters") double radiusMeters,
@Param("status") String status);
/**
* 查找与指定路径相交的路径
*/
@Query(value = "SELECT r2.* FROM transport_routes r1, transport_routes r2 WHERE " +
"r1.route_id = :routeId AND r2.route_id != :routeId " +
"AND ST_Intersects(r1.route_geometry, r2.route_geometry) " +
"AND r2.status = 'ACTIVE'", nativeQuery = true)
List<TransportRoute> findIntersectingRoutes(@Param("routeId") String routeId);
/**
* 查找最近的路径
*/
@Query(value = "SELECT *, ST_Distance(route_geometry, ST_GeomFromText(:point, 4326)) as distance " +
"FROM transport_routes WHERE route_type = :routeType AND status = :status " +
"ORDER BY distance LIMIT :limit", nativeQuery = true)
List<TransportRoute> findNearestRoutes(
@Param("point") String pointWKT,
@Param("routeType") String routeType,
@Param("status") String status,
@Param("limit") int limit);
}

View File

@ -0,0 +1,415 @@
package com.qaup.collision.pathconflict.service;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.operation.distance.DistanceOp;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.context.ApplicationEventPublisher;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 路径冲突检测服务
* 基于路径速度和距离的冲突检测算法
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PathConflictDetectionService {
private final TransportRouteRepository routeRepository;
private final ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
private final ConflictAlertLogRepository conflictAlertLogRepository;
private final ApplicationEventPublisher eventPublisher;
// 预警距离阈值
private static final double WARNING_DISTANCE_THRESHOLD = 200.0;
// 告警距离阈值
private static final double ALERT_DISTANCE_THRESHOLD = 100.0;
// 最大检测时间范围
private static final int MAX_PREDICTION_TIME_SECONDS = 300; // 5分钟
// 最小时间间隔阈值
private static final double MIN_TIME_GAP_SECONDS = 30.0;
/**
* 执行路径冲突检测
*
* @param activeObjects 当前活跃的移动对象无人车和航空器
*/
public void detectPathConflicts(List<MovingObject> activeObjects) {
log.debug("开始路径冲突检测,活跃对象数量: {}", activeObjects.size());
List<ConflictAlertEvent> detectedAlertEvents = new ArrayList<>();
// 生成所有对象组合
for (int i = 0; i < activeObjects.size(); i++) {
for (int j = i + 1; j < activeObjects.size(); j++) {
MovingObject obj1 = activeObjects.get(i);
MovingObject obj2 = activeObjects.get(j);
// 检测两个对象之间的潜在冲突
Optional<ConflictAlertEvent> alertEventOptional = detectConflictBetweenObjects(obj1, obj2);
alertEventOptional.ifPresent(detectedAlertEvents::add);
}
}
// 发布告警事件
for (ConflictAlertEvent event : detectedAlertEvents) {
eventPublisher.publishEvent(event);
log.info("发布冲突告警事件: conflictId={}, alertType={}, alertLevel={}",
event.getConflictId(), event.getAlertType(), event.getAlertLevel());
}
log.info("路径冲突检测完成,检测到并发布 {} 个潜在冲突告警事件", detectedAlertEvents.size());
}
/**
* 检测两个对象之间的路径冲突
*/
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
try {
// 获取对象的路径
TransportRoute route1 = getObjectRoute(obj1);
TransportRoute route2 = getObjectRoute(obj2);
if (route1 == null || route2 == null) {
log.debug("对象 {} 或 {} 没有分配路径,跳过冲突检测", obj1.getObjectId(), obj2.getObjectId());
return Optional.empty();
}
// 计算路径交点
List<Point> intersectionPoints = calculateRouteIntersections(route1, route2);
for (Point intersectionPoint : intersectionPoints) {
// 计算每个对象到交点的距离和时间
ConflictCalculationResult result = calculateConflictDetails(obj1, obj2, route1, route2, intersectionPoint);
if (result != null && isSignificantConflict(result)) {
String conflictId = generateConflictId(obj1.getObjectId(), obj2.getObjectId());
String description = generateConflictDescription(obj1, obj2, route1, route2);
ConflictAlertLog alertLog = ConflictAlertLog.builder()
.conflictId(conflictId)
.alertType(result.getAlertType())
.alertLevel(result.getAlertLevel())
.alertMessage(description)
.object1Distance(result.getDistance1())
.object2Distance(result.getDistance2())
.minimumDistance(Math.min(result.getDistance1(), result.getDistance2())) // 简化处理可根据需要调整
.build();
conflictAlertLogRepository.save(alertLog);
log.info("保存冲突告警日志: {}", alertLog);
return Optional.of(ConflictAlertEvent.builder()
.conflictId(conflictId)
.alertType(result.getAlertType())
.alertLevel(result.getAlertLevel())
.message(description)
.object1Id(obj1.getObjectId())
.object2Id(obj2.getObjectId())
.conflictPoint(intersectionPoint)
.object1Distance(result.getDistance1())
.object2Distance(result.getDistance2())
.estimatedTimeToConflictObj1(result.getTimeToConflict1())
.estimatedTimeToConflictObj2(result.getTimeToConflict2())
.timeGapSeconds(result.getTimeGap())
.build());
}
}
} catch (Exception e) {
log.error("检测对象 {} 和 {} 之间的冲突时发生异常", obj1.getObjectId(), obj2.getObjectId(), e);
}
return Optional.empty();
}
/**
* 获取对象当前使用的路径
*/
private TransportRoute getObjectRoute(MovingObject obj) {
Optional<ObjectRouteAssignment> assignmentOptional = objectRouteAssignmentRepository.findFirstByObjectIdAndObjectTypeOrderByAssignedAtDesc(
obj.getObjectId(),
ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name())
);
return assignmentOptional.map(assignment -> routeRepository.findByRouteId(assignment.getAssignedRouteId()).orElse(null))
.orElse(null);
}
/**
* 查找距离指定位置最近的路径 (此方法可能不再需要因为我们现在通过分配表获取路径)
* 或者可以用于处理未分配路径的对象但当前逻辑主要依赖于已分配路径
*/
private TransportRoute findNearestRoute(Point position, List<TransportRoute> routes) {
TransportRoute nearestRoute = null;
double minDistance = Double.MAX_VALUE;
for (TransportRoute route : routes) {
try {
double distance = position.distance(route.getRouteGeometry());
if (distance < minDistance) {
minDistance = distance;
nearestRoute = route;
}
} catch (Exception e) {
log.warn("计算到路径 {} 的距离时出错", route.getRouteId(), e);
}
}
return nearestRoute;
}
/**
* 计算两条路径的交点
*/
private List<Point> calculateRouteIntersections(TransportRoute route1, TransportRoute route2) {
List<Point> intersections = new ArrayList<>();
try {
LineString line1 = route1.getRouteGeometry();
LineString line2 = route2.getRouteGeometry();
Geometry intersection = line1.intersection(line2);
if (intersection instanceof Point) {
intersections.add((Point) intersection);
} else if (intersection instanceof MultiPoint) {
MultiPoint multiPoint = (MultiPoint) intersection;
for (int i = 0; i < multiPoint.getNumGeometries(); i++) {
intersections.add((Point) multiPoint.getGeometryN(i));
}
} else if (intersection instanceof LineString) {
// 如果是线段重叠取线段的中点
LineString overlapLine = (LineString) intersection;
Point midPoint = overlapLine.getInteriorPoint();
intersections.add(midPoint);
}
} catch (Exception e) {
log.error("计算路径 {} 和 {} 的交点时出错", route1.getRouteId(), route2.getRouteId(), e);
}
return intersections;
}
/**
* 计算冲突详细信息
*/
private ConflictCalculationResult calculateConflictDetails(
MovingObject obj1, MovingObject obj2,
TransportRoute route1, TransportRoute route2,
Point intersectionPoint) {
try {
// 计算对象到交点的距离
double distance1 = calculateDistanceAlongRoute(obj1.getCurrentPosition(), intersectionPoint, route1);
double distance2 = calculateDistanceAlongRoute(obj2.getCurrentPosition(), intersectionPoint, route2);
// 计算到达交点的时间基于当前速度
double speed1 = Math.max(obj1.getCurrentSpeed(), 1.0); // 避免除零最小速度1 km/h
double speed2 = Math.max(obj2.getCurrentSpeed(), 1.0);
// 转换为m/s
double speed1_ms = speed1 * 1000.0 / 3600.0;
double speed2_ms = speed2 * 1000.0 / 3600.0;
int timeToConflict1 = (int) (distance1 / speed1_ms);
int timeToConflict2 = (int) (distance2 / speed2_ms);
// 计算时间差
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
// 评估冲突告警级别和类型
ConflictAlertLog.AlertLevel alertLevel = evaluateAlertLevel(distance1, distance2, timeGap);
ConflictAlertLog.AlertType alertType = evaluateAlertType(alertLevel);
return new ConflictCalculationResult(
distance1, distance2, timeToConflict1, timeToConflict2, timeGap, alertType, alertLevel);
} catch (Exception e) {
log.error("计算冲突详细信息时出错", e);
return null;
}
}
/**
* 评估冲突的告警级别
*/
private ConflictAlertLog.AlertLevel evaluateAlertLevel(
double distance1, double distance2, double timeGap) {
double minDistance = Math.min(distance1, distance2);
if (minDistance <= ALERT_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS) {
return ConflictAlertLog.AlertLevel.CRITICAL;
} else if (minDistance <= WARNING_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
return ConflictAlertLog.AlertLevel.WARNING;
} else if (minDistance <= WARNING_DISTANCE_THRESHOLD * 2 && timeGap <= MAX_PREDICTION_TIME_SECONDS) {
return ConflictAlertLog.AlertLevel.INFO;
} else {
return ConflictAlertLog.AlertLevel.INFO; // 默认信息级别
}
}
/**
* 根据告警级别评估告警类型
*/
private ConflictAlertLog.AlertType evaluateAlertType(ConflictAlertLog.AlertLevel level) {
switch (level) {
case CRITICAL:
case EMERGENCY:
return ConflictAlertLog.AlertType.ALERT; // 达到告警级别
case WARNING:
return ConflictAlertLog.AlertType.WARNING; // 预警级别
case INFO:
default:
return ConflictAlertLog.AlertType.WARNING; // 默认预警
}
}
/**
* 判断是否为显著冲突
*/
private boolean isSignificantConflict(ConflictCalculationResult result) {
return result.getAlertType() == ConflictAlertLog.AlertType.ALERT ||
result.getAlertType() == ConflictAlertLog.AlertType.WARNING;
}
/**
* 计算沿路径的距离
*/
private double calculateDistanceAlongRoute(Point currentPos, Point targetPos, TransportRoute route) {
try {
LineString routeLine = route.getRouteGeometry();
// 找到当前位置在路径上的最近点
DistanceOp distOp1 = new DistanceOp(currentPos, routeLine);
Coordinate[] nearestPoints1 = distOp1.nearestPoints();
Point nearestOnRoute1 = currentPos.getFactory().createPoint(nearestPoints1[1]);
// 找到目标位置在路径上的最近点
DistanceOp distOp2 = new DistanceOp(targetPos, routeLine);
Coordinate[] nearestPoints2 = distOp2.nearestPoints();
Point nearestOnRoute2 = currentPos.getFactory().createPoint(nearestPoints2[1]);
// 计算沿路径的距离
double distanceToCurrentOnRoute = getLinearLengthToPoint(routeLine, nearestOnRoute1);
double distanceToTargetOnRoute = getLinearLengthToPoint(routeLine, nearestOnRoute2);
return Math.abs(distanceToTargetOnRoute - distanceToCurrentOnRoute);
} catch (Exception e) {
log.error("计算沿路径距离时出错: currentPos={}, targetPos={}, routeId={}",
currentPos, targetPos, route.getRouteId(), e);
return -1.0; // 表示错误
}
}
/**
* 生成冲突ID
*/
private String generateConflictId(String obj1Id, String obj2Id) {
// 确保ID的生成是确定性的与对象的顺序无关
String[] ids = {obj1Id, obj2Id};
Arrays.sort(ids);
return "CONFLICT-" + ids[0] + "-" + ids[1] + "-" + UUID.randomUUID().toString().substring(0, 8);
}
/**
* 生成冲突描述
*/
private String generateConflictDescription(
MovingObject obj1, MovingObject obj2,
TransportRoute route1, TransportRoute route2) {
return String.format("对象 %s (%s) 在路径 %s 与对象 %s (%s) 在路径 %s 上可能发生冲突。",
obj1.getObjectId(), obj1.getObjectType(), route1.getRouteId(),
obj2.getObjectId(), obj2.getObjectType(), route2.getRouteId());
}
/**
* 计算沿LineString从起点到给定点的距离
* 假定point已经投影到line上
* 如果点不在line上结果可能不准确
*/
private double getLinearLengthToPoint(LineString line, Point point) {
double length = 0.0;
Coordinate[] coords = line.getCoordinates();
for (int i = 0; i < coords.length - 1; i++) {
Point p1 = line.getFactory().createPoint(coords[i]);
Point p2 = line.getFactory().createPoint(coords[i+1]);
// 如果点在线段 (p1, p2) 则计算到p1的距离然后加上点到p1的距离
if (point.equals(p1)) {
return length;
}
double segmentLength = p1.distance(p2);
// 检查点是否在当前线段上 (使用一个小的容差)
if (point.distance(p1) + point.distance(p2) - segmentLength < 1e-6) { // 容差检查
return length + p1.distance(point);
}
length += segmentLength;
}
// 如果点是最后一个点
if (point.equals(line.getEndPoint())) {
return line.getLength();
}
// 如果没有找到匹配返回-1或者抛异常这里返回线长表示无法准确计算
return -1.0; // 表示未找到精确位置或计算错误
}
/**
* 冲突计算结果内部类
*/
private static class ConflictCalculationResult {
private final double distance1;
private final double distance2;
private final int timeToConflict1;
private final int timeToConflict2;
private final double timeGap;
private final ConflictAlertLog.AlertType alertType;
private final ConflictAlertLog.AlertLevel alertLevel;
public ConflictCalculationResult(double distance1, double distance2,
int timeToConflict1, int timeToConflict2,
double timeGap,
ConflictAlertLog.AlertType alertType,
ConflictAlertLog.AlertLevel alertLevel) {
this.distance1 = distance1;
this.distance2 = distance2;
this.timeToConflict1 = timeToConflict1;
this.timeToConflict2 = timeToConflict2;
this.timeGap = timeGap;
this.alertType = alertType;
this.alertLevel = alertLevel;
}
public double getDistance1() { return distance1; }
public double getDistance2() { return distance2; }
public int getTimeToConflict1() { return timeToConflict1; }
public int getTimeToConflict2() { return timeToConflict2; }
public double getTimeGap() { return timeGap; }
public ConflictAlertLog.AlertType getAlertType() { return alertType; }
public ConflictAlertLog.AlertLevel getAlertLevel() { return alertLevel; }
}
}

View File

@ -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;
}
}

View File

@ -4,6 +4,7 @@ import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.geofence.model.enums.GeofenceAlertLevel;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.event.RuleViolationEventOccurred; // 导入RuleViolationEventOccurred
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.model.enums.ViolationType;
@ -18,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Point;
import org.springframework.stereotype.Service;
import org.springframework.context.event.EventListener; // 导入EventListener
import java.time.LocalDateTime;
import java.util.ArrayList;
@ -53,9 +55,18 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
private final AtomicLong totalDetections = new AtomicLong(0);
private final AtomicLong totalViolations = new AtomicLong(0);
private final AtomicLong totalProcessingTime = new AtomicLong(0);
// 用于统计本次批处理中实际检测到的违规数量
private final AtomicLong currentBatchViolationsCount = new AtomicLong(0);
// ========== 核心违规检测方法 ==========
@EventListener
public void handleRuleViolationEventOccurred(RuleViolationEventOccurred event) {
currentBatchViolationsCount.incrementAndGet();
log.debug("事件监听器收到 RuleViolationEventOccurred当前批次违规数: {}", currentBatchViolationsCount.get());
}
@Override
public List<RuleViolationEvent> detectViolations(VehicleLocation vehicleLocation) {
if (!isRealTimeDetectionEnabled()) {
@ -82,12 +93,12 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
}
// 检测违规
List<RuleViolationEvent> violations = new ArrayList<>();
// RuleViolationEvent将通过事件监听器处理和统计此处不再收集列表
Map<String, Object> vehicleState = buildVehicleStateMap(vehicleLocation);
for (SpatialRule rule : applicableRules) {
try {
RuleViolationEvent violation = ruleExecutionEngine.detectViolation(
ruleExecutionEngine.detectViolation(
rule,
String.valueOf(vehicleLocation.getVehicleId()), // 将Long转换为String
vehicleLocation.getVehicleType(),
@ -96,31 +107,26 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
vehicleLocation.getTimestamp()
);
if (violation != null) {
// 设置检测上下文信息
violations.add(violation);
log.info("检测到违规: vehicleId={}, ruleId={}, violationType={}",
vehicleLocation.getVehicleId(), rule.getRuleId(), violation.getViolationType());
}
// RuleViolationEventOccurred事件由 RuleExecutionEngineImpl 内部发布
// 并由本类的 @EventListener 方法负责计数和处理
// 因此此处不再需要 `violations.add(violation);`
} catch (Exception e) {
log.error("规则违规检测失败: vehicleId={}, ruleId={}",
vehicleLocation.getVehicleId(), rule.getRuleId(), e);
}
}
// 同步处理检测到的违规事件简化架构
if (!violations.isEmpty()) {
processBatchViolationEvents(violations);
}
// 违规事件将由 @EventListener 监听器自动处理这里不需要手动调用 processBatchViolationEvents
// processBatchViolationEvents(violations);
// 更新性能指标
updateMetrics(startTime, violations.size());
updateMetrics(startTime, 0); // 这里的violations.size()将不再有效但totalViolations由EventListener更新
log.debug("违规检测完成: vehicleId={}, 检测到{}个违规",
vehicleLocation.getVehicleId(), violations.size());
log.debug("违规检测完成: vehicleId={}",
vehicleLocation.getVehicleId());
return violations;
return new ArrayList<>(); // 返回空列表因为实际违规通过事件监听器处理
} catch (Exception e) {
log.error("实时违规检测失败: vehicleId={}, detectionId={}",
@ -136,14 +142,19 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
return new ArrayList<>();
}
// 在每次批量检测开始前重置计数器
currentBatchViolationsCount.set(0);
log.info("开始批量违规检测: 车辆数量={}", vehicleLocations.size());
List<RuleViolationEvent> allViolations = new ArrayList<>();
// 不再需要 allViolations 列表通过事件监听器进行统计
// List<RuleViolationEvent> allViolations = new ArrayList<>();
for (VehicleLocation vehicleLocation : vehicleLocations) {
try {
List<RuleViolationEvent> violations = detectViolations(vehicleLocation);
allViolations.addAll(violations);
// 调用 detectViolations它会发布 RuleViolationEventOccurred 事件
detectViolations(vehicleLocation);
// allViolations.addAll(violations); // 不再需要收集列表
} catch (Exception e) {
log.error("批量违规检测中单个车辆处理失败: vehicleId={}",
vehicleLocation.getVehicleId(), e);
@ -151,9 +162,10 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
}
log.info("批量违规检测完成: 处理车辆{}个,检测到违规{}个",
vehicleLocations.size(), allViolations.size());
vehicleLocations.size(), currentBatchViolationsCount.get()); // 使用监听器统计的违规数
log.debug("RealTimeViolationDetectorImpl - 批量检测结束,最终计数: {}", currentBatchViolationsCount.get());
return allViolations;
return new ArrayList<>(); // 返回空列表因为实际违规通过事件监听器处理
}
@Override

View File

@ -9,9 +9,13 @@ import com.qaup.collision.rule.model.enums.RuleExecutionResult;
import com.qaup.collision.websocket.event.RuleExecutionStatusWebSocketEvent;
import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
import com.qaup.collision.websocket.message.RuleStateChangePayload;
import com.qaup.collision.websocket.message.RuleViolationPayload;
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
import com.qaup.collision.common.adapter.QuapDataAdapter; // 导入 QuapDataAdapter
import com.qaup.system.domain.SysVehicleInfo; // 导入 SysVehicleInfo
import org.locationtech.jts.io.WKTWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -22,6 +26,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; // 导入 Optional
/**
* 规则事件WebSocket发布服务简化版
@ -37,10 +42,12 @@ public class RuleEventWebSocketPublisher {
private static final Logger logger = LoggerFactory.getLogger(RuleEventWebSocketPublisher.class);
private final ApplicationEventPublisher eventPublisher;
private final QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
// 构造函数中初始化ObjectMapper
public RuleEventWebSocketPublisher(ApplicationEventPublisher eventPublisher) {
public RuleEventWebSocketPublisher(ApplicationEventPublisher eventPublisher, QuapDataAdapter quapDataAdapter) {
this.eventPublisher = eventPublisher;
this.quapDataAdapter = quapDataAdapter;
}
/**
@ -292,6 +299,26 @@ public class RuleEventWebSocketPublisher {
return "LOW";
}
/**
* 发布路径冲突告警WebSocket消息
*
* @param message 路径冲突告警消息
*/
public void publishPathConflictAlert(PathConflictAlertMessage message) {
try {
// 创建路径冲突告警WebSocket事件
PathConflictAlertWebSocketEvent webSocketEvent = PathConflictAlertWebSocketEvent.create(message);
eventPublisher.publishEvent(webSocketEvent);
logger.info("Published path conflict alert WebSocket event: conflictId={}, alertType={}",
message.getConflictId(), message.getAlertType());
} catch (Exception e) {
logger.error("Failed to publish path conflict alert WebSocket event: conflictId={}",
message.getConflictId(), e);
}
}
/**
* 通过vehicle_id动态查询车牌号
*
@ -304,19 +331,16 @@ public class RuleEventWebSocketPublisher {
}
try {
// 这里可以注入相应的服务来查询车牌号
// 暂时使用硬编码的逻辑实际应该通过服务查询
// TODO: 注入 VehicleService QuapDataAdapter 来查询
// 简化实现根据vehicleId返回对应的车牌号
switch (vehicleId.intValue()) {
case 5: return "鲁B567";
case 1: return "京A123";
case 2: return "沪B456";
default: return "ID-" + vehicleId;
// 通过注入的QuapDataAdapter查询车辆信息
Optional<SysVehicleInfo> vehicleInfoOpt = quapDataAdapter.findVehicleById(vehicleId);
if (vehicleInfoOpt.isPresent()) {
return vehicleInfoOpt.get().getLicensePlate();
} else {
logger.warn("未找到vehicleId: {} 对应的车辆信息,使用默认车牌号。", vehicleId);
return "ID-" + vehicleId; // 如果未找到返回一个标识性的ID
}
} catch (Exception e) {
logger.warn("Failed to query license plate for vehicleId: {}, error: {}", vehicleId, e.getMessage());
logger.error("查询vehicleId: {} 对应的车牌号失败: {}", vehicleId, e.getMessage());
return "UNKNOWN";
}
}

View File

@ -6,6 +6,7 @@ import com.qaup.collision.websocket.event.PositionUpdateEvent;
import com.qaup.collision.websocket.event.RuleExecutionStatusWebSocketEvent;
import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
import com.qaup.collision.websocket.event.TrafficLightStatusEvent;
import com.qaup.collision.websocket.event.VehicleCommandEvent;
import com.qaup.collision.websocket.message.CollisionWarningPayload;
@ -17,6 +18,7 @@ import com.qaup.collision.websocket.message.RuleViolationPayload;
import com.qaup.collision.websocket.message.TrafficLightStatusPayload;
import com.qaup.collision.websocket.message.VehicleCommandPayload;
import com.qaup.collision.websocket.message.UniversalMessage;
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
import org.springframework.stereotype.Component;
@ -159,6 +161,22 @@ public class WebSocketMessageBroadcaster {
broadcastMessage(message);
}
/**
* 处理路径冲突告警事件
* @param event 路径冲突告警事件
*/
@EventListener
public void handlePathConflictAlert(PathConflictAlertWebSocketEvent event) {
UniversalMessage<PathConflictAlertMessage> message = UniversalMessage.<PathConflictAlertMessage>builder()
.type(MessageTypeConstants.PATH_CONFLICT_ALERT)
.timestamp(event.getTimestamp())
.messageId(generateMessageId())
.payload((PathConflictAlertMessage) event.getPayload())
.build();
broadcastMessage(message);
}
/**
* 统一消息广播方法 - 使用原生WebSocket发送JSON消息
* @param message 要广播的消息

View File

@ -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();
}
}

View File

@ -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("常量类不允许实例化");

View File

@ -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);
}
}

View File

@ -0,0 +1,147 @@
-- ============================================
-- 基于路径的冲突检测系统数据库表
-- 创建时间: 2025-01-17
-- 版本: 1.0
-- ============================================
-- 启用PostGIS扩展如果尚未启用
CREATE EXTENSION IF NOT EXISTS postgis;
-- ============================================
-- 1. 路径定义表 (transport_routes)
-- ============================================
CREATE TABLE IF NOT EXISTS transport_routes (
id BIGSERIAL PRIMARY KEY,
-- 路径基本信息
route_id VARCHAR(50) NOT NULL UNIQUE,
route_name VARCHAR(100) NOT NULL,
route_type VARCHAR(20) NOT NULL CHECK (route_type IN ('AIRCRAFT', 'UNMANNED_VEHICLE', 'SPECIAL_VEHICLE')),
-- 路径描述
description VARCHAR(500),
-- 空间路径 (LineString)
route_geometry GEOMETRY(LINESTRING, 4326) NOT NULL,
-- 路径属性
max_speed_kph DOUBLE PRECISION CHECK (max_speed_kph > 0),
typical_speed_kph DOUBLE PRECISION CHECK (typical_speed_kph > 0),
-- 使用条件
active_time_start TIME,
active_time_end TIME,
-- 状态控制
status VARCHAR(20) DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'MAINTENANCE')),
is_bidirectional BOOLEAN DEFAULT false,
-- 审计字段
created_by VARCHAR(50),
updated_by VARCHAR(50),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 路径表索引
CREATE INDEX idx_transport_routes_route_id ON transport_routes(route_id);
CREATE INDEX idx_transport_routes_type ON transport_routes(route_type);
CREATE INDEX idx_transport_routes_status ON transport_routes(status);
CREATE INDEX idx_transport_routes_geometry_gist ON transport_routes USING GIST(route_geometry);
-- ============================================
-- 4. 实时对象路径分配表 (object_route_assignments)
-- ============================================
CREATE TABLE IF NOT EXISTS object_route_assignments (
id BIGSERIAL PRIMARY KEY,
-- 对象信息
object_id VARCHAR(50) NOT NULL, -- 车辆ID或航空器标识
object_type VARCHAR(20) NOT NULL CHECK (object_type IN ('UNMANNED_VEHICLE', 'AIRCRAFT', 'SPECIAL_VEHICLE')),
object_name VARCHAR(100), -- 车牌号或航班号
-- 路径分配
assigned_route_id VARCHAR(50) NOT NULL,
-- 时间信息
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- 外键约束
FOREIGN KEY (assigned_route_id) REFERENCES transport_routes(route_id)
);
-- 对象路径分配表索引
CREATE INDEX idx_object_route_assignments_object ON object_route_assignments(object_id, object_type);
CREATE INDEX idx_object_route_assignments_route ON object_route_assignments(assigned_route_id);
CREATE INDEX idx_object_route_assignments_assigned_at ON object_route_assignments(assigned_at DESC);
-- ============================================
-- 5. 预警/告警记录表 (conflict_alert_logs)
-- ============================================
CREATE TABLE IF NOT EXISTS conflict_alert_logs (
id BIGSERIAL PRIMARY KEY,
-- 关联冲突 (这里假设冲突ID仍然由算法生成并关联即使path_conflict_detections表被删除)
conflict_id VARCHAR(50) NOT NULL, -- 冲突的唯一标识可能来自实时计算或是一个事件ID
-- 告警信息
alert_type VARCHAR(20) NOT NULL CHECK (alert_type IN ('WARNING', 'ALERT', 'EMERGENCY')),
alert_level VARCHAR(20) NOT NULL CHECK (alert_level IN ('INFO', 'WARNING', 'CRITICAL', 'EMERGENCY')),
alert_message TEXT NOT NULL,
-- 距离信息
object1_distance DOUBLE PRECISION, -- 对象1距离冲突点距离
object2_distance DOUBLE PRECISION, -- 对象2距离冲突点距离
minimum_distance DOUBLE PRECISION, -- 两对象之间最小距离
-- 时间信息
alert_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 告警记录表索引
CREATE INDEX idx_conflict_alerts_conflict_id ON conflict_alert_logs(conflict_id);
CREATE INDEX idx_conflict_alerts_type ON conflict_alert_logs(alert_type);
CREATE INDEX idx_conflict_alerts_level ON conflict_alert_logs(alert_level);
CREATE INDEX idx_conflict_alerts_time ON conflict_alert_logs(alert_time DESC);
-- ============================================
-- 6. 创建触发器自动更新时间戳
-- ============================================
CREATE OR REPLACE FUNCTION update_timestamp_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 为相关表创建触发器
CREATE TRIGGER trigger_transport_routes_updated_at
BEFORE UPDATE ON transport_routes
FOR EACH ROW
EXECUTE FUNCTION update_timestamp_column();
-- ============================================
-- 7. 添加表注释
-- ============================================
COMMENT ON TABLE transport_routes IS '交通路径定义表 - 存储航空器和无人车的预定路径';
COMMENT ON TABLE object_route_assignments IS '实时对象路径分配表 - 跟踪对象当前使用的路径';
COMMENT ON TABLE conflict_alert_logs IS '冲突预警告警记录表 - 记录发送的所有预警和告警消息';
-- ============================================
-- 8. 插入示例数据
-- ============================================
-- 示例航空器滑行路径
INSERT INTO transport_routes (route_id, route_name, route_type, description, route_geometry, max_speed_kph, typical_speed_kph) VALUES
('TAXI_01', '1号滑行道路径', 'AIRCRAFT', '从停机坪到跑道的标准滑行路径',
ST_GeomFromText('LINESTRING(120.084174 36.367162, 120.085 36.368, 120.086 36.369)', 4326),
30, 20),
('SERVICE_ROAD_A', 'A区服务道路', 'UNMANNED_VEHICLE', '无人车服务区域A的标准路径',
ST_GeomFromText('LINESTRING(120.084 36.367, 120.085 36.368, 120.086 36.369, 120.087 36.370)', 4326),
25, 15);
-- 示例路径分配
INSERT INTO object_route_assignments (object_id, object_type, object_name, assigned_route_id) VALUES
('UAV001', 'UNMANNED_VEHICLE', '无人车-001', 'SERVICE_ROAD_A'),
('AIRCRAFT001', 'AIRCRAFT', '航班CZ345', 'TAXI_01');