Align collision flow with platform vehicle registration
This commit is contained in:
parent
190d62cd3f
commit
fb649516ee
11
.codex/environments/environment.toml
Normal file
11
.codex/environments/environment.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||||
|
version = 1
|
||||||
|
name = "QAUP_Management"
|
||||||
|
|
||||||
|
[setup]
|
||||||
|
script = ""
|
||||||
|
|
||||||
|
[[actions]]
|
||||||
|
name = "运行"
|
||||||
|
icon = "run"
|
||||||
|
command = "mvn -pl qaup-admin -am clean package -DskipTests"
|
||||||
536
doc/frontend_platform_http_api_integration.md
Normal file
536
doc/frontend_platform_http_api_integration.md
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
# 前端接入文档:新增平台 HTTP 接口
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
本次后端新增了一组 HTTP 接口,供前端在现有 WebSocket 交互之外,补充以下能力:
|
||||||
|
|
||||||
|
- 同步“当前关心的对象及其类型”
|
||||||
|
- 运行时修改部分碰撞检测相关参数
|
||||||
|
|
||||||
|
## 2. 通用约定
|
||||||
|
|
||||||
|
- 所有接口均为 `POST`
|
||||||
|
- 所有请求和响应均为 `application/json`
|
||||||
|
- 当前接口无鉴权、无 token、无签名校验
|
||||||
|
- 当前后端未补 CORS
|
||||||
|
- 如果前端是浏览器并且跨域访问,后续可能还需要后端补 CORS
|
||||||
|
- 配置修改仅在内存中生效
|
||||||
|
- 服务重启后恢复默认配置
|
||||||
|
- `VehicleRegistry` 是“增量更新”
|
||||||
|
- 本次未传入的历史对象不会被清空
|
||||||
|
|
||||||
|
建议前端统一配置接口基础地址,例如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const API_BASE = 'http://<后端IP>:8080';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 接口一:对象注册同步
|
||||||
|
|
||||||
|
### 3.1 接口地址
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/VehicleRegistry
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 用途
|
||||||
|
|
||||||
|
前端将“当前关心的对象及其类型”增量同步给后端。
|
||||||
|
|
||||||
|
该接口会直接影响:
|
||||||
|
|
||||||
|
- 哪些对象参与碰撞检测
|
||||||
|
- 哪些车辆可能被系统下发控制命令
|
||||||
|
|
||||||
|
### 3.3 请求头
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "vehicleID": "QN001", "vehicleType": "WUREN" },
|
||||||
|
{ "vehicleID": "TQ001", "vehicleType": "TEQIN" },
|
||||||
|
{ "vehicleID": "AC001", "vehicleType": "HANGKONG" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `vehicleID` | `string` | 是 | 对象唯一标识,不能为空 |
|
||||||
|
| `vehicleType` | `string` | 是 | 对象类型,只允许固定枚举值 |
|
||||||
|
|
||||||
|
### 3.6 `vehicleType` 可选值
|
||||||
|
|
||||||
|
- `WUREN`
|
||||||
|
- `TEQIN`
|
||||||
|
- `HANGKONG`
|
||||||
|
- `PUTONG`
|
||||||
|
- `JIUYUAN`
|
||||||
|
|
||||||
|
### 3.7 业务语义
|
||||||
|
|
||||||
|
#### `WUREN`
|
||||||
|
|
||||||
|
- 加入“可控车辆集合”
|
||||||
|
- 加入“受管车辆集合”
|
||||||
|
|
||||||
|
#### `TEQIN`
|
||||||
|
|
||||||
|
- 加入“受管车辆集合”
|
||||||
|
- 不属于“可控车辆集合”
|
||||||
|
|
||||||
|
#### `HANGKONG`
|
||||||
|
|
||||||
|
- 加入“已选中航空器集合”
|
||||||
|
- 用于参与航空器相关碰撞/安全区计算
|
||||||
|
|
||||||
|
#### `PUTONG`
|
||||||
|
|
||||||
|
- 仅记录类型
|
||||||
|
- 不进入可控/受管/已选航空器集合
|
||||||
|
|
||||||
|
#### `JIUYUAN`
|
||||||
|
|
||||||
|
- 仅记录类型
|
||||||
|
- 不进入可控/受管/已选航空器集合
|
||||||
|
|
||||||
|
### 3.8 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"updatedAt": 1742280000000,
|
||||||
|
"updated": 3,
|
||||||
|
"controllableCount": 1,
|
||||||
|
"typesCount": {
|
||||||
|
"WUREN": 1,
|
||||||
|
"TEQIN": 1,
|
||||||
|
"HANGKONG": 1
|
||||||
|
},
|
||||||
|
"controllableVehicleIDs": ["QN001"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.9 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `status` | `string` | 固定为 `success` |
|
||||||
|
| `updatedAt` | `number` | 毫秒时间戳 |
|
||||||
|
| `updated` | `number` | 本次成功处理的条目数 |
|
||||||
|
| `controllableCount` | `number` | 当前系统内全部 `WUREN` 总数 |
|
||||||
|
| `typesCount` | `object` | 本次请求内各类型数量统计 |
|
||||||
|
| `controllableVehicleIDs` | `string[]` | 当前系统内全部可控车辆 ID 列表,即所有 `WUREN` |
|
||||||
|
|
||||||
|
### 3.10 错误响应示例
|
||||||
|
|
||||||
|
#### 请求体不是数组
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Body must be an array"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 参数非法
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Invalid request",
|
||||||
|
"errors": [
|
||||||
|
"item must be an object",
|
||||||
|
"missing vehicleID",
|
||||||
|
"missing vehicleType",
|
||||||
|
"vehicleID must be string",
|
||||||
|
"vehicleType must be string",
|
||||||
|
"vehicleID must be non-empty",
|
||||||
|
"invalid vehicleType=xxx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 非法 JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Invalid JSON",
|
||||||
|
"detail": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.11 前端调用示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await fetch(`${API_BASE}/api/VehicleRegistry`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify([
|
||||||
|
{ vehicleID: 'QN001', vehicleType: 'WUREN' },
|
||||||
|
{ vehicleID: 'TQ001', vehicleType: 'TEQIN' },
|
||||||
|
{ vehicleID: 'AC001', vehicleType: 'HANGKONG' }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.12 接入建议
|
||||||
|
|
||||||
|
- 页面首次拿到关注对象列表后调用一次
|
||||||
|
- 当前关注对象发生变化时再次调用
|
||||||
|
- 请求体必须是数组,即使只有一条数据也必须传数组
|
||||||
|
- 后端是“增量更新”,不要假设未传对象会被清空
|
||||||
|
|
||||||
|
## 4. 接口二:跑道区域航空器预警区半径
|
||||||
|
|
||||||
|
### 4.1 接口地址
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /config/runway/warning_zone_radius/aircraft
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 用途
|
||||||
|
|
||||||
|
前端运行时修改跑道区域的“航空器预警区半径”。
|
||||||
|
|
||||||
|
### 4.3 请求头
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "value": 300 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `value` | `number` | 是 | 必须是大于 0 的有限数值 |
|
||||||
|
|
||||||
|
### 4.6 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"area": "runway",
|
||||||
|
"field": "warning_zone_radius.aircraft",
|
||||||
|
"old": 200.0,
|
||||||
|
"new": 300.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.7 错误响应示例
|
||||||
|
|
||||||
|
#### 缺少字段
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Missing field: value"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 值非法
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Invalid value: must be a finite number greater than 0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 非法 JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Invalid JSON",
|
||||||
|
"detail": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段类型错误
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Invalid field type",
|
||||||
|
"detail": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.8 前端调用示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await fetch(`${API_BASE}/config/runway/warning_zone_radius/aircraft`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ value: 300 })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 接口三:跑道区域航空器告警区半径
|
||||||
|
|
||||||
|
### 5.1 接口地址
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /config/runway/alert_zone_radius/aircraft
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 用途
|
||||||
|
|
||||||
|
前端运行时修改跑道区域的“航空器告警区半径”。
|
||||||
|
|
||||||
|
### 5.3 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "value": 200 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"area": "runway",
|
||||||
|
"field": "alert_zone_radius.aircraft",
|
||||||
|
"old": 150.0,
|
||||||
|
"new": 200.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 前端调用示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await fetch(`${API_BASE}/config/runway/alert_zone_radius/aircraft`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ value: 200 })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 说明
|
||||||
|
|
||||||
|
该接口的请求规则、错误规则与“预警区半径”接口一致,唯一差异是:
|
||||||
|
|
||||||
|
- 路径不同
|
||||||
|
- 成功响应中的 `field` 固定为 `alert_zone_radius.aircraft`
|
||||||
|
|
||||||
|
## 6. 接口四:冲突解除距离阈值
|
||||||
|
|
||||||
|
### 6.1 接口地址
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /config/collision/diverging_release_distance
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 用途
|
||||||
|
|
||||||
|
前端运行时修改“冲突解除距离阈值”。
|
||||||
|
|
||||||
|
### 6.3 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "value": 50 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"area": "collision",
|
||||||
|
"field": "collision.diverging_release_distance",
|
||||||
|
"old": 40.0,
|
||||||
|
"new": 50.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 前端调用示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await fetch(`${API_BASE}/config/collision/diverging_release_distance`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ value: 50 })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.6 说明
|
||||||
|
|
||||||
|
该接口的请求规则、错误规则与前两个配置接口一致,唯一差异是:
|
||||||
|
|
||||||
|
- 路径不同
|
||||||
|
- 成功响应中的:
|
||||||
|
- `area` 固定为 `collision`
|
||||||
|
- `field` 固定为 `collision.diverging_release_distance`
|
||||||
|
|
||||||
|
## 7. 推荐前端封装
|
||||||
|
|
||||||
|
### 7.1 通用 POST JSON 请求
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function postJson<T>(url: string, body: unknown): Promise<T> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 类型定义
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type VehicleType = 'WUREN' | 'TEQIN' | 'HANGKONG' | 'PUTONG' | 'JIUYUAN';
|
||||||
|
|
||||||
|
interface VehicleRegistryItem {
|
||||||
|
vehicleID: string;
|
||||||
|
vehicleType: VehicleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VehicleRegistryResponse {
|
||||||
|
status: 'success';
|
||||||
|
updatedAt: number;
|
||||||
|
updated: number;
|
||||||
|
controllableCount: number;
|
||||||
|
typesCount: Record<string, number>;
|
||||||
|
controllableVehicleIDs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigUpdateResponse {
|
||||||
|
status: 'success';
|
||||||
|
area: string;
|
||||||
|
field: string;
|
||||||
|
old: number;
|
||||||
|
new: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
detail?: string;
|
||||||
|
errors?: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 接口封装示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export async function syncVehicleRegistry(items: VehicleRegistryItem[]) {
|
||||||
|
return postJson<VehicleRegistryResponse>(`${API_BASE}/api/VehicleRegistry`, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRunwayWarningRadius(value: number) {
|
||||||
|
return postJson<ConfigUpdateResponse>(
|
||||||
|
`${API_BASE}/config/runway/warning_zone_radius/aircraft`,
|
||||||
|
{ value }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRunwayAlertRadius(value: number) {
|
||||||
|
return postJson<ConfigUpdateResponse>(
|
||||||
|
`${API_BASE}/config/runway/alert_zone_radius/aircraft`,
|
||||||
|
{ value }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDivergingReleaseDistance(value: number) {
|
||||||
|
return postJson<ConfigUpdateResponse>(
|
||||||
|
`${API_BASE}/config/collision/diverging_release_distance`,
|
||||||
|
{ value }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 前端联调注意事项
|
||||||
|
|
||||||
|
### 8.1 `VehicleRegistry`
|
||||||
|
|
||||||
|
- 请求体必须是数组
|
||||||
|
- `vehicleID` 不能为空字符串
|
||||||
|
- `vehicleType` 必须严格使用大写枚举值
|
||||||
|
- 该接口是“增量更新”,未传对象不会被后端自动删除
|
||||||
|
|
||||||
|
### 8.2 配置接口
|
||||||
|
|
||||||
|
- `value` 必须是 `number`
|
||||||
|
- 不要传字符串,例如 `"300"` 是错误的
|
||||||
|
- 必须大于 `0`
|
||||||
|
|
||||||
|
### 8.3 跨域问题
|
||||||
|
|
||||||
|
- 当前后端未配置 CORS
|
||||||
|
- 如果前端页面和后端接口不是同域同端口,浏览器可能拦截
|
||||||
|
- 如出现跨域报错,需要后端后续补 CORS 配置
|
||||||
|
|
||||||
|
### 8.4 生效方式
|
||||||
|
|
||||||
|
- 这 3 个配置接口修改后立即在当前服务进程内生效
|
||||||
|
- 服务重启后恢复默认值,不会持久化
|
||||||
|
|
||||||
|
## 9. 联调建议流程
|
||||||
|
|
||||||
|
### 9.1 第一步:先调对象注册接口
|
||||||
|
|
||||||
|
确认前端能成功将当前关注对象同步给后端。
|
||||||
|
|
||||||
|
### 9.2 第二步:再调配置接口
|
||||||
|
|
||||||
|
确认预警半径、告警半径、冲突解除距离可运行时修改。
|
||||||
|
|
||||||
|
### 9.3 第三步:观察业务联动
|
||||||
|
|
||||||
|
重点观察:
|
||||||
|
|
||||||
|
- 碰撞检测结果是否按新对象范围生效
|
||||||
|
- 跑道区域预警/告警范围是否按新阈值变化
|
||||||
|
- 冲突解除逻辑后续接入时是否读取当前运行时配置
|
||||||
|
|
||||||
|
## 10. 最小联调示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function initPlatformObjects() {
|
||||||
|
await syncVehicleRegistry([
|
||||||
|
{ vehicleID: 'QN001', vehicleType: 'WUREN' },
|
||||||
|
{ vehicleID: 'TQ001', vehicleType: 'TEQIN' },
|
||||||
|
{ vehicleID: 'AC001', vehicleType: 'HANGKONG' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRuntimeConfig() {
|
||||||
|
await updateRunwayWarningRadius(300);
|
||||||
|
await updateRunwayAlertRadius(200);
|
||||||
|
await updateDivergingReleaseDistance(50);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 接口清单汇总
|
||||||
|
|
||||||
|
| 接口 | 方法 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `/api/VehicleRegistry` | `POST` | 同步当前关心对象及类型 |
|
||||||
|
| `/config/runway/warning_zone_radius/aircraft` | `POST` | 修改跑道区域航空器预警区半径 |
|
||||||
|
| `/config/runway/alert_zone_radius/aircraft` | `POST` | 修改跑道区域航空器告警区半径 |
|
||||||
|
| `/config/collision/diverging_release_distance` | `POST` | 修改冲突解除距离阈值 |
|
||||||
@ -42,6 +42,13 @@
|
|||||||
<version>${qaup.version}</version>
|
<version>${qaup.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.qaup</groupId>
|
||||||
|
<artifactId>qaup-framework</artifactId>
|
||||||
|
<version>${qaup.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring Boot Web -->
|
<!-- Spring Boot Web -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
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.service.PlatformRuntimeStateService;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public class PlatformIntegrationController {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
|
public PlatformIntegrationController(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
PlatformRuntimeStateService platformRuntimeStateService) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.platformRuntimeStateService = platformRuntimeStateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/api/VehicleRegistry", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<Map<String, Object>> registerVehicles(@RequestBody String requestBody) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(requestBody);
|
||||||
|
if (!root.isArray()) {
|
||||||
|
return error(HttpStatus.BAD_REQUEST, "Body must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
List<PlatformRuntimeStateService.VehicleRegistryEntry> entries = new ArrayList<>();
|
||||||
|
for (JsonNode item : root) {
|
||||||
|
validateVehicleRegistryItem(item, errors, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "error");
|
||||||
|
payload.put("message", "Invalid request");
|
||||||
|
payload.put("errors", errors);
|
||||||
|
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlatformRuntimeStateService.VehicleRegistryUpdateResult result =
|
||||||
|
platformRuntimeStateService.updateVehicleRegistry(entries);
|
||||||
|
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "success");
|
||||||
|
payload.put("updatedAt", result.updatedAt());
|
||||||
|
payload.put("updated", result.updated());
|
||||||
|
payload.put("controllableCount", result.controllableCount());
|
||||||
|
payload.put("typesCount", result.typesCount());
|
||||||
|
payload.put("controllableVehicleIDs", result.controllableVehicleIDs());
|
||||||
|
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
return invalidJson(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return internalError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/config/runway/warning_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<Map<String, Object>> updateRunwayWarningZoneRadiusAircraft(@RequestBody String requestBody) {
|
||||||
|
return updateNumericConfig(requestBody, "runway", "warning_zone_radius.aircraft",
|
||||||
|
platformRuntimeStateService::updateRunwayWarningZoneRadiusAircraft);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/config/runway/alert_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<Map<String, Object>> updateRunwayAlertZoneRadiusAircraft(@RequestBody String requestBody) {
|
||||||
|
return updateNumericConfig(requestBody, "runway", "alert_zone_radius.aircraft",
|
||||||
|
platformRuntimeStateService::updateRunwayAlertZoneRadiusAircraft);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateVehicleRegistryItem(
|
||||||
|
JsonNode item,
|
||||||
|
List<String> errors,
|
||||||
|
List<PlatformRuntimeStateService.VehicleRegistryEntry> entries) {
|
||||||
|
|
||||||
|
if (!item.isObject()) {
|
||||||
|
errors.add("item must be an object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode vehicleIdNode = item.get("vehicleID");
|
||||||
|
JsonNode vehicleTypeNode = item.get("vehicleType");
|
||||||
|
|
||||||
|
if (vehicleIdNode == null) {
|
||||||
|
errors.add("missing vehicleID");
|
||||||
|
}
|
||||||
|
if (vehicleTypeNode == null) {
|
||||||
|
errors.add("missing vehicleType");
|
||||||
|
}
|
||||||
|
if (vehicleIdNode == null || vehicleTypeNode == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vehicleIdNode.isTextual()) {
|
||||||
|
errors.add("vehicleID must be string");
|
||||||
|
}
|
||||||
|
if (!vehicleTypeNode.isTextual()) {
|
||||||
|
errors.add("vehicleType must be string");
|
||||||
|
}
|
||||||
|
if (!vehicleIdNode.isTextual() || !vehicleTypeNode.isTextual()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String vehicleId = vehicleIdNode.asText();
|
||||||
|
String vehicleType = vehicleTypeNode.asText();
|
||||||
|
|
||||||
|
if (vehicleId.trim().isEmpty()) {
|
||||||
|
errors.add("vehicleID must be non-empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PlatformRuntimeStateService.VehicleRegistryType parsedType =
|
||||||
|
PlatformRuntimeStateService.VehicleRegistryType.valueOf(vehicleType);
|
||||||
|
entries.add(new PlatformRuntimeStateService.VehicleRegistryEntry(vehicleId, parsedType));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
errors.add("invalid vehicleType=" + vehicleType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> updateNumericConfig(
|
||||||
|
String requestBody,
|
||||||
|
String area,
|
||||||
|
String field,
|
||||||
|
NumericUpdater updater) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(requestBody);
|
||||||
|
if (!root.isObject()) {
|
||||||
|
return invalidFieldType("Request body must be a JSON object");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode valueNode = root.get("value");
|
||||||
|
if (valueNode == null) {
|
||||||
|
return error(HttpStatus.BAD_REQUEST, "Missing field: value");
|
||||||
|
}
|
||||||
|
if (!valueNode.isNumber()) {
|
||||||
|
return invalidFieldType("Field 'value' must be a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = updater.update(newValue);
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "success");
|
||||||
|
payload.put("area", area);
|
||||||
|
payload.put("field", field);
|
||||||
|
payload.put("old", oldValue);
|
||||||
|
payload.put("new", newValue);
|
||||||
|
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
return invalidJson(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return internalError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> invalidJson(JsonProcessingException e) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "error");
|
||||||
|
payload.put("message", "Invalid JSON");
|
||||||
|
payload.put("detail", e.getOriginalMessage());
|
||||||
|
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> invalidFieldType(String detail) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "error");
|
||||||
|
payload.put("message", "Invalid field type");
|
||||||
|
payload.put("detail", detail);
|
||||||
|
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> error(HttpStatus status, String message) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "error");
|
||||||
|
payload.put("message", message);
|
||||||
|
return ResponseEntity.status(status).contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> internalError(Exception e) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("status", "error");
|
||||||
|
payload.put("message", "Internal error");
|
||||||
|
payload.put("detail", e.getMessage());
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface NumericUpdater {
|
||||||
|
double update(double newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -744,7 +744,8 @@ public class DataCollectorService {
|
|||||||
.missionStartTime(currentMission.getStartTime())
|
.missionStartTime(currentMission.getStartTime())
|
||||||
.estimatedEndTime(currentMission.getEstimatedEndTime())
|
.estimatedEndTime(currentMission.getEstimatedEndTime())
|
||||||
.progress(currentMission.getProgress())
|
.progress(currentMission.getProgress())
|
||||||
.totalMileage(currentMission.getTotalMileage());
|
.totalMileage(currentMission.getTotalMileage())
|
||||||
|
.missionStatus(resolveMissionStatus(statusData, missionContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取路径点信息
|
// 提取路径点信息
|
||||||
@ -984,6 +985,7 @@ public class DataCollectorService {
|
|||||||
unmannedVehicle.setEstimatedEndTime(currentMission.getEstimatedEndTime());
|
unmannedVehicle.setEstimatedEndTime(currentMission.getEstimatedEndTime());
|
||||||
unmannedVehicle.setProgress(currentMission.getProgress());
|
unmannedVehicle.setProgress(currentMission.getProgress());
|
||||||
unmannedVehicle.setTotalMileage(currentMission.getTotalMileage());
|
unmannedVehicle.setTotalMileage(currentMission.getTotalMileage());
|
||||||
|
unmannedVehicle.setMissionStatus(resolveMissionStatus(null, missionContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新路径点信息
|
// 更新路径点信息
|
||||||
@ -1085,6 +1087,53 @@ public class DataCollectorService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UnmannedVehicle.MissionStatus resolveMissionStatus(
|
||||||
|
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData,
|
||||||
|
MissionContextDTO missionContext) {
|
||||||
|
|
||||||
|
if (missionContext == null || missionContext.getCurrentMission() == null) {
|
||||||
|
return UnmannedVehicle.MissionStatus.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission();
|
||||||
|
if (currentMission.getMissionId() == null || currentMission.getMissionId().isBlank()) {
|
||||||
|
return UnmannedVehicle.MissionStatus.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMission.getProgress() != null && currentMission.getProgress() >= 100.0d) {
|
||||||
|
return UnmannedVehicle.MissionStatus.COMPLETED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missionContext.getWaypoints() != null && !missionContext.getWaypoints().isEmpty()) {
|
||||||
|
boolean allFinished = missionContext.getWaypoints().stream()
|
||||||
|
.allMatch(waypoint -> {
|
||||||
|
String status = waypoint.getStatus();
|
||||||
|
return "COMPLETED".equalsIgnoreCase(status) || "SKIPPED".equalsIgnoreCase(status);
|
||||||
|
});
|
||||||
|
if (allFinished) {
|
||||||
|
return UnmannedVehicle.MissionStatus.COMPLETED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusData != null && statusData.getOperationalStatus() != null) {
|
||||||
|
String operationalMode = statusData.getOperationalStatus().getOperationalMode();
|
||||||
|
if (operationalMode != null) {
|
||||||
|
String normalized = operationalMode.trim().toUpperCase();
|
||||||
|
if (normalized.contains("CANCEL")) {
|
||||||
|
return UnmannedVehicle.MissionStatus.CANCELLED;
|
||||||
|
}
|
||||||
|
if (normalized.contains("PAUSE")) {
|
||||||
|
return UnmannedVehicle.MissionStatus.PAUSED;
|
||||||
|
}
|
||||||
|
if (normalized.contains("IDLE")) {
|
||||||
|
return UnmannedVehicle.MissionStatus.ASSIGNED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnmannedVehicle.MissionStatus.IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用车辆状态缓存条目
|
* 通用车辆状态缓存条目
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,300 @@
|
|||||||
|
package com.qaup.collision.datacollector.service;
|
||||||
|
|
||||||
|
import com.qaup.collision.common.model.MovingObject;
|
||||||
|
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||||
|
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||||
|
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 lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
|
import org.locationtech.jts.geom.GeometryFactory;
|
||||||
|
import org.locationtech.jts.geom.LineString;
|
||||||
|
import org.locationtech.jts.geom.PrecisionModel;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares route inputs before conflict detection.
|
||||||
|
* This service standardizes unmanned vehicle task waypoints into formal routes and assignments.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoutePreparationService {
|
||||||
|
|
||||||
|
private static final String CREATED_BY = "RoutePreparationService";
|
||||||
|
|
||||||
|
private final TransportRouteRepository transportRouteRepository;
|
||||||
|
private final ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||||
|
private final RouteGeometryProcessor routeGeometryProcessor;
|
||||||
|
|
||||||
|
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void synchronizeRoutes(List<MovingObject> activeObjects) {
|
||||||
|
if (activeObjects == null || activeObjects.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MovingObject movingObject : activeObjects) {
|
||||||
|
if (movingObject instanceof UnmannedVehicle unmannedVehicle) {
|
||||||
|
synchronizeUnmannedVehicleRoute(unmannedVehicle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void synchronizeUnmannedVehicleRoute(UnmannedVehicle unmannedVehicle) {
|
||||||
|
if (unmannedVehicle == null || unmannedVehicle.getObjectId() == null || unmannedVehicle.getObjectId().isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String objectName = normalizeObjectName(unmannedVehicle);
|
||||||
|
if (!shouldParticipateInConflictDetection(unmannedVehicle)) {
|
||||||
|
deactivatePreparedRouteIfPresent(objectName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LineString routeGeometry = buildUnmannedVehicleRouteGeometry(unmannedVehicle);
|
||||||
|
|
||||||
|
if (routeGeometry == null) {
|
||||||
|
deactivatePreparedRouteIfPresent(objectName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String routeName = buildUnmannedVehicleRouteName(unmannedVehicle);
|
||||||
|
Optional<TransportRoute> existingRouteOptional = transportRouteRepository
|
||||||
|
.findByRouteNameAndRouteType(routeName, TransportRoute.RouteType.UNMANNED_VEHICLE);
|
||||||
|
|
||||||
|
TransportRoute savedRoute = existingRouteOptional
|
||||||
|
.map(existing -> saveUpdatedRouteIfNeeded(existing, routeGeometry, unmannedVehicle))
|
||||||
|
.orElseGet(() -> transportRouteRepository.save(createNewRoute(routeName, routeGeometry, unmannedVehicle)));
|
||||||
|
|
||||||
|
saveAssignmentIfChanged(objectName, savedRoute.getId());
|
||||||
|
deactivateOtherPreparedRoutes(objectName, savedRoute.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportRoute createNewRoute(String routeName, LineString routeGeometry, UnmannedVehicle unmannedVehicle) {
|
||||||
|
return TransportRoute.builder()
|
||||||
|
.routeName(routeName)
|
||||||
|
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||||
|
.description(buildRouteDescription(unmannedVehicle, routeGeometry))
|
||||||
|
.routeGeometry(routeGeometry)
|
||||||
|
.maxSpeedKph(30.0)
|
||||||
|
.typicalSpeedKph(unmannedVehicle.getCurrentSpeed() != null ? unmannedVehicle.getCurrentSpeed() : 15.0)
|
||||||
|
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||||
|
.isBidirectional(false)
|
||||||
|
.createdBy(CREATED_BY)
|
||||||
|
.updatedBy(CREATED_BY)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportRoute updateExistingRoute(TransportRoute existing, LineString routeGeometry, UnmannedVehicle unmannedVehicle) {
|
||||||
|
existing.setRouteGeometry(routeGeometry);
|
||||||
|
existing.setDescription(buildRouteDescription(unmannedVehicle, routeGeometry));
|
||||||
|
existing.setTypicalSpeedKph(unmannedVehicle.getCurrentSpeed() != null ? unmannedVehicle.getCurrentSpeed() : existing.getTypicalSpeedKph());
|
||||||
|
existing.setStatus(TransportRoute.RouteStatus.ACTIVE);
|
||||||
|
existing.setUpdatedBy(CREATED_BY);
|
||||||
|
existing.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportRoute saveUpdatedRouteIfNeeded(TransportRoute existing, LineString routeGeometry, UnmannedVehicle unmannedVehicle) {
|
||||||
|
String newDescription = buildRouteDescription(unmannedVehicle, routeGeometry);
|
||||||
|
Double newTypicalSpeed = unmannedVehicle.getCurrentSpeed() != null ? unmannedVehicle.getCurrentSpeed() : existing.getTypicalSpeedKph();
|
||||||
|
|
||||||
|
boolean sameGeometry = sameLineString(existing.getRouteGeometry(), routeGeometry);
|
||||||
|
boolean sameDescription = Objects.equals(existing.getDescription(), newDescription);
|
||||||
|
boolean sameTypicalSpeed = Objects.equals(existing.getTypicalSpeedKph(), newTypicalSpeed);
|
||||||
|
boolean alreadyActive = existing.getStatus() == TransportRoute.RouteStatus.ACTIVE;
|
||||||
|
|
||||||
|
if (sameGeometry && sameDescription && sameTypicalSpeed && alreadyActive) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransportRoute updatedRoute = updateExistingRoute(existing, routeGeometry, unmannedVehicle);
|
||||||
|
return Optional.ofNullable(transportRouteRepository.save(updatedRoute)).orElse(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveAssignmentIfChanged(String objectName, Long routeId) {
|
||||||
|
Optional<ObjectRouteAssignment> existingAssignment = objectRouteAssignmentRepository
|
||||||
|
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
objectName,
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingAssignment.isPresent() && Objects.equals(existingAssignment.get().getAssignedRouteId(), routeId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectRouteAssignmentRepository.save(ObjectRouteAssignment.builder()
|
||||||
|
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||||
|
.objectName(objectName)
|
||||||
|
.assignedRouteId(routeId)
|
||||||
|
.assignedAt(LocalDateTime.now())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deactivatePreparedRouteIfPresent(String objectName) {
|
||||||
|
objectRouteAssignmentRepository
|
||||||
|
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
objectName,
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)
|
||||||
|
.flatMap(assignment -> transportRouteRepository.findById(assignment.getAssignedRouteId()))
|
||||||
|
.filter(this::isPreparedUnmannedVehicleRoute)
|
||||||
|
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||||
|
.ifPresent(route -> {
|
||||||
|
route.setStatus(TransportRoute.RouteStatus.INACTIVE);
|
||||||
|
route.setUpdatedBy(CREATED_BY);
|
||||||
|
route.setUpdatedAt(LocalDateTime.now());
|
||||||
|
transportRouteRepository.save(route);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deactivateOtherPreparedRoutes(String objectName, Long activeRouteId) {
|
||||||
|
List<ObjectRouteAssignment> assignments = objectRouteAssignmentRepository
|
||||||
|
.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
objectName,
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
);
|
||||||
|
|
||||||
|
for (ObjectRouteAssignment assignment : assignments) {
|
||||||
|
if (Objects.equals(assignment.getAssignedRouteId(), activeRouteId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
transportRouteRepository.findById(assignment.getAssignedRouteId())
|
||||||
|
.filter(this::isPreparedUnmannedVehicleRoute)
|
||||||
|
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||||
|
.ifPresent(route -> {
|
||||||
|
route.setStatus(TransportRoute.RouteStatus.INACTIVE);
|
||||||
|
route.setUpdatedBy(CREATED_BY);
|
||||||
|
route.setUpdatedAt(LocalDateTime.now());
|
||||||
|
transportRouteRepository.save(route);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPreparedUnmannedVehicleRoute(TransportRoute route) {
|
||||||
|
return route != null
|
||||||
|
&& route.getRouteType() == TransportRoute.RouteType.UNMANNED_VEHICLE
|
||||||
|
&& CREATED_BY.equals(route.getCreatedBy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldParticipateInConflictDetection(UnmannedVehicle unmannedVehicle) {
|
||||||
|
if (unmannedVehicle.getMissionStatus() == UnmannedVehicle.MissionStatus.COMPLETED
|
||||||
|
|| unmannedVehicle.getMissionStatus() == UnmannedVehicle.MissionStatus.CANCELLED
|
||||||
|
|| unmannedVehicle.getMissionStatus() == UnmannedVehicle.MissionStatus.NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unmannedVehicle.getProgress() != null && unmannedVehicle.getProgress() >= 100.0d) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unmannedVehicle.getMissionId() == null || unmannedVehicle.getMissionId().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unmannedVehicle.getWaypoints() == null || unmannedVehicle.getWaypoints().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean allFinished = unmannedVehicle.getWaypoints().stream()
|
||||||
|
.allMatch(waypoint -> waypoint != null
|
||||||
|
&& (waypoint.getStatus() == UnmannedVehicle.WaypointStatus.COMPLETED
|
||||||
|
|| waypoint.getStatus() == UnmannedVehicle.WaypointStatus.SKIPPED));
|
||||||
|
return !allFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildUnmannedVehicleRouteName(UnmannedVehicle unmannedVehicle) {
|
||||||
|
String missionId = unmannedVehicle.getMissionId();
|
||||||
|
if (missionId != null && !missionId.isBlank()) {
|
||||||
|
return "UV_ROUTE_" + unmannedVehicle.getObjectId() + "_" + missionId;
|
||||||
|
}
|
||||||
|
return "UV_ROUTE_" + unmannedVehicle.getObjectId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRouteDescription(UnmannedVehicle unmannedVehicle, LineString routeGeometry) {
|
||||||
|
return String.format(
|
||||||
|
"Unmanned vehicle route prepared from mission context, vehicle=%s, mission=%s, points=%d",
|
||||||
|
unmannedVehicle.getObjectId(),
|
||||||
|
unmannedVehicle.getMissionId(),
|
||||||
|
routeGeometry.getNumPoints()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeObjectName(UnmannedVehicle unmannedVehicle) {
|
||||||
|
if (unmannedVehicle.getObjectName() != null && !unmannedVehicle.getObjectName().isBlank()) {
|
||||||
|
return unmannedVehicle.getObjectName();
|
||||||
|
}
|
||||||
|
return unmannedVehicle.getObjectId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LineString buildUnmannedVehicleRouteGeometry(UnmannedVehicle unmannedVehicle) {
|
||||||
|
List<Coordinate> coordinates = new ArrayList<>();
|
||||||
|
|
||||||
|
if (unmannedVehicle.getCurrentPosition() != null && !unmannedVehicle.getCurrentPosition().isEmpty()) {
|
||||||
|
coordinates.add(unmannedVehicle.getCurrentPosition().getCoordinate());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unmannedVehicle.getWaypoints() != null) {
|
||||||
|
for (UnmannedVehicle.WaypointInfo waypoint : unmannedVehicle.getWaypoints()) {
|
||||||
|
if (waypoint == null || waypoint.getLatitude() == null || waypoint.getLongitude() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Coordinate coordinate = new Coordinate(waypoint.getLongitude(), waypoint.getLatitude());
|
||||||
|
if (coordinates.isEmpty() || !sameCoordinate(coordinates.get(coordinates.size() - 1), coordinate)) {
|
||||||
|
coordinates.add(coordinate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coordinates.size() < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LineString route = geometryFactory.createLineString(coordinates.toArray(Coordinate[]::new));
|
||||||
|
if (!routeGeometryProcessor.isValidLineString(route)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return routeGeometryProcessor.simplifyLineString(route, 0.000001d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameCoordinate(Coordinate left, Coordinate right) {
|
||||||
|
return left != null
|
||||||
|
&& right != null
|
||||||
|
&& Double.compare(left.x, right.x) == 0
|
||||||
|
&& Double.compare(left.y, right.y) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameLineString(LineString left, LineString right) {
|
||||||
|
if (left == null || right == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Coordinate[] leftCoordinates = left.getCoordinates();
|
||||||
|
Coordinate[] rightCoordinates = right.getCoordinates();
|
||||||
|
if (leftCoordinates.length != rightCoordinates.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < leftCoordinates.length; i++) {
|
||||||
|
if (!sameCoordinate(leftCoordinates[i], rightCoordinates[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import com.qaup.collision.common.model.UnmannedVehicle;
|
|||||||
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
||||||
import com.qaup.collision.common.adapter.QuapDataAdapter;
|
import com.qaup.collision.common.adapter.QuapDataAdapter;
|
||||||
import com.qaup.collision.datacollector.service.VehicleDataPersistenceService;
|
import com.qaup.collision.datacollector.service.VehicleDataPersistenceService;
|
||||||
|
import com.qaup.collision.datacollector.service.RoutePreparationService;
|
||||||
import com.qaup.collision.dataprocessing.parser.TrafficLightSignalParser;
|
import com.qaup.collision.dataprocessing.parser.TrafficLightSignalParser;
|
||||||
import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||||
@ -14,6 +15,7 @@ import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
|
|||||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||||
import com.qaup.collision.datacollector.service.DataCollectorService;
|
import com.qaup.collision.datacollector.service.DataCollectorService;
|
||||||
import com.qaup.collision.common.model.FlightNotification;
|
import com.qaup.collision.common.model.FlightNotification;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
import com.qaup.collision.websocket.event.FlightNotificationEvent;
|
import com.qaup.collision.websocket.event.FlightNotificationEvent;
|
||||||
import com.qaup.common.core.redis.RedisCache;
|
import com.qaup.common.core.redis.RedisCache;
|
||||||
import com.qaup.system.domain.SysVehicleInfo;
|
import com.qaup.system.domain.SysVehicleInfo;
|
||||||
@ -75,6 +77,9 @@ public class DataProcessingService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private VehicleDataPersistenceService vehicleDataPersistenceService;
|
private VehicleDataPersistenceService vehicleDataPersistenceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RoutePreparationService routePreparationService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private QuapDataAdapter quapDataAdapter;
|
private QuapDataAdapter quapDataAdapter;
|
||||||
|
|
||||||
@ -88,6 +93,9 @@ public class DataProcessingService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisCache redisCache;
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
// 从DataCollectorService获取缓存的引用
|
// 从DataCollectorService获取缓存的引用
|
||||||
private Map<String, MovingObject> activeMovingObjectsCache;
|
private Map<String, MovingObject> activeMovingObjectsCache;
|
||||||
|
|
||||||
@ -147,18 +155,43 @@ public class DataProcessingService {
|
|||||||
// 注意:采集侧只缓存;发送侧在处理完成后会清理缓存,避免同一通知被每秒重复推送(刷屏)。
|
// 注意:采集侧只缓存;发送侧在处理完成后会清理缓存,避免同一通知被每秒重复推送(刷屏)。
|
||||||
processFlightNotificationUpdates();
|
processFlightNotificationUpdates();
|
||||||
|
|
||||||
// 第五步:执行路径冲突检测
|
List<MovingObject> collisionManagedObjects = filterCollisionManagedObjects(currentActiveObjects);
|
||||||
pathConflictDetectionService.detectPathConflicts(currentActiveObjects);
|
|
||||||
|
|
||||||
// 第六步:执行违规检测
|
// 第五步:将注册的无人车任务路径准备为正式路线输入
|
||||||
|
if (routePreparationService != null) {
|
||||||
|
routePreparationService.synchronizeRoutes(collisionManagedObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第六步:执行路径冲突检测
|
||||||
|
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
|
||||||
|
|
||||||
|
// 第七步:执行违规检测
|
||||||
performViolationDetection(currentActiveObjects);
|
performViolationDetection(currentActiveObjects);
|
||||||
|
|
||||||
// 第七步:保存无人车数据到数据库
|
// 第八步:保存无人车数据到数据库
|
||||||
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
||||||
|
|
||||||
log.info("周期性数据处理完成");
|
log.info("周期性数据处理完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<MovingObject> filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
||||||
|
if (activeObjects == null || activeObjects.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MovingObject> filteredObjects = activeObjects.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.filter(object -> platformRuntimeStateService == null
|
||||||
|
|| platformRuntimeStateService.isRegisteredForCollision(object.getObjectId(), object.getObjectType()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (filteredObjects.size() != activeObjects.size()) {
|
||||||
|
log.debug("碰撞检测对象已按注册表过滤: 原始数量={}, 过滤后数量={}",
|
||||||
|
activeObjects.size(), filteredObjects.size());
|
||||||
|
}
|
||||||
|
return filteredObjects;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为所有缓存对象计算速度和方向
|
* 为所有缓存对象计算速度和方向
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
|||||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.locationtech.jts.geom.Coordinate;
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
@ -39,9 +40,8 @@ public class PathConflictDetectionService {
|
|||||||
private final ConflictAlertLogRepository conflictAlertLogRepository;
|
private final ConflictAlertLogRepository conflictAlertLogRepository;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
private final CoordinateSystemService coordinateSystemService;
|
private final CoordinateSystemService coordinateSystemService;
|
||||||
|
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
private static final double WARNING_DISTANCE_THRESHOLD = 200.0;
|
|
||||||
private static final double ALERT_DISTANCE_THRESHOLD = 100.0;
|
|
||||||
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
||||||
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ public class PathConflictDetectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
|
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
|
||||||
if (obj1.getObjectType() != MovingObjectType.UNMANNED_VEHICLE && obj2.getObjectType() != MovingObjectType.UNMANNED_VEHICLE) {
|
if (!isSupportedConflictPair(obj1.getObjectType(), obj2.getObjectType())) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,6 +200,7 @@ public class PathConflictDetectionService {
|
|||||||
|
|
||||||
return assignmentOptional
|
return assignmentOptional
|
||||||
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
|
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
|
||||||
|
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,27 +289,25 @@ public class PathConflictDetectionService {
|
|||||||
double distance2,
|
double distance2,
|
||||||
MovingObjectType obj2Type,
|
MovingObjectType obj2Type,
|
||||||
double timeGap) {
|
double timeGap) {
|
||||||
|
if (!isSupportedConflictPair(obj1Type, obj2Type)) {
|
||||||
boolean obj1IsUnmannedVehicle = MovingObjectType.UNMANNED_VEHICLE.equals(obj1Type);
|
|
||||||
boolean obj2IsUnmannedVehicle = MovingObjectType.UNMANNED_VEHICLE.equals(obj2Type);
|
|
||||||
|
|
||||||
if (!obj1IsUnmannedVehicle && !obj2IsUnmannedVehicle) {
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
double distanceToEvaluate;
|
double distanceToEvaluate;
|
||||||
|
if (obj1Type == MovingObjectType.AIRCRAFT) {
|
||||||
if (obj1IsUnmannedVehicle && obj2IsUnmannedVehicle) {
|
distanceToEvaluate = distance2;
|
||||||
distanceToEvaluate = Math.min(distance1, distance2);
|
} else if (obj2Type == MovingObjectType.AIRCRAFT) {
|
||||||
} else if (obj1IsUnmannedVehicle) {
|
|
||||||
distanceToEvaluate = distance1;
|
distanceToEvaluate = distance1;
|
||||||
} else {
|
} else {
|
||||||
distanceToEvaluate = distance2;
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distanceToEvaluate <= ALERT_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS) {
|
double alertDistanceThreshold = platformRuntimeStateService.getRunwayAlertZoneRadiusAircraft();
|
||||||
|
double warningDistanceThreshold = platformRuntimeStateService.getRunwayWarningZoneRadiusAircraft();
|
||||||
|
|
||||||
|
if (distanceToEvaluate <= alertDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS) {
|
||||||
return Optional.of(ConflictAlertLog.AlertLevel.CRITICAL);
|
return Optional.of(ConflictAlertLog.AlertLevel.CRITICAL);
|
||||||
} else if (distanceToEvaluate <= WARNING_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
|
} else if (distanceToEvaluate <= warningDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
|
||||||
return Optional.of(ConflictAlertLog.AlertLevel.WARNING);
|
return Optional.of(ConflictAlertLog.AlertLevel.WARNING);
|
||||||
} else {
|
} else {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@ -370,6 +369,13 @@ public class PathConflictDetectionService {
|
|||||||
return dot >= HEADING_ALIGNMENT_MIN_COS;
|
return dot >= HEADING_ALIGNMENT_MIN_COS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSupportedConflictPair(MovingObjectType left, MovingObjectType right) {
|
||||||
|
return (left == MovingObjectType.UNMANNED_VEHICLE && right == MovingObjectType.AIRCRAFT)
|
||||||
|
|| (left == MovingObjectType.AIRCRAFT && right == MovingObjectType.UNMANNED_VEHICLE)
|
||||||
|
|| (left == MovingObjectType.SPECIAL_VEHICLE && right == MovingObjectType.AIRCRAFT)
|
||||||
|
|| (left == MovingObjectType.AIRCRAFT && right == MovingObjectType.SPECIAL_VEHICLE);
|
||||||
|
}
|
||||||
|
|
||||||
private Point toLocalPoint(Point wgs84Point) {
|
private Point toLocalPoint(Point wgs84Point) {
|
||||||
try {
|
try {
|
||||||
double[] local = coordinateSystemService.convertToLocalCoordinate(wgs84Point.getX(), wgs84Point.getY());
|
double[] local = coordinateSystemService.convertToLocalCoordinate(wgs84Point.getX(), wgs84Point.getY());
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
package com.qaup.collision.service;
|
||||||
|
|
||||||
|
import com.qaup.collision.common.model.MovingObject;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PlatformRuntimeStateService {
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, VehicleRegistryType> vehicleTypes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private volatile double runwayWarningZoneRadiusAircraft;
|
||||||
|
private volatile double runwayAlertZoneRadiusAircraft;
|
||||||
|
private volatile double collisionDivergingReleaseDistance;
|
||||||
|
|
||||||
|
public PlatformRuntimeStateService(
|
||||||
|
@Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft,
|
||||||
|
@Value("${qaup.runtime-config.runway.alert-zone-radius.aircraft:100.0}") double runwayAlertZoneRadiusAircraft,
|
||||||
|
@Value("${qaup.runtime-config.collision.diverging-release-distance:40.0}") double collisionDivergingReleaseDistance) {
|
||||||
|
|
||||||
|
this.runwayWarningZoneRadiusAircraft = runwayWarningZoneRadiusAircraft;
|
||||||
|
this.runwayAlertZoneRadiusAircraft = runwayAlertZoneRadiusAircraft;
|
||||||
|
this.collisionDivergingReleaseDistance = collisionDivergingReleaseDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
|
||||||
|
Objects.requireNonNull(entries, "entries");
|
||||||
|
|
||||||
|
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
|
||||||
|
for (VehicleRegistryEntry entry : entries) {
|
||||||
|
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
|
||||||
|
requestTypeCounts.merge(entry.vehicleType(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> controllableVehicleIds = new TreeSet<>();
|
||||||
|
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||||
|
if (entry.getValue() == VehicleRegistryType.WUREN) {
|
||||||
|
controllableVehicleIds.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Integer> responseTypeCounts = new LinkedHashMap<>();
|
||||||
|
for (VehicleRegistryType type : VehicleRegistryType.values()) {
|
||||||
|
Integer count = requestTypeCounts.get(type);
|
||||||
|
if (count != null && count > 0) {
|
||||||
|
responseTypeCounts.put(type.name(), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VehicleRegistryUpdateResult(
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
entries.size(),
|
||||||
|
controllableVehicleIds.size(),
|
||||||
|
responseTypeCounts,
|
||||||
|
new ArrayList<>(controllableVehicleIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getControllableVehicleIds() {
|
||||||
|
TreeSet<String> vehicleIds = new TreeSet<>();
|
||||||
|
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||||
|
if (entry.getValue() == VehicleRegistryType.WUREN) {
|
||||||
|
vehicleIds.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(new ArrayList<>(vehicleIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
public VehicleRegistryType getVehicleRegistryType(String vehicleId) {
|
||||||
|
if (vehicleId == null || vehicleId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return vehicleTypes.get(vehicleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRegisteredForCollision(String vehicleId, MovingObject.MovingObjectType objectType) {
|
||||||
|
VehicleRegistryType registryType = getVehicleRegistryType(vehicleId);
|
||||||
|
if (registryType == null || objectType == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (registryType) {
|
||||||
|
case WUREN -> objectType == MovingObject.MovingObjectType.UNMANNED_VEHICLE;
|
||||||
|
case TEQIN -> objectType == MovingObject.MovingObjectType.SPECIAL_VEHICLE;
|
||||||
|
case HANGKONG -> objectType == MovingObject.MovingObjectType.AIRCRAFT;
|
||||||
|
case PUTONG, JIUYUAN -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getRunwayWarningZoneRadiusAircraft() {
|
||||||
|
return runwayWarningZoneRadiusAircraft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double updateRunwayWarningZoneRadiusAircraft(double newValue) {
|
||||||
|
double oldValue = runwayWarningZoneRadiusAircraft;
|
||||||
|
runwayWarningZoneRadiusAircraft = newValue;
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getRunwayAlertZoneRadiusAircraft() {
|
||||||
|
return runwayAlertZoneRadiusAircraft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double updateRunwayAlertZoneRadiusAircraft(double newValue) {
|
||||||
|
double oldValue = runwayAlertZoneRadiusAircraft;
|
||||||
|
runwayAlertZoneRadiusAircraft = newValue;
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCollisionDivergingReleaseDistance() {
|
||||||
|
return collisionDivergingReleaseDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double updateCollisionDivergingReleaseDistance(double newValue) {
|
||||||
|
double oldValue = collisionDivergingReleaseDistance;
|
||||||
|
collisionDivergingReleaseDistance = newValue;
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record VehicleRegistryEntry(String vehicleID, VehicleRegistryType vehicleType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record VehicleRegistryUpdateResult(
|
||||||
|
long updatedAt,
|
||||||
|
int updated,
|
||||||
|
int controllableCount,
|
||||||
|
Map<String, Integer> typesCount,
|
||||||
|
List<String> controllableVehicleIDs) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VehicleRegistryType {
|
||||||
|
WUREN,
|
||||||
|
TEQIN,
|
||||||
|
HANGKONG,
|
||||||
|
PUTONG,
|
||||||
|
JIUYUAN
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
package com.qaup.collision.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
class PlatformIntegrationControllerTest {
|
||||||
|
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||||
|
PlatformIntegrationController controller =
|
||||||
|
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
|
||||||
|
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAcceptVehicleRegistryPayload() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/VehicleRegistry")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""
|
||||||
|
[
|
||||||
|
{ "vehicleID": "QN001", "vehicleType": "WUREN" },
|
||||||
|
{ "vehicleID": "TQ001", "vehicleType": "TEQIN" },
|
||||||
|
{ "vehicleID": "AC001", "vehicleType": "HANGKONG" }
|
||||||
|
]
|
||||||
|
"""))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(jsonPath("$.status").value("success"))
|
||||||
|
.andExpect(jsonPath("$.updated").value(3))
|
||||||
|
.andExpect(jsonPath("$.controllableCount").value(1))
|
||||||
|
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
|
||||||
|
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
|
||||||
|
.andExpect(jsonPath("$.typesCount.HANGKONG").value(1))
|
||||||
|
.andExpect(jsonPath("$.controllableVehicleIDs[0]").value("QN001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectInvalidVehicleRegistryPayload() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/VehicleRegistry")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""
|
||||||
|
[
|
||||||
|
{ "vehicleID": "", "vehicleType": "WUREN" },
|
||||||
|
{ "vehicleID": "QN002", "vehicleType": "UNKNOWN" },
|
||||||
|
1
|
||||||
|
]
|
||||||
|
"""))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.status").value("error"))
|
||||||
|
.andExpect(jsonPath("$.message").value("Invalid request"))
|
||||||
|
.andExpect(jsonPath("$.errors[0]").value("vehicleID must be non-empty"))
|
||||||
|
.andExpect(jsonPath("$.errors[1]").value("invalid vehicleType=UNKNOWN"))
|
||||||
|
.andExpect(jsonPath("$.errors[2]").value("item must be an object"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectNonArrayVehicleRegistryPayload() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/VehicleRegistry")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"vehicleID\":\"QN001\",\"vehicleType\":\"WUREN\"}"))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.message").value("Body must be an array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateRunwayWarningZoneRadius() throws Exception {
|
||||||
|
mockMvc.perform(post("/config/runway/warning_zone_radius/aircraft")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"value\":300}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value("success"))
|
||||||
|
.andExpect(jsonPath("$.area").value("runway"))
|
||||||
|
.andExpect(jsonPath("$.field").value("warning_zone_radius.aircraft"))
|
||||||
|
.andExpect(jsonPath("$.old").value(200.0))
|
||||||
|
.andExpect(jsonPath("$.new").value(300.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectMissingNumericConfigValue() throws Exception {
|
||||||
|
mockMvc.perform(post("/config/collision/diverging_release_distance")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{}"))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.message").value("Missing field: value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectInvalidNumericConfigValueType() throws Exception {
|
||||||
|
mockMvc.perform(post("/config/runway/alert_zone_radius/aircraft")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"value\":\"oops\"}"))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.message").value("Invalid field type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectInvalidJson() throws Exception {
|
||||||
|
mockMvc.perform(post("/config/runway/alert_zone_radius/aircraft")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"value\":"))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.message").value("Invalid JSON"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
package com.qaup.collision.controller;
|
||||||
|
|
||||||
|
import com.qaup.framework.config.SecurityConfig;
|
||||||
|
import com.qaup.framework.config.properties.PermitAllUrlProperties;
|
||||||
|
import com.qaup.framework.security.filter.JwtAuthenticationTokenFilter;
|
||||||
|
import com.qaup.framework.security.handle.AuthenticationEntryPointImpl;
|
||||||
|
import com.qaup.framework.security.handle.LogoutSuccessHandlerImpl;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.SpringBootConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@WebMvcTest(controllers = PlatformIntegrationController.class)
|
||||||
|
@ContextConfiguration(classes = {
|
||||||
|
PlatformIntegrationSecurityTest.TestApplication.class,
|
||||||
|
PlatformIntegrationController.class,
|
||||||
|
SecurityConfig.class
|
||||||
|
})
|
||||||
|
@Import(SecurityConfig.class)
|
||||||
|
class PlatformIntegrationSecurityTest {
|
||||||
|
|
||||||
|
@SpringBootConfiguration
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
static class TestApplication {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private AuthenticationEntryPointImpl authenticationEntryPoint;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private LogoutSuccessHandlerImpl logoutSuccessHandler;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private CorsFilter corsFilter;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private PermitAllUrlProperties permitAllUrlProperties;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
Mockito.when(permitAllUrlProperties.getUrls()).thenReturn(List.of());
|
||||||
|
Mockito.when(userDetailsService.loadUserByUsername(Mockito.anyString()))
|
||||||
|
.thenReturn(User.withUsername("user").password("{noop}pwd").authorities("ROLE_USER").build());
|
||||||
|
|
||||||
|
Mockito.doAnswer(invocation -> {
|
||||||
|
HttpServletRequest request = invocation.getArgument(0);
|
||||||
|
HttpServletResponse response = invocation.getArgument(1);
|
||||||
|
FilterChain chain = invocation.getArgument(2);
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
return null;
|
||||||
|
}).when(jwtAuthenticationTokenFilter).doFilter(
|
||||||
|
Mockito.any(HttpServletRequest.class),
|
||||||
|
Mockito.any(HttpServletResponse.class),
|
||||||
|
Mockito.any(FilterChain.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
Mockito.doAnswer(invocation -> {
|
||||||
|
HttpServletRequest request = invocation.getArgument(0);
|
||||||
|
HttpServletResponse response = invocation.getArgument(1);
|
||||||
|
FilterChain chain = invocation.getArgument(2);
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
return null;
|
||||||
|
}).when(corsFilter).doFilter(
|
||||||
|
Mockito.any(HttpServletRequest.class),
|
||||||
|
Mockito.any(HttpServletResponse.class),
|
||||||
|
Mockito.any(FilterChain.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
Mockito.when(platformRuntimeStateService.updateCollisionDivergingReleaseDistance(Mockito.anyDouble()))
|
||||||
|
.thenReturn(40.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowAnonymousAccessToNewPlatformEndpoints() throws Exception {
|
||||||
|
mockMvc.perform(post("/config/collision/diverging_release_distance")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"value\":50}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value("success"))
|
||||||
|
.andExpect(jsonPath("$.old").value(40.0))
|
||||||
|
.andExpect(jsonPath("$.new").value(50.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,228 @@
|
|||||||
|
package com.qaup.collision.datacollector.service;
|
||||||
|
|
||||||
|
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||||
|
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||||
|
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 org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
|
import org.locationtech.jts.geom.GeometryFactory;
|
||||||
|
import org.locationtech.jts.geom.Point;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class RoutePreparationServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TransportRouteRepository transportRouteRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||||
|
|
||||||
|
private RoutePreparationService routePreparationService;
|
||||||
|
|
||||||
|
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||||
|
|
||||||
|
private RoutePreparationService service() {
|
||||||
|
if (routePreparationService == null) {
|
||||||
|
routePreparationService = new RoutePreparationService(
|
||||||
|
transportRouteRepository,
|
||||||
|
objectRouteAssignmentRepository,
|
||||||
|
new RouteGeometryProcessor()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return routePreparationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPrepareFormalRouteForUnmannedVehicleMission() {
|
||||||
|
UnmannedVehicle vehicle = buildVehicleWithWaypoints("UV-100", "MISSION-1");
|
||||||
|
|
||||||
|
when(transportRouteRepository.findByRouteNameAndRouteType(
|
||||||
|
"UV_ROUTE_UV-100_MISSION-1",
|
||||||
|
TransportRoute.RouteType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(Optional.empty());
|
||||||
|
when(transportRouteRepository.save(any(TransportRoute.class))).thenAnswer(invocation -> {
|
||||||
|
TransportRoute route = invocation.getArgument(0);
|
||||||
|
route.setId(11L);
|
||||||
|
return route;
|
||||||
|
});
|
||||||
|
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
"UV-100",
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(Optional.empty());
|
||||||
|
when(objectRouteAssignmentRepository.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
"UV-100",
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(List.of());
|
||||||
|
|
||||||
|
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||||
|
|
||||||
|
ArgumentCaptor<TransportRoute> routeCaptor = ArgumentCaptor.forClass(TransportRoute.class);
|
||||||
|
verify(transportRouteRepository).save(routeCaptor.capture());
|
||||||
|
TransportRoute savedRoute = routeCaptor.getValue();
|
||||||
|
assertEquals(TransportRoute.RouteType.UNMANNED_VEHICLE, savedRoute.getRouteType());
|
||||||
|
assertEquals(TransportRoute.RouteStatus.ACTIVE, savedRoute.getStatus());
|
||||||
|
assertEquals("UV_ROUTE_UV-100_MISSION-1", savedRoute.getRouteName());
|
||||||
|
assertNotNull(savedRoute.getRouteGeometry());
|
||||||
|
assertTrue(savedRoute.getRouteGeometry().getNumPoints() >= 2);
|
||||||
|
|
||||||
|
ArgumentCaptor<ObjectRouteAssignment> assignmentCaptor = ArgumentCaptor.forClass(ObjectRouteAssignment.class);
|
||||||
|
verify(objectRouteAssignmentRepository).save(assignmentCaptor.capture());
|
||||||
|
assertEquals("UV-100", assignmentCaptor.getValue().getObjectName());
|
||||||
|
assertEquals(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE, assignmentCaptor.getValue().getObjectType());
|
||||||
|
assertEquals(11L, assignmentCaptor.getValue().getAssignedRouteId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeactivatePreparedRouteWhenMissionPathMissing() {
|
||||||
|
UnmannedVehicle vehicle = UnmannedVehicle.builder()
|
||||||
|
.objectId("UV-200")
|
||||||
|
.objectName("UV-200")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ObjectRouteAssignment latestAssignment = ObjectRouteAssignment.builder()
|
||||||
|
.objectName("UV-200")
|
||||||
|
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||||
|
.assignedRouteId(21L)
|
||||||
|
.build();
|
||||||
|
TransportRoute existingRoute = TransportRoute.builder()
|
||||||
|
.id(21L)
|
||||||
|
.routeName("UV_ROUTE_UV-200_MISSION-X")
|
||||||
|
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||||
|
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||||
|
.createdBy("RoutePreparationService")
|
||||||
|
.routeGeometry(geometryFactory.createLineString(new Coordinate[]{
|
||||||
|
new Coordinate(120.0, 36.0),
|
||||||
|
new Coordinate(120.1, 36.1)
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
"UV-200",
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(Optional.of(latestAssignment));
|
||||||
|
when(transportRouteRepository.findById(21L)).thenReturn(Optional.of(existingRoute));
|
||||||
|
|
||||||
|
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||||
|
|
||||||
|
verify(transportRouteRepository).save(existingRoute);
|
||||||
|
assertEquals(TransportRoute.RouteStatus.INACTIVE, existingRoute.getStatus());
|
||||||
|
verify(objectRouteAssignmentRepository, never()).save(any(ObjectRouteAssignment.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnmannedVehicle buildVehicleWithWaypoints(String vehicleId, String missionId) {
|
||||||
|
Point currentPosition = geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879));
|
||||||
|
return UnmannedVehicle.builder()
|
||||||
|
.objectId(vehicleId)
|
||||||
|
.objectName(vehicleId)
|
||||||
|
.missionId(missionId)
|
||||||
|
.missionStatus(UnmannedVehicle.MissionStatus.IN_PROGRESS)
|
||||||
|
.currentPosition(currentPosition)
|
||||||
|
.waypoints(List.of(
|
||||||
|
UnmannedVehicle.WaypointInfo.builder()
|
||||||
|
.waypointId("1")
|
||||||
|
.latitude(36.35416879)
|
||||||
|
.longitude(120.0835104)
|
||||||
|
.status(UnmannedVehicle.WaypointStatus.PENDING)
|
||||||
|
.build(),
|
||||||
|
UnmannedVehicle.WaypointInfo.builder()
|
||||||
|
.waypointId("2")
|
||||||
|
.latitude(36.35426879)
|
||||||
|
.longitude(120.0836104)
|
||||||
|
.status(UnmannedVehicle.WaypointStatus.PENDING)
|
||||||
|
.build()
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRewriteRouteWhenPreparedInputIsUnchanged() {
|
||||||
|
UnmannedVehicle vehicle = buildVehicleWithWaypoints("UV-300", "MISSION-3");
|
||||||
|
TransportRoute existingRoute = TransportRoute.builder()
|
||||||
|
.id(31L)
|
||||||
|
.routeName("UV_ROUTE_UV-300_MISSION-3")
|
||||||
|
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||||
|
.description("Unmanned vehicle route prepared from mission context, vehicle=UV-300, mission=MISSION-3, points=2")
|
||||||
|
.routeGeometry(new GeometryFactory().createLineString(new Coordinate[]{
|
||||||
|
new Coordinate(120.0834104, 36.35406879),
|
||||||
|
new Coordinate(120.0836104, 36.35426879)
|
||||||
|
}))
|
||||||
|
.typicalSpeedKph(15.0)
|
||||||
|
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||||
|
.createdBy("RoutePreparationService")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(transportRouteRepository.findByRouteNameAndRouteType(
|
||||||
|
"UV_ROUTE_UV-300_MISSION-3",
|
||||||
|
TransportRoute.RouteType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(Optional.of(existingRoute));
|
||||||
|
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
"UV-300",
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(Optional.of(ObjectRouteAssignment.builder()
|
||||||
|
.objectName("UV-300")
|
||||||
|
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||||
|
.assignedRouteId(31L)
|
||||||
|
.build()));
|
||||||
|
when(objectRouteAssignmentRepository.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
"UV-300",
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(List.of());
|
||||||
|
|
||||||
|
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||||
|
|
||||||
|
verify(transportRouteRepository, never()).save(any(TransportRoute.class));
|
||||||
|
verify(objectRouteAssignmentRepository, never()).save(any(ObjectRouteAssignment.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeactivatePreparedRouteWhenMissionIsCompleted() {
|
||||||
|
UnmannedVehicle vehicle = buildVehicleWithWaypoints("UV-400", "MISSION-4");
|
||||||
|
vehicle.setMissionStatus(UnmannedVehicle.MissionStatus.COMPLETED);
|
||||||
|
|
||||||
|
ObjectRouteAssignment latestAssignment = ObjectRouteAssignment.builder()
|
||||||
|
.objectName("UV-400")
|
||||||
|
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||||
|
.assignedRouteId(41L)
|
||||||
|
.build();
|
||||||
|
TransportRoute existingRoute = TransportRoute.builder()
|
||||||
|
.id(41L)
|
||||||
|
.routeName("UV_ROUTE_UV-400_MISSION-4")
|
||||||
|
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||||
|
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||||
|
.createdBy("RoutePreparationService")
|
||||||
|
.routeGeometry(new GeometryFactory().createLineString(new Coordinate[]{
|
||||||
|
new Coordinate(120.0, 36.0),
|
||||||
|
new Coordinate(120.1, 36.1)
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||||
|
"UV-400",
|
||||||
|
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||||
|
)).thenReturn(Optional.of(latestAssignment));
|
||||||
|
when(transportRouteRepository.findById(41L)).thenReturn(Optional.of(existingRoute));
|
||||||
|
|
||||||
|
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||||
|
|
||||||
|
verify(transportRouteRepository).save(existingRoute);
|
||||||
|
assertEquals(TransportRoute.RouteStatus.INACTIVE, existingRoute.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ import com.qaup.collision.dataprocessing.service.SpeedCalculationService;
|
|||||||
import com.qaup.collision.datacollector.service.VehicleDataPersistenceService;
|
import com.qaup.collision.datacollector.service.VehicleDataPersistenceService;
|
||||||
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
|
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
|
||||||
import com.qaup.collision.rule.service.RuleExecutionEngine;
|
import com.qaup.collision.rule.service.RuleExecutionEngine;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
import com.qaup.common.core.redis.RedisCache;
|
import com.qaup.common.core.redis.RedisCache;
|
||||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -55,6 +56,7 @@ class VehicleStatusUpdateIntegrationTest {
|
|||||||
private QuapDataAdapter quapDataAdapter;
|
private QuapDataAdapter quapDataAdapter;
|
||||||
private RuleExecutionEngine ruleExecutionEngine;
|
private RuleExecutionEngine ruleExecutionEngine;
|
||||||
private RedisCache redisCache;
|
private RedisCache redisCache;
|
||||||
|
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
@ -97,6 +99,7 @@ class VehicleStatusUpdateIntegrationTest {
|
|||||||
quapDataAdapter = mock(QuapDataAdapter.class);
|
quapDataAdapter = mock(QuapDataAdapter.class);
|
||||||
ruleExecutionEngine = mock(RuleExecutionEngine.class);
|
ruleExecutionEngine = mock(RuleExecutionEngine.class);
|
||||||
redisCache = mock(RedisCache.class);
|
redisCache = mock(RedisCache.class);
|
||||||
|
platformRuntimeStateService = mock(PlatformRuntimeStateService.class);
|
||||||
|
|
||||||
lenient().when(speedCalculationService.calculateRealtimeSpeed(anyString(), anyDouble(), anyDouble(), anyLong()))
|
lenient().when(speedCalculationService.calculateRealtimeSpeed(anyString(), anyDouble(), anyDouble(), anyLong()))
|
||||||
.thenReturn(20.0);
|
.thenReturn(20.0);
|
||||||
@ -117,7 +120,11 @@ class VehicleStatusUpdateIntegrationTest {
|
|||||||
ReflectionTestUtils.setField(dataProcessingService, "quapDataAdapter", quapDataAdapter);
|
ReflectionTestUtils.setField(dataProcessingService, "quapDataAdapter", quapDataAdapter);
|
||||||
ReflectionTestUtils.setField(dataProcessingService, "ruleExecutionEngine", ruleExecutionEngine);
|
ReflectionTestUtils.setField(dataProcessingService, "ruleExecutionEngine", ruleExecutionEngine);
|
||||||
ReflectionTestUtils.setField(dataProcessingService, "redisCache", redisCache);
|
ReflectionTestUtils.setField(dataProcessingService, "redisCache", redisCache);
|
||||||
|
ReflectionTestUtils.setField(dataProcessingService, "platformRuntimeStateService", platformRuntimeStateService);
|
||||||
dataProcessingService.setActiveMovingObjectsCache(activeMovingObjectsCache);
|
dataProcessingService.setActiveMovingObjectsCache(activeMovingObjectsCache);
|
||||||
|
|
||||||
|
lenient().when(platformRuntimeStateService.isRegisteredForCollision(anyString(), any()))
|
||||||
|
.thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UniversalVehicleStatusDTO buildSampleStatus(String vehicleId) {
|
private UniversalVehicleStatusDTO buildSampleStatus(String vehicleId) {
|
||||||
@ -280,6 +287,40 @@ class VehicleStatusUpdateIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPathConflictDetectionUsesRegisteredObjectsOnly() {
|
||||||
|
String registeredUvId = "QN001";
|
||||||
|
String unregisteredUvId = "QN999";
|
||||||
|
|
||||||
|
activeMovingObjectsCache.put(registeredUvId, MovingObject.builder()
|
||||||
|
.objectId(registeredUvId)
|
||||||
|
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||||
|
.objectName(registeredUvId)
|
||||||
|
.currentPosition(geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879)))
|
||||||
|
.altitude(0.0)
|
||||||
|
.build());
|
||||||
|
activeMovingObjectsCache.put(unregisteredUvId, MovingObject.builder()
|
||||||
|
.objectId(unregisteredUvId)
|
||||||
|
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||||
|
.objectName(unregisteredUvId)
|
||||||
|
.currentPosition(geometryFactory.createPoint(new Coordinate(120.0844104, 36.35506879)))
|
||||||
|
.altitude(0.0)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
when(platformRuntimeStateService.isRegisteredForCollision(eq(registeredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
|
||||||
|
.thenReturn(true);
|
||||||
|
when(platformRuntimeStateService.isRegisteredForCollision(eq(unregisteredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
dataProcessingService.performPeriodicDataProcessing();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<List<MovingObject>> movingObjectsCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(pathConflictDetectionService).detectPathConflicts(movingObjectsCaptor.capture());
|
||||||
|
assertEquals(1, movingObjectsCaptor.getValue().size());
|
||||||
|
assertEquals(registeredUvId, movingObjectsCaptor.getValue().get(0).getObjectId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试无活跃车辆情况的处理
|
* 测试无活跃车辆情况的处理
|
||||||
* 如果没有活跃车辆,应该跳过处理而不是报错
|
* 如果没有活跃车辆,应该跳过处理而不是报错
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
|||||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.locationtech.jts.geom.Coordinate;
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
@ -46,6 +47,9 @@ class PathConflictDetectionDirectionalTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private CoordinateSystemService coordinateSystemService;
|
private CoordinateSystemService coordinateSystemService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PathConflictDetectionService pathConflictDetectionService;
|
private PathConflictDetectionService pathConflictDetectionService;
|
||||||
|
|
||||||
@ -131,4 +135,34 @@ class PathConflictDetectionDirectionalTest {
|
|||||||
verify(conflictAlertLogRepository, never()).save(any());
|
verify(conflictAlertLogRepository, never()).save(any());
|
||||||
verify(eventPublisher, never()).publishEvent(any());
|
verify(eventPublisher, never()).publishEvent(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldIgnoreUnmannedVehicleAndSpecialVehiclePairWithoutAircraft() throws Exception {
|
||||||
|
GeometryFactory geometryFactory = new GeometryFactory();
|
||||||
|
|
||||||
|
MovingObject obj1 = MovingObject.builder()
|
||||||
|
.objectId("UV-1")
|
||||||
|
.objectName("UV-1")
|
||||||
|
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||||
|
.currentPosition(geometryFactory.createPoint(new Coordinate(0.0, 0.0)))
|
||||||
|
.currentSpeed(20.0)
|
||||||
|
.currentHeading(0.0)
|
||||||
|
.altitude(0.0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
MovingObject obj2 = MovingObject.builder()
|
||||||
|
.objectId("SV-1")
|
||||||
|
.objectName("SV-1")
|
||||||
|
.objectType(MovingObjectType.SPECIAL_VEHICLE)
|
||||||
|
.currentPosition(geometryFactory.createPoint(new Coordinate(0.0, 1.0)))
|
||||||
|
.currentSpeed(20.0)
|
||||||
|
.currentHeading(180.0)
|
||||||
|
.altitude(0.0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
||||||
|
|
||||||
|
verify(conflictAlertLogRepository, never()).save(any());
|
||||||
|
verify(eventPublisher, never()).publishEvent(any());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.qaup.collision.pathconflict.service;
|
||||||
|
|
||||||
|
import com.qaup.collision.common.model.MovingObject;
|
||||||
|
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
||||||
|
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||||
|
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||||
|
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||||
|
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||||
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
class PathConflictDetectionServiceRuntimeConfigTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseRuntimeThresholdsWhenEvaluatingAlertLevel() {
|
||||||
|
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||||
|
PathConflictDetectionService service = new PathConflictDetectionService(
|
||||||
|
mock(TransportRouteRepository.class),
|
||||||
|
mock(ObjectRouteAssignmentRepository.class),
|
||||||
|
mock(ConflictAlertLogRepository.class),
|
||||||
|
mock(ApplicationEventPublisher.class),
|
||||||
|
mock(CoordinateSystemService.class),
|
||||||
|
runtimeStateService
|
||||||
|
);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Optional<ConflictAlertLog.AlertLevel> beforeUpdate =
|
||||||
|
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||||
|
service,
|
||||||
|
"evaluateAlertLevel",
|
||||||
|
140.0,
|
||||||
|
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||||
|
500.0,
|
||||||
|
MovingObject.MovingObjectType.AIRCRAFT,
|
||||||
|
20.0
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL), beforeUpdate);
|
||||||
|
|
||||||
|
runtimeStateService.updateRunwayAlertZoneRadiusAircraft(100.0);
|
||||||
|
runtimeStateService.updateRunwayWarningZoneRadiusAircraft(130.0);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Optional<ConflictAlertLog.AlertLevel> afterUpdate =
|
||||||
|
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||||
|
service,
|
||||||
|
"evaluateAlertLevel",
|
||||||
|
140.0,
|
||||||
|
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||||
|
500.0,
|
||||||
|
MovingObject.MovingObjectType.AIRCRAFT,
|
||||||
|
20.0
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Optional.empty(), afterUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.qaup.collision.service;
|
||||||
|
|
||||||
|
import com.qaup.collision.common.model.MovingObject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class PlatformRuntimeStateServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepPreviousVehicleRegistrationsOnIncrementalUpdate() {
|
||||||
|
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||||
|
|
||||||
|
PlatformRuntimeStateService.VehicleRegistryUpdateResult first = service.updateVehicleRegistry(List.of(
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(3, first.updated());
|
||||||
|
assertEquals(1, first.controllableCount());
|
||||||
|
assertEquals(List.of("QN001"), first.controllableVehicleIDs());
|
||||||
|
assertEquals(1, first.typesCount().get("WUREN"));
|
||||||
|
assertEquals(1, first.typesCount().get("TEQIN"));
|
||||||
|
assertEquals(1, first.typesCount().get("HANGKONG"));
|
||||||
|
|
||||||
|
PlatformRuntimeStateService.VehicleRegistryUpdateResult second = service.updateVehicleRegistry(List.of(
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("QN002", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(1, second.updated());
|
||||||
|
assertEquals(2, second.controllableCount());
|
||||||
|
assertEquals(List.of("QN001", "QN002"), second.controllableVehicleIDs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateRuntimeThresholdValuesInMemory() {
|
||||||
|
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||||
|
|
||||||
|
assertEquals(200.0, service.updateRunwayWarningZoneRadiusAircraft(300.0));
|
||||||
|
assertEquals(300.0, service.getRunwayWarningZoneRadiusAircraft());
|
||||||
|
|
||||||
|
assertEquals(150.0, service.updateRunwayAlertZoneRadiusAircraft(220.0));
|
||||||
|
assertEquals(220.0, service.getRunwayAlertZoneRadiusAircraft());
|
||||||
|
|
||||||
|
assertEquals(40.0, service.updateCollisionDivergingReleaseDistance(55.0));
|
||||||
|
assertEquals(55.0, service.getCollisionDivergingReleaseDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOnlyAllowRegisteredCollisionTypesToParticipate() {
|
||||||
|
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||||
|
service.updateVehicleRegistry(List.of(
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG),
|
||||||
|
new PlatformRuntimeStateService.VehicleRegistryEntry("PT001", PlatformRuntimeStateService.VehicleRegistryType.PUTONG)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertTrue(service.isRegisteredForCollision("QN001", MovingObject.MovingObjectType.UNMANNED_VEHICLE));
|
||||||
|
assertTrue(service.isRegisteredForCollision("TQ001", MovingObject.MovingObjectType.SPECIAL_VEHICLE));
|
||||||
|
assertTrue(service.isRegisteredForCollision("AC001", MovingObject.MovingObjectType.AIRCRAFT));
|
||||||
|
|
||||||
|
assertFalse(service.isRegisteredForCollision("QN001", MovingObject.MovingObjectType.SPECIAL_VEHICLE));
|
||||||
|
assertFalse(service.isRegisteredForCollision("PT001", MovingObject.MovingObjectType.NORMAL_VEHICLE));
|
||||||
|
assertFalse(service.isRegisteredForCollision("UNKNOWN", MovingObject.MovingObjectType.AIRCRAFT));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -120,6 +120,12 @@ public class SecurityConfig
|
|||||||
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
|
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
|
||||||
// WebSocket端点,允许匿名访问
|
// WebSocket端点,允许匿名访问
|
||||||
.requestMatchers("/collision", "/collision/**", "/VehicleCommandInfo", "/test/websocket/**").permitAll()
|
.requestMatchers("/collision", "/collision/**", "/VehicleCommandInfo", "/test/websocket/**").permitAll()
|
||||||
|
.requestMatchers(
|
||||||
|
"/api/VehicleRegistry",
|
||||||
|
"/config/runway/warning_zone_radius/aircraft",
|
||||||
|
"/config/runway/alert_zone_radius/aircraft",
|
||||||
|
"/config/collision/diverging_release_distance"
|
||||||
|
).permitAll()
|
||||||
// 调试接口:仅在显式开启开关时放行(默认不放行)
|
// 调试接口:仅在显式开启开关时放行(默认不放行)
|
||||||
.requestMatchers(runwayPathPlanningDebugEnabled ? "/debug/runway-path-planning/**" : "/__debug_disabled__").permitAll()
|
.requestMatchers(runwayPathPlanningDebugEnabled ? "/debug/runway-path-planning/**" : "/__debug_disabled__").permitAll()
|
||||||
// 静态资源,可匿名访问
|
// 静态资源,可匿名访问
|
||||||
|
|||||||
4
命令.md
4
命令.md
@ -374,3 +374,7 @@ Date: Sat, 28 Feb 2026 10:56:18 GMT
|
|||||||
root@root:/home/project_20250804/qaup# echo $TOKEN
|
root@root:/home/project_20250804/qaup# echo $TOKEN
|
||||||
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NzIzNjA0NTYsInVzZXJuYW1lIjoiZGlhbnhpbiJ9.kTPxkoFR64eJT7eZOZWSN_ed-qvWbMFqr2WlGofBE60
|
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NzIzNjA0NTYsInVzZXJuYW1lIjoiZGlhbnhpbiJ9.kTPxkoFR64eJT7eZOZWSN_ed-qvWbMFqr2WlGofBE60
|
||||||
root@root:/home/project_20250804/qaup#
|
root@root:/home/project_20250804/qaup#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
docker logs --since 5m qaup-app 2>&1 | grep '处理航空器数据并更新缓存' | grep -E '([A-Z]{1,3})?4963\b'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user