diff --git a/AGENTS.md b/AGENTS.md index 9ad26ba..61e10e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,17 @@ qaup-postgis - 如需看路线名称,连接 `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;"` - 历史上曾出现同一对象编号跨类型或旧路线残留导致误判,注册覆盖时需要同步清理旧路径绑定。 +### 碰撞现场测试快速流程 +- 测试开始后先定位本次独立测试日志:`docker exec qaup-app sh -c 'ls -lt /logs/collision-tests | head -20'`。 +- 已知车牌或航班号时,直接按对象编号找日志文件:`docker exec qaup-app sh -c 'grep -R -l "TEST003" /logs/collision-tests 2>/dev/null'`。 +- 找到日志后优先看方向锁定和碰撞计算链路:`docker exec qaup-app sh -c 'grep -E "DIRECTION_LOCK|DIRECTION_LOCK_FAILED|PAIR_RESULT|PATH_CONFLICT_POINT_SENT|EVENT_PUBLISHED|CYCLE" /logs/collision-tests/具体文件名.log'`。 +- 不知道具体日志名但知道对象编号时,可一条命令定位并查看:`docker exec qaup-app sh -c 'f=$(grep -R -l "TEST003" /logs/collision-tests 2>/dev/null | tail -1); echo $f; grep -E "DIRECTION_LOCK|DIRECTION_LOCK_FAILED|PAIR_RESULT|PATH_CONFLICT_POINT_SENT|EVENT_PUBLISHED|CYCLE" "$f"'`。 +- 方向锁定判断:正常应看到 `DIRECTION_LOCK`;如果看到 `DIRECTION_LOCK_FAILED`,先看原因是否为 `initial_position_too_close_to_conflict_point` 或 `conflict_points_on_both_sides`。 +- 本项目碰撞测试要求:车和飞机不必停在路线起点,但应能投影到绑定路线,并且测试开始时不要贴近冲突点,建议距离冲突点至少 `10-20米`,避免定位抖动触发方向无法锁定。 +- 前端 WebSocket 调试消息看 `type=path_conflict_status`,重点看 `payload.calculationStatus`、`payload.directionLockStatus`、`payload.directionLockReason`、`payload.vehicleDistanceToConflictMeters`、`payload.aircraftDistanceToConflictMeters`、`payload.object1ForwardDistanceMeters`、`payload.object2ForwardDistanceMeters`。 +- 正常测试现象:`directionLockStatus=DIRECTION_LOCKED`,车和飞机到冲突点的距离逐步变小;达到预警/告警阈值后后端发预警/告警,车辆停车;飞机越过冲突点并超过解除距离后车辆恢复通行。 +- 如果前端看到距离不变大/不变小,先对照测试日志中的 `forwardDistance`、`PAIR_RESULT` 和路线绑定表,判断是定位数据问题、路线绑定问题,还是方向未锁定问题。 + ### 红绿灯接入说明 - 正式环境中,qaup-app 的红绿灯接入方式为 MQTT,不是 HTTP。 - 正式环境配置中 `traffic.light.tcp.enabled=false`,`traffic.light.mqtt.enabled=true`。 diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/PlatformIntegrationController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/PlatformIntegrationController.java index 21f58be..3f1c7ca 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/controller/PlatformIntegrationController.java +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/PlatformIntegrationController.java @@ -103,11 +103,11 @@ public class PlatformIntegrationController { } /** - * Updates path crossing distance thresholds. + * Updates path crossing alert thresholds or the conflict release distance. * * Supported payloads: * - {"value": 40} - * Applies the same threshold to both vehicle and aircraft. + * Updates the release distance after the aircraft has passed the crossing point. * - {"vehicleDistance": 50, "aircraftDistance": 80} * Updates each threshold independently. * @@ -115,7 +115,7 @@ public class PlatformIntegrationController { * - vehicleDistance: vehicle distance to the crossing point * - aircraftDistance: aircraft distance to the crossing point * - * Alert evaluation: + * Alert threshold evaluation: * - both sides inside their thresholds -> alert * - only one side inside its threshold -> warning */ @@ -219,6 +219,7 @@ public class PlatformIntegrationController { thresholds.put("vehicleDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle()); thresholds.put("aircraftDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()); payload.put("thresholds", thresholds); + payload.put("releaseDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistance()); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload); } catch (Exception e) { return internalError(e); @@ -374,7 +375,7 @@ public class PlatformIntegrationController { Map payload = new LinkedHashMap<>(); payload.put("status", "success"); payload.put("area", "collision"); - payload.put("field", "crossing_distance_threshold"); + payload.put("field", "collision.diverging_release_distance"); payload.put("old", oldValue); payload.put("new", newValue); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload); diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java index 41559bd..b6f4528 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java @@ -56,6 +56,7 @@ public class PathConflictDetectionService { private CollisionTestSessionLogService collisionTestSessionLogService; private final Map routeTravelDirections = new ConcurrentHashMap<>(); + private final Map lockedConflictProgressMeters = new ConcurrentHashMap<>(); private final Map pathConflictStatusPublishedAt = new ConcurrentHashMap<>(); private volatile String observedDirectionSessionId; @@ -250,10 +251,24 @@ public class PathConflictDetectionService { return PairDetectionResult.of(PairOutcomeReason.DIRECTION_LOCK_FAILED); } + RouteTravelDirection direction1 = directionLock1.direction(); + RouteTravelDirection direction2 = directionLock2.direction(); + List effectiveIntersections = selectLockedConflictPoint( + obj1, + obj2, + route1, + route2, + projected1, + projected2, + direction1, + direction2, + projectedIntersections + ); + boolean intersectionBehind = false; boolean thresholdNotReached = false; VehicleCommandService.ConflictRecoveryObservation recoveryObservation = null; - for (IntersectionProjection projection : projectedIntersections) { + for (IntersectionProjection projection : effectiveIntersections) { Point intersectionPoint = projection.intersectionPoint(); Point localIntersection = projection.localIntersection(); ProjectedPosition projectedIntersection1 = projection.obj1Intersection(); @@ -261,8 +276,6 @@ public class PathConflictDetectionService { double distanceToIntersection1 = localObjPos1.distance(localIntersection); double distanceToIntersection2 = localObjPos2.distance(localIntersection); - RouteTravelDirection direction1 = directionLock1.direction(); - RouteTravelDirection direction2 = directionLock2.direction(); boolean obj1IntersectionAhead = isIntersectionAheadByRouteProgress(projected1, projectedIntersection1, direction1); boolean obj2IntersectionAhead = isIntersectionAheadByRouteProgress(projected2, projectedIntersection2, direction2); double forwardDistance1 = obj1IntersectionAhead ? distanceToIntersection1 : -distanceToIntersection1; @@ -272,6 +285,23 @@ public class PathConflictDetectionService { if (!obj1IntersectionAhead || !obj2IntersectionAhead || distanceToIntersection1 <= MIN_FORWARD_DISTANCE_METERS || distanceToIntersection2 <= MIN_FORWARD_DISTANCE_METERS) { + ConflictCalculationResult behindStatus = buildStatusOnlyResult( + obj1, + obj2, + forwardDistance1, + forwardDistance2 + ); + publishPathConflictStatusIfDue( + obj1, + obj2, + toExternalPoint(intersectionPoint), + behindStatus, + obj1IntersectionAhead, + obj2IntersectionAhead, + buildDirectionDebug(directionLock1, projected1, projectedIntersection1, obj1IntersectionAhead, forwardDistance1), + buildDirectionDebug(directionLock2, projected2, projectedIntersection2, obj2IntersectionAhead, forwardDistance2), + "INTERSECTION_BEHIND" + ); writePairEvaluation( "INTERSECTION_BEHIND", obj1, @@ -412,6 +442,25 @@ public class PathConflictDetectionService { } } + private ConflictCalculationResult buildStatusOnlyResult( + MovingObject obj1, + MovingObject obj2, + double forwardDistance1, + double forwardDistance2) { + + return new ConflictCalculationResult( + Math.abs(forwardDistance1), + Math.abs(forwardDistance2), + MAX_PREDICTION_TIME_SECONDS, + MAX_PREDICTION_TIME_SECONDS, + 0.0, + Optional.empty(), + Optional.empty(), + obj1.getObjectType(), + obj2.getObjectType() + ); + } + private ConflictAlertEvent buildConflictAlertEvent( MovingObject obj1, MovingObject obj2, @@ -1063,10 +1112,95 @@ public class PathConflictDetectionService { } routeTravelDirections.clear(); + lockedConflictProgressMeters.clear(); pathConflictStatusPublishedAt.clear(); observedDirectionSessionId = currentTestSessionId; } + private List selectLockedConflictPoint( + MovingObject obj1, + MovingObject obj2, + TransportRoute route1, + TransportRoute route2, + ProjectedPosition projected1, + ProjectedPosition projected2, + RouteTravelDirection direction1, + RouteTravelDirection direction2, + List projectedIntersections) { + + if (projectedIntersections.size() <= 1) { + cacheLockedConflictProgress(obj1, obj2, route1, route2, projectedIntersections.get(0).obj1Intersection(), projectedIntersections.get(0).obj2Intersection()); + return projectedIntersections; + } + + String key1 = conflictProgressKey(obj1, obj2, route1, route2); + String key2 = conflictProgressKey(obj2, obj1, route2, route1); + Double cached1 = lockedConflictProgressMeters.get(key1); + Double cached2 = lockedConflictProgressMeters.get(key2); + + IntersectionProjection selected = null; + double selectedScore = Double.MAX_VALUE; + for (IntersectionProjection projection : projectedIntersections) { + double score; + if (cached1 != null || cached2 != null) { + double score1 = cached1 == null ? 0.0 : Math.abs(projection.obj1Intersection().distanceAlongRouteMeters() - cached1); + double score2 = cached2 == null ? 0.0 : Math.abs(projection.obj2Intersection().distanceAlongRouteMeters() - cached2); + score = score1 + score2; + } else { + double forward1 = signedForwardProgress(projected1, projection.obj1Intersection(), direction1); + double forward2 = signedForwardProgress(projected2, projection.obj2Intersection(), direction2); + boolean bothAhead = forward1 > MIN_FORWARD_DISTANCE_METERS && forward2 > MIN_FORWARD_DISTANCE_METERS; + score = Math.abs(forward1) + Math.abs(forward2); + if (!bothAhead) { + score += 1_000_000.0; + } + } + + if (score < selectedScore) { + selectedScore = score; + selected = projection; + } + } + + if (selected == null) { + return projectedIntersections; + } + + cacheLockedConflictProgress(obj1, obj2, route1, route2, selected.obj1Intersection(), selected.obj2Intersection()); + return List.of(selected); + } + + private void cacheLockedConflictProgress( + MovingObject obj1, + MovingObject obj2, + TransportRoute route1, + TransportRoute route2, + ProjectedPosition projectedIntersection1, + ProjectedPosition projectedIntersection2) { + + lockedConflictProgressMeters.putIfAbsent( + conflictProgressKey(obj1, obj2, route1, route2), + projectedIntersection1.distanceAlongRouteMeters() + ); + lockedConflictProgressMeters.putIfAbsent( + conflictProgressKey(obj2, obj1, route2, route1), + projectedIntersection2.distanceAlongRouteMeters() + ); + } + + private String conflictProgressKey(MovingObject object, MovingObject otherObject, TransportRoute route, TransportRoute otherRoute) { + String sessionId = currentTestSessionId(); + return (sessionId == null ? "default" : sessionId) + + "|" + + object.getObjectName() + + "|" + + route.getId() + + "|" + + otherObject.getObjectName() + + "|" + + otherRoute.getId(); + } + private DirectionLockResult resolveRouteTravelDirection( MovingObject object, TransportRoute route, @@ -1181,10 +1315,18 @@ public class PathConflictDetectionService { ProjectedPosition projectedIntersection, RouteTravelDirection direction) { + return signedForwardProgress(projectedObject, projectedIntersection, direction) > MIN_FORWARD_DISTANCE_METERS; + } + + private double signedForwardProgress( + ProjectedPosition projectedObject, + ProjectedPosition projectedIntersection, + RouteTravelDirection direction) { + double progressDelta = projectedIntersection.distanceAlongRouteMeters() - projectedObject.distanceAlongRouteMeters(); return direction == RouteTravelDirection.START_TO_END - ? progressDelta > MIN_FORWARD_DISTANCE_METERS - : -progressDelta > MIN_FORWARD_DISTANCE_METERS; + ? progressDelta + : -progressDelta; } private boolean isSupportedConflictPair(MovingObjectType left, MovingObjectType right) { diff --git a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java index 779c3b4..960d22c 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/VehicleCommandService.java @@ -100,7 +100,7 @@ public class VehicleCommandService { + ", vehicleId=" + vehicleId + ", aircraftPassedConflictPoint=true" + ", aircraftDistanceMeters=" + observation.aircraftDistanceMeters() - + ", releaseThresholdMeters=" + platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft() + + ", releaseThresholdMeters=" + platformRuntimeStateService.getCollisionDivergingReleaseDistance() + ", currentCommandSuppressed=true"); currentCommands.remove(vehicleId); } @@ -204,7 +204,7 @@ public class VehicleCommandService { + ", vehicleId=" + vehicleId + ", aircraftPassedConflictPoint=true" + ", aircraftDistanceMeters=" + observation.aircraftDistanceMeters() - + ", releaseThresholdMeters=" + platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft() + + ", releaseThresholdMeters=" + platformRuntimeStateService.getCollisionDivergingReleaseDistance() + ", confirmationCycle=" + (previousState.missingConflictCycles() + 1) + ", requiredConfirmationCycles=" + RESUME_CONFIRMATION_CYCLES); @@ -572,7 +572,7 @@ public class VehicleCommandService { } return observation.aircraftIntersectionBehind() - && observation.aircraftDistanceMeters() > platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft(); + && observation.aircraftDistanceMeters() > platformRuntimeStateService.getCollisionDivergingReleaseDistance(); } private void publishResumeToFrontend( diff --git a/qaup-collision/src/main/java/com/qaup/collision/service/CollisionTestSessionLogService.java b/qaup-collision/src/main/java/com/qaup/collision/service/CollisionTestSessionLogService.java index f93bebb..9010624 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/service/CollisionTestSessionLogService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/service/CollisionTestSessionLogService.java @@ -2,6 +2,7 @@ package com.qaup.collision.service; import lombok.extern.slf4j.Slf4j; import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -36,6 +37,7 @@ public class CollisionTestSessionLogService { @Value("${qaup.collision.test-log.enabled:true}") private boolean enabled = true; + @Autowired public CollisionTestSessionLogService( @Value("${qaup.collision.test-log.dir:}") String configuredLogDirectory, @Value("${LOG_PATH:logs}") String logPath) { diff --git a/qaup-collision/src/main/java/com/qaup/collision/service/PlatformRuntimeStateService.java b/qaup-collision/src/main/java/com/qaup/collision/service/PlatformRuntimeStateService.java index 0a6b0b2..a50b30a 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/service/PlatformRuntimeStateService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/service/PlatformRuntimeStateService.java @@ -35,10 +35,13 @@ import java.util.concurrent.atomic.AtomicLong; public class PlatformRuntimeStateService { private static final String CONFIG_KEY_CROSSING_VEHICLE_DISTANCE = "qaup.collision.crossing.vehicleDistance"; private static final String CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE = "qaup.collision.crossing.aircraftDistance"; + private static final String CONFIG_KEY_RELEASE_DISTANCE = "qaup.collision.release.distance"; private static final String CONFIG_NAME_CROSSING_VEHICLE_DISTANCE = "Path crossing threshold for vehicle"; private static final String CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE = "Path crossing threshold for aircraft"; + private static final String CONFIG_NAME_RELEASE_DISTANCE = "Path conflict release distance"; private static final String CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE = "Vehicle distance threshold to the route crossing point"; private static final String CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE = "Aircraft distance threshold to the route crossing point"; + private static final String CONFIG_REMARK_RELEASE_DISTANCE = "Aircraft distance after passing the conflict point before traffic resumes"; private static final String CONFIG_UPDATER = "system"; private final ConcurrentHashMap vehicleTypes = new ConcurrentHashMap<>(); @@ -53,6 +56,9 @@ public class PlatformRuntimeStateService { // Threshold used when the aircraft approaches a route crossing point. private volatile double collisionDivergingReleaseDistanceForAircraft; + + // Distance after the aircraft has passed the crossing point before the vehicle may resume. + private volatile double collisionDivergingReleaseDistance; private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); private static final String DEFAULT_ROUTE_SOURCE = "VehicleRouteIngestion"; private static final String DEFAULT_ROUTE_DESCRIPTION = "Manual route uploaded via platform integration API"; @@ -78,6 +84,7 @@ public class PlatformRuntimeStateService { this.runwayAlertZoneRadiusAircraft = runwayAlertZoneRadiusAircraft; this.collisionDivergingReleaseDistanceForVehicle = collisionDivergingReleaseDistance; this.collisionDivergingReleaseDistanceForAircraft = collisionDivergingReleaseDistance; + this.collisionDivergingReleaseDistance = collisionDivergingReleaseDistance; } @PostConstruct @@ -95,6 +102,11 @@ public class PlatformRuntimeStateService { if (persistedAircraftDistance != null) { collisionDivergingReleaseDistanceForAircraft = persistedAircraftDistance; } + + Double persistedReleaseDistance = readConfigDouble(CONFIG_KEY_RELEASE_DISTANCE); + if (persistedReleaseDistance != null) { + collisionDivergingReleaseDistance = persistedReleaseDistance; + } } @Transactional @@ -338,23 +350,19 @@ public class PlatformRuntimeStateService { } /** - * Returns the shared crossing threshold when both vehicle and aircraft use the same value. - * The value mirrors the vehicle threshold for backward compatibility. + * Returns the release distance used after the aircraft has passed the conflict point. */ public double getCollisionDivergingReleaseDistance() { - return collisionDivergingReleaseDistanceForVehicle; + return collisionDivergingReleaseDistance; } /** - * Updates both vehicle and aircraft crossing thresholds to the same value. - * This keeps the existing API contract that accepts a single numeric value. + * Updates the release distance used after the aircraft has passed the conflict point. */ public double updateCollisionDivergingReleaseDistance(double newValue) { - double oldValue = collisionDivergingReleaseDistanceForVehicle; - collisionDivergingReleaseDistanceForVehicle = newValue; - collisionDivergingReleaseDistanceForAircraft = newValue; - persistCrossingThreshold(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE, CONFIG_NAME_CROSSING_VEHICLE_DISTANCE, newValue, CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE); - persistCrossingThreshold(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE, CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE, newValue, CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE); + double oldValue = collisionDivergingReleaseDistance; + collisionDivergingReleaseDistance = newValue; + persistCrossingThreshold(CONFIG_KEY_RELEASE_DISTANCE, CONFIG_NAME_RELEASE_DISTANCE, newValue, CONFIG_REMARK_RELEASE_DISTANCE); return oldValue; } diff --git a/qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java b/qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java index d7f7170..f4ba048 100644 --- a/qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java +++ b/qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java @@ -113,6 +113,23 @@ class PlatformIntegrationControllerTest { .andExpect(jsonPath("$.message").value("Missing field: value")); } + @Test + void shouldUpdateReleaseDistanceWithoutChangingCrossingAlertDistances() throws Exception { + mockMvc.perform(post("/config/collision/diverging_release_distance") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"value\":25}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.field").value("collision.diverging_release_distance")) + .andExpect(jsonPath("$.old").value(40.0)) + .andExpect(jsonPath("$.new").value(25.0)); + + mockMvc.perform(post("/api/collision/preparation/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.thresholds.vehicleDistance").value(40.0)) + .andExpect(jsonPath("$.thresholds.aircraftDistance").value(40.0)) + .andExpect(jsonPath("$.releaseDistance").value(25.0)); + } + @Test void shouldRejectInvalidNumericConfigValueType() throws Exception { mockMvc.perform(post("/config/runway/alert_zone_radius/aircraft") @@ -184,7 +201,8 @@ class PlatformIntegrationControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.testSessionId").doesNotExist()) .andExpect(jsonPath("$.thresholds.vehicleDistance").value(40.0)) - .andExpect(jsonPath("$.thresholds.aircraftDistance").value(40.0)); + .andExpect(jsonPath("$.thresholds.aircraftDistance").value(40.0)) + .andExpect(jsonPath("$.releaseDistance").value(40.0)); } @Test diff --git a/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java b/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java index baacd84..20fc146 100644 --- a/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java +++ b/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java @@ -27,6 +27,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import java.util.List; +import java.util.Map; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -533,7 +534,10 @@ class PathConflictDetectionDirectionalTest { ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(PathConflictAlertMessage.class); verify(webSocketPublisher, atLeastOnce()).publishPathConflictStatus(statusCaptor.capture()); - PathConflictAlertMessage status = statusCaptor.getValue(); + PathConflictAlertMessage status = statusCaptor.getAllValues().stream() + .filter(message -> "MONITORING".equals(message.getCalculationStatus())) + .findFirst() + .orElseThrow(); org.junit.jupiter.api.Assertions.assertEquals("PATH_CONFLICT_STATUS", status.getMessageType()); org.junit.jupiter.api.Assertions.assertEquals("MONITORING", status.getCalculationStatus()); org.junit.jupiter.api.Assertions.assertEquals(0.0, status.getConflictPoint().getLatitude()); @@ -839,6 +843,125 @@ class PathConflictDetectionDirectionalTest { org.junit.jupiter.api.Assertions.assertEquals(1, summary.eventsPublished()); } + @Test + void shouldKeepOverlapEntryPointLockedAfterAircraftPassesIt() throws Exception { + GeometryFactory geometryFactory = new GeometryFactory(); + + LineString vehicleRouteLine = geometryFactory.createLineString(new Coordinate[]{ + new Coordinate(0.0, 0.0), + new Coordinate(100.0, 0.0) + }); + LineString aircraftRouteLine = geometryFactory.createLineString(new Coordinate[]{ + new Coordinate(30.0, 50.0), + new Coordinate(30.0, 0.0), + new Coordinate(80.0, 0.0) + }); + + MovingObject vehicle = MovingObject.builder() + .objectId("UV-1") + .objectName("UV-1") + .objectType(MovingObjectType.UNMANNED_VEHICLE) + .currentPosition(geometryFactory.createPoint(new Coordinate(20.0, 0.0))) + .currentSpeed(20.0) + .currentHeading(90.0) + .altitude(0.0) + .build(); + MovingObject aircraftBeforeEntry = MovingObject.builder() + .objectId("AC-1") + .objectName("AC-1") + .objectType(MovingObjectType.AIRCRAFT) + .currentPosition(geometryFactory.createPoint(new Coordinate(30.0, 10.0))) + .currentSpeed(20.0) + .currentHeading(270.0) + .altitude(0.0) + .build(); + MovingObject aircraftAfterEntry = MovingObject.builder() + .objectId("AC-1") + .objectName("AC-1") + .objectType(MovingObjectType.AIRCRAFT) + .currentPosition(geometryFactory.createPoint(new Coordinate(50.0, 0.0))) + .currentSpeed(20.0) + .currentHeading(90.0) + .altitude(0.0) + .build(); + + ObjectRouteAssignment vehicleAssignment = ObjectRouteAssignment.builder() + .objectName("UV-1") + .objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE) + .assignedRouteId(1L) + .build(); + ObjectRouteAssignment aircraftAssignment = ObjectRouteAssignment.builder() + .objectName("AC-1") + .objectType(ObjectRouteAssignment.ObjectType.AIRCRAFT) + .assignedRouteId(2L) + .build(); + + TransportRoute vehicleRoute = TransportRoute.builder() + .id(1L) + .routeName("UV_ROUTE") + .routeType(TransportRoute.RouteType.UNMANNED_VEHICLE) + .status(TransportRoute.RouteStatus.ACTIVE) + .routeGeometry(vehicleRouteLine) + .build(); + TransportRoute aircraftRoute = TransportRoute.builder() + .id(2L) + .routeName("AC_ROUTE") + .routeType(TransportRoute.RouteType.AIRCRAFT) + .status(TransportRoute.RouteStatus.ACTIVE) + .routeGeometry(aircraftRouteLine) + .build(); + + when(platformRuntimeStateService.getCurrentTestSessionId()).thenReturn("session-overlap"); + when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc("UV-1", ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)) + .thenReturn(Optional.of(vehicleAssignment)); + when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc("AC-1", ObjectRouteAssignment.ObjectType.AIRCRAFT)) + .thenReturn(Optional.of(aircraftAssignment)); + when(routeRepository.findById(1L)).thenReturn(Optional.of(vehicleRoute)); + when(routeRepository.findById(2L)).thenReturn(Optional.of(aircraftRoute)); + when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble())) + .thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)}); + when(platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle()).thenReturn(20.0); + when(platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()).thenReturn(20.0); + when(conflictAlertLogRepository.save(any())).thenAnswer(invocation -> { + com.qaup.collision.pathconflict.model.entity.ConflictAlertLog logEntry = invocation.getArgument(0); + logEntry.setId(1L); + return logEntry; + }); + + pathConflictDetectionService.detectPathConflicts(List.of(vehicle, aircraftBeforeEntry)); + clearInvocations(vehicleCommandService); + clearInvocations(webSocketPublisher); + @SuppressWarnings("unchecked") + Map publishedStatusTimes = (Map) ReflectionTestUtils.getField( + pathConflictDetectionService, + "pathConflictStatusPublishedAt"); + publishedStatusTimes.clear(); + + PathConflictDetectionService.ConflictDetectionSummary summary = + pathConflictDetectionService.detectPathConflicts(List.of(vehicle, aircraftAfterEntry)); + + org.junit.jupiter.api.Assertions.assertEquals(1, summary.intersectionBehindPairs()); + org.junit.jupiter.api.Assertions.assertEquals(0, summary.thresholdNotReachedPairs()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> observationCaptor = + ArgumentCaptor.forClass(List.class); + verify(vehicleCommandService).synchronizeConflictCommands(any(), observationCaptor.capture()); + List observations = observationCaptor.getValue(); + org.junit.jupiter.api.Assertions.assertEquals(1, observations.size()); + org.junit.jupiter.api.Assertions.assertTrue(observations.get(0).aircraftIntersectionBehind()); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(PathConflictAlertMessage.class); + verify(webSocketPublisher, atLeastOnce()).publishPathConflictStatus(statusCaptor.capture()); + PathConflictAlertMessage status = statusCaptor.getValue(); + org.junit.jupiter.api.Assertions.assertEquals("INTERSECTION_BEHIND", status.getCalculationStatus()); + org.junit.jupiter.api.Assertions.assertEquals(20.0, status.getAircraftDistanceToConflictMeters(), 0.001); + org.junit.jupiter.api.Assertions.assertEquals( + -20.0, + (Double) ReflectionTestUtils.getField(status, "object2ForwardDistanceMeters"), + 0.001); + } + @Test void shouldPublishMonitoringStatusForValidConflictPointBeforeAlertThreshold() throws Exception { GeometryFactory geometryFactory = new GeometryFactory(); diff --git a/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/VehicleCommandServiceTest.java b/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/VehicleCommandServiceTest.java index 495d253..111887c 100644 --- a/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/VehicleCommandServiceTest.java +++ b/qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/VehicleCommandServiceTest.java @@ -160,6 +160,50 @@ class VehicleCommandServiceTest { )); } + @Test + void shouldUseReleaseDistanceInsteadOfAircraftAlertDistanceWhenResuming() { + runtimeStateService.updateVehicleRegistry(List.of( + new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN) + )); + runtimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(100.0); + 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")) + .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( diff --git a/qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java b/qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java index 5ba9a93..0667c24 100644 --- a/qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java +++ b/qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java @@ -63,6 +63,8 @@ class PlatformRuntimeStateServiceTest { assertEquals(40.0, service.updateCollisionDivergingReleaseDistance(55.0)); assertEquals(55.0, service.getCollisionDivergingReleaseDistance()); + assertEquals(40.0, service.getCollisionDivergingReleaseDistanceForVehicle()); + assertEquals(40.0, service.getCollisionDivergingReleaseDistanceForAircraft()); } @Test @@ -74,8 +76,9 @@ class PlatformRuntimeStateServiceTest { service.updateCollisionDivergingReleaseDistanceForVehicle(60.0); service.updateCollisionDivergingReleaseDistanceForAircraft(80.0); + service.updateCollisionDivergingReleaseDistance(25.0); - verify(sysConfigService, times(2)).insertConfig(any(SysConfig.class)); + verify(sysConfigService, times(3)).insertConfig(any(SysConfig.class)); } @Test @@ -84,12 +87,14 @@ class PlatformRuntimeStateServiceTest { ISysConfigService sysConfigService = mock(ISysConfigService.class); when(sysConfigService.selectConfigByKey("qaup.collision.crossing.vehicleDistance")).thenReturn("65"); when(sysConfigService.selectConfigByKey("qaup.collision.crossing.aircraftDistance")).thenReturn("85"); + when(sysConfigService.selectConfigByKey("qaup.collision.release.distance")).thenReturn("30"); ReflectionTestUtils.setField(service, "sysConfigService", sysConfigService); service.loadPersistedCrossingThresholds(); assertEquals(65.0, service.getCollisionDivergingReleaseDistanceForVehicle()); assertEquals(85.0, service.getCollisionDivergingReleaseDistanceForAircraft()); + assertEquals(30.0, service.getCollisionDivergingReleaseDistance()); } @Test