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 85bd65a..a419868 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 @@ -3,6 +3,8 @@ package com.qaup.collision.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment; +import com.qaup.collision.pathconflict.model.entity.TransportRoute; import com.qaup.collision.service.PlatformRuntimeStateService; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -11,8 +13,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.locationtech.jts.geom.LineString; import java.util.ArrayList; +import java.util.Locale; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -85,8 +89,84 @@ public class PlatformIntegrationController { @PostMapping(path = "/config/collision/diverging_release_distance", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> updateCollisionDivergingReleaseDistance(@RequestBody String requestBody) { - return updateNumericConfig(requestBody, "collision", "collision.diverging_release_distance", - platformRuntimeStateService::updateCollisionDivergingReleaseDistance); + return updateCollisionDistanceConfig(requestBody); + } + + @PostMapping(path = "/api/vehicle-route/assignment", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> submitVehicleRoute(@RequestBody String requestBody) { + try { + JsonNode root = objectMapper.readTree(requestBody); + if (!root.isObject()) { + return invalidFieldType("Request body must be a JSON object"); + } + + String objectId = getTextValue(root, "vehicleID"); + if (objectId == null) { + objectId = getTextValue(root, "objectName"); + } + if (objectId == null || objectId.isBlank()) { + return error(HttpStatus.BAD_REQUEST, "Missing field: vehicleID or objectName"); + } + + String objectTypeValue = getTextValue(root, "vehicleType"); + if (objectTypeValue == null) { + objectTypeValue = getTextValue(root, "objectType"); + } + ObjectRouteAssignment.ObjectType objectType = parseObjectType(objectTypeValue); + if (objectType == null) { + return error(HttpStatus.BAD_REQUEST, "Invalid or missing field: vehicleType/objectType"); + } + + JsonNode routeNode = root.get("route"); + List points = parseRoutePoints(routeNode); + if (points.isEmpty()) { + points = parsePointArray(root.get("points")); + } + if (points.isEmpty()) { + return error(HttpStatus.BAD_REQUEST, "Missing field: route.points (at least 2 points)"); + } + + LineString routeGeometry = platformRuntimeStateService.buildLineStringFromCoordinates(points); + if (routeGeometry == null) { + return error(HttpStatus.BAD_REQUEST, "Invalid route points"); + } + + Double maxSpeedKph = parseOptionalDouble(root.get("maxSpeedKph"), "maxSpeedKph"); + Double typicalSpeedKph = parseOptionalDouble(root.get("typicalSpeedKph"), "typicalSpeedKph"); + Boolean isBidirectional = parseOptionalBoolean(root.get("isBidirectional"), "isBidirectional"); + TransportRoute.RouteStatus status = parseRouteStatus(root.get("status")); + String routeName = getTextValue(root, "routeName"); + String description = getTextValue(root, "description"); + + var result = platformRuntimeStateService.submitVehicleRoute( + objectId, + objectType, + routeGeometry, + routeName, + maxSpeedKph, + typicalSpeedKph, + isBidirectional, + status, + description + ); + + Map payload = new LinkedHashMap<>(); + payload.put("status", "success"); + payload.put("objectId", objectId); + payload.put("objectType", objectType.name()); + payload.put("routeId", result.routeId()); + payload.put("routeName", result.routeName()); + payload.put("routeType", result.routeType()); + payload.put("assignmentId", result.assignmentId()); + payload.put("pointsCount", points.size()); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload); + } catch (JsonProcessingException e) { + return invalidJson(e); + } catch (IllegalArgumentException e) { + return error(HttpStatus.BAD_REQUEST, e.getMessage()); + } catch (Exception e) { + return internalError(e); + } } private void validateVehicleRegistryItem( @@ -179,6 +259,189 @@ public class PlatformIntegrationController { } } + private ResponseEntity> updateCollisionDistanceConfig(String requestBody) { + try { + JsonNode root = objectMapper.readTree(requestBody); + if (!root.isObject()) { + return invalidFieldType("Request body must be a JSON object"); + } + + JsonNode valueNode = root.get("value"); + JsonNode vehicleNode = root.get("vehicleDistance"); + JsonNode aircraftNode = root.get("aircraftDistance"); + + if ((vehicleNode == null || vehicleNode.isNull()) + && (aircraftNode == null || aircraftNode.isNull()) + && (valueNode == null || valueNode.isNull())) { + return error(HttpStatus.BAD_REQUEST, "Missing field: value"); + } + + if (vehicleNode != null && !vehicleNode.isNull() && !vehicleNode.isNumber()) { + return invalidFieldType("Field 'vehicleDistance' must be a number"); + } + if (aircraftNode != null && !aircraftNode.isNull() && !aircraftNode.isNumber()) { + return invalidFieldType("Field 'aircraftDistance' must be a number"); + } + if (valueNode != null && !valueNode.isNull() && !valueNode.isNumber()) { + return invalidFieldType("Field 'value' must be a number"); + } + + Double newVehicleDistance = null; + Double newAircraftDistance = null; + + if (vehicleNode != null && !vehicleNode.isNull()) { + if (!vehicleNode.isNumber()) { + return invalidFieldType("Field 'vehicleDistance' must be a number"); + } + newVehicleDistance = vehicleNode.asDouble(); + if (!Double.isFinite(newVehicleDistance) || newVehicleDistance <= 0) { + return error(HttpStatus.BAD_REQUEST, "Invalid value: must be a finite number greater than 0"); + } + } + + if (aircraftNode != null && !aircraftNode.isNull()) { + if (!aircraftNode.isNumber()) { + return invalidFieldType("Field 'aircraftDistance' must be a number"); + } + newAircraftDistance = aircraftNode.asDouble(); + if (!Double.isFinite(newAircraftDistance) || newAircraftDistance <= 0) { + return error(HttpStatus.BAD_REQUEST, "Invalid value: must be a finite number greater than 0"); + } + } + + if (valueNode != null && !valueNode.isNull() && vehicleNode == null && aircraftNode == null) { + double newValue = valueNode.asDouble(); + if (!Double.isFinite(newValue) || newValue <= 0) { + return error(HttpStatus.BAD_REQUEST, "Invalid value: must be a finite number greater than 0"); + } + double oldValue = platformRuntimeStateService.updateCollisionDivergingReleaseDistance(newValue); + Map payload = new LinkedHashMap<>(); + payload.put("status", "success"); + payload.put("area", "collision"); + payload.put("field", "collision.diverging_release_distance"); + payload.put("old", oldValue); + payload.put("new", newValue); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload); + } + + double oldVehicleDistance = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle(); + double oldAircraftDistance = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft(); + if (newVehicleDistance != null) { + oldVehicleDistance = platformRuntimeStateService.updateCollisionDivergingReleaseDistanceForVehicle(newVehicleDistance); + } + if (newAircraftDistance != null) { + oldAircraftDistance = platformRuntimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(newAircraftDistance); + } + + Map payload = new LinkedHashMap<>(); + payload.put("status", "success"); + payload.put("area", "collision"); + payload.put("field", "collision.diverging_release_distance"); + payload.put("oldVehicleDistance", oldVehicleDistance); + payload.put("newVehicleDistance", newVehicleDistance != null ? newVehicleDistance : platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle()); + payload.put("oldAircraftDistance", oldAircraftDistance); + payload.put("newAircraftDistance", newAircraftDistance != null ? newAircraftDistance : platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()); + payload.put("updatedVehicle", newVehicleDistance != null); + payload.put("updatedAircraft", newAircraftDistance != null); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload); + } catch (JsonProcessingException e) { + return invalidJson(e); + } catch (Exception e) { + return internalError(e); + } + } + + private static ObjectRouteAssignment.ObjectType parseObjectType(String objectTypeValue) { + if (objectTypeValue == null || objectTypeValue.isBlank()) { + return null; + } + String normalized = objectTypeValue.trim().toUpperCase(Locale.ROOT); + return switch (normalized) { + case "UNMANNED_VEHICLE", "UNMANNED", "UV", "WUREN" -> ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE; + case "SPECIAL_VEHICLE", "SPECIAL", "TEQIN", "SPECIALVEHICLE" -> ObjectRouteAssignment.ObjectType.SPECIAL_VEHICLE; + default -> null; + }; + } + + private List parseRoutePoints(JsonNode routeNode) { + if (routeNode == null || !routeNode.isObject()) { + return List.of(); + } + + JsonNode pointsNode = routeNode.get("points"); + return parsePointArray(pointsNode); + } + + private List parsePointArray(JsonNode pointsNode) { + if (pointsNode == null || !pointsNode.isArray() || pointsNode.size() < 2) { + return List.of(); + } + + List points = new ArrayList<>(); + for (JsonNode pointNode : pointsNode) { + if (!pointNode.isObject()) { + return List.of(); + } + Double lon = parseOptionalDouble(pointNode.get("lon"), "lon"); + if (lon == null) { + lon = parseOptionalDouble(pointNode.get("longitude"), "longitude"); + } + Double lat = parseOptionalDouble(pointNode.get("lat"), "lat"); + if (lat == null) { + lat = parseOptionalDouble(pointNode.get("latitude"), "latitude"); + } + if (lon == null || lat == null) { + return List.of(); + } + points.add(new double[]{lon, lat}); + } + return points; + } + + private static TransportRoute.RouteStatus parseRouteStatus(JsonNode statusNode) { + if (statusNode == null || statusNode.isNull() || !statusNode.isTextual()) { + return null; + } + try { + return TransportRoute.RouteStatus.valueOf(statusNode.asText().trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + throw new IllegalArgumentException("Field 'status' must be ACTIVE, INACTIVE or MAINTENANCE"); + } + } + + private static Double parseOptionalDouble(JsonNode node, String fieldName) { + if (node == null || node.isNull()) { + return null; + } + if (!node.isNumber()) { + throw new IllegalArgumentException("Field '" + fieldName + "' must be a number"); + } + double value = node.asDouble(); + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("Field '" + fieldName + "' must be finite"); + } + return value; + } + + private static Boolean parseOptionalBoolean(JsonNode node, String fieldName) { + if (node == null || node.isNull()) { + return null; + } + if (!node.isBoolean()) { + throw new IllegalArgumentException("Field '" + fieldName + "' must be boolean"); + } + return node.asBoolean(); + } + + private static String getTextValue(JsonNode root, String fieldName) { + JsonNode node = root.get(fieldName); + if (node == null || node.isNull() || !node.isTextual()) { + return null; + } + String value = node.asText(); + return value == null ? null : value.trim(); + } + private ResponseEntity> invalidJson(JsonProcessingException e) { Map payload = new LinkedHashMap<>(); payload.put("status", "error"); diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java index 878a79c..ebf23b0 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java @@ -52,6 +52,9 @@ import org.locationtech.jts.geom.PrecisionModel; @Service public class DataCollectorService { + private static final boolean VEHICLE_MANAGER_ROUTE_INGEST_ENABLED = false; + private static final java.util.concurrent.atomic.AtomicBoolean ROUTE_INGEST_DISABLED_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false); + // 机场数据源相关配置 @Value("${data.collector.airport-api.endpoints.vehicle}") private String airportVehicleEndpoint; @@ -337,7 +340,13 @@ public class DataCollectorService { case "VehicleSuspendReport" -> handleVehicleSuspendReport(dataNode); case "VehicleTailerNum" -> handleVehicleTailerNum(dataNode); case "VehiclePositionInfo" -> handleVehiclePositionInfo(dataNode); - case "NaviShortPathReport" -> handleVehiclePathInfo(dataNode); + case "NaviShortPathReport" -> { + if (VEHICLE_MANAGER_ROUTE_INGEST_ENABLED) { + handleVehiclePathInfo(dataNode); + } else { + logWarnRouteIngestionDisabled("NaviShortPathReport"); + } + } case "GetFmsMessage" -> handleVehicleFmsMessage(dataNode); default -> log.debug("未识别的车辆管理消息类型: {}", messageName); } @@ -738,8 +747,9 @@ public class DataCollectorService { .currentHeading(null) // 不在采集阶段计算方向 .altitude(0.0); // 默认高度 - // 提取任务上下文信息 - if (statusData.getMissionContext() != null) { + // Task context is disabled for route ingestion. + // Keep position and other realtime data updates, skip route/waypoint injection. + if (VEHICLE_MANAGER_ROUTE_INGEST_ENABLED && statusData.getMissionContext() != null) { MissionContextDTO missionContext = statusData.getMissionContext(); if (missionContext.getCurrentMission() != null) { @@ -818,6 +828,10 @@ public class DataCollectorService { // 缓存通用状态数据,供后续DataProcessingService推送 vehicle_status_update(如前端需要) cacheUniversalVehicleStatus(vehicleId, statusData); + if (!VEHICLE_MANAGER_ROUTE_INGEST_ENABLED && statusData != null && statusData.getMissionContext() != null + && statusData.getMissionContext().getWaypoints() != null) { + logWarnRouteIngestionDisabled("vehicle-manager:http:missionContext"); + } successCount++; log.debug("处理无人车完整状态数据并更新缓存: (车辆ID: {}, 位置: {}, {}, 任务ID: {}, 里程: {}米, 电量: {}%)", @@ -939,19 +953,27 @@ public class DataCollectorService { getUniversalStatusCache().put(cacheKey, cacheEntry); // 如果存在missionContext数据,更新到activeMovingObjectsCache中的无人车对象 - if (statusData.getMissionContext() != null) { + if (VEHICLE_MANAGER_ROUTE_INGEST_ENABLED && statusData.getMissionContext() != null) { log.info("========== 开始更新任务上下文 =========="); log.info("vehicleId: {}", vehicleId); log.info("missionContext: {}", statusData.getMissionContext()); log.info("waypoints: {}", statusData.getMissionContext().getWaypoints()); updateUnmannedVehicleMissionContext(vehicleId, statusData.getMissionContext()); log.info("========== 任务上下文更新完成 =========="); + } else if (statusData.getMissionContext() != null && statusData.getMissionContext().getWaypoints() != null) { + logWarnRouteIngestionDisabled("vehicle-manager:universal_status"); } log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}, 包含任务上下文: {}", vehicleId, cacheKey, statusData.getMissionContext() != null); } + private void logWarnRouteIngestionDisabled(String source) { + if (ROUTE_INGEST_DISABLED_LOGGED.compareAndSet(false, true)) { + log.warn("已硬编码关闭车辆管理来源路线/任务上下文入库:{}。前端手工提交路线将保留其来源优先,但车辆管理下行(NaviShortPathReport 与 missionContext路径点)暂不注入。", source); + } + } + /** * 更新无人车的任务上下文信息 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 af10bc7..73d03fd 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 @@ -1,8 +1,18 @@ package com.qaup.collision.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 com.qaup.collision.common.model.MovingObject; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.geom.GeometryFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collections; @@ -22,7 +32,17 @@ public class PlatformRuntimeStateService { private volatile double runwayWarningZoneRadiusAircraft; private volatile double runwayAlertZoneRadiusAircraft; - private volatile double collisionDivergingReleaseDistance; + private volatile double collisionDivergingReleaseDistanceForVehicle; + private volatile double collisionDivergingReleaseDistanceForAircraft; + 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"; + + @Autowired + private TransportRouteRepository transportRouteRepository; + + @Autowired + private ObjectRouteAssignmentRepository objectRouteAssignmentRepository; public PlatformRuntimeStateService( @Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft, @@ -31,7 +51,8 @@ public class PlatformRuntimeStateService { this.runwayWarningZoneRadiusAircraft = runwayWarningZoneRadiusAircraft; this.runwayAlertZoneRadiusAircraft = runwayAlertZoneRadiusAircraft; - this.collisionDivergingReleaseDistance = collisionDivergingReleaseDistance; + this.collisionDivergingReleaseDistanceForVehicle = collisionDivergingReleaseDistance; + this.collisionDivergingReleaseDistanceForAircraft = collisionDivergingReleaseDistance; } public VehicleRegistryUpdateResult updateVehicleRegistry(List entries) { @@ -130,15 +151,140 @@ public class PlatformRuntimeStateService { } public double getCollisionDivergingReleaseDistance() { - return collisionDivergingReleaseDistance; + return collisionDivergingReleaseDistanceForVehicle; } public double updateCollisionDivergingReleaseDistance(double newValue) { - double oldValue = collisionDivergingReleaseDistance; - collisionDivergingReleaseDistance = newValue; + double oldValue = collisionDivergingReleaseDistanceForVehicle; + collisionDivergingReleaseDistanceForVehicle = newValue; + collisionDivergingReleaseDistanceForAircraft = newValue; return oldValue; } + public double getCollisionDivergingReleaseDistanceForVehicle() { + return collisionDivergingReleaseDistanceForVehicle; + } + + public double getCollisionDivergingReleaseDistanceForAircraft() { + return collisionDivergingReleaseDistanceForAircraft; + } + + public double updateCollisionDivergingReleaseDistanceForVehicle(double newValue) { + double oldValue = collisionDivergingReleaseDistanceForVehicle; + collisionDivergingReleaseDistanceForVehicle = newValue; + return oldValue; + } + + public double updateCollisionDivergingReleaseDistanceForAircraft(double newValue) { + double oldValue = collisionDivergingReleaseDistanceForAircraft; + collisionDivergingReleaseDistanceForAircraft = newValue; + return oldValue; + } + + @Transactional + public VehicleRouteSubmitResult submitVehicleRoute( + String objectId, + ObjectRouteAssignment.ObjectType objectType, + LineString routeGeometry, + String routeName, + Double maxSpeedKph, + Double typicalSpeedKph, + Boolean isBidirectional, + TransportRoute.RouteStatus status, + String description) { + + if (objectType == null) { + throw new IllegalArgumentException("objectType is required"); + } + + if (routeGeometry == null) { + throw new IllegalArgumentException("routeGeometry is required"); + } + + TransportRoute.RouteType routeType = mapToRouteType(objectType); + if (routeType == null) { + throw new IllegalArgumentException("Unsupported objectType: " + objectType); + } + + String finalRouteName = normalizeRouteName(objectId, routeType, routeName); + TransportRoute route = transportRouteRepository.findByRouteNameAndRouteType(finalRouteName, routeType) + .map(existing -> { + existing.setRouteGeometry(routeGeometry); + existing.setDescription(description != null && !description.isBlank() ? description : DEFAULT_ROUTE_DESCRIPTION); + if (maxSpeedKph != null) { + existing.setMaxSpeedKph(maxSpeedKph); + } + if (typicalSpeedKph != null) { + existing.setTypicalSpeedKph(typicalSpeedKph); + } + existing.setUpdatedBy(DEFAULT_ROUTE_SOURCE); + existing.setStatus(status != null ? status : TransportRoute.RouteStatus.ACTIVE); + existing.setIsBidirectional(isBidirectional != null ? isBidirectional : Boolean.FALSE); + existing.setUpdatedAt(java.time.LocalDateTime.now()); + return existing; + }) + .orElseGet(() -> TransportRoute.builder() + .routeName(finalRouteName) + .routeType(routeType) + .description(description != null && !description.isBlank() ? description : DEFAULT_ROUTE_DESCRIPTION) + .routeGeometry(routeGeometry) + .maxSpeedKph(maxSpeedKph) + .typicalSpeedKph(typicalSpeedKph) + .status(status != null ? status : TransportRoute.RouteStatus.ACTIVE) + .isBidirectional(isBidirectional != null ? isBidirectional : Boolean.FALSE) + .createdBy(DEFAULT_ROUTE_SOURCE) + .updatedBy(DEFAULT_ROUTE_SOURCE) + .createdAt(java.time.LocalDateTime.now()) + .updatedAt(java.time.LocalDateTime.now()) + .build()); + + TransportRoute savedRoute = transportRouteRepository.save(route); + ObjectRouteAssignment assignment = ObjectRouteAssignment.builder() + .objectType(objectType) + .objectName(objectId) + .assignedRouteId(savedRoute.getId()) + .assignedAt(java.time.LocalDateTime.now()) + .build(); + ObjectRouteAssignment savedAssignment = objectRouteAssignmentRepository.save(assignment); + + return new VehicleRouteSubmitResult(savedRoute.getId(), savedRoute.getRouteName(), savedAssignment.getId(), objectId, objectType.name(), routeType.name()); + } + + public LineString buildLineStringFromCoordinates(List coordinates) { + if (coordinates == null || coordinates.size() < 2) { + return null; + } + + List coordinateList = new ArrayList<>(coordinates.size()); + for (double[] point : coordinates) { + if (point == null || point.length < 2) { + return null; + } + if (Double.isNaN(point[0]) || Double.isNaN(point[1]) || Double.isInfinite(point[0]) || Double.isInfinite(point[1])) { + return null; + } + coordinateList.add(new Coordinate(point[0], point[1])); + } + + return geometryFactory.createLineString(coordinateList.toArray(Coordinate[]::new)); + } + + private static String normalizeRouteName(String objectId, TransportRoute.RouteType routeType, String routeName) { + if (routeName != null && !routeName.isBlank()) { + return routeName; + } + String safeObjectId = objectId == null || objectId.isBlank() ? "UNKNOWN" : objectId; + return "MANUAL_" + routeType.name() + "_" + safeObjectId; + } + + private static TransportRoute.RouteType mapToRouteType(ObjectRouteAssignment.ObjectType objectType) { + return switch (objectType) { + case UNMANNED_VEHICLE -> TransportRoute.RouteType.UNMANNED_VEHICLE; + case SPECIAL_VEHICLE -> TransportRoute.RouteType.SPECIAL_VEHICLE; + default -> null; + }; + } + public record VehicleRegistryEntry(String vehicleID, VehicleRegistryType vehicleType) { } @@ -150,6 +296,15 @@ public class PlatformRuntimeStateService { List controllableVehicleIDs) { } + public record VehicleRouteSubmitResult( + Long routeId, + String routeName, + Long assignmentId, + String objectId, + String objectType, + String routeType) { + } + public enum VehicleRegistryType { WUREN, TEQIN, 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 ba75765..29c7aff 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 @@ -107,6 +107,34 @@ class PlatformIntegrationControllerTest { .andExpect(jsonPath("$.message").value("Invalid field type")); } + @Test + void shouldRejectVehicleDistanceUpdateWhenAircraftDistanceInvalidAndPreservePreviousValue() throws Exception { + mockMvc.perform(post("/config/collision/diverging_release_distance") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"vehicleDistance\":500,\"aircraftDistance\":\"oops\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Invalid field type")); + + mockMvc.perform(post("/config/collision/diverging_release_distance") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"vehicleDistance\":500}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.oldVehicleDistance").value(40.0)) + .andExpect(jsonPath("$.newVehicleDistance").value(500.0)); + } + + @Test + void shouldUpdateBothCollisionDistancesAtomically() throws Exception { + mockMvc.perform(post("/config/collision/diverging_release_distance") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"vehicleDistance\":500,\"aircraftDistance\":600}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.newVehicleDistance").value(500.0)) + .andExpect(jsonPath("$.newAircraftDistance").value(600.0)) + .andExpect(jsonPath("$.updatedVehicle").value(true)) + .andExpect(jsonPath("$.updatedAircraft").value(true)); + } + @Test void shouldRejectInvalidJson() throws Exception { mockMvc.perform(post("/config/runway/alert_zone_radius/aircraft") diff --git a/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceVehicleManagerRouteIngestionTest.java b/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceVehicleManagerRouteIngestionTest.java new file mode 100644 index 0000000..128d0c3 --- /dev/null +++ b/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceVehicleManagerRouteIngestionTest.java @@ -0,0 +1,74 @@ +package com.qaup.collision.datacollector.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qaup.collision.datacollector.dao.DataCollectorDao; +import com.qaup.collision.datacollector.filter.VehicleLocationFilter; +import com.qaup.collision.datacollector.sdk.AdxpFlightServiceHttpClient; +import com.qaup.collision.dataprocessing.service.DataProcessingService; +import com.qaup.collision.common.service.VehicleLocationService; +import com.qaup.collision.datacollector.config.VehicleManagerProperties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.lang.reflect.Method; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DataCollectorServiceVehicleManagerRouteIngestionTest { + + @Mock + private DataCollectorDao dataCollectorDao; + @Mock + private VehicleLocationService vehicleLocationService; + @Mock + private DataProcessingService dataProcessingService; + @Mock + private VehicleLocationFilter vehicleLocationFilter; + @Mock + private TrafficLightDataCollector trafficLightDataCollector; + @Mock + private AdxpFlightServiceHttpClient adxpFlightServiceClient; + @Mock + private com.qaup.collision.datacollector.websocket.AdxpFlightServiceWebSocketClient adxpFlightServiceWebSocketClient; + @Mock + private com.qaup.collision.datacollector.websocket.VehicleManagerWebSocketClient vehicleManagerWebSocketClient; + @Mock + private VehicleManagerProperties vehicleManagerProperties; + @Mock + private VehicleManagerCacheService vehicleManagerCacheService; + @Mock + private VehicleStatusAggregationService vehicleStatusAggregationService; + + @InjectMocks + private DataCollectorService dataCollectorService; + + @Test + void shouldNotPersistNaviShortPathReportWhenRouteIngestionDisabled() throws Exception { + ReflectionTestUtils.setField(dataCollectorService, "vehicleManagerCacheService", vehicleManagerCacheService); + ReflectionTestUtils.setField(dataCollectorService, "collectorDisabled", false); + Method handler = DataCollectorService.class.getDeclaredMethod("handleVehicleManagerMessage", JsonNode.class); + handler.setAccessible(true); + + JsonNode messageNode = new ObjectMapper().readTree(""" + { + "messageName": "NaviShortPathReport", + "data": { + "vehicleId": "V001", + "path": [] + } + } + """); + handler.invoke(dataCollectorService, messageNode); + + verify(vehicleManagerCacheService, never()).updateVehiclePath(eq("V001"), any()); + } +}