Update collision logging and route assignment handling
This commit is contained in:
parent
1cac6fb811
commit
43bf9488d6
24
AGENTS.md
24
AGENTS.md
@ -6,6 +6,30 @@
|
||||
这是开发环境,运行环境在centos7的容器里,分别为
|
||||
qaup-app
|
||||
qaup-redis
|
||||
qaup-postgis
|
||||
|
||||
### Docker/日志/数据库定位
|
||||
- 后端应用容器:`qaup-app`。
|
||||
- Redis 容器:`qaup-redis`。
|
||||
- PostGIS/PostgreSQL 容器:`qaup-postgis`。
|
||||
- 后端主日志优先看宿主机路径:`/home/project_20250804/offline-deploy/logs/app/sys-info.log`,历史滚动日志类似 `sys-info.2026-04-28.2.log`。
|
||||
- 容器 overlay 中也可能有同一份日志:`/var/lib/docker/overlay2/.../merged/logs/sys-info*.log`。
|
||||
- 碰撞测试独立日志在 qaup-app 容器内:`/logs/collision-tests/`。启动时主日志会打印:`Collision test session file log initialized: enabled=true, directory=/logs/collision-tests`。
|
||||
- 按车牌或航班查测试日志:`docker exec qaup-app sh -c 'grep -R -l "TEST003" /logs/collision-tests 2>/dev/null'`。
|
||||
- `/home/project_20250804/qaup/logs` 下看到的 `mock_traffic_light.log`、`mock_unmanned_vehicle.log` 是模拟器日志,不是 qaup-app 主应用日志。
|
||||
- qaup-app 容器内通常没有 `psql`,查库优先用:`docker exec qaup-postgis psql -U postgres -d qaup -c "SQL"`。
|
||||
- 已确认数据库环境变量示例:`DB_HOST=172.17.0.4`、`DB_PORT=5432`、`DB_USERNAME=postgres`、`DB_PASSWORD=123456`、`DB_NAME=qaup`。
|
||||
|
||||
### 碰撞测试流程排查要点
|
||||
- `/api/VehicleRegistry` 是全量覆盖注册,不是增量追加。
|
||||
- 测试会话日志关键字:`collision test session started`、`collision test session replaced`、`collision-diagnostic`。
|
||||
- 诊断日志中重点看:`collisionManagedObjects`、`pairsSupported`、`missingRoute`、`routeDeviation`、`speedTooLow`、`thresholdNotReached`、`eventsPublished`。
|
||||
- 如果 `pairsTotal=0`,说明参与碰撞检测的对象不足两个,先查注册对象和实时位置对象是否匹配。
|
||||
- 如果 `routeDeviation>0`,说明实时位置距离后端绑定路线过远,优先查本轮路线绑定是否正确。
|
||||
- 路径绑定表是 `object_route_assignments`,真实字段为:`id`、`object_type`、`object_name`、`assigned_route_id`、`assigned_at`。不要使用 `object_id`、`route_id`、`route_name` 这些不存在的字段。
|
||||
- 查对象路线绑定示例:`docker exec qaup-postgis psql -U postgres -d qaup -c "select object_type, object_name, assigned_route_id, assigned_at from object_route_assignments where object_name in ('MU2465','TEST003') order by assigned_at desc;"`
|
||||
- 如需看路线名称,连接 `transport_routes`:`docker exec qaup-postgis psql -U postgres -d qaup -c "select a.object_type, a.object_name, a.assigned_route_id, r.name, a.assigned_at from object_route_assignments a left join transport_routes r on r.id = a.assigned_route_id where a.object_name in ('MU2465','TEST003') order by a.assigned_at desc;"`
|
||||
- 历史上曾出现同一对象编号跨类型或旧路线残留导致误判,注册覆盖时需要同步清理旧路径绑定。
|
||||
|
||||
### 红绿灯接入说明
|
||||
- 正式环境中,qaup-app 的红绿灯接入方式为 MQTT,不是 HTTP。
|
||||
|
||||
@ -569,7 +569,7 @@ public class DataCollectorService {
|
||||
|
||||
upsertActiveMovingObject(movingObject);
|
||||
|
||||
log.debug("处理航空器数据并更新缓存: (航班号: {}, 位置: {}, {}, 速度: {})",
|
||||
log.trace("处理航空器数据并更新缓存: (航班号: {}, 位置: {}, {}, 速度: {})",
|
||||
aircraft.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
@ -580,7 +580,7 @@ public class DataCollectorService {
|
||||
}
|
||||
}
|
||||
|
||||
log.info("航空器数据处理和事件发布完成,处理数量: {}", deduplicatedAircrafts.size());
|
||||
log.trace("航空器数据处理和事件发布完成,处理数量: {}", deduplicatedAircrafts.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集航空器数据异常", e);
|
||||
@ -612,7 +612,7 @@ public class DataCollectorService {
|
||||
}
|
||||
|
||||
int originalCount = vehicles.size();
|
||||
log.info("采集到 {} 条机场车辆数据,开始过滤处理", originalCount);
|
||||
log.trace("采集到 {} 条机场车辆数据,开始过滤处理", originalCount);
|
||||
|
||||
// 应用车辆位置过滤器
|
||||
List<AirportVehicle> filteredVehicles = vehicleLocationFilter.filterVehicles(vehicles);
|
||||
@ -656,7 +656,7 @@ public class DataCollectorService {
|
||||
// 将最新数据更新到缓存(不发送WebSocket消息,统一在周期性检测中发送)
|
||||
upsertActiveMovingObject(movingObject);
|
||||
|
||||
log.debug("处理机场车辆数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
log.trace("处理机场车辆数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
vehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
@ -667,7 +667,7 @@ public class DataCollectorService {
|
||||
}
|
||||
}
|
||||
|
||||
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", filteredVehicles.size());
|
||||
log.trace("机场车辆数据处理和事件发布完成,处理数量: {}", filteredVehicles.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集机场车辆数据异常", e);
|
||||
@ -710,7 +710,7 @@ public class DataCollectorService {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("开始采集 {} 辆无人车的状态数据(HTTP轮询)", unmannedVehicleIds.size());
|
||||
log.trace("开始采集 {} 辆无人车的状态数据(HTTP轮询)", unmannedVehicleIds.size());
|
||||
int successCount = 0;
|
||||
|
||||
for (String vehicleId : unmannedVehicleIds) {
|
||||
@ -838,7 +838,7 @@ public class DataCollectorService {
|
||||
}
|
||||
successCount++;
|
||||
|
||||
log.debug("处理无人车完整状态数据并更新缓存: (车辆ID: {}, 位置: {}, {}, 任务ID: {}, 里程: {}米, 电量: {}%)",
|
||||
log.trace("处理无人车完整状态数据并更新缓存: (车辆ID: {}, 位置: {}, {}, 任务ID: {}, 里程: {}米, 电量: {}%)",
|
||||
vehicleId,
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
@ -851,7 +851,7 @@ public class DataCollectorService {
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("无人车状态数据采集完成,成功处理: {}/{}", successCount, unmannedVehicleIds.size());
|
||||
log.trace("无人车状态数据采集完成,成功处理: {}/{}", successCount, unmannedVehicleIds.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集无人车数据异常", e);
|
||||
|
||||
@ -201,7 +201,10 @@ public class RoutePersistenceService {
|
||||
|
||||
// 如果路由ID完全相同,则跳过重复记录
|
||||
if (existingRouteId.equals(routeId)) {
|
||||
log.info("路由分配未变更,跳过重复记录: objectName={}, routeType={}, routeId={}",
|
||||
ObjectRouteAssignment assignment = existingAssignment.get();
|
||||
assignment.setAssignedAt(LocalDateTime.now());
|
||||
objectRouteAssignmentRepository.save(assignment);
|
||||
log.info("路由分配未变更,已刷新分配时间: objectName={}, routeType={}, routeId={}",
|
||||
objectName, routeType, routeId);
|
||||
return;
|
||||
}
|
||||
@ -210,7 +213,11 @@ public class RoutePersistenceService {
|
||||
Optional<TransportRoute> existingRoute = transportRouteRepository.findById(existingRouteId);
|
||||
if (existingRoute.isPresent() &&
|
||||
existingRoute.get().getRouteName().equals(currentRouteName)) {
|
||||
log.info("路由名称相同,跳过重复记录: objectName={}, routeName={}, existingRouteId={}, currentRouteId={}",
|
||||
ObjectRouteAssignment assignment = existingAssignment.get();
|
||||
assignment.setAssignedRouteId(routeId);
|
||||
assignment.setAssignedAt(LocalDateTime.now());
|
||||
objectRouteAssignmentRepository.save(assignment);
|
||||
log.info("路由名称相同,已刷新分配记录: objectName={}, routeName={}, existingRouteId={}, currentRouteId={}",
|
||||
objectName, currentRouteName, existingRouteId, routeId);
|
||||
return;
|
||||
}
|
||||
@ -233,4 +240,4 @@ public class RoutePersistenceService {
|
||||
objectName, routeType, routeId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,6 +135,9 @@ public class RoutePreparationService {
|
||||
);
|
||||
|
||||
if (existingAssignment.isPresent() && Objects.equals(existingAssignment.get().getAssignedRouteId(), routeId)) {
|
||||
ObjectRouteAssignment assignment = existingAssignment.get();
|
||||
assignment.setAssignedAt(LocalDateTime.now());
|
||||
objectRouteAssignmentRepository.save(assignment);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,10 @@ import org.locationtech.jts.geom.Coordinate;
|
||||
public class AirportCoordinateSystem {
|
||||
private final CoordinateReferenceSystem sourceCRS;
|
||||
private final CoordinateReferenceSystem targetCRS;
|
||||
private final CoordinateReferenceSystem airportProjectedCRS;
|
||||
private final MathTransform transform;
|
||||
private final MathTransform airportProjectedTransform;
|
||||
private final MathTransform airportProjectedToWgs84Transform;
|
||||
// 本地坐标系原点
|
||||
private final double[] origin;
|
||||
|
||||
@ -19,9 +22,15 @@ public class AirportCoordinateSystem {
|
||||
this.sourceCRS = CRS.decode("EPSG:4326",true);
|
||||
String utmCode = calculateUtmZone(centerLon, centerLat);
|
||||
this.targetCRS = CRS.decode(utmCode);
|
||||
int gaussKrugerZone = (int) Math.round(centerLon / 3.0);
|
||||
double centralMeridian = gaussKrugerZone * 3.0;
|
||||
double falseEasting = gaussKrugerZone * 1_000_000.0 + 500_000.0;
|
||||
this.airportProjectedCRS = CRS.parseWKT(buildGaussKrugerWkt(centralMeridian, falseEasting));
|
||||
|
||||
// 创建转换器并计算原点
|
||||
this.transform = CRS.findMathTransform(sourceCRS, targetCRS);
|
||||
this.airportProjectedTransform = CRS.findMathTransform(sourceCRS, airportProjectedCRS);
|
||||
this.airportProjectedToWgs84Transform = CRS.findMathTransform(airportProjectedCRS, sourceCRS);
|
||||
this.origin = transformCoordinate(centerLon, centerLat);
|
||||
}
|
||||
|
||||
@ -34,6 +43,20 @@ public class AirportCoordinateSystem {
|
||||
};
|
||||
}
|
||||
|
||||
public double[] convertToAirportProjected(double lon, double lat) throws Exception {
|
||||
Coordinate source = new Coordinate(lon, lat);
|
||||
Coordinate target = new Coordinate();
|
||||
JTS.transform(source, target, airportProjectedTransform);
|
||||
return new double[]{target.x, target.y};
|
||||
}
|
||||
|
||||
public double[] convertAirportProjectedToWgs84(double x, double y) throws Exception {
|
||||
Coordinate source = new Coordinate(x, y);
|
||||
Coordinate target = new Coordinate();
|
||||
JTS.transform(source, target, airportProjectedToWgs84Transform);
|
||||
return new double[]{target.x, target.y};
|
||||
}
|
||||
|
||||
private double[] transformCoordinate(double lon, double lat) throws Exception {
|
||||
Coordinate source = new Coordinate(lon, lat);
|
||||
Coordinate target = new Coordinate();
|
||||
@ -46,4 +69,20 @@ public class AirportCoordinateSystem {
|
||||
// 北半球编码
|
||||
return "EPSG:326" + (zone < 10 ? "0" : "") + zone;
|
||||
}
|
||||
|
||||
private static String buildGaussKrugerWkt(double centralMeridian, double falseEasting) {
|
||||
return "PROJCS[\"Airport_Gauss_Kruger\","
|
||||
+ "GEOGCS[\"WGS 84\","
|
||||
+ "DATUM[\"WGS_1984\","
|
||||
+ "SPHEROID[\"WGS 84\",6378137,298.257223563]],"
|
||||
+ "PRIMEM[\"Greenwich\",0],"
|
||||
+ "UNIT[\"degree\",0.0174532925199433]],"
|
||||
+ "PROJECTION[\"Transverse_Mercator\"],"
|
||||
+ "PARAMETER[\"latitude_of_origin\",0],"
|
||||
+ "PARAMETER[\"central_meridian\"," + centralMeridian + "],"
|
||||
+ "PARAMETER[\"scale_factor\",1],"
|
||||
+ "PARAMETER[\"false_easting\"," + falseEasting + "],"
|
||||
+ "PARAMETER[\"false_northing\",0],"
|
||||
+ "UNIT[\"metre\",1]]";
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,4 +28,12 @@ public class CoordinateSystemService {
|
||||
public double[] convertToLocalCoordinate(double longitude, double latitude) throws Exception {
|
||||
return airportCoordinateSystem.convertToLocal(longitude, latitude);
|
||||
}
|
||||
|
||||
public double[] convertToAirportProjectedCoordinate(double longitude, double latitude) throws Exception {
|
||||
return airportCoordinateSystem.convertToAirportProjected(longitude, latitude);
|
||||
}
|
||||
|
||||
public double[] convertAirportProjectedToWgs84Coordinate(double x, double y) throws Exception {
|
||||
return airportCoordinateSystem.convertAirportProjectedToWgs84(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
|
||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||
import com.qaup.collision.datacollector.service.DataCollectorService;
|
||||
import com.qaup.collision.common.model.FlightNotification;
|
||||
import com.qaup.collision.service.CollisionTestSessionLogService;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.collision.websocket.event.FlightNotificationEvent;
|
||||
import com.qaup.common.core.redis.RedisCache;
|
||||
@ -101,6 +102,9 @@ public class DataProcessingService {
|
||||
@Autowired
|
||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private CollisionTestSessionLogService collisionTestSessionLogService;
|
||||
|
||||
// 从DataCollectorService获取缓存的引用
|
||||
private Map<String, MovingObject> activeMovingObjectsCache;
|
||||
private final Map<String, Long> lastPublishedPositionSampleTimestamps = new ConcurrentHashMap<>();
|
||||
@ -143,7 +147,7 @@ public class DataProcessingService {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始周期性数据处理,活跃对象数量: {}", activeMovingObjectsCache.size());
|
||||
log.trace("开始周期性数据处理,活跃对象数量: {}", activeMovingObjectsCache.size());
|
||||
|
||||
// 获取所有活跃对象的快照,避免处理线程回写覆盖采集线程最新状态
|
||||
List<MovingObject> currentActiveObjects = createProcessingSnapshot(activeMovingObjectsCache.values());
|
||||
@ -182,7 +186,7 @@ public class DataProcessingService {
|
||||
logCycleDiagnostics(currentActiveObjects.size(), collisionManagedObjects.size(),
|
||||
positionDispatchSummary, collisionManagedResult, conflictDetectionSummary);
|
||||
|
||||
log.info("周期性数据处理完成");
|
||||
log.trace("周期性数据处理完成");
|
||||
}
|
||||
|
||||
private CollisionManagedObjectsResult filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
||||
@ -247,7 +251,7 @@ public class DataProcessingService {
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
log.debug("开始为 {} 个对象计算速度和方向", activeObjects.size());
|
||||
log.trace("开始为 {} 个对象计算速度和方向", activeObjects.size());
|
||||
|
||||
for (MovingObject movingObject : activeObjects) {
|
||||
try {
|
||||
@ -269,7 +273,7 @@ public class DataProcessingService {
|
||||
movingObject.setCurrentSpeed(calculatedSpeed);
|
||||
movingObject.setCurrentHeading(calculatedDirection);
|
||||
|
||||
log.debug("对象 {} 计算完成: 速度={} km/h, 方向={}度",
|
||||
log.trace("对象 {} 计算完成: 速度={} km/h, 方向={}度",
|
||||
movingObject.getObjectId(), movingObject.getCurrentSpeed(), movingObject.getCurrentHeading());
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -277,7 +281,7 @@ public class DataProcessingService {
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("所有对象的速度和方向计算完成");
|
||||
log.trace("所有对象的速度和方向计算完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -285,12 +289,12 @@ public class DataProcessingService {
|
||||
*/
|
||||
private PositionDispatchSummary sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
|
||||
if (activeObjects.isEmpty()) {
|
||||
log.debug("没有活跃对象,跳过位置更新消息发送");
|
||||
log.trace("没有活跃对象,跳过位置更新消息发送");
|
||||
return new PositionDispatchSummary(0, 0, 0);
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
log.debug("发送位置更新消息,对象数量: {}", activeObjects.size());
|
||||
log.trace("发送位置更新消息,对象数量: {}", activeObjects.size());
|
||||
int publishedCount = 0;
|
||||
int duplicateSampleSkippedCount = 0;
|
||||
Set<String> currentObjectIds = new HashSet<>();
|
||||
@ -329,7 +333,7 @@ public class DataProcessingService {
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
publishedCount++;
|
||||
|
||||
log.debug("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
|
||||
log.trace("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
|
||||
movingObject.getObjectId(),
|
||||
movingObject.getObjectType(),
|
||||
movingObject.getCurrentPosition().getX(),
|
||||
@ -342,7 +346,7 @@ public class DataProcessingService {
|
||||
}
|
||||
|
||||
cleanupPublishedPositionState(currentObjectIds);
|
||||
log.info("位置更新消息发送完成,发送数量: {}, 跳过重复采样数量: {}", publishedCount, duplicateSampleSkippedCount);
|
||||
log.trace("位置更新消息发送完成,发送数量: {}, 跳过重复采样数量: {}", publishedCount, duplicateSampleSkippedCount);
|
||||
return new PositionDispatchSummary(activeObjects.size(), publishedCount, duplicateSampleSkippedCount);
|
||||
}
|
||||
|
||||
@ -378,7 +382,7 @@ public class DataProcessingService {
|
||||
log.info(
|
||||
"[test-session={}] [collision-diagnostic] activeObjects={}, collisionManagedObjects={}, positionPublished={}, "
|
||||
+ "positionDuplicateSkipped={}, registrationFiltered={}, typeOverrides={}, pairsTotal={}, pairsSupported={}, missingRoute={}, missingPosition={}, "
|
||||
+ "routeDeviation={}, speedTooLow={}, noIntersection={}, intersectionBehind={}, headingMismatch={}, "
|
||||
+ "routeDeviation={}, speedTooLow={}, noIntersection={}, intersectionBehind={}, headingMismatch={}, directionLockFailed={}, "
|
||||
+ "thresholdNotReached={}, detectionErrors={}, eventsPublished={}",
|
||||
testSessionId != null ? testSessionId : "none",
|
||||
activeObjectCount,
|
||||
@ -396,10 +400,36 @@ public class DataProcessingService {
|
||||
conflictDetectionSummary.noIntersectionPairs(),
|
||||
conflictDetectionSummary.intersectionBehindPairs(),
|
||||
conflictDetectionSummary.headingMismatchPairs(),
|
||||
conflictDetectionSummary.directionLockFailedPairs(),
|
||||
conflictDetectionSummary.thresholdNotReachedPairs(),
|
||||
conflictDetectionSummary.errorPairs(),
|
||||
conflictDetectionSummary.eventsPublished()
|
||||
);
|
||||
|
||||
if (collisionTestSessionLogService != null && testSessionId != null) {
|
||||
collisionTestSessionLogService.write(
|
||||
testSessionId,
|
||||
"CYCLE activeObjects=" + activeObjectCount
|
||||
+ ", collisionManagedObjects=" + collisionManagedObjectCount
|
||||
+ ", positionPublished=" + positionDispatchSummary.publishedCount()
|
||||
+ ", positionDuplicateSkipped=" + positionDispatchSummary.duplicateSampleSkippedCount()
|
||||
+ ", registrationFiltered=" + collisionManagedResult.registrationFilteredCount()
|
||||
+ ", typeOverrides=" + collisionManagedResult.typeOverrideCount()
|
||||
+ ", pairsTotal=" + conflictDetectionSummary.totalPairs()
|
||||
+ ", pairsSupported=" + conflictDetectionSummary.supportedPairs()
|
||||
+ ", missingRoute=" + conflictDetectionSummary.missingRoutePairs()
|
||||
+ ", missingPosition=" + conflictDetectionSummary.missingPositionPairs()
|
||||
+ ", routeDeviation=" + conflictDetectionSummary.routeDeviationPairs()
|
||||
+ ", speedTooLow=" + conflictDetectionSummary.speedTooLowPairs()
|
||||
+ ", noIntersection=" + conflictDetectionSummary.noIntersectionPairs()
|
||||
+ ", intersectionBehind=" + conflictDetectionSummary.intersectionBehindPairs()
|
||||
+ ", headingMismatch=" + conflictDetectionSummary.headingMismatchPairs()
|
||||
+ ", directionLockFailed=" + conflictDetectionSummary.directionLockFailedPairs()
|
||||
+ ", thresholdNotReached=" + conflictDetectionSummary.thresholdNotReachedPairs()
|
||||
+ ", detectionErrors=" + conflictDetectionSummary.errorPairs()
|
||||
+ ", eventsPublished=" + conflictDetectionSummary.eventsPublished()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private List<MovingObject> createProcessingSnapshot(java.util.Collection<MovingObject> sourceObjects) {
|
||||
@ -413,12 +443,38 @@ public class DataProcessingService {
|
||||
continue;
|
||||
}
|
||||
|
||||
Point copiedPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
|
||||
position.getX(), position.getY()));
|
||||
if (source instanceof UnmannedVehicle unmannedVehicle) {
|
||||
snapshot.add(UnmannedVehicle.builder()
|
||||
.objectId(source.getObjectId())
|
||||
.objectType(source.getObjectType())
|
||||
.objectName(source.getObjectName())
|
||||
.currentPosition(copiedPosition)
|
||||
.currentSpeed(source.getCurrentSpeed())
|
||||
.currentHeading(source.getCurrentHeading())
|
||||
.altitude(source.getAltitude())
|
||||
.sourceTimestampMs(source.getSourceTimestampMs())
|
||||
.batteryLevel(unmannedVehicle.getBatteryLevel())
|
||||
.vehicleStatus(unmannedVehicle.getVehicleStatus())
|
||||
.missionId(unmannedVehicle.getMissionId())
|
||||
.missionStatus(unmannedVehicle.getMissionStatus())
|
||||
.targetPosition(unmannedVehicle.getTargetPosition())
|
||||
.missionType(unmannedVehicle.getMissionType())
|
||||
.missionStartTime(unmannedVehicle.getMissionStartTime())
|
||||
.estimatedEndTime(unmannedVehicle.getEstimatedEndTime())
|
||||
.progress(unmannedVehicle.getProgress())
|
||||
.totalMileage(unmannedVehicle.getTotalMileage())
|
||||
.waypoints(unmannedVehicle.getWaypoints())
|
||||
.build());
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot.add(MovingObject.builder()
|
||||
.objectId(source.getObjectId())
|
||||
.objectType(source.getObjectType())
|
||||
.objectName(source.getObjectName())
|
||||
.currentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
|
||||
position.getX(), position.getY())))
|
||||
.currentPosition(copiedPosition)
|
||||
.currentSpeed(source.getCurrentSpeed())
|
||||
.currentHeading(source.getCurrentHeading())
|
||||
.altitude(source.getAltitude())
|
||||
|
||||
@ -50,6 +50,13 @@ public class ConflictAlertEventListener {
|
||||
|
||||
private void sendWebSocketAlert(ConflictAlertEvent event) {
|
||||
try {
|
||||
PathConflictAlertMessage.Position conflictPoint = event.getConflictPointLatitude() != null && event.getConflictPointLongitude() != null
|
||||
? PathConflictAlertMessage.Position.builder()
|
||||
.latitude(event.getConflictPointLatitude())
|
||||
.longitude(event.getConflictPointLongitude())
|
||||
.build()
|
||||
: null;
|
||||
|
||||
PathConflictAlertMessage message = PathConflictAlertMessage.builder()
|
||||
.conflictId(event.getConflictId().orElse(null))
|
||||
.alertType(event.getAlertType().orElse(null))
|
||||
@ -63,14 +70,19 @@ public class ConflictAlertEventListener {
|
||||
.objectName(event.getObject2Name())
|
||||
.objectType(event.getObject2Type().name())
|
||||
.build())
|
||||
.position(event.getConflictPointLatitude() != null && event.getConflictPointLongitude() != null
|
||||
? PathConflictAlertMessage.Position.builder()
|
||||
.latitude(event.getConflictPointLatitude())
|
||||
.longitude(event.getConflictPointLongitude())
|
||||
.build()
|
||||
: null)
|
||||
.object1Distance(new BigDecimal(event.getObject1Distance()).setScale(2, RoundingMode.HALF_UP).doubleValue())
|
||||
.object2Distance(new BigDecimal(event.getObject2Distance()).setScale(2, RoundingMode.HALF_UP).doubleValue())
|
||||
.position(conflictPoint)
|
||||
.conflictPoint(conflictPoint)
|
||||
.aircraftName(event.getAircraftName())
|
||||
.aircraftObjectType(event.getAircraftObjectType())
|
||||
.aircraftDistanceToConflictMeters(roundMeters(event.getAircraftDistanceToConflictMeters()))
|
||||
.aircraftAlertThresholdMeters(event.getAircraftAlertThresholdMeters())
|
||||
.vehicleName(event.getVehicleName())
|
||||
.vehicleObjectType(event.getVehicleObjectType())
|
||||
.vehicleDistanceToConflictMeters(roundMeters(event.getVehicleDistanceToConflictMeters()))
|
||||
.vehicleAlertThresholdMeters(event.getVehicleAlertThresholdMeters())
|
||||
.vehicleMovingTowardConflictPoint(event.getVehicleMovingTowardConflictPoint())
|
||||
.object1Distance(roundMeters(event.getObject1Distance()))
|
||||
.object2Distance(roundMeters(event.getObject2Distance()))
|
||||
.timeToConflict1(event.getEstimatedTimeToConflictObj1())
|
||||
.timeToConflict2(event.getEstimatedTimeToConflictObj2())
|
||||
.timeGap(event.getTimeGapSeconds())
|
||||
@ -83,4 +95,11 @@ public class ConflictAlertEventListener {
|
||||
log.error("Failed to send WebSocket path conflict alert: conflictId={}", event.getConflictId().map(String::valueOf).orElse(null), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Double roundMeters(Double value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,15 @@ public class ConflictAlertEvent {
|
||||
private Integer estimatedTimeToConflictObj2; // 对象2预计到达冲突点的时间 (秒)
|
||||
private Double timeGapSeconds; // 两个对象到达冲突点的时间差 (秒)
|
||||
private LocalDateTime eventTime; // 事件发生时间
|
||||
private String aircraftName;
|
||||
private String aircraftObjectType;
|
||||
private Double aircraftDistanceToConflictMeters;
|
||||
private Double aircraftAlertThresholdMeters;
|
||||
private String vehicleName;
|
||||
private String vehicleObjectType;
|
||||
private Double vehicleDistanceToConflictMeters;
|
||||
private Double vehicleAlertThresholdMeters;
|
||||
private Boolean vehicleMovingTowardConflictPoint;
|
||||
|
||||
// 辅助方法,用于判断告警级别
|
||||
public boolean isEmergencyAlert() {
|
||||
@ -52,4 +61,4 @@ public class ConflictAlertEvent {
|
||||
public Double getConflictPointLongitude() {
|
||||
return conflictPoint != null ? conflictPoint.getX() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.qaup.collision.pathconflict.repository;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@ -42,4 +43,13 @@ public interface ObjectRouteAssignmentRepository extends JpaRepository<ObjectRou
|
||||
@Param("objectName") String objectName,
|
||||
@Param("objectType") ObjectRouteAssignment.ObjectType objectType
|
||||
);
|
||||
}
|
||||
|
||||
@Modifying
|
||||
void deleteByObjectNameIn(List<String> objectNames);
|
||||
|
||||
@Modifying
|
||||
void deleteByObjectNameAndObjectTypeNot(
|
||||
String objectName,
|
||||
ObjectRouteAssignment.ObjectType objectType
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,11 +7,15 @@ import com.qaup.collision.datacollector.model.dto.ExternalVehicleCommandRequest;
|
||||
import com.qaup.collision.datacollector.model.enums.CommandReason;
|
||||
import com.qaup.collision.datacollector.model.enums.CommandType;
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.service.CollisionTestSessionLogService;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService.VehicleRegistryType;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@ -21,10 +25,12 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@ -36,10 +42,16 @@ public class VehicleCommandService {
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||
private final RuleEventWebSocketPublisher webSocketPublisher;
|
||||
|
||||
@Autowired(required = false)
|
||||
private CollisionTestSessionLogService collisionTestSessionLogService;
|
||||
|
||||
private final Map<String, ActiveVehicleCommandState> activeCommandStates = new ConcurrentHashMap<>();
|
||||
private final AtomicLong transIdSequence = new AtomicLong();
|
||||
private volatile String observedTestSessionId;
|
||||
private static final String UNIFIED_VEHICLE_COMMAND_URL = "http://10.232.18.23:8020/api/VehicleCommandInfo";
|
||||
private static final int RESUME_CONFIRMATION_CYCLES = 3;
|
||||
|
||||
@Value("${data.collector.vehicle-api.base-url:}")
|
||||
private String vehicleApiBaseUrl;
|
||||
@ -48,6 +60,15 @@ public class VehicleCommandService {
|
||||
private String vehicleCommandEndpoint;
|
||||
|
||||
public void synchronizeConflictCommands(List<ConflictAlertEvent> activeConflicts) {
|
||||
synchronizeConflictCommands(activeConflicts, List.of());
|
||||
}
|
||||
|
||||
public void synchronizeConflictCommands(
|
||||
List<ConflictAlertEvent> activeConflicts,
|
||||
List<ConflictRecoveryObservation> recoveryObservations) {
|
||||
|
||||
clearActiveCommandStatesOnTestSessionChange();
|
||||
|
||||
Map<String, VehicleConflictCommand> currentCommands = new LinkedHashMap<>();
|
||||
for (ConflictAlertEvent event : activeConflicts) {
|
||||
VehicleConflictCommand candidate = toVehicleConflictCommand(event);
|
||||
@ -62,6 +83,29 @@ public class VehicleCommandService {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, ConflictRecoveryObservation> observationsByVehicleId = new LinkedHashMap<>();
|
||||
if (recoveryObservations != null) {
|
||||
for (ConflictRecoveryObservation observation : recoveryObservations) {
|
||||
if (observation != null && observation.vehicleId() != null && !observation.vehicleId().isBlank()) {
|
||||
observationsByVehicleId.put(observation.vehicleId(), observation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String vehicleId : new ArrayList<>(currentCommands.keySet())) {
|
||||
ActiveVehicleCommandState previousState = activeCommandStates.get(vehicleId);
|
||||
ConflictRecoveryObservation observation = observationsByVehicleId.get(vehicleId);
|
||||
if (isRecoveryConfirmedAfterAircraftPassed(previousState, observation)) {
|
||||
writeTestSessionLog("EVENT PATH_CONFLICT_RELEASE_CONFIRMED"
|
||||
+ ", vehicleId=" + vehicleId
|
||||
+ ", aircraftPassedConflictPoint=true"
|
||||
+ ", aircraftDistanceMeters=" + observation.aircraftDistanceMeters()
|
||||
+ ", releaseThresholdMeters=" + platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()
|
||||
+ ", currentCommandSuppressed=true");
|
||||
currentCommands.remove(vehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
for (VehicleConflictCommand command : currentCommands.values()) {
|
||||
synchronizeVehicleCommand(command);
|
||||
}
|
||||
@ -69,11 +113,29 @@ public class VehicleCommandService {
|
||||
List<String> trackedVehicleIds = new ArrayList<>(activeCommandStates.keySet());
|
||||
for (String vehicleId : trackedVehicleIds) {
|
||||
if (!currentCommands.containsKey(vehicleId)) {
|
||||
sendResumeIfNeeded(vehicleId);
|
||||
sendResumeIfNeeded(vehicleId, observationsByVehicleId.get(vehicleId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearActiveCommandStatesOnTestSessionChange() {
|
||||
String currentTestSessionId = platformRuntimeStateService.getCurrentTestSessionId();
|
||||
if (Objects.equals(observedTestSessionId, currentTestSessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeCommandStates.isEmpty()) {
|
||||
log.info(
|
||||
"Clearing active vehicle command states for collision test session change: previousSession={}, currentSession={}, activeVehicles={}",
|
||||
observedTestSessionId,
|
||||
currentTestSessionId,
|
||||
activeCommandStates.keySet()
|
||||
);
|
||||
}
|
||||
activeCommandStates.clear();
|
||||
observedTestSessionId = currentTestSessionId;
|
||||
}
|
||||
|
||||
private void synchronizeVehicleCommand(VehicleConflictCommand currentCommand) {
|
||||
ActiveVehicleCommandState previousState = activeCommandStates.get(currentCommand.vehicleId());
|
||||
if (previousState == null) {
|
||||
@ -81,9 +143,9 @@ public class VehicleCommandService {
|
||||
return;
|
||||
}
|
||||
|
||||
// De-escalation is intentionally handled by a separate recovery flow. WARNING follow-up behavior is not implemented here yet.
|
||||
if (previousState.level() == VehicleRiskLevel.WARNING && currentCommand.level() == VehicleRiskLevel.ALERT) {
|
||||
issueAlertAndParking(currentCommand);
|
||||
if (currentCommand.level() == VehicleRiskLevel.WARNING) {
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_SKIP vehicleId=" + currentCommand.vehicleId()
|
||||
+ ", level=WARNING, reason=warning_not_sent_to_vehicle");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -95,17 +157,22 @@ public class VehicleCommandService {
|
||||
if (parkingIssued) {
|
||||
activeCommandStates.put(
|
||||
currentCommand.vehicleId(),
|
||||
new ActiveVehicleCommandState(VehicleRiskLevel.ALERT, true)
|
||||
ActiveVehicleCommandState.fromCommand(VehicleRiskLevel.ALERT, true, 0, currentCommand)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
activeCommandStates.put(
|
||||
currentCommand.vehicleId(),
|
||||
ActiveVehicleCommandState.fromCommand(previousState.level(), previousState.parkingIssued(), 0, currentCommand)
|
||||
);
|
||||
}
|
||||
|
||||
private void issueInitialRiskCommand(VehicleConflictCommand command) {
|
||||
if (command.level() == VehicleRiskLevel.WARNING) {
|
||||
if (sendCommand(command, CommandType.WARNING, CommandReason.AIRCRAFT_CROSSING)) {
|
||||
activeCommandStates.put(command.vehicleId(), new ActiveVehicleCommandState(VehicleRiskLevel.WARNING, true));
|
||||
}
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_SKIP vehicleId=" + command.vehicleId()
|
||||
+ ", level=WARNING, reason=warning_not_sent_to_vehicle");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -119,26 +186,58 @@ public class VehicleCommandService {
|
||||
}
|
||||
|
||||
boolean parkingIssued = sendCommand(command, CommandType.PARKING, CommandReason.AIRCRAFT_CROSSING);
|
||||
activeCommandStates.put(command.vehicleId(), new ActiveVehicleCommandState(VehicleRiskLevel.ALERT, parkingIssued));
|
||||
activeCommandStates.put(command.vehicleId(), ActiveVehicleCommandState.fromCommand(VehicleRiskLevel.ALERT, parkingIssued, 0, command));
|
||||
}
|
||||
|
||||
private void sendResumeIfNeeded(String vehicleId) {
|
||||
private void sendResumeIfNeeded(String vehicleId, ConflictRecoveryObservation observation) {
|
||||
ActiveVehicleCommandState previousState = activeCommandStates.get(vehicleId);
|
||||
if (previousState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecoveryConfirmedAfterAircraftPassed(previousState, observation)) {
|
||||
activeCommandStates.put(vehicleId, previousState.withObservation(observation, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
writeTestSessionLog("EVENT PATH_CONFLICT_RELEASE_CONFIRMED"
|
||||
+ ", vehicleId=" + vehicleId
|
||||
+ ", aircraftPassedConflictPoint=true"
|
||||
+ ", aircraftDistanceMeters=" + observation.aircraftDistanceMeters()
|
||||
+ ", releaseThresholdMeters=" + platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()
|
||||
+ ", confirmationCycle=" + (previousState.missingConflictCycles() + 1)
|
||||
+ ", requiredConfirmationCycles=" + RESUME_CONFIRMATION_CYCLES);
|
||||
|
||||
int confirmedCycles = previousState.missingConflictCycles() + 1;
|
||||
if (confirmedCycles < RESUME_CONFIRMATION_CYCLES) {
|
||||
activeCommandStates.put(vehicleId, previousState.withObservation(observation, confirmedCycles));
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveVehicleCommandState updatedState = new ActiveVehicleCommandState(
|
||||
previousState.level(),
|
||||
previousState.parkingIssued(),
|
||||
confirmedCycles,
|
||||
observation.vehicleDistanceMeters(),
|
||||
observation.aircraftDistanceMeters()
|
||||
);
|
||||
|
||||
VehicleConflictCommand resumeContext = new VehicleConflictCommand(
|
||||
vehicleId,
|
||||
previousState.level(),
|
||||
null,
|
||||
null,
|
||||
resolveRegistryType(vehicleId)
|
||||
resolveRegistryType(vehicleId),
|
||||
previousState.vehicleDistanceMeters(),
|
||||
previousState.aircraftDistanceMeters()
|
||||
);
|
||||
|
||||
boolean resumeIssued = sendCommand(resumeContext, CommandType.RESUME, CommandReason.RESUME_TRAFFIC);
|
||||
if (resumeIssued) {
|
||||
activeCommandStates.remove(vehicleId);
|
||||
publishResumeToFrontend(vehicleId, previousState, observation);
|
||||
} else {
|
||||
activeCommandStates.put(vehicleId, updatedState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +255,9 @@ public class VehicleCommandService {
|
||||
event.getObject1Name(),
|
||||
event.getObject1Type(),
|
||||
level,
|
||||
event.getConflictPoint()
|
||||
event.getConflictPoint(),
|
||||
event.getObject1Distance(),
|
||||
event.getObject2Distance()
|
||||
);
|
||||
if (leftCandidate != null) {
|
||||
return leftCandidate;
|
||||
@ -166,7 +267,9 @@ public class VehicleCommandService {
|
||||
event.getObject2Name(),
|
||||
event.getObject2Type(),
|
||||
level,
|
||||
event.getConflictPoint()
|
||||
event.getConflictPoint(),
|
||||
event.getObject2Distance(),
|
||||
event.getObject1Distance()
|
||||
);
|
||||
}
|
||||
|
||||
@ -174,7 +277,9 @@ public class VehicleCommandService {
|
||||
String vehicleId,
|
||||
MovingObjectType objectType,
|
||||
VehicleRiskLevel level,
|
||||
Point conflictPoint) {
|
||||
Point conflictPoint,
|
||||
Double candidateDistance,
|
||||
Double otherDistance) {
|
||||
|
||||
if (vehicleId == null || vehicleId.isBlank()) {
|
||||
return null;
|
||||
@ -197,7 +302,18 @@ public class VehicleCommandService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VehicleConflictCommand(vehicleId, level, conflictPoint, objectType, registryType);
|
||||
Double vehicleDistance = objectType == MovingObjectType.AIRCRAFT ? otherDistance : candidateDistance;
|
||||
Double aircraftDistance = objectType == MovingObjectType.AIRCRAFT ? candidateDistance : otherDistance;
|
||||
|
||||
return new VehicleConflictCommand(
|
||||
vehicleId,
|
||||
level,
|
||||
conflictPoint,
|
||||
objectType,
|
||||
registryType,
|
||||
vehicleDistance,
|
||||
aircraftDistance
|
||||
);
|
||||
}
|
||||
|
||||
private VehicleRiskLevel toRiskLevel(ConflictAlertEvent event) {
|
||||
@ -242,6 +358,9 @@ public class VehicleCommandService {
|
||||
);
|
||||
|
||||
if (response.getStatusCode().value() != 200) {
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_FAILED vehicleId=" + command.vehicleId()
|
||||
+ ", commandType=" + commandType
|
||||
+ ", reason=http_status_" + response.getStatusCode().value());
|
||||
log.warn(
|
||||
"Vehicle command rejected by HTTP status: vehicleId={}, commandType={}, status={}",
|
||||
command.vehicleId(),
|
||||
@ -253,6 +372,9 @@ public class VehicleCommandService {
|
||||
|
||||
JsonNode responseBody = parseResponseBody(response.getBody());
|
||||
if (responseBody == null || !responseBody.has("code") || !responseBody.has("msg")) {
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_FAILED vehicleId=" + command.vehicleId()
|
||||
+ ", commandType=" + commandType
|
||||
+ ", reason=invalid_response_body");
|
||||
log.warn(
|
||||
"Vehicle command response missing required fields: vehicleId={}, commandType={}, body={}",
|
||||
command.vehicleId(),
|
||||
@ -264,6 +386,9 @@ public class VehicleCommandService {
|
||||
|
||||
int responseCode = responseBody.path("code").asInt(Integer.MIN_VALUE);
|
||||
if (responseCode != 200) {
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_FAILED vehicleId=" + command.vehicleId()
|
||||
+ ", commandType=" + commandType
|
||||
+ ", reason=upstream_code_" + responseCode);
|
||||
log.warn(
|
||||
"Vehicle command rejected by upstream response code: vehicleId={}, commandType={}, code={}, msg={}",
|
||||
command.vehicleId(),
|
||||
@ -281,8 +406,18 @@ public class VehicleCommandService {
|
||||
commandType,
|
||||
commandReason
|
||||
);
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_SENT vehicleId=" + command.vehicleId()
|
||||
+ ", registryType=" + command.registryType()
|
||||
+ ", commandType=" + commandType
|
||||
+ ", reason=" + commandReason
|
||||
+ ", vehicleDistanceMeters=" + command.vehicleDistanceMeters()
|
||||
+ ", aircraftDistanceMeters=" + command.aircraftDistanceMeters());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
writeTestSessionLog("EVENT VEHICLE_COMMAND_FAILED vehicleId=" + command.vehicleId()
|
||||
+ ", commandType=" + commandType
|
||||
+ ", reason=" + e.getClass().getSimpleName()
|
||||
+ ", message=" + (e.getMessage() == null ? "" : e.getMessage()));
|
||||
log.error(
|
||||
"Failed to send vehicle command: vehicleId={}, commandType={}, reason={}",
|
||||
command.vehicleId(),
|
||||
@ -428,6 +563,68 @@ public class VehicleCommandService {
|
||||
return platformRuntimeStateService.getVehicleRegistryType(vehicleId);
|
||||
}
|
||||
|
||||
private boolean isRecoveryConfirmedAfterAircraftPassed(
|
||||
ActiveVehicleCommandState previousState,
|
||||
ConflictRecoveryObservation observation) {
|
||||
|
||||
if (previousState == null || observation == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return observation.aircraftIntersectionBehind()
|
||||
&& observation.aircraftDistanceMeters() > platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft();
|
||||
}
|
||||
|
||||
private void publishResumeToFrontend(
|
||||
String vehicleId,
|
||||
ActiveVehicleCommandState previousState,
|
||||
ConflictRecoveryObservation observation) {
|
||||
|
||||
try {
|
||||
PathConflictAlertMessage message = PathConflictAlertMessage.builder()
|
||||
.messageType("PATH_CONFLICT_RESUME")
|
||||
.message("Path conflict resolved, traffic resumed")
|
||||
.object1(PathConflictAlertMessage.ConflictObject.builder()
|
||||
.objectName(vehicleId)
|
||||
.objectType(toMovingObjectTypeName(resolveRegistryType(vehicleId)))
|
||||
.build())
|
||||
.object1Distance(observation != null ? observation.vehicleDistanceMeters() : previousState.vehicleDistanceMeters())
|
||||
.object2Distance(observation != null ? observation.aircraftDistanceMeters() : previousState.aircraftDistanceMeters())
|
||||
.eventTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
webSocketPublisher.publishPathConflictAlert(message);
|
||||
log.info("WebSocket path conflict resume sent: vehicleId={}", vehicleId);
|
||||
writeTestSessionLog("EVENT VEHICLE_RESUME_FRONTEND_SENT vehicleId=" + vehicleId
|
||||
+ ", vehicleDistanceMeters=" + (observation != null ? observation.vehicleDistanceMeters() : previousState.vehicleDistanceMeters())
|
||||
+ ", aircraftDistanceMeters=" + (observation != null ? observation.aircraftDistanceMeters() : previousState.aircraftDistanceMeters()));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send WebSocket path conflict resume: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTestSessionLog(String message) {
|
||||
if (collisionTestSessionLogService == null || platformRuntimeStateService == null) {
|
||||
return;
|
||||
}
|
||||
String testSessionId = platformRuntimeStateService.getCurrentTestSessionId();
|
||||
if (testSessionId != null) {
|
||||
collisionTestSessionLogService.write(testSessionId, message);
|
||||
}
|
||||
}
|
||||
|
||||
private String toMovingObjectTypeName(VehicleRegistryType registryType) {
|
||||
if (registryType == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (registryType) {
|
||||
case WUREN -> MovingObjectType.UNMANNED_VEHICLE.name();
|
||||
case TEQIN -> MovingObjectType.SPECIAL_VEHICLE.name();
|
||||
case HANGKONG -> MovingObjectType.AIRCRAFT.name();
|
||||
case PUTONG, JIUYUAN -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private String nextTransId() {
|
||||
long serialValue = System.currentTimeMillis() * 1000 + Math.floorMod(transIdSequence.incrementAndGet(), 1000);
|
||||
return Long.toHexString(serialValue);
|
||||
@ -438,12 +635,58 @@ public class VehicleCommandService {
|
||||
VehicleRiskLevel level,
|
||||
Point conflictPoint,
|
||||
MovingObjectType objectType,
|
||||
VehicleRegistryType registryType) {
|
||||
VehicleRegistryType registryType,
|
||||
Double vehicleDistanceMeters,
|
||||
Double aircraftDistanceMeters) {
|
||||
}
|
||||
|
||||
private record ActiveVehicleCommandState(
|
||||
VehicleRiskLevel level,
|
||||
boolean parkingIssued) {
|
||||
boolean parkingIssued,
|
||||
int missingConflictCycles,
|
||||
Double vehicleDistanceMeters,
|
||||
Double aircraftDistanceMeters) {
|
||||
|
||||
private static ActiveVehicleCommandState fromCommand(
|
||||
VehicleRiskLevel level,
|
||||
boolean parkingIssued,
|
||||
int missingConflictCycles,
|
||||
VehicleConflictCommand command) {
|
||||
|
||||
return new ActiveVehicleCommandState(
|
||||
level,
|
||||
parkingIssued,
|
||||
missingConflictCycles,
|
||||
command.vehicleDistanceMeters(),
|
||||
command.aircraftDistanceMeters()
|
||||
);
|
||||
}
|
||||
|
||||
private ActiveVehicleCommandState withObservation(
|
||||
ConflictRecoveryObservation observation,
|
||||
int missingConflictCycles) {
|
||||
|
||||
if (observation == null) {
|
||||
return new ActiveVehicleCommandState(level, parkingIssued, missingConflictCycles, vehicleDistanceMeters, aircraftDistanceMeters);
|
||||
}
|
||||
return new ActiveVehicleCommandState(
|
||||
level,
|
||||
parkingIssued,
|
||||
missingConflictCycles,
|
||||
observation.vehicleDistanceMeters(),
|
||||
observation.aircraftDistanceMeters()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record ConflictRecoveryObservation(
|
||||
String vehicleId,
|
||||
double vehicleDistanceMeters,
|
||||
double aircraftDistanceMeters,
|
||||
boolean vehicleIntersectionBehind,
|
||||
boolean aircraftIntersectionBehind,
|
||||
boolean vehicleHeadingAway,
|
||||
boolean aircraftHeadingAway) {
|
||||
}
|
||||
|
||||
private enum VehicleRiskLevel {
|
||||
|
||||
@ -0,0 +1,135 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CollisionTestSessionLogService {
|
||||
|
||||
private static final DateTimeFormatter TIMESTAMP_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
|
||||
private static final Duration MAX_SESSION_LOG_DURATION = Duration.ofMinutes(20);
|
||||
|
||||
private final Path logDirectory;
|
||||
private final Clock clock;
|
||||
private final Duration maxSessionLogDuration;
|
||||
private final Map<String, Instant> sessionStartedAt = new ConcurrentHashMap<>();
|
||||
private final Set<String> expiredSessions = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Value("${qaup.collision.test-log.enabled:true}")
|
||||
private boolean enabled = true;
|
||||
|
||||
public CollisionTestSessionLogService(
|
||||
@Value("${qaup.collision.test-log.dir:}") String configuredLogDirectory,
|
||||
@Value("${LOG_PATH:logs}") String logPath) {
|
||||
this(configuredLogDirectory, logPath, Clock.systemDefaultZone(), MAX_SESSION_LOG_DURATION);
|
||||
}
|
||||
|
||||
CollisionTestSessionLogService(
|
||||
String configuredLogDirectory,
|
||||
String logPath,
|
||||
Clock clock,
|
||||
Duration maxSessionLogDuration) {
|
||||
|
||||
this.clock = clock;
|
||||
this.maxSessionLogDuration = maxSessionLogDuration;
|
||||
|
||||
if (configuredLogDirectory != null && !configuredLogDirectory.isBlank()) {
|
||||
this.logDirectory = Paths.get(configuredLogDirectory);
|
||||
} else {
|
||||
String baseLogPath = logPath == null || logPath.isBlank() ? "logs" : logPath;
|
||||
this.logDirectory = Paths.get(baseLogPath, "collision-tests");
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void logStartupState() {
|
||||
log.info("Collision test session file log initialized: enabled={}, directory={}", enabled, logDirectory.toAbsolutePath());
|
||||
}
|
||||
|
||||
public void startSession(String testSessionId, String message) {
|
||||
sessionStartedAt.put(testSessionId, Instant.now(clock));
|
||||
expiredSessions.remove(testSessionId);
|
||||
append(testSessionId, "SESSION_START " + safeMessage(message), true, true);
|
||||
}
|
||||
|
||||
public void endSession(String testSessionId, String message) {
|
||||
append(testSessionId, "SESSION_END " + safeMessage(message), false, true);
|
||||
sessionStartedAt.remove(testSessionId);
|
||||
expiredSessions.remove(testSessionId);
|
||||
}
|
||||
|
||||
public void write(String testSessionId, String message) {
|
||||
append(testSessionId, safeMessage(message), false, false);
|
||||
}
|
||||
|
||||
private synchronized void append(String testSessionId, String message, boolean truncate, boolean forceWrite) {
|
||||
if (!enabled || testSessionId == null || testSessionId.isBlank()) {
|
||||
if (!enabled) {
|
||||
log.warn("Collision test session file log is disabled; skip write: testSessionId={}", testSessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!forceWrite && isExpired(testSessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(logDirectory);
|
||||
String line = LocalDateTime.now(clock).format(TIMESTAMP_FORMATTER) + " | " + message + System.lineSeparator();
|
||||
StandardOpenOption[] options = truncate
|
||||
? new StandardOpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE}
|
||||
: new StandardOpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE};
|
||||
Files.writeString(logFilePath(testSessionId), line, StandardCharsets.UTF_8, options);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to write collision test session log: testSessionId={}", testSessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExpired(String testSessionId) {
|
||||
Instant startedAt = sessionStartedAt.computeIfAbsent(testSessionId, ignored -> Instant.now(clock));
|
||||
if (Duration.between(startedAt, Instant.now(clock)).compareTo(maxSessionLogDuration) <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expiredSessions.add(testSessionId)) {
|
||||
append(testSessionId,
|
||||
"SESSION_LOG_EXPIRED maxDurationMinutes=" + maxSessionLogDuration.toMinutes()
|
||||
+ ", reason=test_session_not_ended",
|
||||
false,
|
||||
true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Path logFilePath(String testSessionId) {
|
||||
return logDirectory.resolve(sanitizeFileName(testSessionId) + ".log");
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String value) {
|
||||
String sanitized = value.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||
return sanitized.isBlank() ? "unknown-session" : sanitized;
|
||||
}
|
||||
|
||||
private String safeMessage(String message) {
|
||||
return message == null ? "" : message;
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,9 @@ public class PlatformRuntimeStateService {
|
||||
@Autowired(required = false)
|
||||
private ISysConfigService sysConfigService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private CollisionTestSessionLogService collisionTestSessionLogService;
|
||||
|
||||
public PlatformRuntimeStateService(
|
||||
@Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft,
|
||||
@Value("${qaup.runtime-config.runway.alert-zone-radius.aircraft:100.0}") double runwayAlertZoneRadiusAircraft,
|
||||
@ -94,6 +97,7 @@ public class PlatformRuntimeStateService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
|
||||
Objects.requireNonNull(entries, "entries");
|
||||
|
||||
@ -113,16 +117,19 @@ public class PlatformRuntimeStateService {
|
||||
endedTestSessionId = currentTestSessionId;
|
||||
if (endedTestSessionId != null) {
|
||||
log.info("[test-session={}] collision test session ended by empty registry update", endedTestSessionId);
|
||||
writeTestSessionEnd(endedTestSessionId, "ended by empty registry update");
|
||||
}
|
||||
currentTestSessionId = null;
|
||||
} else {
|
||||
endedTestSessionId = currentTestSessionId;
|
||||
if (endedTestSessionId != null) {
|
||||
log.info("[test-session={}] collision test session replaced by new registry update", endedTestSessionId);
|
||||
writeTestSessionEnd(endedTestSessionId, "replaced by new registry update");
|
||||
}
|
||||
activeTestSessionId = createTestSessionId(entries);
|
||||
currentTestSessionId = activeTestSessionId;
|
||||
log.info("[test-session={}] collision test session started: objects={}", activeTestSessionId, entries);
|
||||
writeTestSessionStart(activeTestSessionId, describeRegisteredObjects(entries));
|
||||
}
|
||||
|
||||
Set<String> controllableVehicleIds = new TreeSet<>();
|
||||
@ -140,6 +147,8 @@ public class PlatformRuntimeStateService {
|
||||
}
|
||||
}
|
||||
|
||||
cleanupStaleRouteAssignments(previousVehicleIds, entries);
|
||||
|
||||
return new VehicleRegistryUpdateResult(
|
||||
System.currentTimeMillis(),
|
||||
entries.size(),
|
||||
@ -151,6 +160,68 @@ public class PlatformRuntimeStateService {
|
||||
endedTestSessionId);
|
||||
}
|
||||
|
||||
private void cleanupStaleRouteAssignments(Set<String> removedVehicleIds, List<VehicleRegistryEntry> currentEntries) {
|
||||
if (objectRouteAssignmentRepository == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TreeSet<String> routeAssignmentObjectIdsToDelete = new TreeSet<>();
|
||||
if (removedVehicleIds != null && !removedVehicleIds.isEmpty()) {
|
||||
routeAssignmentObjectIdsToDelete.addAll(removedVehicleIds);
|
||||
}
|
||||
|
||||
for (VehicleRegistryEntry entry : currentEntries) {
|
||||
routeAssignmentObjectIdsToDelete.add(entry.vehicleID());
|
||||
}
|
||||
|
||||
if (!routeAssignmentObjectIdsToDelete.isEmpty()) {
|
||||
objectRouteAssignmentRepository.deleteByObjectNameIn(new ArrayList<>(routeAssignmentObjectIdsToDelete));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTestSessionStart(String testSessionId, String message) {
|
||||
if (collisionTestSessionLogService != null) {
|
||||
collisionTestSessionLogService.startSession(testSessionId, message);
|
||||
} else {
|
||||
log.warn("[test-session={}] collision test session file log service is not available; start event was not written", testSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTestSessionEnd(String testSessionId, String message) {
|
||||
if (collisionTestSessionLogService != null) {
|
||||
collisionTestSessionLogService.endSession(testSessionId, message);
|
||||
} else {
|
||||
log.warn("[test-session={}] collision test session file log service is not available; end event was not written", testSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private String describeRegisteredObjects(List<VehicleRegistryEntry> entries) {
|
||||
List<String> flightIds = new ArrayList<>();
|
||||
List<String> vehicleIds = new ArrayList<>();
|
||||
List<String> otherIds = new ArrayList<>();
|
||||
|
||||
for (VehicleRegistryEntry entry : entries) {
|
||||
if (entry == null || entry.vehicleID() == null || entry.vehicleID().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.vehicleType() == VehicleRegistryType.HANGKONG) {
|
||||
flightIds.add(entry.vehicleID());
|
||||
} else if (entry.vehicleType() == VehicleRegistryType.WUREN || entry.vehicleType() == VehicleRegistryType.TEQIN) {
|
||||
vehicleIds.add(entry.vehicleID());
|
||||
} else {
|
||||
otherIds.add(entry.vehicleID() + "(" + entry.vehicleType() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(flightIds);
|
||||
Collections.sort(vehicleIds);
|
||||
Collections.sort(otherIds);
|
||||
return "flightIds=" + flightIds
|
||||
+ ", vehicleIds=" + vehicleIds
|
||||
+ ", otherIds=" + otherIds
|
||||
+ ", rawObjects=" + entries;
|
||||
}
|
||||
|
||||
public String getCurrentTestSessionId() {
|
||||
return currentTestSessionId;
|
||||
}
|
||||
|
||||
@ -319,6 +319,19 @@ public class RuleEventWebSocketPublisher {
|
||||
}
|
||||
}
|
||||
|
||||
public void publishPathConflictStatus(PathConflictAlertMessage message) {
|
||||
try {
|
||||
PathConflictAlertWebSocketEvent webSocketEvent = PathConflictAlertWebSocketEvent.create(message);
|
||||
eventPublisher.publishEvent(webSocketEvent);
|
||||
|
||||
logger.debug("Published path conflict status WebSocket event: vehicle={}, aircraft={}",
|
||||
message.getVehicleName(), message.getAircraftName());
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to publish path conflict status WebSocket event: vehicle={}, aircraft={}",
|
||||
message.getVehicleName(), message.getAircraftName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过vehicle_id动态查询车牌号
|
||||
*
|
||||
@ -344,4 +357,4 @@ public class RuleEventWebSocketPublisher {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,16 +332,24 @@ public class WebSocketMessageBroadcaster {
|
||||
|
||||
@EventListener
|
||||
public void handlePathConflictAlert(PathConflictAlertWebSocketEvent event) {
|
||||
PathConflictAlertMessage payload = (PathConflictAlertMessage) event.getPayload();
|
||||
UniversalMessage<PathConflictAlertMessage> message = UniversalMessage.<PathConflictAlertMessage>builder()
|
||||
.type(MessageTypeConstants.PATH_CONFLICT_ALERT)
|
||||
.type(resolvePathConflictMessageType(payload))
|
||||
.timestamp(event.getTimestamp())
|
||||
.messageId(generateMessageId())
|
||||
.payload((PathConflictAlertMessage) event.getPayload())
|
||||
.payload(payload)
|
||||
.build();
|
||||
|
||||
broadcastMessageInternal(message);
|
||||
}
|
||||
|
||||
private String resolvePathConflictMessageType(PathConflictAlertMessage payload) {
|
||||
if (payload != null && "PATH_CONFLICT_STATUS".equals(payload.getMessageType())) {
|
||||
return MessageTypeConstants.PATH_CONFLICT_STATUS;
|
||||
}
|
||||
return MessageTypeConstants.PATH_CONFLICT_ALERT;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handleGeofenceAlert(GeofenceAlertWebSocketEvent event) {
|
||||
UniversalMessage<GeofenceAlertPayload> message = UniversalMessage.<GeofenceAlertPayload>builder()
|
||||
|
||||
@ -67,6 +67,11 @@ public final class MessageTypeConstants {
|
||||
* 路径冲突告警消息
|
||||
*/
|
||||
public static final String PATH_CONFLICT_ALERT = "path_conflict_alert";
|
||||
|
||||
/**
|
||||
* 路径冲突计算状态消息
|
||||
*/
|
||||
public static final String PATH_CONFLICT_STATUS = "path_conflict_status";
|
||||
|
||||
/**
|
||||
* 电子围栏告警消息
|
||||
@ -83,4 +88,4 @@ public final class MessageTypeConstants {
|
||||
private MessageTypeConstants() {
|
||||
throw new UnsupportedOperationException("常量类不允许实例化");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,63 @@ public class PathConflictAlertMessage {
|
||||
* 冲突位置
|
||||
*/
|
||||
private Position position;
|
||||
|
||||
/**
|
||||
* Frontend display conflict point. Same coordinates as position, with clearer business meaning.
|
||||
*/
|
||||
private Position conflictPoint;
|
||||
|
||||
private String aircraftName;
|
||||
|
||||
private String aircraftObjectType;
|
||||
|
||||
private Double aircraftDistanceToConflictMeters;
|
||||
|
||||
private Double aircraftAlertThresholdMeters;
|
||||
|
||||
private String vehicleName;
|
||||
|
||||
private String vehicleObjectType;
|
||||
|
||||
private Double vehicleDistanceToConflictMeters;
|
||||
|
||||
private Double vehicleAlertThresholdMeters;
|
||||
|
||||
private Boolean vehicleMovingTowardConflictPoint;
|
||||
|
||||
private String calculationStatus;
|
||||
|
||||
private String directionLockStatus;
|
||||
|
||||
private String directionLockReason;
|
||||
|
||||
private String object1DirectionLockStatus;
|
||||
|
||||
private String object1DirectionLockReason;
|
||||
|
||||
private String object1RouteDirection;
|
||||
|
||||
private Double object1RouteProgressMeters;
|
||||
|
||||
private Double object1ConflictProgressMeters;
|
||||
|
||||
private Boolean object1ConflictPointAhead;
|
||||
|
||||
private Double object1ForwardDistanceMeters;
|
||||
|
||||
private String object2DirectionLockStatus;
|
||||
|
||||
private String object2DirectionLockReason;
|
||||
|
||||
private String object2RouteDirection;
|
||||
|
||||
private Double object2RouteProgressMeters;
|
||||
|
||||
private Double object2ConflictProgressMeters;
|
||||
|
||||
private Boolean object2ConflictPointAhead;
|
||||
|
||||
private Double object2ForwardDistanceMeters;
|
||||
|
||||
/**
|
||||
* 对象1距离冲突点距离 (米)
|
||||
@ -165,4 +222,4 @@ public class PathConflictAlertMessage {
|
||||
public boolean isAlert() {
|
||||
return this.alertLevel == ConflictAlertLog.AlertLevel.CRITICAL || this.alertLevel == ConflictAlertLog.AlertLevel.EMERGENCY || this.alertType == ConflictAlertLog.AlertType.CONFLICT_ALERT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,9 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
@ -24,11 +26,13 @@ class PlatformIntegrationControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private PlatformRuntimeStateService runtimeStateService;
|
||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
ReflectionTestUtils.setField(runtimeStateService, "objectRouteAssignmentRepository", mock(ObjectRouteAssignmentRepository.class));
|
||||
objectRouteAssignmentRepository = mock(ObjectRouteAssignmentRepository.class);
|
||||
ReflectionTestUtils.setField(runtimeStateService, "objectRouteAssignmentRepository", objectRouteAssignmentRepository);
|
||||
ReflectionTestUtils.setField(runtimeStateService, "transportRouteRepository", mock(TransportRouteRepository.class));
|
||||
PlatformIntegrationController controller =
|
||||
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
|
||||
@ -203,4 +207,21 @@ class PlatformIntegrationControllerTest {
|
||||
.andExpect(jsonPath("$.testSessionId").doesNotExist())
|
||||
.andExpect(jsonPath("$.endedTestSessionId").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClearRouteAssignmentsForRegisteredObjectsWhenNewSessionStarts() throws Exception {
|
||||
mockMvc.perform(post("/api/VehicleRegistry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
[
|
||||
{ "vehicleID": "WR01", "vehicleType": "WUREN" },
|
||||
{ "vehicleID": "CD423", "vehicleType": "HANGKONG" }
|
||||
]
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(objectRouteAssignmentRepository).deleteByObjectNameIn(argThat(objectNames ->
|
||||
objectNames.contains("WR01") && objectNames.contains("CD423")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
package com.qaup.collision.datacollector.service;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoutePersistenceServiceTest {
|
||||
|
||||
@Mock
|
||||
private TransportRouteRepository transportRouteRepository;
|
||||
|
||||
@Mock
|
||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
private RoutePersistenceService service() {
|
||||
RoutePersistenceService service = new RoutePersistenceService();
|
||||
ReflectionTestUtils.setField(service, "transportRouteRepository", transportRouteRepository);
|
||||
ReflectionTestUtils.setField(service, "objectRouteAssignmentRepository", objectRouteAssignmentRepository);
|
||||
return service;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRefreshAssignmentTimeWhenAircraftRouteIdIsUnchanged() {
|
||||
TransportRoute route = TransportRoute.builder()
|
||||
.id(1093L)
|
||||
.routeName("FLIGHT_CD423_OUT")
|
||||
.routeType(TransportRoute.RouteType.AIRCRAFT)
|
||||
.build();
|
||||
LocalDateTime oldAssignedAt = LocalDateTime.now().minusDays(1);
|
||||
ObjectRouteAssignment assignment = ObjectRouteAssignment.builder()
|
||||
.id(10L)
|
||||
.objectName("CD423")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.AIRCRAFT)
|
||||
.assignedRouteId(1093L)
|
||||
.assignedAt(oldAssignedAt)
|
||||
.build();
|
||||
|
||||
when(transportRouteRepository.findById(1093L)).thenReturn(Optional.of(route));
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"CD423",
|
||||
ObjectRouteAssignment.ObjectType.AIRCRAFT
|
||||
)).thenReturn(Optional.of(assignment));
|
||||
|
||||
service().saveRouteAssignment("CD423", 1093L, "OUT");
|
||||
|
||||
assertTrue(assignment.getAssignedAt().isAfter(oldAssignedAt));
|
||||
verify(objectRouteAssignmentRepository).save(assignment);
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -180,6 +181,7 @@ class RoutePreparationServiceTest {
|
||||
.objectName("UV-300")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.assignedRouteId(31L)
|
||||
.assignedAt(LocalDateTime.now().minusHours(1))
|
||||
.build()));
|
||||
when(objectRouteAssignmentRepository.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-300",
|
||||
@ -189,7 +191,10 @@ class RoutePreparationServiceTest {
|
||||
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||
|
||||
verify(transportRouteRepository, never()).save(any(TransportRoute.class));
|
||||
verify(objectRouteAssignmentRepository, never()).save(any(ObjectRouteAssignment.class));
|
||||
ArgumentCaptor<ObjectRouteAssignment> assignmentCaptor = ArgumentCaptor.forClass(ObjectRouteAssignment.class);
|
||||
verify(objectRouteAssignmentRepository).save(assignmentCaptor.capture());
|
||||
assertEquals(31L, assignmentCaptor.getValue().getAssignedRouteId());
|
||||
assertTrue(assignmentCaptor.getValue().getAssignedAt().isAfter(LocalDateTime.now().minusMinutes(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package com.qaup.collision.dataprocessing.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@ -13,6 +16,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DataProcessingServiceCollisionRegistrationTest {
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
@Test
|
||||
void shouldUseRegisteredCollisionTypeInsteadOfCollectedSourceType() throws Exception {
|
||||
DataProcessingService service = new DataProcessingService();
|
||||
@ -54,4 +59,37 @@ class DataProcessingServiceCollisionRegistrationTest {
|
||||
assertEquals(MovingObject.MovingObjectType.UNMANNED_VEHICLE, wr01.getObjectType());
|
||||
assertEquals(MovingObject.MovingObjectType.AIRCRAFT, cd423.getObjectType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveUnmannedVehicleMissionDataInProcessingSnapshot() throws Exception {
|
||||
DataProcessingService service = new DataProcessingService();
|
||||
UnmannedVehicle source = UnmannedVehicle.builder()
|
||||
.objectId("WR01")
|
||||
.objectName("WR01")
|
||||
.objectType(MovingObject.MovingObjectType.UNMANNED_VEHICLE)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879)))
|
||||
.missionId("MISSION-1")
|
||||
.missionStatus(UnmannedVehicle.MissionStatus.IN_PROGRESS)
|
||||
.waypoints(List.of(
|
||||
UnmannedVehicle.WaypointInfo.builder()
|
||||
.waypointId("1")
|
||||
.latitude(36.35416879)
|
||||
.longitude(120.0835104)
|
||||
.status(UnmannedVehicle.WaypointStatus.PENDING)
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
Method method = DataProcessingService.class.getDeclaredMethod("createProcessingSnapshot", java.util.Collection.class);
|
||||
method.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<MovingObject> snapshot = (List<MovingObject>) method.invoke(service, List.of(source));
|
||||
|
||||
assertEquals(1, snapshot.size());
|
||||
assertEquals(UnmannedVehicle.class, snapshot.get(0).getClass());
|
||||
UnmannedVehicle copied = (UnmannedVehicle) snapshot.get(0);
|
||||
assertEquals("MISSION-1", copied.getMissionId());
|
||||
assertEquals(UnmannedVehicle.MissionStatus.IN_PROGRESS, copied.getMissionStatus());
|
||||
assertEquals(1, copied.getWaypoints().size());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
package com.qaup.collision.pathconflict.event;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ConflictAlertEventListenerTest {
|
||||
|
||||
@Mock
|
||||
private RuleEventWebSocketPublisher webSocketPublisher;
|
||||
|
||||
@InjectMocks
|
||||
private ConflictAlertEventListener listener;
|
||||
|
||||
@Test
|
||||
void shouldPublishFrontendDisplayFieldsByBusinessRole() {
|
||||
GeometryFactory geometryFactory = new GeometryFactory();
|
||||
ConflictAlertEvent event = ConflictAlertEvent.builder()
|
||||
.conflictId(Optional.of(99L))
|
||||
.alertType(Optional.of(ConflictAlertLog.AlertType.CONFLICT_ALERT))
|
||||
.alertLevel(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL))
|
||||
.message("route conflict")
|
||||
.object1Name("UV-1")
|
||||
.object1Type(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.object2Name("AC-1")
|
||||
.object2Type(MovingObjectType.AIRCRAFT)
|
||||
.conflictPoint(geometryFactory.createPoint(new Coordinate(120.5, 31.2)))
|
||||
.object1Distance(42.345)
|
||||
.object2Distance(188.765)
|
||||
.estimatedTimeToConflictObj1(8)
|
||||
.estimatedTimeToConflictObj2(34)
|
||||
.timeGapSeconds(26.0)
|
||||
.eventTime(LocalDateTime.of(2026, 4, 29, 10, 0))
|
||||
.aircraftName("AC-1")
|
||||
.aircraftObjectType(MovingObjectType.AIRCRAFT.name())
|
||||
.aircraftDistanceToConflictMeters(188.765)
|
||||
.aircraftAlertThresholdMeters(200.0)
|
||||
.vehicleName("UV-1")
|
||||
.vehicleObjectType(MovingObjectType.UNMANNED_VEHICLE.name())
|
||||
.vehicleDistanceToConflictMeters(42.345)
|
||||
.vehicleAlertThresholdMeters(50.0)
|
||||
.vehicleMovingTowardConflictPoint(true)
|
||||
.build();
|
||||
|
||||
listener.handleConflictAlert(event);
|
||||
|
||||
ArgumentCaptor<PathConflictAlertMessage> captor = ArgumentCaptor.forClass(PathConflictAlertMessage.class);
|
||||
verify(webSocketPublisher).publishPathConflictAlert(captor.capture());
|
||||
|
||||
PathConflictAlertMessage message = captor.getValue();
|
||||
assertEquals(31.2, message.getConflictPoint().getLatitude());
|
||||
assertEquals(120.5, message.getConflictPoint().getLongitude());
|
||||
assertEquals("AC-1", message.getAircraftName());
|
||||
assertEquals("AIRCRAFT", message.getAircraftObjectType());
|
||||
assertEquals(188.77, message.getAircraftDistanceToConflictMeters());
|
||||
assertEquals(200.0, message.getAircraftAlertThresholdMeters());
|
||||
assertEquals("UV-1", message.getVehicleName());
|
||||
assertEquals("UNMANNED_VEHICLE", message.getVehicleObjectType());
|
||||
assertEquals(42.35, message.getVehicleDistanceToConflictMeters());
|
||||
assertEquals(50.0, message.getVehicleAlertThresholdMeters());
|
||||
assertTrue(message.getVehicleMovingTowardConflictPoint());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@ -32,7 +33,8 @@ class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService,
|
||||
mock(VehicleCommandService.class)
|
||||
mock(VehicleCommandService.class),
|
||||
mock(RuleEventWebSocketPublisher.class)
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -78,7 +80,8 @@ class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService,
|
||||
mock(VehicleCommandService.class)
|
||||
mock(VehicleCommandService.class),
|
||||
mock(RuleEventWebSocketPublisher.class)
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -108,7 +111,8 @@ class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService,
|
||||
mock(VehicleCommandService.class)
|
||||
mock(VehicleCommandService.class),
|
||||
mock(RuleEventWebSocketPublisher.class)
|
||||
);
|
||||
|
||||
MovingObject aircraft = MovingObject.builder()
|
||||
|
||||
@ -13,6 +13,7 @@ import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
@ -26,6 +27,10 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
class VehicleCommandServiceTest {
|
||||
|
||||
@ -35,39 +40,24 @@ class VehicleCommandServiceTest {
|
||||
private MockRestServiceServer server;
|
||||
private PlatformRuntimeStateService runtimeStateService;
|
||||
private GeometryFactory geometryFactory;
|
||||
private RuleEventWebSocketPublisher webSocketPublisher;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
||||
webSocketPublisher = mock(RuleEventWebSocketPublisher.class);
|
||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService, webSocketPublisher);
|
||||
geometryFactory = new GeometryFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSendWarningOnceAndResumeAfterConflictClears() {
|
||||
void shouldNotSendVehicleCommandForWarning() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("WARNING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andExpect(jsonPath("$.latitude").value(36.3544))
|
||||
.andExpect(jsonPath("$.longitude").value(120.085))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("RESUME"))
|
||||
.andExpect(jsonPath("$.commandReason").value("RESUME_TRAFFIC"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent warningEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
@ -76,9 +66,336 @@ class VehicleCommandServiceTest {
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(warningEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(warningEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of());
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 45.0, 45.0, false, false, false, false)));
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message -> "PATH_CONFLICT_RESUME".equals(message.getMessageType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotResumeWhenAircraftDistanceIncreasesBeforeConflictPoint() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andExpect(jsonPath("$.latitude").value(36.3544))
|
||||
.andExpect(jsonPath("$.longitude").value(120.085))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 12.0, 45.0 + i, false, false, false, false)));
|
||||
}
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResumeAfterAircraftPassesConflictPointAndExceedsReleaseDistanceAfterAlert() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistance(25.0);
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("RESUME"))
|
||||
.andExpect(jsonPath("$.commandReason").value("RESUME_TRAFFIC"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 12.0, 30.0 + i, false, true, false, true)));
|
||||
}
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotResumeAfterAircraftPassesConflictPointButBeforeReleaseDistance() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistance(25.0);
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 12.0, 20.0 + i, false, true, false, true)));
|
||||
}
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResumeWhenAircraftPassedConflictPointAndReleaseDistanceEvenIfConflictStillReported() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistance(25.0);
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("RESUME"))
|
||||
.andExpect(jsonPath("$.commandReason").value("RESUME_TRAFFIC"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 12.0, 30.0 + i, false, true, false, true)));
|
||||
}
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotResumeWhenParkedVehicleStillReceivesActiveAlertBeforeIntersection() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistance(25.0);
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent initialAlert = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL,
|
||||
12.0,
|
||||
20.0
|
||||
);
|
||||
ConflictAlertEvent activeAlertBeforeIntersection = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL,
|
||||
62.0,
|
||||
76.0
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(initialAlert));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(activeAlertBeforeIntersection));
|
||||
}
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotResumeOnSingleRecoveryObservationAfterAlert() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 12.0, 45.0, false, false, false, true)));
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotResumeWhenOnlyVehicleMovesAway() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
for (int i = 0; i < 5; i++) {
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 45.0 + i, 35.0, false, false, false, false)));
|
||||
}
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClearActiveVehicleCommandStateWhenRegistryStartsNewSession() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC-001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC-001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(), List.of(new VehicleCommandService.ConflictRecoveryObservation(
|
||||
"UV-001", 12.0, 45.0, false, false, false, true)));
|
||||
|
||||
server.verify();
|
||||
verify(webSocketPublisher, never()).publishPathConflictAlert(argThat(message ->
|
||||
"PATH_CONFLICT_RESUME".equals(message.getMessageType())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -87,13 +404,6 @@ class VehicleCommandServiceTest {
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("SP-001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("SP-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("WARNING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("SP-001"))
|
||||
@ -144,6 +454,16 @@ class VehicleCommandServiceTest {
|
||||
MovingObject.MovingObjectType vehicleType,
|
||||
ConflictAlertLog.AlertLevel alertLevel) {
|
||||
|
||||
return buildEvent(vehicleId, vehicleType, alertLevel, 12.0, 35.0);
|
||||
}
|
||||
|
||||
private ConflictAlertEvent buildEvent(
|
||||
String vehicleId,
|
||||
MovingObject.MovingObjectType vehicleType,
|
||||
ConflictAlertLog.AlertLevel alertLevel,
|
||||
double vehicleDistance,
|
||||
double aircraftDistance) {
|
||||
|
||||
Point conflictPoint = geometryFactory.createPoint(new Coordinate(120.0850, 36.3544));
|
||||
|
||||
return ConflictAlertEvent.builder()
|
||||
@ -160,8 +480,8 @@ class VehicleCommandServiceTest {
|
||||
.object2Name("CA1234")
|
||||
.object2Type(MovingObject.MovingObjectType.AIRCRAFT)
|
||||
.conflictPoint(conflictPoint)
|
||||
.object1Distance(12.0)
|
||||
.object2Distance(35.0)
|
||||
.object1Distance(vehicleDistance)
|
||||
.object2Distance(aircraftDistance)
|
||||
.estimatedTimeToConflictObj1(10)
|
||||
.estimatedTimeToConflictObj2(20)
|
||||
.timeGapSeconds(10.0)
|
||||
|
||||
@ -2,6 +2,7 @@ package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@ -18,6 +19,7 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class VehicleCommandServiceTrafficLightTest {
|
||||
|
||||
@ -32,7 +34,12 @@ class VehicleCommandServiceTrafficLightTest {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
||||
vehicleCommandService = new VehicleCommandService(
|
||||
restTemplate,
|
||||
new ObjectMapper(),
|
||||
runtimeStateService,
|
||||
mock(RuleEventWebSocketPublisher.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CollisionTestSessionLogServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldCreateDedicatedFilePerCollisionTestSession() throws Exception {
|
||||
CollisionTestSessionLogService service = new CollisionTestSessionLogService(tempDir.toString(), "ignored");
|
||||
|
||||
service.startSession("collision-test-1/unsafe", "flightIds=CA123, vehicleIds=V001");
|
||||
service.write("collision-test-1/unsafe", "direction locked");
|
||||
service.startSession("collision-test-2", "second session started");
|
||||
|
||||
Path firstSessionLog = tempDir.resolve("collision-test-1_unsafe.log");
|
||||
Path secondSessionLog = tempDir.resolve("collision-test-2.log");
|
||||
|
||||
assertTrue(Files.exists(firstSessionLog));
|
||||
assertTrue(Files.exists(secondSessionLog));
|
||||
|
||||
String firstLog = Files.readString(firstSessionLog);
|
||||
String secondLog = Files.readString(secondSessionLog);
|
||||
|
||||
assertTrue(firstLog.contains("SESSION_START"));
|
||||
assertTrue(firstLog.contains("flightIds=CA123"));
|
||||
assertTrue(firstLog.contains("vehicleIds=V001"));
|
||||
assertTrue(firstLog.contains("direction locked"));
|
||||
assertFalse(firstLog.contains("second session started"));
|
||||
assertTrue(secondLog.contains("second session started"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStopAppendingAfterSessionLogDurationLimit() throws Exception {
|
||||
MutableClock clock = new MutableClock(Instant.parse("2026-04-29T10:00:00Z"));
|
||||
CollisionTestSessionLogService service = new CollisionTestSessionLogService(
|
||||
tempDir.toString(),
|
||||
"ignored",
|
||||
clock,
|
||||
Duration.ofMinutes(20));
|
||||
|
||||
service.startSession("collision-test-long", "started");
|
||||
service.write("collision-test-long", "before expiry");
|
||||
clock.advance(Duration.ofMinutes(21));
|
||||
service.write("collision-test-long", "after expiry");
|
||||
service.write("collision-test-long", "after expiry again");
|
||||
service.endSession("collision-test-long", "ended");
|
||||
|
||||
String log = Files.readString(tempDir.resolve("collision-test-long.log"));
|
||||
assertTrue(log.contains("SESSION_START"));
|
||||
assertTrue(log.contains("before expiry"));
|
||||
assertTrue(log.contains("SESSION_LOG_EXPIRED maxDurationMinutes=20"));
|
||||
assertFalse(log.contains("after expiry"));
|
||||
assertFalse(log.contains("after expiry again"));
|
||||
assertTrue(log.contains("SESSION_END"));
|
||||
}
|
||||
|
||||
private static final class MutableClock extends Clock {
|
||||
private Instant instant;
|
||||
|
||||
private MutableClock(Instant instant) {
|
||||
this.instant = instant;
|
||||
}
|
||||
|
||||
private void advance(Duration duration) {
|
||||
instant = instant.plus(duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZoneId getZone() {
|
||||
return ZoneId.of("UTC");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Clock withZone(ZoneId zone) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant instant() {
|
||||
return instant;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.system.domain.SysConfig;
|
||||
import com.qaup.system.service.ISysConfigService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -12,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@ -127,6 +129,39 @@ class PlatformRuntimeStateServiceTest {
|
||||
assertEquals(List.of("AC001", "TQ001"), second.removedVehicleIDs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCleanStaleRouteAssignmentsWhenVehicleRegistryIsResubmitted() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
ObjectRouteAssignmentRepository assignmentRepository = mock(ObjectRouteAssignmentRepository.class);
|
||||
ReflectionTestUtils.setField(service, "objectRouteAssignmentRepository", assignmentRepository);
|
||||
|
||||
service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("WR01", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("CD423", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TEST003", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("WR01", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("CD423", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
verify(assignmentRepository, times(2)).deleteByObjectNameIn(eq(List.of("CD423", "TEST003", "WR01")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCleanRouteAssignmentsForObjectsRegisteredAsNonCollisionTypes() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
ObjectRouteAssignmentRepository assignmentRepository = mock(ObjectRouteAssignmentRepository.class);
|
||||
ReflectionTestUtils.setField(service, "objectRouteAssignmentRepository", assignmentRepository);
|
||||
|
||||
service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TEST003", PlatformRuntimeStateService.VehicleRegistryType.PUTONG)
|
||||
));
|
||||
|
||||
verify(assignmentRepository).deleteByObjectNameIn(eq(List.of("TEST003")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAndEndCollisionTestSessionFromRegistryUpdates() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package com.qaup.collision.websocket.broadcaster;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.cache.MessageCacheService;
|
||||
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -11,6 +13,7 @@ import java.lang.reflect.Method;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class WebSocketMessageBroadcasterTest {
|
||||
|
||||
@ -33,6 +36,24 @@ class WebSocketMessageBroadcasterTest {
|
||||
assertEquals(120.100100, sanitized.getPosition().getLongitude());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBroadcastPathConflictStatusWithStatusMessageType() {
|
||||
CollisionWebSocketHandler handler = mock(CollisionWebSocketHandler.class);
|
||||
WebSocketMessageBroadcaster broadcaster = new WebSocketMessageBroadcaster(
|
||||
mock(MessageCacheService.class),
|
||||
handler,
|
||||
new ObjectMapper()
|
||||
);
|
||||
PathConflictAlertMessage payload = PathConflictAlertMessage.builder()
|
||||
.messageType("PATH_CONFLICT_STATUS")
|
||||
.calculationStatus("MONITORING")
|
||||
.build();
|
||||
|
||||
broadcaster.handlePathConflictAlert(PathConflictAlertWebSocketEvent.create(payload));
|
||||
|
||||
verify(handler).broadcastMessage(org.mockito.ArgumentMatchers.contains("\"type\":\"path_conflict_status\""));
|
||||
}
|
||||
|
||||
private PositionUpdatePayload invokeSanitize(WebSocketMessageBroadcaster broadcaster,
|
||||
PositionUpdatePayload payload) throws Exception {
|
||||
Method method = WebSocketMessageBroadcaster.class.getDeclaredMethod("sanitizePositionPayload", PositionUpdatePayload.class);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user