Add vehicle-route assignment endpoint and split distance config handling
This commit is contained in:
parent
9342f8a161
commit
20af9b8288
@ -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<Map<String, Object>> 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<Map<String, Object>> 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<double[]> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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<double[]> parseRoutePoints(JsonNode routeNode) {
|
||||
if (routeNode == null || !routeNode.isObject()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
JsonNode pointsNode = routeNode.get("points");
|
||||
return parsePointArray(pointsNode);
|
||||
}
|
||||
|
||||
private List<double[]> parsePointArray(JsonNode pointsNode) {
|
||||
if (pointsNode == null || !pointsNode.isArray() || pointsNode.size() < 2) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<double[]> 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<Map<String, Object>> invalidJson(JsonProcessingException e) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "error");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* 更新无人车的任务上下文信息
|
||||
|
||||
@ -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<VehicleRegistryEntry> 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<double[]> coordinates) {
|
||||
if (coordinates == null || coordinates.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Coordinate> 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<String> controllableVehicleIDs) {
|
||||
}
|
||||
|
||||
public record VehicleRouteSubmitResult(
|
||||
Long routeId,
|
||||
String routeName,
|
||||
Long assignmentId,
|
||||
String objectId,
|
||||
String objectType,
|
||||
String routeType) {
|
||||
}
|
||||
|
||||
public enum VehicleRegistryType {
|
||||
WUREN,
|
||||
TEQIN,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user