Improve collision conflict selection and release handling
This commit is contained in:
parent
43bf9488d6
commit
fb5badbaba
11
AGENTS.md
11
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`。
|
||||
|
||||
@ -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<String, Object> 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);
|
||||
|
||||
@ -56,6 +56,7 @@ public class PathConflictDetectionService {
|
||||
private CollisionTestSessionLogService collisionTestSessionLogService;
|
||||
|
||||
private final Map<String, RouteTravelDirection> routeTravelDirections = new ConcurrentHashMap<>();
|
||||
private final Map<String, Double> lockedConflictProgressMeters = new ConcurrentHashMap<>();
|
||||
private final Map<String, Long> 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<IntersectionProjection> 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<IntersectionProjection> selectLockedConflictPoint(
|
||||
MovingObject obj1,
|
||||
MovingObject obj2,
|
||||
TransportRoute route1,
|
||||
TransportRoute route2,
|
||||
ProjectedPosition projected1,
|
||||
ProjectedPosition projected2,
|
||||
RouteTravelDirection direction1,
|
||||
RouteTravelDirection direction2,
|
||||
List<IntersectionProjection> 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) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<String, VehicleRegistryType> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<PathConflictAlertMessage> 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<String, Long> publishedStatusTimes = (Map<String, Long>) 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<List<VehicleCommandService.ConflictRecoveryObservation>> observationCaptor =
|
||||
ArgumentCaptor.forClass(List.class);
|
||||
verify(vehicleCommandService).synchronizeConflictCommands(any(), observationCaptor.capture());
|
||||
List<VehicleCommandService.ConflictRecoveryObservation> observations = observationCaptor.getValue();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, observations.size());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(observations.get(0).aircraftIntersectionBehind());
|
||||
|
||||
ArgumentCaptor<PathConflictAlertMessage> 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();
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user