清理原有的无人车位置和状态接口。
This commit is contained in:
parent
243d5734f1
commit
c7a52469dd
@ -272,7 +272,8 @@ curl -X GET "https://api.example.com/api/v1/vehicles/AV-001/status?fields=vehicl
|
||||
"missionType": "CARGO_TRANSPORT",
|
||||
"startTime": 1736175000000,
|
||||
"estimatedEndTime": 1736178600000,
|
||||
"progress": 65.5
|
||||
"progress": 65.5,
|
||||
"totalMileage": 1250.8
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
@ -527,6 +528,7 @@ curl -X GET "https://api.example.com/api/v1/vehicles/AV-001/status?fields=vehicl
|
||||
- **startTime**: 开始时间戳 [可选]
|
||||
- **estimatedEndTime**: 预计结束时间戳 [可选]
|
||||
- **progress**: 任务进度百分比 [可选]
|
||||
- **totalMileage**: 累计行驶里程 (米) [可选]
|
||||
|
||||
#### 路径点 (waypoints) [可选]
|
||||
- **waypointId**: 路径点ID [可选]
|
||||
|
||||
@ -95,6 +95,20 @@
|
||||
- wifiStatus: CONNECTED | DISCONNECTED | FAULT
|
||||
- cloudConnectivity: ONLINE | OFFLINE | FAULT
|
||||
|
||||
10) missionContext.currentMission
|
||||
- missionId: string
|
||||
- missionType: string
|
||||
- startTime: number (ms, UTC)
|
||||
- estimatedEndTime: number (ms, UTC)
|
||||
- progress: number (0-100)
|
||||
- totalMileage: number (m)
|
||||
|
||||
11) missionContext.waypoints
|
||||
- waypointId: string
|
||||
- latitude: number
|
||||
- longitude: number
|
||||
- status: PENDING | COMPLETED | SKIPPED
|
||||
|
||||
---
|
||||
|
||||
## 3. 统一要求
|
||||
@ -213,6 +227,30 @@ curl -sS -X GET "${BASE_URL}/api/v1/vehicles/${VEHICLE_ID}/status" \
|
||||
"cellularSignalStrength": -65,
|
||||
"wifiStatus": "CONNECTED",
|
||||
"cloudConnectivity": "ONLINE"
|
||||
},
|
||||
"missionContext": {
|
||||
"currentMission": {
|
||||
"missionId": "MISSION_001",
|
||||
"missionType": "CARGO_TRANSPORT",
|
||||
"startTime": 1736175000000,
|
||||
"estimatedEndTime": 1736178600000,
|
||||
"progress": 65.5,
|
||||
"totalMileage": 1250.8
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "WP_001",
|
||||
"latitude": 36.354068,
|
||||
"longitude": 120.083410,
|
||||
"status": "COMPLETED"
|
||||
},
|
||||
{
|
||||
"waypointId": "WP_002",
|
||||
"latitude": 36.355123,
|
||||
"longitude": 120.084567,
|
||||
"status": "PENDING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -346,6 +384,46 @@ curl -sS -X GET "${BASE_URL}/api/v1/vehicles/${VEHICLE_ID}/status" \
|
||||
- communicationStatus.cloudConnectivity
|
||||
- 类型:enum(ONLINE|OFFLINE|FAULT);必填:是
|
||||
|
||||
- missionContext.currentMission.missionId
|
||||
- 类型:string;必填:是
|
||||
- 说明:当前任务的唯一标识符
|
||||
|
||||
- missionContext.currentMission.missionType
|
||||
- 类型:string;必填:是
|
||||
- 说明:任务类型,如 CARGO_TRANSPORT、PATROL_TRANSPORT 等
|
||||
|
||||
- missionContext.currentMission.startTime
|
||||
- 类型:number(ms,UTC);必填:是
|
||||
- 说明:任务开始时间
|
||||
|
||||
- missionContext.currentMission.estimatedEndTime
|
||||
- 类型:number(ms,UTC);必填:是
|
||||
- 说明:预计任务结束时间
|
||||
|
||||
- missionContext.currentMission.progress
|
||||
- 类型:number;必填:是;范围:0~100(%)
|
||||
- 说明:当前任务执行进度百分比
|
||||
|
||||
- missionContext.currentMission.totalMileage
|
||||
- 类型:number;必填:是;单位:m
|
||||
- 说明:累计行驶里程(米)
|
||||
|
||||
- missionContext.waypoints.waypointId
|
||||
- 类型:string;必填:是
|
||||
- 说明:路径点唯一标识符
|
||||
|
||||
- missionContext.waypoints.latitude
|
||||
- 类型:number;必填:是;单位:WGS84 度
|
||||
- 说明:路径点纬度
|
||||
|
||||
- missionContext.waypoints.longitude
|
||||
- 类型:number;必填:是;单位:WGS84 度
|
||||
- 说明:路径点经度
|
||||
|
||||
- missionContext.waypoints.status
|
||||
- 类型:enum(PENDING|COMPLETED|SKIPPED);必填:是
|
||||
- 说明:路径点状态,PENDING=待到达,COMPLETED=已完成,SKIPPED=已跳过
|
||||
|
||||
---
|
||||
|
||||
## 8. 兼容性与扩展
|
||||
|
||||
@ -192,11 +192,10 @@ data:
|
||||
username: dianxin
|
||||
password: dianxin@123
|
||||
vehicle-api:
|
||||
base-url: http://localhost:8090
|
||||
base-url: http://localhost:8091
|
||||
endpoints:
|
||||
vehicle-location: /api/VehicleLocationInfo
|
||||
vehicle-state: /api/VehicleStateInfo
|
||||
vehicle-command: /api/VehicleCommandInfo
|
||||
universal-status: /api/v1/vehicles/{vehicleId}/status
|
||||
timeout: 1000
|
||||
retry-attempts: 3
|
||||
unmanned-vehicle:
|
||||
|
||||
@ -221,8 +221,6 @@ data:
|
||||
vehicle-api:
|
||||
base-url: http://localhost:8091
|
||||
endpoints:
|
||||
vehicle-location: /api/VehicleLocationInfo
|
||||
vehicle-state: /api/VehicleStateInfo
|
||||
vehicle-command: /api/VehicleCommandInfo
|
||||
# 通用车辆状态API端点(符合universal_autonomous_vehicle_api规范)
|
||||
universal-status: /api/v1/vehicles/{vehicleId}/status
|
||||
|
||||
@ -45,6 +45,36 @@ public class UnmannedVehicle extends MovingObject {
|
||||
* 目标位置
|
||||
*/
|
||||
private Point targetPosition;
|
||||
|
||||
/**
|
||||
* 任务类型
|
||||
*/
|
||||
private String missionType;
|
||||
|
||||
/**
|
||||
* 任务开始时间戳
|
||||
*/
|
||||
private Long missionStartTime;
|
||||
|
||||
/**
|
||||
* 预计任务结束时间戳
|
||||
*/
|
||||
private Long estimatedEndTime;
|
||||
|
||||
/**
|
||||
* 任务进度百分比 (0-100)
|
||||
*/
|
||||
private Double progress;
|
||||
|
||||
/**
|
||||
* 累计行驶里程(米)
|
||||
*/
|
||||
private Double totalMileage;
|
||||
|
||||
/**
|
||||
* 路径点列表
|
||||
*/
|
||||
private java.util.List<WaypointInfo> waypoints;
|
||||
|
||||
/**
|
||||
* 无人车运行状态枚举
|
||||
@ -69,6 +99,29 @@ public class UnmannedVehicle extends MovingObject {
|
||||
PAUSED, // 暂停
|
||||
CANCELLED // 已取消
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径点状态枚举
|
||||
*/
|
||||
public enum WaypointStatus {
|
||||
PENDING, // 待到达
|
||||
COMPLETED, // 已完成
|
||||
SKIPPED // 已跳过
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径点信息
|
||||
*/
|
||||
@Data
|
||||
@lombok.Builder
|
||||
@lombok.NoArgsConstructor
|
||||
@lombok.AllArgsConstructor
|
||||
public static class WaypointInfo {
|
||||
private String waypointId;
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private WaypointStatus status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以接收新指令
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.qaup.collision.common.model.dto.Response;
|
||||
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
||||
import com.qaup.collision.datacollector.model.dto.VehicleCommand;
|
||||
import com.qaup.collision.datacollector.model.dto.VehicleStateInfo;
|
||||
import com.qaup.collision.datacollector.model.dto.VehicleStateRequest;
|
||||
import com.qaup.collision.datacollector.service.UnmannedVehicleControlService;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -15,7 +12,6 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 无人车控制接口控制器
|
||||
@ -65,68 +61,4 @@ public class UnmannedVehicleController {
|
||||
.body(Response.error("控制指令执行失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无人车位置上报接口
|
||||
* 查询指定车辆或所有无人车的当前位置信息
|
||||
*
|
||||
* @param vehicleId 车辆ID(可选,为空时返回所有无人车位置)
|
||||
* @return 车辆位置信息列表
|
||||
*/
|
||||
@GetMapping("/VehicleLocationInfo")
|
||||
public ResponseEntity<Response<List<VehicleLocation>>> getVehicleLocationInfo(
|
||||
@RequestParam(required = false) String vehicleId) {
|
||||
|
||||
logger.info("查询无人车位置信息: vehicleId={}", vehicleId);
|
||||
|
||||
try {
|
||||
List<VehicleLocation> locations = unmannedVehicleControlService.getVehicleLocations(vehicleId);
|
||||
|
||||
logger.info("查询无人车位置信息成功: vehicleId={}, count={}", vehicleId, locations.size());
|
||||
|
||||
return ResponseEntity.ok(Response.success("位置信息查询成功", locations));
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询无人车位置信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
|
||||
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Response.error("位置信息查询失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无人车状态查询接口
|
||||
* 查询指定车辆的状态信息
|
||||
*
|
||||
* @param request 状态查询请求对象
|
||||
* @return 车辆状态信息
|
||||
*/
|
||||
@PostMapping("/VehicleStateInfo")
|
||||
public ResponseEntity<Response<VehicleStateInfo>> getVehicleStateInfo(
|
||||
@Valid @RequestBody VehicleStateRequest request) {
|
||||
|
||||
logger.info("查询无人车状态信息: vehicleId={}", request.getVehicleId());
|
||||
|
||||
try {
|
||||
// 将Long类型的vehicleId转换为String类型
|
||||
VehicleStateInfo vehicleState = unmannedVehicleControlService.getVehicleState(String.valueOf(request.getVehicleId()));
|
||||
|
||||
if (vehicleState != null) {
|
||||
logger.info("查询无人车状态信息成功: vehicleId={}", request.getVehicleId());
|
||||
|
||||
return ResponseEntity.ok(Response.success("状态信息查询成功", vehicleState));
|
||||
} else {
|
||||
logger.warn("未找到指定车辆的状态信息: vehicleId={}", request.getVehicleId());
|
||||
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Response.error("未找到指定车辆的状态信息"));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询无人车状态信息失败: vehicleId={}, error={}", request.getVehicleId(), e.getMessage(), e);
|
||||
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Response.error("状态信息查询失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,6 @@ package com.qaup.collision.datacollector.dao;
|
||||
|
||||
import com.qaup.collision.common.model.Aircraft;
|
||||
import com.qaup.collision.common.model.AirportVehicle;
|
||||
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.common.model.dto.Response;
|
||||
import com.qaup.collision.datacollector.service.AuthService;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
@ -53,10 +51,6 @@ public class DataCollectorDao {
|
||||
@Value("${data.collector.vehicle-api.base-url}")
|
||||
private String vehicleBaseUrl;
|
||||
|
||||
@Value("${data.collector.vehicle-api.endpoints.vehicle-location}")
|
||||
private String vehicleLocationEndpoint;
|
||||
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final AuthService authService;
|
||||
private final GeometryFactory geometryFactory;
|
||||
@ -163,61 +157,6 @@ public class DataCollectorDao {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取无人车位置信息
|
||||
* 数据来源:第2章 无人车位置上报 (/api/VehicleLocationInfo)
|
||||
* 说明:此接口专门用于获取无人车的实时位置数据,用于数据持久化
|
||||
*
|
||||
* @return 无人车位置信息列表
|
||||
*/
|
||||
public List<UnmannedVehicle> getVehicleLocationInfo() {
|
||||
try {
|
||||
String url = UriComponentsBuilder
|
||||
.fromUriString(vehicleBaseUrl)
|
||||
.path(vehicleLocationEndpoint)
|
||||
.toUriString();
|
||||
|
||||
log.debug("正在从 {} 获取无人车位置信息", url);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<List<ExternalUnmannedVehicleData>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<List<ExternalUnmannedVehicleData>>() {}
|
||||
);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
List<ExternalUnmannedVehicleData> rawDataList = response.getBody();
|
||||
if (rawDataList != null) {
|
||||
List<UnmannedVehicle> processedUnmannedVehicles = rawDataList.stream().map(rawData -> {
|
||||
if (rawData.getVehicleID() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
|
||||
log.warn("原始无人车数据缺失必要字段 (vehicleID, longitude, latitude),跳过处理: {}", rawData);
|
||||
return null;
|
||||
}
|
||||
UnmannedVehicle unmannedVehicle = new UnmannedVehicle();
|
||||
unmannedVehicle.setObjectId(rawData.getVehicleID());
|
||||
unmannedVehicle.setObjectName(rawData.getVehicleID()); // 使用 vehicleID 作为 objectName
|
||||
unmannedVehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
|
||||
unmannedVehicle.setCurrentSpeed(rawData.getSpeed() != null ? rawData.getSpeed() * 3.6 : 0.0); // m/s to km/h
|
||||
unmannedVehicle.setCurrentHeading(rawData.getDirection() != null ? Math.toDegrees(rawData.getDirection()) : 0.0); // 弧度 to 角度
|
||||
unmannedVehicle.setAltitude(0.0); // 假设默认高度为0,如果API有提供可以设置
|
||||
unmannedVehicle.setObjectType(MovingObjectType.UNMANNED_VEHICLE); // 显式设置对象类型
|
||||
return unmannedVehicle;
|
||||
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
log.info("成功获取无人车定位信息,数量: {}", processedUnmannedVehicles.size());
|
||||
return processedUnmannedVehicles;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取无人车定位信息时发生异常", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 内部 DTOs,精确匹配外部 API 接口返回的 JSON 结构
|
||||
|
||||
@ -252,26 +191,6 @@ public class DataCollectorDao {
|
||||
private Long time;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static class ExternalUnmannedVehicleData {
|
||||
@JsonProperty("transId")
|
||||
private String transId;
|
||||
@JsonProperty("timestamp")
|
||||
private Long timestamp;
|
||||
@JsonProperty("vehicleID")
|
||||
private String vehicleID;
|
||||
@JsonProperty("latitude")
|
||||
private Double latitude;
|
||||
@JsonProperty("longitude")
|
||||
private Double longitude;
|
||||
@JsonProperty("speed")
|
||||
private Double speed;
|
||||
@JsonProperty("direction")
|
||||
private Double direction;
|
||||
}
|
||||
|
||||
// 新增航空器路由和状态数据采集方法
|
||||
|
||||
@ -576,12 +495,15 @@ public class DataCollectorDao {
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> apiResponse = response.getBody();
|
||||
|
||||
if (apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
// 检查 apiResponse 是否为 null,避免空指针异常
|
||||
if (apiResponse != null && apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
log.debug("成功获取车辆 {} 的通用状态数据", vehicleId);
|
||||
return apiResponse.getData();
|
||||
} else {
|
||||
log.warn("API返回错误: 车辆={}, code={}, message={}",
|
||||
vehicleId, apiResponse.getCode(), apiResponse.getMessage());
|
||||
vehicleId,
|
||||
apiResponse != null ? apiResponse.getCode() : null,
|
||||
apiResponse != null ? apiResponse.getMessage() : "响应为空");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务上下文DTO
|
||||
* 符合universal_autonomous_vehicle_api规范的任务上下文数据结构
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class MissionContextDTO {
|
||||
|
||||
/**
|
||||
* 当前任务信息
|
||||
*/
|
||||
private CurrentMissionDTO currentMission;
|
||||
|
||||
/**
|
||||
* 路径点列表
|
||||
*/
|
||||
private List<WaypointDTO> waypoints;
|
||||
|
||||
/**
|
||||
* 当前任务DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class CurrentMissionDTO {
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
private String missionId;
|
||||
|
||||
/**
|
||||
* 任务类型
|
||||
*/
|
||||
private String missionType;
|
||||
|
||||
/**
|
||||
* 开始时间戳 (ms, UTC)
|
||||
*/
|
||||
private Long startTime;
|
||||
|
||||
/**
|
||||
* 预计结束时间戳 (ms, UTC)
|
||||
*/
|
||||
private Long estimatedEndTime;
|
||||
|
||||
/**
|
||||
* 任务进度百分比 (0-100)
|
||||
*/
|
||||
private Double progress;
|
||||
|
||||
/**
|
||||
* 累计行驶里程(米)
|
||||
*/
|
||||
private Double totalMileage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径点DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class WaypointDTO {
|
||||
/**
|
||||
* 路径点ID
|
||||
*/
|
||||
private String waypointId;
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 状态 (PENDING, COMPLETED, SKIPPED)
|
||||
*/
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@ -68,4 +68,11 @@ public class UniversalVehicleStatusDTO {
|
||||
* 包含:v2xStatus, cellularSignalStrength, wifiStatus, cloudConnectivity
|
||||
*/
|
||||
private CommunicationStatusDTO communicationStatus;
|
||||
|
||||
/**
|
||||
* 任务上下文字段组
|
||||
* 包含:currentMission (missionId, missionType, startTime, estimatedEndTime, progress, totalMileage),
|
||||
* waypoints (waypointId, latitude, longitude, status)
|
||||
*/
|
||||
private MissionContextDTO missionContext;
|
||||
}
|
||||
@ -5,15 +5,12 @@ import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.common.model.Aircraft;
|
||||
import com.qaup.collision.common.model.AirportVehicle;
|
||||
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.common.model.AircraftRoute;
|
||||
import com.qaup.collision.common.service.VehicleLocationService;
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.datacollector.dto.AircraftStatusDTO;
|
||||
import com.qaup.collision.datacollector.dto.FlightNotificationDTO;
|
||||
import com.qaup.collision.common.model.FlightNotification;
|
||||
import com.qaup.collision.datacollector.model.dto.MissionContextDTO;
|
||||
import com.qaup.collision.datacollector.filter.VehicleLocationFilter;
|
||||
import com.qaup.collision.websocket.event.AircraftRouteUpdateEvent;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
@ -23,12 +20,10 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -36,7 +31,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||
|
||||
/**
|
||||
* 数据采集服务 - 重构版本
|
||||
@ -71,8 +65,6 @@ public class DataCollectorService {
|
||||
@Value("${data.collector.detection.interval:1000}")
|
||||
private long detectionInterval;
|
||||
|
||||
@Value("${data.collector.route.cache-expiry-hours:2}")
|
||||
private int routeCacheExpiryHours;
|
||||
|
||||
@Value("${data.collector.route.periodic-collection-enabled:false}")
|
||||
private boolean periodicRouteCollectionEnabled;
|
||||
@ -83,18 +75,9 @@ public class DataCollectorService {
|
||||
@Autowired
|
||||
private VehicleLocationService vehicleLocationService;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Autowired
|
||||
private com.qaup.collision.dataprocessing.service.DataProcessingService dataProcessingService; // 注入数据处理服务
|
||||
|
||||
@Autowired
|
||||
private RouteGeometryProcessor routeGeometryProcessor; // 注入路由几何处理器
|
||||
|
||||
@Autowired
|
||||
private RoutePersistenceService routePersistenceService; // 注入路由持久化服务
|
||||
|
||||
@Autowired
|
||||
private VehicleLocationFilter vehicleLocationFilter; // 注入车辆位置过滤器
|
||||
|
||||
@ -106,8 +89,6 @@ public class DataCollectorService {
|
||||
// 用于缓存所有活跃的MovingObject的最新状态
|
||||
private final Map<String, MovingObject> activeMovingObjectsCache = new ConcurrentHashMap<>();
|
||||
|
||||
// 路由获取状态缓存:Key=flightNo:type:time, Value=获取时间戳
|
||||
private final Map<String, Long> routeRetrievalCache = new ConcurrentHashMap<>();
|
||||
|
||||
// 航班通知缓存:Key=flightNo:type, Value=最新的航班通知
|
||||
private final Map<String, FlightNotification> flightNotificationCache = new ConcurrentHashMap<>();
|
||||
@ -119,99 +100,6 @@ public class DataCollectorService {
|
||||
log.info("DataCollectorService 初始化完成,缓存引用已传递给DataProcessingService");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将AircraftRouteDTO转换为AircraftRoute对象
|
||||
* 使用JTS将多个LineString段合并为单一连续路径
|
||||
*/
|
||||
private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) {
|
||||
if (routeDTO == null || routeDTO.getGeoPath() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建路由段列表
|
||||
List<AircraftRoute.RouteSegment> routeSegments = new ArrayList<>();
|
||||
List<org.locationtech.jts.geom.LineString> lineStringSegments = new ArrayList<>();
|
||||
|
||||
if (routeDTO.getGeoPath().getFeatures() != null) {
|
||||
for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) {
|
||||
if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) {
|
||||
// 转换坐标格式:从List<List<Double>>到List<Point>
|
||||
List<org.locationtech.jts.geom.Point> points = feature.getGeometry().getCoordinates().stream()
|
||||
.map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1))))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
// 创建路由段
|
||||
AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder()
|
||||
.code(feature.getProperties() != null ? feature.getProperties().getCode() : "")
|
||||
.coordinates(points)
|
||||
.build();
|
||||
routeSegments.add(segment);
|
||||
|
||||
// 创建LineString段用于合并
|
||||
if (points.size() >= 2) {
|
||||
org.locationtech.jts.geom.Coordinate[] coords = points.stream()
|
||||
.map(point -> point.getCoordinate())
|
||||
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
|
||||
org.locationtech.jts.geom.LineString lineString = geometryFactory.createLineString(coords);
|
||||
lineStringSegments.add(lineString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用JTS将多个LineString段合并为单一连续路径
|
||||
org.locationtech.jts.geom.LineString mergedGeometry = null;
|
||||
if (!lineStringSegments.isEmpty()) {
|
||||
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
|
||||
|
||||
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
|
||||
// 可选:简化路径以减少冗余点(容差:1米)
|
||||
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
|
||||
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
|
||||
lineStringSegments.size(), mergedGeometry.getNumPoints());
|
||||
} else {
|
||||
log.warn("路由段合并失败或结果无效");
|
||||
mergedGeometry = null;
|
||||
}
|
||||
}
|
||||
|
||||
return AircraftRoute.builder()
|
||||
.type(routeDTO.getType())
|
||||
.status(routeDTO.getStatus())
|
||||
.codes(routeDTO.getCodes())
|
||||
.geometry(mergedGeometry) // 使用合并后的单一LineString
|
||||
.routeSegments(routeSegments)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换航空器路由数据失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存航空器路由到数据库(独立于缓存)
|
||||
*/
|
||||
private void saveAircraftRouteToDatabase(String flightNo, AircraftRoute route) {
|
||||
try {
|
||||
log.info("开始保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
|
||||
boolean saveSuccess = routePersistenceService.saveAircraftRoute(flightNo, route);
|
||||
|
||||
if (saveSuccess) {
|
||||
log.info("成功保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
|
||||
|
||||
// TODO: 保存到路由分配表
|
||||
// saveRouteAssignment(flightNo, route);
|
||||
|
||||
} else {
|
||||
log.warn("保存航空器路由到数据库失败: flightNo={}, type={}", flightNo, route.getType());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存航空器路由到数据库异常: flightNo={}, type={}", flightNo, route.getType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -233,10 +121,6 @@ public class DataCollectorService {
|
||||
|
||||
log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size());
|
||||
|
||||
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
|
||||
|
||||
// 航空器数据仅用于实时处理,不存储到数据库
|
||||
// 数据处理完成后发布WebSocket事件进行实时推送
|
||||
for (Aircraft aircraft : newAircrafts) {
|
||||
try {
|
||||
if (aircraft.getCurrentPosition() == null) {
|
||||
@ -259,7 +143,6 @@ public class DataCollectorService {
|
||||
.altitude(aircraft.getAltitude())
|
||||
.build();
|
||||
|
||||
// 将最新数据更新到缓存(不发送WebSocket消息,统一在周期性检测中发送)
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
log.debug("处理航空器数据并更新缓存: (航班号: {}, 位置: {}, {}, 速度: {})",
|
||||
@ -272,24 +155,6 @@ public class DataCollectorService {
|
||||
log.error("处理航空器数据异常: objectId={}", aircraft.getObjectId(), e); // Changed from flightNo
|
||||
}
|
||||
}
|
||||
|
||||
// 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!activeMovingObjects.isEmpty()) {
|
||||
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
|
||||
// }
|
||||
|
||||
// 执行实时违规检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// for (MovingObject movingObject : activeMovingObjects) {
|
||||
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
|
||||
// if (tempVehicleLocation != null) {
|
||||
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
|
||||
// if (!violations.isEmpty()) {
|
||||
// log.warn("检测到航空器违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
|
||||
// // 如果需要,可以在这里进一步处理这些违规事件,例如持久化或发送告警
|
||||
// // realTimeViolationDetector.processBatchViolationEvents(violations);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
log.info("航空器数据处理和事件发布完成,处理数量: {}", newAircrafts.size());
|
||||
|
||||
@ -376,24 +241,6 @@ public class DataCollectorService {
|
||||
log.error("处理机场车辆数据异常: objectId={}", vehicle.getObjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!activeMovingObjects.isEmpty()) {
|
||||
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
|
||||
// }
|
||||
|
||||
// 执行实时违规检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// for (MovingObject movingObject : activeMovingObjects) {
|
||||
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
|
||||
// if (tempVehicleLocation != null) {
|
||||
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
|
||||
// if (!violations.isEmpty()) {
|
||||
// log.warn("检测到机场车辆违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
|
||||
// // 如果需要,可以在这里进一步处理这些违规事件,例如持久化或发送告警
|
||||
// // realTimeViolationDetector.processBatchViolationEvents(violations);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", filteredVehicles.size());
|
||||
|
||||
@ -404,13 +251,13 @@ public class DataCollectorService {
|
||||
|
||||
/**
|
||||
* 定时采集无人车数据 (外部API)
|
||||
*
|
||||
* 数据来源:第2章 无人车位置上报 (/api/VehicleLocationInfo)
|
||||
* 说明:仅传递目前无人车平台已接入的无人车位置数据
|
||||
*
|
||||
* 数据来源:通用无人车状态API (/api/v1/vehicles/{vehicleId}/status)
|
||||
* 说明:采集完整的无人车状态数据,包含位置和任务上下文信息
|
||||
* 重构说明:
|
||||
* - 无人车数据仅用于实时处理,不存储到数据库
|
||||
* - 数据采集后直接用于碰撞检测等实时计算
|
||||
* - 不进行数据持久化
|
||||
* - 使用通用状态API替代基础位置API,获取更完整的数据
|
||||
* - 包含missionContext任务信息,支持里程和路径点数据
|
||||
* - 数据仅用于实时处理,不存储到数据库
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
@Async // 异步执行
|
||||
@ -418,88 +265,150 @@ public class DataCollectorService {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
List<UnmannedVehicle> unmannedVehicles = dataCollectorDao.getVehicleLocationInfo();
|
||||
if (unmannedVehicles.isEmpty()) {
|
||||
log.debug("未获取到无人车数据");
|
||||
// 获取当前活跃的无人车列表(从之前的缓存或配置中获取)
|
||||
java.util.Set<String> knownVehicleIds = getKnownUnmannedVehicleIds();
|
||||
|
||||
if (knownVehicleIds.isEmpty()) {
|
||||
log.debug("当前没有已知的无人车ID,跳过数据采集");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("采集到 {} 条无人车数据", unmannedVehicles.size());
|
||||
|
||||
// 将采集到的无人车数据转换为MovingObject并添加到缓存
|
||||
// List<VehicleLocation> unmannedVehicleLocations = new ArrayList<>(); // 移除此行
|
||||
|
||||
for (UnmannedVehicle unmannedVehicle : unmannedVehicles) {
|
||||
log.info("开始采集 {} 辆无人车的完整状态数据", knownVehicleIds.size());
|
||||
int successCount = 0;
|
||||
|
||||
for (String vehicleId : knownVehicleIds) {
|
||||
try {
|
||||
if (unmannedVehicle.getCurrentPosition() == null) {
|
||||
log.warn("无人车 {} 位置信息缺失,跳过处理。", unmannedVehicle.getObjectId());
|
||||
continue; // 跳过此无人车
|
||||
// 调用通用状态API获取完整数据
|
||||
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
|
||||
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
|
||||
|
||||
if (statusData == null || statusData.getMotionStatus() == null) {
|
||||
log.debug("无人车 {} 未返回有效状态数据,跳过处理", vehicleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
|
||||
unmannedVehicle.getCurrentPosition().getX(),
|
||||
unmannedVehicle.getCurrentPosition().getY()
|
||||
));
|
||||
// 提取位置信息
|
||||
if (statusData.getMotionStatus().getPosition() == null) {
|
||||
log.warn("无人车 {} 位置信息缺失,跳过处理", vehicleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 数据采集阶段:只采集和缓存位置数据,不进行任何计算
|
||||
MovingObject movingObject = MovingObject.builder()
|
||||
.objectId(unmannedVehicle.getObjectId())
|
||||
Double latitude = statusData.getMotionStatus().getPosition().getLatitude();
|
||||
Double longitude = statusData.getMotionStatus().getPosition().getLongitude();
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
log.warn("无人车 {} 经纬度信息缺失,跳过处理", vehicleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude));
|
||||
|
||||
// 创建增强的UnmannedVehicle对象
|
||||
var vehicleBuilder = UnmannedVehicle.builder()
|
||||
.objectId(vehicleId)
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(unmannedVehicle.getObjectName())
|
||||
.objectName(vehicleId)
|
||||
.currentPosition(currentPosition)
|
||||
.currentSpeed(null) // 不在采集阶段计算速度
|
||||
.currentHeading(null) // 不在采集阶段计算方向
|
||||
.altitude(unmannedVehicle.getAltitude())
|
||||
.build();
|
||||
.altitude(0.0); // 默认高度
|
||||
|
||||
log.debug("在DataCollectorService中,MovingObject的速度: {}", movingObject.getCurrentSpeed());
|
||||
// 提取任务上下文信息
|
||||
if (statusData.getMissionContext() != null) {
|
||||
MissionContextDTO missionContext = statusData.getMissionContext();
|
||||
|
||||
// 注意:电子围栏检测已移至周期性检测方法中,以降低检测频率
|
||||
// 仅在此处更新缓存,实际检测在performPeriodicViolationDetection()中执行
|
||||
if (missionContext.getCurrentMission() != null) {
|
||||
MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission();
|
||||
vehicleBuilder
|
||||
.missionId(currentMission.getMissionId())
|
||||
.missionType(currentMission.getMissionType())
|
||||
.missionStartTime(currentMission.getStartTime())
|
||||
.estimatedEndTime(currentMission.getEstimatedEndTime())
|
||||
.progress(currentMission.getProgress())
|
||||
.totalMileage(currentMission.getTotalMileage());
|
||||
}
|
||||
|
||||
// 将最新数据更新到缓存(不保存到数据库,统一在周期性检测中保存)
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
log.debug("处理无人车数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
unmannedVehicle.getObjectId(),
|
||||
// 提取路径点信息
|
||||
if (missionContext.getWaypoints() != null) {
|
||||
java.util.List<UnmannedVehicle.WaypointInfo> waypoints = missionContext.getWaypoints().stream()
|
||||
.map(wp -> UnmannedVehicle.WaypointInfo.builder()
|
||||
.waypointId(wp.getWaypointId())
|
||||
.latitude(wp.getLatitude())
|
||||
.longitude(wp.getLongitude())
|
||||
.status(UnmannedVehicle.WaypointStatus.valueOf(wp.getStatus()))
|
||||
.build())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
vehicleBuilder.waypoints(waypoints);
|
||||
}
|
||||
}
|
||||
|
||||
// 提取电池信息
|
||||
if (statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null) {
|
||||
Double chargeLevel = statusData.getBatteryStatus().getMainBattery().getChargeLevel();
|
||||
if (chargeLevel != null) {
|
||||
vehicleBuilder.batteryLevel(chargeLevel.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置车辆运行状态
|
||||
if (statusData.getOperationalStatus() != null) {
|
||||
String systemHealth = statusData.getOperationalStatus().getSystemHealth();
|
||||
if ("HEALTHY".equals(systemHealth)) {
|
||||
vehicleBuilder.vehicleStatus(UnmannedVehicle.VehicleStatus.WORKING);
|
||||
} else {
|
||||
vehicleBuilder.vehicleStatus(UnmannedVehicle.VehicleStatus.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
UnmannedVehicle enhancedUnmannedVehicle = vehicleBuilder.build();
|
||||
|
||||
// 将最新数据更新到缓存
|
||||
activeMovingObjectsCache.put(enhancedUnmannedVehicle.getObjectId(), enhancedUnmannedVehicle);
|
||||
successCount++;
|
||||
|
||||
log.debug("处理无人车完整状态数据并更新缓存: (车辆ID: {}, 位置: {}, {}, 任务ID: {}, 里程: {}米, 电量: {}%)",
|
||||
vehicleId,
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
enhancedUnmannedVehicle.getMissionId(),
|
||||
enhancedUnmannedVehicle.getTotalMileage(),
|
||||
enhancedUnmannedVehicle.getBatteryLevel());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理无人车数据异常: objectId={}", unmannedVehicle.getObjectId(), e);
|
||||
log.error("处理无人车状态数据异常: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!unmannedVehicleLocations.isEmpty()) {
|
||||
// pathConflictDetectionService.detectPathConflicts(unmannedVehicleLocations.stream()
|
||||
// .map(v -> MovingObject.builder()
|
||||
// .objectId(String.valueOf(v.getVehicleId()))
|
||||
// .objectType(v.getVehicleType())
|
||||
// .objectName(v.getVehicleLicense())
|
||||
// .currentPosition(v.getLocation())
|
||||
// .currentSpeed(v.getSpeed())
|
||||
// .currentHeading(v.getHeading())
|
||||
// .altitude(v.getAltitude())
|
||||
// .build())
|
||||
// .collect(Collectors.toList()));
|
||||
// }
|
||||
|
||||
// 执行实时违规检测 (临时移除,将在新的定时任务中统一处理)
|
||||
// if (!unmannedVehicleLocations.isEmpty()) {
|
||||
// realTimeViolationDetector.detectBatchViolations(unmannedVehicleLocations);
|
||||
// }
|
||||
|
||||
log.info("无人车数据处理和事件发布完成,处理数量: {}", unmannedVehicles.size());
|
||||
log.info("无人车完整状态数据采集完成,成功处理: {}/{}", successCount, knownVehicleIds.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集无人车数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已知的无人车ID列表
|
||||
* 可以从配置、缓存或其他数据源获取
|
||||
*/
|
||||
private java.util.Set<String> getKnownUnmannedVehicleIds() {
|
||||
java.util.Set<String> vehicleIds = new java.util.HashSet<>();
|
||||
|
||||
// 从当前缓存中获取已存在的无人车ID
|
||||
activeMovingObjectsCache.values().stream()
|
||||
.filter(obj -> obj.getObjectType() == MovingObjectType.UNMANNED_VEHICLE)
|
||||
.forEach(obj -> vehicleIds.add(obj.getObjectId()));
|
||||
|
||||
// 如果缓存为空,使用默认的无人车ID(与mock服务对应)
|
||||
if (vehicleIds.isEmpty()) {
|
||||
vehicleIds.add("鲁B567");
|
||||
vehicleIds.add("鲁B579");
|
||||
}
|
||||
|
||||
return vehicleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时采集通用车辆状态数据 (符合universal_autonomous_vehicle_api规范)
|
||||
*
|
||||
@ -567,14 +476,14 @@ public class DataCollectorService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存通用车辆状态数据
|
||||
* 缓存通用无人车状态数据
|
||||
* 将状态数据存储到专门的缓存中供DataProcessingService处理
|
||||
* 同时将missionContext信息更新到activeMovingObjectsCache中的无人车对象
|
||||
*/
|
||||
private void cacheUniversalVehicleStatus(String vehicleId, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData) {
|
||||
// 使用专门的缓存存储通用状态数据
|
||||
// 注意:这里不影响activeMovingObjectsCache中的位置数据,两者独立存储
|
||||
String cacheKey = "universal_status_" + vehicleId;
|
||||
|
||||
|
||||
// 创建带时间戳的状态数据包装器
|
||||
UniversalVehicleStatusCacheEntry cacheEntry = UniversalVehicleStatusCacheEntry.builder()
|
||||
.vehicleId(vehicleId)
|
||||
@ -582,12 +491,63 @@ public class DataCollectorService {
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.dataSource("COLLECTOR")
|
||||
.build();
|
||||
|
||||
// 存储到缓存中(这里简化为使用activeMovingObjectsCache的扩展,实际可以使用Redis等)
|
||||
// 临时解决方案:使用一个Map来存储状态数据
|
||||
|
||||
// 存储到通用状态缓存中
|
||||
getUniversalStatusCache().put(cacheKey, cacheEntry);
|
||||
|
||||
log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}", vehicleId, cacheKey);
|
||||
|
||||
// 如果存在missionContext数据,更新到activeMovingObjectsCache中的无人车对象
|
||||
if (statusData.getMissionContext() != null) {
|
||||
updateUnmannedVehicleMissionContext(vehicleId, statusData.getMissionContext());
|
||||
}
|
||||
|
||||
log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}, 包含任务上下文: {}",
|
||||
vehicleId, cacheKey, statusData.getMissionContext() != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新无人车的任务上下文信息
|
||||
*/
|
||||
private void updateUnmannedVehicleMissionContext(String vehicleId, MissionContextDTO missionContext) {
|
||||
MovingObject existingObject = activeMovingObjectsCache.get(vehicleId);
|
||||
|
||||
if (existingObject instanceof UnmannedVehicle) {
|
||||
UnmannedVehicle unmannedVehicle = (UnmannedVehicle) existingObject;
|
||||
|
||||
// 更新任务上下文信息
|
||||
if (missionContext.getCurrentMission() != null) {
|
||||
MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission();
|
||||
unmannedVehicle.setMissionId(currentMission.getMissionId());
|
||||
unmannedVehicle.setMissionType(currentMission.getMissionType());
|
||||
unmannedVehicle.setMissionStartTime(currentMission.getStartTime());
|
||||
unmannedVehicle.setEstimatedEndTime(currentMission.getEstimatedEndTime());
|
||||
unmannedVehicle.setProgress(currentMission.getProgress());
|
||||
unmannedVehicle.setTotalMileage(currentMission.getTotalMileage());
|
||||
}
|
||||
|
||||
// 更新路径点信息
|
||||
if (missionContext.getWaypoints() != null) {
|
||||
java.util.List<UnmannedVehicle.WaypointInfo> waypoints = missionContext.getWaypoints().stream()
|
||||
.map(wp -> UnmannedVehicle.WaypointInfo.builder()
|
||||
.waypointId(wp.getWaypointId())
|
||||
.latitude(wp.getLatitude())
|
||||
.longitude(wp.getLongitude())
|
||||
.status(UnmannedVehicle.WaypointStatus.valueOf(wp.getStatus()))
|
||||
.build())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
unmannedVehicle.setWaypoints(waypoints);
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
activeMovingObjectsCache.put(vehicleId, unmannedVehicle);
|
||||
|
||||
log.debug("更新无人车任务上下文: vehicleId={}, 任务ID={}, 里程={}米, 路径点数量={}",
|
||||
vehicleId,
|
||||
unmannedVehicle.getMissionId(),
|
||||
unmannedVehicle.getTotalMileage(),
|
||||
unmannedVehicle.getWaypoints() != null ? unmannedVehicle.getWaypoints().size() : 0);
|
||||
} else {
|
||||
log.debug("缓存中未找到无人车对象或类型不匹配,跳过任务上下文更新: vehicleId={}", vehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
// 用于存储通用车辆状态数据的缓存
|
||||
@ -614,8 +574,6 @@ public class DataCollectorService {
|
||||
private String dataSource;
|
||||
}
|
||||
|
||||
// 移除周期性处理方法 - 现在由DataProcessingService处理
|
||||
|
||||
/**
|
||||
* 统计当前数据采集服务的健康状态和关键指标
|
||||
* @return 统计信息字符串
|
||||
@ -674,13 +632,9 @@ public class DataCollectorService {
|
||||
notification.getSeat(),
|
||||
notification.getEventDateTime());
|
||||
|
||||
// 缓存航班通知供DataProcessingService推送给前端
|
||||
// 缓存航班通知供DataProcessingService处理
|
||||
cacheFlightNotification(notification);
|
||||
|
||||
// 🚀 新增:事件驱动的路由查询
|
||||
// 基于航班通知触发路由查询和更新
|
||||
triggerRouteQueryByFlightNotification(dto);
|
||||
|
||||
} else {
|
||||
log.warn("⚠️ 航班进出港通知数据无效,跳过处理: {}", dto);
|
||||
}
|
||||
@ -757,211 +711,6 @@ public class DataCollectorService {
|
||||
return flightNotificationCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定航班事件的路由是否已经获取过
|
||||
*
|
||||
* @param flightNo 航班号
|
||||
* @param type 路由类型(IN/OUT)
|
||||
* @param time 事件时间戳
|
||||
* @return true表示已获取过,false表示未获取过
|
||||
*/
|
||||
private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) {
|
||||
String cacheKey = flightNo + ":" + type + ":" + time;
|
||||
return routeRetrievalCache.containsKey(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定航班事件的路由已获取
|
||||
*
|
||||
* @param flightNo 航班号
|
||||
* @param type 路由类型(IN/OUT)
|
||||
* @param time 事件时间戳
|
||||
*/
|
||||
private void markRouteAsRetrieved(String flightNo, String type, Long time) {
|
||||
String cacheKey = flightNo + ":" + type + ":" + time;
|
||||
routeRetrievalCache.put(cacheKey, System.currentTimeMillis());
|
||||
log.debug("标记路由已获取: cacheKey={}, 当前缓存数量={}", cacheKey, routeRetrievalCache.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理过期的路由获取缓存记录
|
||||
*/
|
||||
@Scheduled(fixedRate = 3600000) // 每小时执行一次
|
||||
public void cleanExpiredRouteCache() {
|
||||
if (routeRetrievalCache.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long expiryTime = System.currentTimeMillis() - (routeCacheExpiryHours * 3600 * 1000L);
|
||||
int sizeBefore = routeRetrievalCache.size();
|
||||
|
||||
routeRetrievalCache.entrySet().removeIf(entry -> entry.getValue() < expiryTime);
|
||||
|
||||
int sizeAfter = routeRetrievalCache.size();
|
||||
if (sizeBefore != sizeAfter) {
|
||||
log.info("清理过期路由缓存完成:清理前{}条,清理后{}条,清理了{}条记录",
|
||||
sizeBefore, sizeAfter, sizeBefore - sizeAfter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于航班通知触发路由查询
|
||||
*
|
||||
* 实现事件驱动的路由更新流程:
|
||||
* 1. 收到航班进出港通知
|
||||
* 2. 使用航班号和类型查询路由参数
|
||||
* 3. 调用路由查询接口获取完整路由数据
|
||||
* 4. 发布WebSocket路由更新事件
|
||||
*
|
||||
* @param flightNotification 航班进出港通知
|
||||
*/
|
||||
private void triggerRouteQueryByFlightNotification(FlightNotificationDTO flightNotification) {
|
||||
try {
|
||||
String flightNo = flightNotification.getFlightNo();
|
||||
String routeType = flightNotification.getType();
|
||||
|
||||
// 检查是否已获取过该航班事件的路由
|
||||
if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getTime())) {
|
||||
log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}",
|
||||
flightNo, routeType, flightNotification.getTime());
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getTime());
|
||||
|
||||
// 步骤1: 查询航班路由参数
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams =
|
||||
dataCollectorDao.getAircraftRouteParams(flightNo, routeType);
|
||||
|
||||
if (routeParams == null || !routeParams.isValid()) {
|
||||
log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}",
|
||||
routeParams.getFlightNo(),
|
||||
routeParams.getInRunway(),
|
||||
routeParams.getOutRunway(),
|
||||
routeParams.getContactCross(),
|
||||
routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat());
|
||||
|
||||
// 步骤2: 基于路由参数调用相应的路由查询接口
|
||||
AircraftRouteDTO routeData = null;
|
||||
|
||||
if (routeParams.isArrivalRoute()) {
|
||||
// 进港路由查询
|
||||
routeData = dataCollectorDao.getArrivalRoute(
|
||||
routeParams.getInRunway(),
|
||||
routeParams.getOutRunway(),
|
||||
routeParams.getContactCross(),
|
||||
routeParams.getSeat()
|
||||
);
|
||||
log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}",
|
||||
routeParams.getInRunway(), routeParams.getOutRunway(),
|
||||
routeParams.getContactCross(), routeParams.getSeat());
|
||||
} else if (routeParams.isDepartureRoute()) {
|
||||
// 出港路由查询
|
||||
routeData = dataCollectorDao.getDepartureRoute(
|
||||
routeParams.getInRunway(),
|
||||
routeParams.getOutRunway(),
|
||||
routeParams.getStartSeat()
|
||||
);
|
||||
log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}",
|
||||
routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat());
|
||||
}
|
||||
|
||||
// 步骤3: 处理路由查询结果
|
||||
if (routeData != null) {
|
||||
log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}",
|
||||
routeData.getType(), routeData.getCodes(), routeData.getStatus());
|
||||
|
||||
// 转换DTO为航空器路由对象
|
||||
AircraftRoute aircraftRoute = convertToAircraftRoute(routeData);
|
||||
|
||||
if (aircraftRoute != null) {
|
||||
// 保存路由到数据库
|
||||
saveAircraftRouteToDatabase(flightNo, aircraftRoute);
|
||||
|
||||
// 更新缓存中的航空器路由信息
|
||||
updateAircraftRouteInCacheFromNotification(flightNo, aircraftRoute, routeParams);
|
||||
|
||||
// 发布WebSocket路由更新事件
|
||||
publishAircraftRouteUpdateEventFromNotification(flightNo, aircraftRoute, routeParams);
|
||||
|
||||
// 标记该航班事件的路由已获取
|
||||
markRouteAsRetrieved(flightNo, routeType, flightNotification.getTime());
|
||||
|
||||
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getTime());
|
||||
} else {
|
||||
log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo);
|
||||
}
|
||||
} else {
|
||||
log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 航班通知触发路由查询异常: flightNo={}",
|
||||
flightNotification.getFlightNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于航班通知更新缓存中的航空器路由信息
|
||||
*/
|
||||
private void updateAircraftRouteInCacheFromNotification(String flightNo, AircraftRoute route,
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
|
||||
MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo);
|
||||
|
||||
if (cachedAircraft != null && cachedAircraft instanceof Aircraft) {
|
||||
Aircraft aircraft = (Aircraft) cachedAircraft;
|
||||
|
||||
// 根据路由类型更新路由
|
||||
if (routeParams.isArrivalRoute()) {
|
||||
aircraft.setArrivalRoute(route);
|
||||
aircraft.activateArrivalRoute();
|
||||
} else if (routeParams.isDepartureRoute()) {
|
||||
aircraft.setDepartureRoute(route);
|
||||
aircraft.activateDepartureRoute();
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
activeMovingObjectsCache.put(flightNo, aircraft);
|
||||
|
||||
log.debug("✅ 成功更新航空器路由缓存 (事件驱动): flightNo={}, type={}, codes={}",
|
||||
flightNo, route.getType(), route.getCodes());
|
||||
} else {
|
||||
log.debug("🔍 缓存中暂无航空器对象,跳过缓存更新 (事件驱动): flightNo={}", flightNo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于航班通知发布航空器路由更新事件
|
||||
*/
|
||||
private void publishAircraftRouteUpdateEventFromNotification(String flightNo, AircraftRoute route,
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
|
||||
try {
|
||||
// 创建路由更新事件
|
||||
AircraftRouteUpdateEvent routeUpdateEvent = AircraftRouteUpdateEvent.builder()
|
||||
.flightNo(flightNo)
|
||||
.routeType(route.getType())
|
||||
.routeStatus(route.getStatus())
|
||||
.routeCodes(route.getCodes())
|
||||
.aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型
|
||||
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.eventSource("FLIGHT_NOTIFICATION") // 标识事件来源
|
||||
.build();
|
||||
|
||||
// 发布WebSocket事件
|
||||
eventPublisher.publishEvent(routeUpdateEvent);
|
||||
|
||||
log.info("📡 发布航空器路由更新事件 (事件驱动): 航班号={}, 路由类型={}, 事件来源=航班通知",
|
||||
flightNo, route.getType());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 发布航空器路由更新事件失败 (事件驱动): flightNo={}", flightNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
|
||||
@ -4,7 +4,6 @@ import com.qaup.collision.common.model.AircraftRoute;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -234,11 +233,4 @@ public class RoutePersistenceService {
|
||||
objectName, routeType, routeId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证LineString的有效性
|
||||
*/
|
||||
private boolean isValidLineString(LineString lineString) {
|
||||
return lineString != null && lineString.isValid() && lineString.getNumPoints() >= 2;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ package com.qaup.collision.datacollector.service;
|
||||
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
||||
import com.qaup.collision.datacollector.model.converter.VehicleCommandConverter;
|
||||
import com.qaup.collision.datacollector.model.dto.VehicleCommand;
|
||||
import com.qaup.collision.datacollector.model.dto.VehicleStateInfo;
|
||||
import com.qaup.collision.datacollector.model.entity.VehicleCommandEntity;
|
||||
import com.qaup.collision.datacollector.repository.VehicleCommandRepository;
|
||||
import com.qaup.collision.common.model.repository.VehicleLocationRepository;
|
||||
@ -22,7 +21,6 @@ import org.springframework.web.client.RestTemplate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 无人车控制服务类
|
||||
@ -59,11 +57,7 @@ public class UnmannedVehicleControlService {
|
||||
@Value("${data.collector.vehicle-api.endpoints.vehicle-command:#{null}}")
|
||||
private String vehicleCommandEndpoint;
|
||||
|
||||
@Value("${data.collector.vehicle-api.endpoints.vehicle-location}")
|
||||
private String vehicleLocationEndpoint;
|
||||
|
||||
@Value("${data.collector.vehicle-api.endpoints.vehicle-state:#{null}}")
|
||||
private String vehicleStateEndpoint;
|
||||
|
||||
/**
|
||||
* 处理无人车控制指令
|
||||
@ -155,29 +149,90 @@ public class UnmannedVehicleControlService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取无人车状态信息
|
||||
*
|
||||
* 获取完整的无人车状态信息 (符合universal_autonomous_vehicle_api规范)
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @return 车辆状态信息
|
||||
* @return 完整的无人车状态数据
|
||||
*/
|
||||
public VehicleStateInfo getVehicleState(String vehicleId) {
|
||||
logger.info("查询无人车状态信息: vehicleId={}", vehicleId);
|
||||
@Transactional(readOnly = true)
|
||||
public com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO getCompleteVehicleStatus(String vehicleId) {
|
||||
logger.info("查询无人车完整状态信息: vehicleId={}", vehicleId);
|
||||
|
||||
try {
|
||||
// 如果配置了外部状态查询API,则调用外部系统
|
||||
if (vehicleStateEndpoint != null && !vehicleStateEndpoint.trim().isEmpty()) {
|
||||
return queryVehicleStateFromExternalSystem(vehicleId);
|
||||
// 直接调用DataCollectorDao的通用状态API
|
||||
com.qaup.collision.datacollector.dao.DataCollectorDao dataCollectorDao =
|
||||
applicationContext.getBean(com.qaup.collision.datacollector.dao.DataCollectorDao.class);
|
||||
|
||||
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
|
||||
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
|
||||
|
||||
if (statusData != null) {
|
||||
logger.info("成功获取无人车完整状态: vehicleId={}, 电池电量={}%",
|
||||
vehicleId,
|
||||
statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null
|
||||
? statusData.getBatteryStatus().getMainBattery().getChargeLevel() : "未知");
|
||||
return statusData;
|
||||
} else {
|
||||
// 否则基于本地数据构造状态信息
|
||||
return buildVehicleStateFromLocalData(vehicleId);
|
||||
logger.warn("未能获取无人车完整状态: vehicleId={}", vehicleId);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询无人车状态信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
|
||||
throw new RuntimeException("状态信息查询失败: " + e.getMessage(), e);
|
||||
logger.error("查询无人车完整状态信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
|
||||
throw new RuntimeException("完整状态信息查询失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取无人车完整状态信息
|
||||
*
|
||||
* @param vehicleIds 车辆ID列表
|
||||
* @return 车辆状态映射表 (vehicleId -> 状态数据)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public java.util.Map<String, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> getBatchVehicleStatus(List<String> vehicleIds) {
|
||||
logger.info("批量查询无人车完整状态信息: vehicleIds={}", vehicleIds);
|
||||
|
||||
java.util.Map<String, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> statusMap = new java.util.HashMap<>();
|
||||
|
||||
if (vehicleIds == null || vehicleIds.isEmpty()) {
|
||||
logger.warn("车辆ID列表为空");
|
||||
return statusMap;
|
||||
}
|
||||
|
||||
try {
|
||||
com.qaup.collision.datacollector.dao.DataCollectorDao dataCollectorDao =
|
||||
applicationContext.getBean(com.qaup.collision.datacollector.dao.DataCollectorDao.class);
|
||||
|
||||
int successCount = 0;
|
||||
for (String vehicleId : vehicleIds) {
|
||||
try {
|
||||
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
|
||||
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
|
||||
|
||||
if (statusData != null) {
|
||||
statusMap.put(vehicleId, statusData);
|
||||
successCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("获取单个车辆状态失败: vehicleId={}, error={}", vehicleId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("批量查询无人车状态完成: 总数={}, 成功={}, 失败={}",
|
||||
vehicleIds.size(), successCount, vehicleIds.size() - successCount);
|
||||
|
||||
return statusMap;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("批量查询无人车完整状态信息失败: vehicleIds={}, error={}", vehicleIds, e.getMessage(), e);
|
||||
throw new RuntimeException("批量状态信息查询失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private org.springframework.context.ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* 验证控制指令参数
|
||||
*/
|
||||
@ -238,107 +293,5 @@ public class UnmannedVehicleControlService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从外部系统查询车辆状态
|
||||
*/
|
||||
private VehicleStateInfo queryVehicleStateFromExternalSystem(String vehicleId) {
|
||||
try {
|
||||
String url = vehicleApiBaseUrl + vehicleStateEndpoint;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
// 构造查询请求参数
|
||||
VehicleStateQueryRequest queryRequest = new VehicleStateQueryRequest();
|
||||
queryRequest.setTransId(UUID.randomUUID().toString());
|
||||
queryRequest.setTimestamp(System.currentTimeMillis());
|
||||
queryRequest.setVehicleId(vehicleId);
|
||||
queryRequest.setSingle(true);
|
||||
|
||||
HttpEntity<VehicleStateQueryRequest> requestEntity = new HttpEntity<>(queryRequest, headers);
|
||||
|
||||
ResponseEntity<VehicleStateInfo[]> response = restTemplate.exchange(
|
||||
url, HttpMethod.POST, requestEntity, VehicleStateInfo[].class);
|
||||
|
||||
|
||||
if (response != null && response.getStatusCode().is2xxSuccessful()) {
|
||||
VehicleStateInfo[] body = response.getBody();
|
||||
if (body != null && body.length > 0) {
|
||||
VehicleStateInfo stateInfo = body[0];
|
||||
logger.info("从外部系统获取车辆状态成功: vehicleId={}", vehicleId);
|
||||
return stateInfo;
|
||||
} else {
|
||||
logger.warn("外部系统未返回车辆状态: vehicleId={}", vehicleId);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
logger.warn("外部系统调用失败: vehicleId={}, status={}", vehicleId, response != null ? response.getStatusCode() : "无响应");
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("从外部系统查询车辆状态异常: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于本地数据构造车辆状态信息
|
||||
*/
|
||||
private VehicleStateInfo buildVehicleStateFromLocalData(String vehicleId) {
|
||||
// 查询最近的位置信息判断车辆是否在线
|
||||
try {
|
||||
Long vehicleIdLong = Long.parseLong(vehicleId);
|
||||
Optional<VehicleLocation> recentLocation = vehicleLocationRepository.findLatestByVehicleId(vehicleIdLong);
|
||||
|
||||
boolean isOnline = recentLocation.isPresent();
|
||||
|
||||
// 构造基本状态信息
|
||||
VehicleStateInfo stateInfo = new VehicleStateInfo();
|
||||
stateInfo.setTransId(UUID.randomUUID().toString());
|
||||
stateInfo.setTimestamp(System.currentTimeMillis());
|
||||
stateInfo.setVehicleId(vehicleIdLong); // 使用Long类型
|
||||
stateInfo.setLoginStatus(isOnline);
|
||||
stateInfo.setFaultInfo(List.of()); // 空故障列表
|
||||
stateInfo.setActiveSafety(false);
|
||||
stateInfo.setRc(false);
|
||||
stateInfo.setCommand(0); // 0表示恢复状态
|
||||
stateInfo.setAirportInfo(List.of());
|
||||
stateInfo.setVehicleMode(isOnline ? 2 : 5); // 2:自动, 5:故障等待
|
||||
stateInfo.setGearState(2); // 2:D档
|
||||
stateInfo.setChassisReady(isOnline);
|
||||
stateInfo.setCollisionStatus(false);
|
||||
stateInfo.setClearance(0);
|
||||
stateInfo.setTurnSignalStatus(0);
|
||||
stateInfo.setPointCloud(List.of());
|
||||
|
||||
logger.info("基于本地数据构造车辆状态: vehicleId={}, isOnline={}", vehicleId, isOnline);
|
||||
return stateInfo;
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("无效的vehicleId格式: {}", vehicleId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 车辆状态查询请求内部类
|
||||
*/
|
||||
private static class VehicleStateQueryRequest {
|
||||
private String transId;
|
||||
private long timestamp;
|
||||
private String vehicleId;
|
||||
private boolean single;
|
||||
|
||||
public String getTransId() { return transId; }
|
||||
public void setTransId(String transId) { this.transId = transId; }
|
||||
|
||||
public long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
|
||||
|
||||
public String getVehicleId() { return vehicleId; }
|
||||
public void setVehicleId(String vehicleId) { this.vehicleId = vehicleId; }
|
||||
|
||||
public boolean isSingle() { return single; }
|
||||
public void setSingle(boolean single) { this.single = single; }
|
||||
}
|
||||
}
|
||||
@ -32,8 +32,13 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.common.model.AircraftRoute;
|
||||
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
@ -503,6 +508,9 @@ public class DataProcessingService {
|
||||
|
||||
log.info("📡 发布航班进出港通知WebSocket事件: 航班号={}, 事件类型={}, 通知级别={}",
|
||||
event.getFlightNo(), event.getEventType(), event.getNotificationLevel());
|
||||
|
||||
// 触发基于航班通知的路由查询和更新处理
|
||||
triggerRouteQueryByFlightNotification(notification);
|
||||
} else {
|
||||
log.warn("⚠️ 航班进出港通知WebSocket事件创建失败或无效: {}", notification.getFlightNo());
|
||||
}
|
||||
@ -977,4 +985,287 @@ public class DataProcessingService {
|
||||
log.error("🚦 发布红绿灯状态事件失败: intersectionId={}", payload.getIntersectionId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 航班路由处理相关方法 ====================
|
||||
|
||||
// 路由获取状态缓存:Key=flightNo:type:time, Value=获取时间戳
|
||||
private final Map<String, Long> routeRetrievalCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 基于航班通知触发路由查询
|
||||
*
|
||||
* 实现事件驱动的路由更新流程:
|
||||
* 1. 收到航班进出港通知
|
||||
* 2. 使用航班号和类型查询路由参数
|
||||
* 3. 调用路由查询接口获取完整路由数据
|
||||
* 4. 发布WebSocket路由更新事件
|
||||
*
|
||||
* @param flightNotification 航班进出港通知
|
||||
*/
|
||||
private void triggerRouteQueryByFlightNotification(FlightNotification flightNotification) {
|
||||
try {
|
||||
String flightNo = flightNotification.getFlightNo();
|
||||
String routeType = flightNotification.getType().name();
|
||||
|
||||
// 检查是否已获取过该航班事件的路由
|
||||
if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getEventTime())) {
|
||||
log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}",
|
||||
flightNo, routeType, flightNotification.getEventTime());
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime());
|
||||
|
||||
// 获取DataCollectorDao进行路由数据查询
|
||||
DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class);
|
||||
|
||||
// 步骤1: 查询航班路由参数
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams =
|
||||
dataCollectorDao.getAircraftRouteParams(flightNo, routeType);
|
||||
|
||||
if (routeParams == null || !routeParams.isValid()) {
|
||||
log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}",
|
||||
routeParams.getFlightNo(),
|
||||
routeParams.getInRunway(),
|
||||
routeParams.getOutRunway(),
|
||||
routeParams.getContactCross(),
|
||||
routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat());
|
||||
|
||||
// 步骤2: 基于路由参数调用相应的路由查询接口
|
||||
AircraftRouteDTO routeData = null;
|
||||
|
||||
if (routeParams.isArrivalRoute()) {
|
||||
// 进港路由查询
|
||||
routeData = dataCollectorDao.getArrivalRoute(
|
||||
routeParams.getInRunway(),
|
||||
routeParams.getOutRunway(),
|
||||
routeParams.getContactCross(),
|
||||
routeParams.getSeat()
|
||||
);
|
||||
log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}",
|
||||
routeParams.getInRunway(), routeParams.getOutRunway(),
|
||||
routeParams.getContactCross(), routeParams.getSeat());
|
||||
} else if (routeParams.isDepartureRoute()) {
|
||||
// 出港路由查询
|
||||
routeData = dataCollectorDao.getDepartureRoute(
|
||||
routeParams.getInRunway(),
|
||||
routeParams.getOutRunway(),
|
||||
routeParams.getStartSeat()
|
||||
);
|
||||
log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}",
|
||||
routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat());
|
||||
}
|
||||
|
||||
// 步骤3: 处理路由查询结果
|
||||
if (routeData != null) {
|
||||
log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}",
|
||||
routeData.getType(), routeData.getCodes(), routeData.getStatus());
|
||||
|
||||
// 转换DTO为航空器路由对象
|
||||
AircraftRoute aircraftRoute = convertToAircraftRoute(routeData);
|
||||
|
||||
if (aircraftRoute != null) {
|
||||
// 保存路由到数据库
|
||||
saveAircraftRouteToDatabase(flightNo, aircraftRoute);
|
||||
|
||||
// 更新缓存中的航空器路由信息
|
||||
updateAircraftRouteInCache(flightNo, aircraftRoute, routeParams);
|
||||
|
||||
// 发布WebSocket路由更新事件
|
||||
publishAircraftRouteUpdateEvent(flightNo, aircraftRoute, routeParams);
|
||||
|
||||
// 标记该航班事件的路由已获取
|
||||
markRouteAsRetrieved(flightNo, routeType, flightNotification.getEventTime());
|
||||
|
||||
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime());
|
||||
} else {
|
||||
log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo);
|
||||
}
|
||||
} else {
|
||||
log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 航班通知触发路由查询异常: flightNo={}",
|
||||
flightNotification.getFlightNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定航班事件的路由是否已经获取过
|
||||
*/
|
||||
private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) {
|
||||
String cacheKey = flightNo + ":" + type + ":" + time;
|
||||
return routeRetrievalCache.containsKey(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定航班事件的路由已获取
|
||||
*/
|
||||
private void markRouteAsRetrieved(String flightNo, String type, Long time) {
|
||||
String cacheKey = flightNo + ":" + type + ":" + time;
|
||||
routeRetrievalCache.put(cacheKey, System.currentTimeMillis());
|
||||
log.debug("标记路由已获取: cacheKey={}, 当前缓存数量={}", cacheKey, routeRetrievalCache.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将AircraftRouteDTO转换为AircraftRoute对象
|
||||
* 使用JTS将多个LineString段合并为单一连续路径
|
||||
*/
|
||||
private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) {
|
||||
if (routeDTO == null || routeDTO.getGeoPath() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取路由几何处理器
|
||||
com.qaup.collision.datacollector.util.RouteGeometryProcessor routeGeometryProcessor =
|
||||
applicationContext.getBean(com.qaup.collision.datacollector.util.RouteGeometryProcessor.class);
|
||||
|
||||
// 创建路由段列表
|
||||
List<AircraftRoute.RouteSegment> routeSegments = new ArrayList<>();
|
||||
List<org.locationtech.jts.geom.LineString> lineStringSegments = new ArrayList<>();
|
||||
|
||||
if (routeDTO.getGeoPath().getFeatures() != null) {
|
||||
for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) {
|
||||
if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) {
|
||||
// 转换坐标格式:从List<List<Double>>到List<Point>
|
||||
List<org.locationtech.jts.geom.Point> points = feature.getGeometry().getCoordinates().stream()
|
||||
.map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1))))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
// 创建路由段
|
||||
AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder()
|
||||
.code(feature.getProperties() != null ? feature.getProperties().getCode() : "")
|
||||
.coordinates(points)
|
||||
.build();
|
||||
routeSegments.add(segment);
|
||||
|
||||
// 创建LineString段用于合并
|
||||
if (points.size() >= 2) {
|
||||
org.locationtech.jts.geom.Coordinate[] coords = points.stream()
|
||||
.map(point -> point.getCoordinate())
|
||||
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
|
||||
org.locationtech.jts.geom.LineString lineString = geometryFactory.createLineString(coords);
|
||||
lineStringSegments.add(lineString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用JTS将多个LineString段合并为单一连续路径
|
||||
org.locationtech.jts.geom.LineString mergedGeometry = null;
|
||||
if (!lineStringSegments.isEmpty()) {
|
||||
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
|
||||
|
||||
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
|
||||
// 可选:简化路径以减少冗余点(容差:1米)
|
||||
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
|
||||
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
|
||||
lineStringSegments.size(), mergedGeometry.getNumPoints());
|
||||
} else {
|
||||
log.warn("路由段合并失败或结果无效");
|
||||
mergedGeometry = null;
|
||||
}
|
||||
}
|
||||
|
||||
return AircraftRoute.builder()
|
||||
.type(routeDTO.getType())
|
||||
.status(routeDTO.getStatus())
|
||||
.codes(routeDTO.getCodes())
|
||||
.geometry(mergedGeometry) // 使用合并后的单一LineString
|
||||
.routeSegments(routeSegments)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换航空器路由数据失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存航空器路由到数据库(独立于缓存)
|
||||
*/
|
||||
private void saveAircraftRouteToDatabase(String flightNo, AircraftRoute route) {
|
||||
try {
|
||||
log.info("开始保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
|
||||
|
||||
// 获取路由持久化服务
|
||||
com.qaup.collision.datacollector.service.RoutePersistenceService routePersistenceService =
|
||||
applicationContext.getBean(com.qaup.collision.datacollector.service.RoutePersistenceService.class);
|
||||
|
||||
boolean saveSuccess = routePersistenceService.saveAircraftRoute(flightNo, route);
|
||||
|
||||
if (saveSuccess) {
|
||||
log.info("成功保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
|
||||
} else {
|
||||
log.warn("保存航空器路由到数据库失败: flightNo={}, type={}", flightNo, route.getType());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存航空器路由到数据库异常: flightNo={}, type={}", flightNo, route.getType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存中的航空器路由信息
|
||||
*/
|
||||
private void updateAircraftRouteInCache(String flightNo, AircraftRoute route,
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
|
||||
MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo);
|
||||
|
||||
if (cachedAircraft != null && cachedAircraft instanceof com.qaup.collision.common.model.Aircraft) {
|
||||
com.qaup.collision.common.model.Aircraft aircraft = (com.qaup.collision.common.model.Aircraft) cachedAircraft;
|
||||
|
||||
// 根据路由类型更新路由
|
||||
if (routeParams.isArrivalRoute()) {
|
||||
aircraft.setArrivalRoute(route);
|
||||
aircraft.activateArrivalRoute();
|
||||
} else if (routeParams.isDepartureRoute()) {
|
||||
aircraft.setDepartureRoute(route);
|
||||
aircraft.activateDepartureRoute();
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
activeMovingObjectsCache.put(flightNo, aircraft);
|
||||
|
||||
log.debug("✅ 成功更新航空器路由缓存: flightNo={}, type={}, codes={}",
|
||||
flightNo, route.getType(), route.getCodes());
|
||||
} else {
|
||||
log.debug("🔍 缓存中暂无航空器对象,跳过缓存更新: flightNo={}", flightNo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布航空器路由更新事件
|
||||
*/
|
||||
private void publishAircraftRouteUpdateEvent(String flightNo, AircraftRoute route,
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
|
||||
try {
|
||||
// 创建路由更新事件
|
||||
com.qaup.collision.websocket.event.AircraftRouteUpdateEvent routeUpdateEvent =
|
||||
com.qaup.collision.websocket.event.AircraftRouteUpdateEvent.builder()
|
||||
.flightNo(flightNo)
|
||||
.routeType(route.getType())
|
||||
.routeStatus(route.getStatus())
|
||||
.routeCodes(route.getCodes())
|
||||
.aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型
|
||||
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.eventSource("FLIGHT_NOTIFICATION") // 标识事件来源
|
||||
.build();
|
||||
|
||||
// 发布WebSocket事件
|
||||
eventPublisher.publishEvent(routeUpdateEvent);
|
||||
|
||||
log.info("📡 发布航空器路由更新事件: 航班号={}, 路由类型={}, 事件来源=航班通知",
|
||||
flightNo, route.getType());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 发布航空器路由更新事件失败: flightNo={}", flightNo, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -427,17 +427,6 @@ public class LocationRuleQueryServiceImpl implements LocationRuleQueryService {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查复杂时间模式匹配
|
||||
* @param rule 规则
|
||||
* @param timestamp 时间戳
|
||||
* @return 如果匹配返回true
|
||||
*/
|
||||
private boolean matchesComplexTimePatterns(SpatialRule rule, LocalDateTime timestamp) {
|
||||
// 使用TimeWindowMatchingService处理复杂时间模式匹配
|
||||
return timeWindowMatchingService.matchesComplexTimePatterns(rule.getTimePatterns(), timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从区域检查获取几何形状
|
||||
* @param areaId 区域ID
|
||||
|
||||
@ -1083,26 +1083,6 @@ public class SpatialRuleServiceImpl implements SpatialRuleService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查星期名称匹配 "MON,WED,FRI"
|
||||
*/
|
||||
private boolean matchesWeekdayNames(String weekdays, LocalDateTime timestamp) {
|
||||
try {
|
||||
String[] dayNames = weekdays.split(",");
|
||||
String currentDay = timestamp.getDayOfWeek().name().substring(0, 3); // MON, TUE, etc.
|
||||
|
||||
for (String dayName : dayNames) {
|
||||
if (dayName.trim().equalsIgnoreCase(currentDay)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
logger.warn("检查星期名称失败: {}", weekdays, e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode parseJsonString(String jsonString) {
|
||||
try {
|
||||
if (jsonString == null || jsonString.trim().isEmpty()) {
|
||||
|
||||
@ -8,7 +8,6 @@ import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.TrafficLightStatusEvent;
|
||||
import com.qaup.collision.websocket.event.VehicleCommandEvent;
|
||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||
import com.qaup.collision.websocket.message.CollisionWarningPayload;
|
||||
@ -18,7 +17,6 @@ import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
|
||||
import com.qaup.collision.websocket.message.RuleStateChangePayload;
|
||||
import com.qaup.collision.websocket.message.RuleViolationPayload;
|
||||
import com.qaup.collision.websocket.message.TrafficLightStatusPayload;
|
||||
import com.qaup.collision.websocket.message.VehicleCommandPayload;
|
||||
import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
|
||||
import com.qaup.collision.websocket.message.UniversalMessage;
|
||||
@ -119,21 +117,6 @@ public class WebSocketMessageBroadcaster {
|
||||
broadcastMessageInternal(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理红绿灯状态事件
|
||||
* @param event 红绿灯状态事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleTrafficLightStatus(TrafficLightStatusEvent event) {
|
||||
UniversalMessage<TrafficLightStatusPayload> message = UniversalMessage.<TrafficLightStatusPayload>builder()
|
||||
.type(MessageTypeConstants.TRAFFIC_LIGHT_STATUS)
|
||||
.timestamp(event.getTimestamp())
|
||||
.messageId(generateMessageId())
|
||||
.payload((TrafficLightStatusPayload) event.getPayload())
|
||||
.build();
|
||||
|
||||
broadcastMessageInternal(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理碰撞预警事件
|
||||
|
||||
BIN
tools/__pycache__/mock_unmanned_vehicle.cpython-313.pyc
Normal file
BIN
tools/__pycache__/mock_unmanned_vehicle.cpython-313.pyc
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -181,6 +181,12 @@ def update_position_with_vector(
|
||||
speed_mps = float(speed) * 1000.0 / 3600.0
|
||||
distance = speed_mps * float(elapsed_time)
|
||||
|
||||
# 累计里程到总里程中
|
||||
if "total_mileage" not in obj:
|
||||
obj["total_mileage"] = 0.0
|
||||
obj["total_mileage"] = to_float_safe(obj.get("total_mileage", 0.0)) or 0.0
|
||||
obj["total_mileage"] += distance
|
||||
|
||||
# 安全读取并转换当前位置与目标点/起点经纬度为 float
|
||||
cur_lat = to_float_safe(obj.get("latitude"))
|
||||
cur_lon = to_float_safe(obj.get("longitude"))
|
||||
@ -287,28 +293,39 @@ def update_vehicle_position(vehicle: dict[str, Any], elapsed_time: float) -> Non
|
||||
update_object_position(vehicle, elapsed_time)
|
||||
|
||||
# 无人车数据配置
|
||||
current_timestamp = time.time()
|
||||
unmanned_vehicle_data = [
|
||||
{
|
||||
"vehicleNo": "鲁B567", # 无人车A
|
||||
"longitude": UNMANNED_A_START["longitude"],
|
||||
"latitude": UNMANNED_A_START["latitude"],
|
||||
"time": int(time.time() * 1000),
|
||||
"time": int(current_timestamp * 1000),
|
||||
"direction": 90.0, # 90度
|
||||
"speed": 25.0, # 25km/h
|
||||
"start_point": UNMANNED_A_START, # 起点
|
||||
"end_point": UNMANNED_A_END, # 终点
|
||||
"moving_to_end": True # 当前是否向终点移动
|
||||
"moving_to_end": True, # 当前是否向终点移动
|
||||
# 任务和里程相关字段
|
||||
"total_mileage": 0.0, # 总里程(米)
|
||||
"mission_start_time": int(current_timestamp * 1000), # 任务开始时间
|
||||
"mission_id": "MISSION_A_001", # 任务ID
|
||||
"mission_type": "PATROL_TRANSPORT" # 任务类型:巡逻运输
|
||||
},
|
||||
{
|
||||
"vehicleNo": "鲁B579", # 无人车B
|
||||
"longitude": UNMANNED_B_START["longitude"],
|
||||
"latitude": UNMANNED_B_START["latitude"],
|
||||
"time": int(time.time() * 1000),
|
||||
"time": int(current_timestamp * 1000),
|
||||
"direction": 270.0, # 270度
|
||||
"speed": 25.0, # 25km/h
|
||||
"start_point": UNMANNED_B_START, # 起点
|
||||
"end_point": UNMANNED_B_END, # 终点
|
||||
"moving_to_end": True # 当前是否向终点移动
|
||||
"moving_to_end": True, # 当前是否向终点移动
|
||||
# 任务和里程相关字段
|
||||
"total_mileage": 0.0, # 总里程(米)
|
||||
"mission_start_time": int(current_timestamp * 1000), # 任务开始时间
|
||||
"mission_id": "MISSION_B_001", # 任务ID
|
||||
"mission_type": "CARGO_TRANSPORT" # 任务类型:货物运输
|
||||
}
|
||||
]
|
||||
|
||||
@ -473,24 +490,6 @@ class RequestVehicleCommandPayload(TypedDict, total=False):
|
||||
longitude: float
|
||||
transId: str
|
||||
|
||||
# 无人车位置数据生成函数
|
||||
def generate_unmanned_vehicle_location_data() -> list[dict[str, Any]]:
|
||||
"""生成无人车位置数据,符合官方API格式"""
|
||||
unmanned_vehicles = []
|
||||
|
||||
for vehicle in unmanned_vehicle_data:
|
||||
lon_val = to_float_safe(vehicle.get("longitude"))
|
||||
lat_val = to_float_safe(vehicle.get("latitude"))
|
||||
location_info = {
|
||||
"transId": str(uuid.uuid4()),
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"vehicleID": vehicle["vehicleNo"],
|
||||
"longitude": round(lon_val, 6) if lon_val is not None else vehicle.get("longitude"),
|
||||
"latitude": round(lat_val, 6) if lat_val is not None else vehicle.get("latitude")
|
||||
}
|
||||
unmanned_vehicles.append(location_info)
|
||||
|
||||
return unmanned_vehicles
|
||||
|
||||
# 生成符合API规范的完整车辆状态数据
|
||||
def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]:
|
||||
@ -574,11 +573,102 @@ def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]:
|
||||
"cellularSignalStrength": -65, # -65dBm
|
||||
"wifiStatus": "CONNECTED",
|
||||
"cloudConnectivity": "ONLINE"
|
||||
},
|
||||
"missionContext": {
|
||||
"currentMission": {
|
||||
"missionId": vehicle_data_item.get("mission_id", "UNKNOWN"),
|
||||
"missionType": vehicle_data_item.get("mission_type", "UNKNOWN"),
|
||||
"startTime": vehicle_data_item.get("mission_start_time", timestamp_ms),
|
||||
"estimatedEndTime": vehicle_data_item.get("mission_start_time", timestamp_ms) + 3600000, # 1小时后
|
||||
"progress": round(calculate_mission_progress(vehicle_data_item), 1),
|
||||
"totalMileage": round(to_float_safe(vehicle_data_item.get("total_mileage", 0.0)) or 0.0, 1)
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": f"{vehicle_id}_START",
|
||||
"latitude": round(to_float_safe(vehicle_data_item.get("start_point", {}).get("latitude", 0.0)) or 0.0, 6),
|
||||
"longitude": round(to_float_safe(vehicle_data_item.get("start_point", {}).get("longitude", 0.0)) or 0.0, 6),
|
||||
"status": "COMPLETED" if not vehicle_data_item.get("moving_to_end", True) else "PENDING"
|
||||
},
|
||||
{
|
||||
"waypointId": f"{vehicle_id}_END",
|
||||
"latitude": round(to_float_safe(vehicle_data_item.get("end_point", {}).get("latitude", 0.0)) or 0.0, 6),
|
||||
"longitude": round(to_float_safe(vehicle_data_item.get("end_point", {}).get("longitude", 0.0)) or 0.0, 6),
|
||||
"status": "COMPLETED" if vehicle_data_item.get("moving_to_end", True) else "PENDING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return status_data
|
||||
|
||||
def calculate_mission_progress(vehicle_data: dict[str, Any]) -> float:
|
||||
"""计算任务进度百分比"""
|
||||
try:
|
||||
# 获取车辆当前位置
|
||||
cur_lat = to_float_safe(vehicle_data.get("latitude"))
|
||||
cur_lon = to_float_safe(vehicle_data.get("longitude"))
|
||||
|
||||
# 获取起点和终点坐标
|
||||
start_point = vehicle_data.get("start_point", {})
|
||||
end_point = vehicle_data.get("end_point", {})
|
||||
|
||||
start_lat = to_float_safe(start_point.get("latitude"))
|
||||
start_lon = to_float_safe(start_point.get("longitude"))
|
||||
end_lat = to_float_safe(end_point.get("latitude"))
|
||||
end_lon = to_float_safe(end_point.get("longitude"))
|
||||
|
||||
if any(v is None for v in [cur_lat, cur_lon, start_lat, start_lon, end_lat, end_lon]):
|
||||
return 0.0
|
||||
|
||||
# 确保所有值都不为None,使用断言来帮助类型检查器理解
|
||||
assert cur_lat is not None and cur_lon is not None
|
||||
assert start_lat is not None and start_lon is not None
|
||||
assert end_lat is not None and end_lon is not None
|
||||
|
||||
# 现在可以安全地转换为float类型
|
||||
start_lat_safe = float(start_lat)
|
||||
start_lon_safe = float(start_lon)
|
||||
end_lat_safe = float(end_lat)
|
||||
end_lon_safe = float(end_lon)
|
||||
cur_lat_safe = float(cur_lat)
|
||||
cur_lon_safe = float(cur_lon)
|
||||
|
||||
# 计算起点到终点的总距离
|
||||
total_distance = calculate_distance_to_target(
|
||||
{"latitude": start_lat_safe, "longitude": start_lon_safe},
|
||||
end_lat_safe, end_lon_safe
|
||||
)
|
||||
|
||||
if total_distance == 0:
|
||||
return 100.0
|
||||
|
||||
# 计算当前位置到起点的距离
|
||||
distance_from_start = calculate_distance_to_target(
|
||||
{"latitude": cur_lat_safe, "longitude": cur_lon_safe},
|
||||
start_lat_safe, start_lon_safe
|
||||
)
|
||||
|
||||
# 计算当前位置到终点的距离
|
||||
distance_to_end = calculate_distance_to_target(
|
||||
{"latitude": cur_lat_safe, "longitude": cur_lon_safe},
|
||||
end_lat_safe, end_lon_safe
|
||||
)
|
||||
|
||||
# 根据移动方向计算进度
|
||||
if vehicle_data.get("moving_to_end", True):
|
||||
# 向终点移动:进度 = (已走距离 / 总距离) * 100
|
||||
progress = (distance_from_start / total_distance) * 100.0
|
||||
else:
|
||||
# 向起点移动:进度 = (1 - 距离起点的距离 / 总距离) * 100 + 100
|
||||
progress = 100.0 + ((total_distance - distance_from_start) / total_distance) * 100.0
|
||||
|
||||
# 确保进度在合理范围内
|
||||
return max(0.0, min(200.0, progress))
|
||||
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]:
|
||||
"""根据fields参数过滤车辆状态字段"""
|
||||
if not fields:
|
||||
@ -600,59 +690,6 @@ def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None
|
||||
|
||||
return filtered_data
|
||||
|
||||
# 无人车状态数据生成函数
|
||||
def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]:
|
||||
"""生成无人车状态数据,符合官方API格式"""
|
||||
vehicle_states_data = []
|
||||
|
||||
if is_single and vehicle_id:
|
||||
# 单个车辆状态查询
|
||||
vehicle_state = vehicle_states.get(vehicle_id)
|
||||
if vehicle_state:
|
||||
state_info = {
|
||||
"transId": str(uuid.uuid4()),
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"vehicleID": vehicle_id,
|
||||
"loginState": True,
|
||||
"faultInfo": [],
|
||||
"activeSafety": not vehicle_state.is_running,
|
||||
"RC": False,
|
||||
"Command": 1 if vehicle_state.current_command == "ALERT" else 0,
|
||||
"airportInfo": [],
|
||||
"vehicleMode": 2, # 自动模式
|
||||
"gearState": 2, # D档
|
||||
"chassisReady": vehicle_state.is_running,
|
||||
"collisionStatus": False,
|
||||
"clearance": 1 if vehicle_state.is_running else 0,
|
||||
"turnSignalStstus": 0,
|
||||
"pointCloud": []
|
||||
}
|
||||
vehicle_states_data.append(state_info)
|
||||
else:
|
||||
# 所有车辆状态查询
|
||||
for vehicle in unmanned_vehicle_data:
|
||||
vehicle_state = vehicle_states.get(vehicle["vehicleNo"])
|
||||
state_info = {
|
||||
"transId": str(uuid.uuid4()),
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"vehicleID": vehicle["vehicleNo"],
|
||||
"loginState": True,
|
||||
"faultInfo": [],
|
||||
"activeSafety": not vehicle_state.is_running if vehicle_state else False,
|
||||
"RC": False,
|
||||
"Command": 1 if vehicle_state and vehicle_state.current_command == "ALERT" else 0,
|
||||
"airportInfo": [],
|
||||
"vehicleMode": 2,
|
||||
"gearState": 2,
|
||||
"chassisReady": vehicle_state.is_running if vehicle_state else True,
|
||||
"collisionStatus": False,
|
||||
"clearance": 1 if vehicle_state and vehicle_state.is_running else 0,
|
||||
"turnSignalStstus": 0,
|
||||
"pointCloud": []
|
||||
}
|
||||
vehicle_states_data.append(state_info)
|
||||
|
||||
return vehicle_states_data
|
||||
|
||||
# API 端点实现
|
||||
|
||||
@ -805,51 +842,7 @@ def handle_vehicle_command():
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/VehicleLocationInfo', methods=['GET', 'OPTIONS'])
|
||||
def get_unmanned_vehicle_location():
|
||||
"""无人车位置上报接口"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
try:
|
||||
current_time = time.time()
|
||||
global last_unmanned_vehicle_update_time
|
||||
elapsed_time = current_time - last_unmanned_vehicle_update_time
|
||||
if elapsed_time >= UPDATE_INTERVAL:
|
||||
for vehicle in unmanned_vehicle_data:
|
||||
update_vehicle_position(vehicle, UPDATE_INTERVAL)
|
||||
vehicle["time"] = int(current_time * 1000)
|
||||
last_unmanned_vehicle_update_time = current_time
|
||||
# 生成无人车位置数据
|
||||
location_data = generate_unmanned_vehicle_location_data()
|
||||
|
||||
logging.info(f"📍 返回无人车位置数据,车辆数量: {len(location_data)}")
|
||||
return jsonify(location_data)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in get_unmanned_vehicle_location: {str(e)}")
|
||||
return jsonify([]), 500
|
||||
|
||||
@app.route('/api/VehicleStateInfo', methods=['POST', 'OPTIONS'])
|
||||
def get_unmanned_vehicle_state():
|
||||
"""无人车状态查询接口"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
try:
|
||||
data = request.json
|
||||
vehicle_id = data.get("vehicleID") if data else None
|
||||
is_single = data.get("isSingle", True) if data else False
|
||||
|
||||
logging.info(f"📊 收到无人车状态查询请求: vehicle_id={vehicle_id}, is_single={is_single}")
|
||||
|
||||
# 生成无人车状态数据
|
||||
state_data = generate_unmanned_vehicle_state_data(vehicle_id, is_single)
|
||||
|
||||
logging.info(f"返回无人车状态数据,数量: {len(state_data)}")
|
||||
return jsonify(state_data)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in get_unmanned_vehicle_state: {str(e)}")
|
||||
return jsonify([]), 500
|
||||
|
||||
@app.route('/api/v1/vehicles/<vehicle_id>/status', methods=['GET', 'OPTIONS'])
|
||||
def get_vehicle_status_universal(vehicle_id):
|
||||
@ -944,43 +937,6 @@ def get_vehicle_status_universal(vehicle_id):
|
||||
}
|
||||
}), 500
|
||||
|
||||
@app.route('/openApi/getVehicleStatus', methods=['GET', 'OPTIONS'])
|
||||
def get_vehicle_status():
|
||||
"""获取无人车状态信息"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
vehicle_id = request.args.get('vehicleId')
|
||||
if not vehicle_id:
|
||||
return jsonify({
|
||||
"status": 400,
|
||||
"msg": "缺少 vehicleId 参数",
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 查找对应的车辆
|
||||
vehicle_state = vehicle_states.get(vehicle_id)
|
||||
if not vehicle_state:
|
||||
return jsonify({
|
||||
"status": 404,
|
||||
"msg": f"未找到无人车 {vehicle_id}",
|
||||
"data": None
|
||||
}), 404
|
||||
|
||||
# 返回车辆状态
|
||||
logging.info(f"📋 返回无人车状态: vehicleId={vehicle_id}")
|
||||
return jsonify({
|
||||
"status": 200,
|
||||
"msg": "获取无人车状态成功",
|
||||
"data": {
|
||||
"vehicleId": vehicle_id,
|
||||
"status": "NORMAL" if vehicle_state.is_running else "STOPPED",
|
||||
"command": vehicle_state.current_command,
|
||||
"commandPriority": vehicle_state.command_priority,
|
||||
"trafficLightState": vehicle_state.traffic_light_state,
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}
|
||||
})
|
||||
|
||||
# 设置CORS
|
||||
@app.after_request
|
||||
@ -995,8 +951,6 @@ if __name__ == '__main__':
|
||||
print("🚗 无人车厂商平台模拟服务启动中...")
|
||||
print("📍 服务端点:")
|
||||
print(" - 车辆控制指令: POST /api/VehicleCommandInfo")
|
||||
print(" - 车辆位置上报: GET /api/VehicleLocationInfo")
|
||||
print(" - 车辆状态查询: POST /api/VehicleStateInfo")
|
||||
print(" - 通用状态接口: GET /api/v1/vehicles/<vehicle_id>/status")
|
||||
print(" - 车辆状态获取: GET /openApi/getVehicleStatus")
|
||||
print(f"🌐 服务地址: http://localhost:8091")
|
||||
|
||||
@ -121,7 +121,6 @@
|
||||
<div><strong>路口红绿灯:</strong> <span id="intersectionTrafficLightCount">0</span></div>
|
||||
<div><strong>航空器路由:</strong> <span id="aircraftRouteCount">0</span></div>
|
||||
<div><strong>事件驱动路由:</strong> <span id="eventDrivenRouteCount">0</span></div>
|
||||
<div><strong>定时采集路由:</strong> <span id="scheduledRouteCount">0</span></div>
|
||||
<div><strong>航班进出港:</strong> <span id="flightNotificationCount">0</span></div>
|
||||
<div><strong>路径冲突:</strong> <span id="pathConflictCount">0</span></div>
|
||||
<div><strong>车辆指令:</strong> <span id="vehicleCommandCount">0</span></div>
|
||||
@ -189,7 +188,6 @@
|
||||
intersection_traffic_light_status: 0,
|
||||
aircraftRouteUpdate: 0,
|
||||
eventDrivenRoute: 0, // 事件驱动的路由更新
|
||||
scheduledRoute: 0, // 定时采集的路由更新
|
||||
flight_notification: 0,
|
||||
path_conflict_alert: 0,
|
||||
vehicle_command: 0,
|
||||
@ -257,7 +255,6 @@
|
||||
document.getElementById('intersectionTrafficLightCount').textContent = messageStats.intersection_traffic_light_status;
|
||||
document.getElementById('aircraftRouteCount').textContent = messageStats.aircraftRouteUpdate;
|
||||
document.getElementById('eventDrivenRouteCount').textContent = messageStats.eventDrivenRoute;
|
||||
document.getElementById('scheduledRouteCount').textContent = messageStats.scheduledRoute;
|
||||
document.getElementById('flightNotificationCount').textContent = messageStats.flight_notification;
|
||||
document.getElementById('pathConflictCount').textContent = messageStats.path_conflict_alert;
|
||||
document.getElementById('vehicleCommandCount').textContent = messageStats.vehicle_command;
|
||||
@ -421,8 +418,6 @@
|
||||
// 根据事件来源更新分类统计
|
||||
if (eventSource === 'FLIGHT_NOTIFICATION') {
|
||||
messageStats.eventDrivenRoute++;
|
||||
} else if (eventSource === 'SCHEDULED') {
|
||||
messageStats.scheduledRoute++;
|
||||
}
|
||||
|
||||
// 根据事件来源设置不同的日志类型和图标
|
||||
@ -431,8 +426,6 @@
|
||||
if (eventSource === 'FLIGHT_NOTIFICATION') {
|
||||
logType = 'success';
|
||||
icon = '🚀'; // 事件驱动的路由更新用火箭图标
|
||||
} else if (eventSource === 'SCHEDULED') {
|
||||
icon = '🕐'; // 定时采集的路由更新用时钟图标
|
||||
}
|
||||
|
||||
log('collisionLog', `${icon} 航空器路由更新: ${flightNo} | 路由:${routeType} 状态:${routeStatus} | 航空器:${aircraftStatus} | 编码:${routeCodes} | 来源:${eventSource} | ${routeGeometry}`, logType);
|
||||
@ -598,7 +591,6 @@
|
||||
document.getElementById('intersectionTrafficLightCount').textContent = '0';
|
||||
document.getElementById('aircraftRouteCount').textContent = '0';
|
||||
document.getElementById('eventDrivenRouteCount').textContent = '0';
|
||||
document.getElementById('scheduledRouteCount').textContent = '0';
|
||||
document.getElementById('flightNotificationCount').textContent = '0';
|
||||
document.getElementById('pathConflictCount').textContent = '0';
|
||||
document.getElementById('vehicleCommandCount').textContent = '0';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user