Clean up collision flow changes
This commit is contained in:
parent
20af9b8288
commit
1cac6fb811
2
.gitignore
vendored
2
.gitignore
vendored
@ -55,6 +55,8 @@ Users/
|
||||
|
||||
qaup-deploy/
|
||||
deploy/offline_packages/
|
||||
*.lck
|
||||
mqtt协议验证/账户信息.txt
|
||||
|
||||
######################################################################
|
||||
# Python
|
||||
|
||||
@ -6,3 +6,9 @@
|
||||
这是开发环境,运行环境在centos7的容器里,分别为
|
||||
qaup-app
|
||||
qaup-redis
|
||||
|
||||
### 红绿灯接入说明
|
||||
- 正式环境中,qaup-app 的红绿灯接入方式为 MQTT,不是 HTTP。
|
||||
- 正式环境配置中 `traffic.light.tcp.enabled=false`,`traffic.light.mqtt.enabled=true`。
|
||||
- MQTT Broker 地址示例:`tcp://10.64.58.228:8082`,订阅 Topic 示例:`cusc/v2/SF053/QingDrsu001/data`。
|
||||
- 注意区分 MQTT Broker 的 TCP 连接地址与业务 TCP 直连服务,前者不是“红绿灯走 TCP 直连”的意思。
|
||||
|
||||
80
deploy/collision_prepare_test_plan.md
Normal file
80
deploy/collision_prepare_test_plan.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Collision Prepare Flow Test Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Verify the deployed `qaup-app` service can complete the interface-only preparation flow for simulated collision objects before live position and alert testing.
|
||||
|
||||
## Scope
|
||||
|
||||
This plan only verifies:
|
||||
|
||||
1. Health endpoint is reachable.
|
||||
2. Simulated aircraft, unmanned vehicle, and special vehicle can be registered.
|
||||
3. Aircraft route query both returns route data and binds the route.
|
||||
4. Unmanned and special vehicle routes can be directly submitted and bound.
|
||||
5. Preparation status changes from `not ready` to `ready`.
|
||||
|
||||
This plan does not verify:
|
||||
|
||||
1. Real position ingestion.
|
||||
2. Real collision point calculation from live tracks.
|
||||
3. WebSocket alert delivery.
|
||||
4. Real stop/recover signal dispatch.
|
||||
|
||||
## Recommended Execution Location
|
||||
|
||||
Run inside the `qaup-app` container if possible, or on a host that can access the container port.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
docker exec -it qaup-app sh -lc 'bash /opt/qaup/deploy/test_collision_prepare_flow.sh'
|
||||
```
|
||||
|
||||
```bash
|
||||
BASE_URL="http://127.0.0.1:8080" bash deploy/test_collision_prepare_flow.sh
|
||||
```
|
||||
|
||||
## Script Inputs
|
||||
|
||||
The script supports environment variables:
|
||||
|
||||
- `BASE_URL`
|
||||
- `HEALTH_PATH`
|
||||
- `AIRCRAFT_ID`
|
||||
- `UNMANNED_ID`
|
||||
- `SPECIAL_ID`
|
||||
- `AIRCRAFT_ROUTE_TYPE`
|
||||
- `AIRCRAFT_IN_RUNWAY`
|
||||
- `AIRCRAFT_OUT_RUNWAY`
|
||||
- `AIRCRAFT_CONTACT_CROSS`
|
||||
- `AIRCRAFT_SEAT`
|
||||
- `AIRCRAFT_START_SEAT`
|
||||
- `AUTH_HEADER`
|
||||
|
||||
Default execution assumes no authentication because the current platform HTTP integration document states these endpoints are unauthenticated.
|
||||
|
||||
## Expected Result
|
||||
|
||||
The script should end with:
|
||||
|
||||
```text
|
||||
PASS: collision preparation interface flow is ready for deployment testing
|
||||
```
|
||||
|
||||
The preparation status before binding should report missing routes.
|
||||
|
||||
The preparation status after binding should report:
|
||||
|
||||
- `ready=true`
|
||||
- `registeredCount=3`
|
||||
- `missingRouteCount=0`
|
||||
|
||||
## Failure Handling
|
||||
|
||||
If the script fails:
|
||||
|
||||
1. Confirm `qaup-app` is healthy.
|
||||
2. Confirm the service port matches `BASE_URL`.
|
||||
3. Confirm the aircraft route query parameters are valid in the target environment.
|
||||
4. If formal environment adds authentication later, set `AUTH_HEADER`.
|
||||
266
deploy/test_collision_prepare_flow.sh
Normal file
266
deploy/test_collision_prepare_flow.sh
Normal file
@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
|
||||
HEALTH_PATH="${HEALTH_PATH:-/actuator/health}"
|
||||
|
||||
AIRCRAFT_ID="${AIRCRAFT_ID:-SIM-AC-001}"
|
||||
UNMANNED_ID="${UNMANNED_ID:-SIM-UV-001}"
|
||||
SPECIAL_ID="${SPECIAL_ID:-SIM-SP-001}"
|
||||
|
||||
AIRCRAFT_ROUTE_TYPE="${AIRCRAFT_ROUTE_TYPE:-OUT}"
|
||||
AIRCRAFT_IN_RUNWAY="${AIRCRAFT_IN_RUNWAY:-35}"
|
||||
AIRCRAFT_OUT_RUNWAY="${AIRCRAFT_OUT_RUNWAY:-34}"
|
||||
AIRCRAFT_CONTACT_CROSS="${AIRCRAFT_CONTACT_CROSS:-}"
|
||||
AIRCRAFT_SEAT="${AIRCRAFT_SEAT:-165}"
|
||||
AIRCRAFT_START_SEAT="${AIRCRAFT_START_SEAT:-165}"
|
||||
|
||||
UV_ROUTE_NAME="${UV_ROUTE_NAME:-SIM_UV_ROUTE_001}"
|
||||
SP_ROUTE_NAME="${SP_ROUTE_NAME:-SIM_SP_ROUTE_001}"
|
||||
|
||||
AUTH_HEADER="${AUTH_HEADER:-}"
|
||||
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-5}"
|
||||
MAX_TIME="${MAX_TIME:-20}"
|
||||
WORK_DIR="${WORK_DIR:-/tmp/qaup_collision_prepare_test}"
|
||||
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "FAIL: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
have_jq() {
|
||||
command -v jq >/dev/null 2>&1
|
||||
}
|
||||
|
||||
json_get() {
|
||||
local file="$1"
|
||||
local expr="$2"
|
||||
if have_jq; then
|
||||
jq -r "$expr" "$file"
|
||||
else
|
||||
python3 - "$file" "$expr" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
path = sys.argv[2].strip().lstrip(".")
|
||||
parts = [p for p in path.split(".") if p]
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for part in parts:
|
||||
if isinstance(data, list):
|
||||
data = data[int(part)]
|
||||
else:
|
||||
data = data.get(part)
|
||||
print("" if data is None else data)
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
json_contains_text() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
grep -Fq "$expected" "$file"
|
||||
}
|
||||
|
||||
curl_json() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local body_file="${3:-}"
|
||||
local response_file="$4"
|
||||
|
||||
local curl_args=(
|
||||
--silent
|
||||
--show-error
|
||||
--connect-timeout "$CONNECT_TIMEOUT"
|
||||
--max-time "$MAX_TIME"
|
||||
--request "$method"
|
||||
--header 'Content-Type: application/json'
|
||||
--write-out '%{http_code}'
|
||||
--output "$response_file"
|
||||
)
|
||||
|
||||
if [[ -n "$AUTH_HEADER" ]]; then
|
||||
curl_args+=(--header "Authorization: $AUTH_HEADER")
|
||||
fi
|
||||
|
||||
if [[ -n "$body_file" ]]; then
|
||||
curl_args+=(--data @"$body_file")
|
||||
fi
|
||||
|
||||
curl "${curl_args[@]}" "${BASE_URL}${path}"
|
||||
}
|
||||
|
||||
assert_http_ok() {
|
||||
local code="$1"
|
||||
local context="$2"
|
||||
[[ "$code" == "200" ]] || fail "$context returned HTTP $code"
|
||||
}
|
||||
|
||||
write_json() {
|
||||
local file="$1"
|
||||
shift
|
||||
cat > "$file" <<EOF
|
||||
$*
|
||||
EOF
|
||||
}
|
||||
|
||||
print_json() {
|
||||
local file="$1"
|
||||
if have_jq; then
|
||||
jq . "$file"
|
||||
else
|
||||
cat "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
run_health_check() {
|
||||
log "Checking application health: ${BASE_URL}${HEALTH_PATH}"
|
||||
local response_file="$WORK_DIR/health.json"
|
||||
local code
|
||||
code="$(curl --silent --show-error --connect-timeout "$CONNECT_TIMEOUT" --max-time "$MAX_TIME" --output "$response_file" --write-out '%{http_code}' "${BASE_URL}${HEALTH_PATH}")"
|
||||
assert_http_ok "$code" "Health check"
|
||||
log "Health check passed"
|
||||
}
|
||||
|
||||
sync_registry() {
|
||||
local body_file="$WORK_DIR/vehicle_registry.json"
|
||||
local response_file="$WORK_DIR/vehicle_registry_response.json"
|
||||
write_json "$body_file" "[
|
||||
{\"vehicleID\":\"${UNMANNED_ID}\",\"vehicleType\":\"WUREN\"},
|
||||
{\"vehicleID\":\"${SPECIAL_ID}\",\"vehicleType\":\"TEQIN\"},
|
||||
{\"vehicleID\":\"${AIRCRAFT_ID}\",\"vehicleType\":\"HANGKONG\"}
|
||||
]"
|
||||
|
||||
log "Registering simulated collision objects"
|
||||
local code
|
||||
code="$(curl_json POST /api/VehicleRegistry "$body_file" "$response_file")"
|
||||
assert_http_ok "$code" "Vehicle registry"
|
||||
[[ "$(json_get "$response_file" '.status')" == "success" ]] || fail "Vehicle registry response status is not success"
|
||||
log "Registry synchronized"
|
||||
}
|
||||
|
||||
query_preparation_status() {
|
||||
local tag="$1"
|
||||
local response_file="$WORK_DIR/preparation_status_${tag}.json"
|
||||
local code
|
||||
code="$(curl_json POST /api/collision/preparation/status "" "$response_file")"
|
||||
assert_http_ok "$code" "Collision preparation status"
|
||||
[[ "$(json_get "$response_file" '.status')" == "success" ]] || fail "Preparation status response status is not success"
|
||||
print_json "$response_file"
|
||||
}
|
||||
|
||||
assert_not_ready_before_binding() {
|
||||
local response_file="$WORK_DIR/preparation_status_before.json"
|
||||
local ready
|
||||
ready="$(json_get "$response_file" '.ready')"
|
||||
[[ "$ready" == "false" ]] || fail "Preparation status should be false before routes are bound"
|
||||
json_contains_text "$response_file" "$AIRCRAFT_ID" || fail "Preparation status missing aircraft id before binding"
|
||||
json_contains_text "$response_file" "$UNMANNED_ID" || fail "Preparation status missing unmanned id before binding"
|
||||
json_contains_text "$response_file" "$SPECIAL_ID" || fail "Preparation status missing special vehicle id before binding"
|
||||
log "Preparation status correctly reports missing route bindings"
|
||||
}
|
||||
|
||||
bind_aircraft_route() {
|
||||
local body_file="$WORK_DIR/aircraft_route_query.json"
|
||||
local response_file="$WORK_DIR/aircraft_route_query_response.json"
|
||||
|
||||
if [[ "$AIRCRAFT_ROUTE_TYPE" == "OUT" ]]; then
|
||||
write_json "$body_file" "{
|
||||
\"objectId\": \"${AIRCRAFT_ID}\",
|
||||
\"routeType\": \"OUT\",
|
||||
\"outRunway\": \"${AIRCRAFT_OUT_RUNWAY}\",
|
||||
\"inRunway\": \"${AIRCRAFT_IN_RUNWAY}\",
|
||||
\"seat\": \"${AIRCRAFT_SEAT}\",
|
||||
\"startSeat\": \"${AIRCRAFT_START_SEAT:-$AIRCRAFT_SEAT}\"
|
||||
}"
|
||||
else
|
||||
write_json "$body_file" "{
|
||||
\"objectId\": \"${AIRCRAFT_ID}\",
|
||||
\"routeType\": \"IN\",
|
||||
\"inRunway\": \"${AIRCRAFT_IN_RUNWAY}\",
|
||||
\"outRunway\": \"${AIRCRAFT_OUT_RUNWAY}\",
|
||||
\"contactCross\": \"${AIRCRAFT_CONTACT_CROSS}\",
|
||||
\"seat\": \"${AIRCRAFT_SEAT}\"
|
||||
}"
|
||||
fi
|
||||
|
||||
log "Querying and binding aircraft route"
|
||||
local code
|
||||
code="$(curl_json POST /api/aircraft-routes/query "$body_file" "$response_file")"
|
||||
assert_http_ok "$code" "Aircraft route query"
|
||||
[[ "$(json_get "$response_file" '.code')" == "200" ]] || fail "Aircraft route query code is not 200"
|
||||
[[ "$(json_get "$response_file" '.data.bindingObjectId')" == "$AIRCRAFT_ID" ]] || fail "Aircraft route bindingObjectId mismatch"
|
||||
[[ "$(json_get "$response_file" '.data.routeBound')" == "true" ]] || fail "Aircraft route was not bound"
|
||||
log "Aircraft route bound successfully"
|
||||
}
|
||||
|
||||
bind_vehicle_route() {
|
||||
local vehicle_id="$1"
|
||||
local vehicle_type="$2"
|
||||
local route_name="$3"
|
||||
local point_a_lon="$4"
|
||||
local point_a_lat="$5"
|
||||
local point_b_lon="$6"
|
||||
local point_b_lat="$7"
|
||||
local response_tag="$8"
|
||||
|
||||
local body_file="$WORK_DIR/${response_tag}_route_assignment.json"
|
||||
local response_file="$WORK_DIR/${response_tag}_route_assignment_response.json"
|
||||
|
||||
write_json "$body_file" "{
|
||||
\"vehicleID\": \"${vehicle_id}\",
|
||||
\"vehicleType\": \"${vehicle_type}\",
|
||||
\"routeName\": \"${route_name}\",
|
||||
\"route\": {
|
||||
\"points\": [
|
||||
{\"lon\": ${point_a_lon}, \"lat\": ${point_a_lat}},
|
||||
{\"lon\": ${point_b_lon}, \"lat\": ${point_b_lat}}
|
||||
]
|
||||
}
|
||||
}"
|
||||
|
||||
log "Binding route for ${vehicle_type} ${vehicle_id}"
|
||||
local code
|
||||
code="$(curl_json POST /api/vehicle-route/assignment "$body_file" "$response_file")"
|
||||
assert_http_ok "$code" "Vehicle route assignment for ${vehicle_id}"
|
||||
[[ "$(json_get "$response_file" '.status')" == "success" ]] || fail "Route assignment failed for ${vehicle_id}"
|
||||
[[ "$(json_get "$response_file" '.objectId')" == "$vehicle_id" ]] || fail "Route assignment objectId mismatch for ${vehicle_id}"
|
||||
log "Route bound for ${vehicle_id}"
|
||||
}
|
||||
|
||||
assert_ready_after_binding() {
|
||||
local response_file="$WORK_DIR/preparation_status_after.json"
|
||||
[[ "$(json_get "$response_file" '.ready')" == "true" ]] || fail "Preparation status should be true after routes are bound"
|
||||
[[ "$(json_get "$response_file" '.summary.registeredCount')" == "3" ]] || fail "Expected 3 registered collision objects"
|
||||
[[ "$(json_get "$response_file" '.summary.missingRouteCount')" == "0" ]] || fail "Expected 0 missing routes after binding"
|
||||
log "Preparation status confirms all registered objects are ready"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "Starting collision preparation interface test"
|
||||
log "BASE_URL=${BASE_URL}"
|
||||
log "AIRCRAFT_ID=${AIRCRAFT_ID}, UNMANNED_ID=${UNMANNED_ID}, SPECIAL_ID=${SPECIAL_ID}"
|
||||
|
||||
run_health_check
|
||||
sync_registry
|
||||
query_preparation_status before
|
||||
assert_not_ready_before_binding
|
||||
|
||||
bind_aircraft_route
|
||||
bind_vehicle_route "$UNMANNED_ID" "WUREN" "$UV_ROUTE_NAME" 120.1000 36.1000 120.2000 36.2000 "uv"
|
||||
bind_vehicle_route "$SPECIAL_ID" "TEQIN" "$SP_ROUTE_NAME" 120.1000 36.2000 120.2000 36.1000 "sp"
|
||||
|
||||
query_preparation_status after
|
||||
assert_ready_after_binding
|
||||
|
||||
log "PASS: collision preparation interface flow is ready for deployment testing"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,159 @@
|
||||
# Collision Registration And Thresholds Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make collision participation follow full-sync registration semantics, expose current crossing thresholds for现场排查, and relax route deviation tolerance so near-route equipment is still evaluated.
|
||||
|
||||
**Architecture:** Keep the current periodic detection loop intact and make the behavior clearer at the boundaries: registration remains the source of truth, the preparation/status API exposes current threshold values, and path-conflict detection accepts a wider route deviation before dropping a pair. This stays inside the existing controller/runtime-state/detection services to avoid spreading new concepts.
|
||||
|
||||
**Tech Stack:** Spring Boot, JUnit 5, MockMvc, Mockito, JTS
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock Expected API Behavior With Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldExposeCurrentCrossingThresholdsInPreparationStatus() throws Exception {
|
||||
mockMvc.perform(post("/api/collision/preparation/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.thresholds.vehicleDistance").value(40.0))
|
||||
.andExpect(jsonPath("$.thresholds.aircraftDistance").value(40.0));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -pl qaup-collision -Dtest=PlatformIntegrationControllerTest#shouldExposeCurrentCrossingThresholdsInPreparationStatus test`
|
||||
Expected: FAIL because `$.thresholds.vehicleDistance` does not exist yet
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```java
|
||||
Map<String, Object> thresholds = new LinkedHashMap<>();
|
||||
thresholds.put("vehicleDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
|
||||
thresholds.put("aircraftDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft());
|
||||
payload.put("thresholds", thresholds);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -pl qaup-collision -Dtest=PlatformIntegrationControllerTest#shouldExposeCurrentCrossingThresholdsInPreparationStatus test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java qaup-collision/src/main/java/com/qaup/collision/controller/PlatformIntegrationController.java
|
||||
git commit -m "feat: expose collision thresholds in preparation status"
|
||||
```
|
||||
|
||||
### Task 2: Lock Wider Route Participation With Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldStillEvaluateConflictWhenAircraftIsWithinTwoHundredMetersOfAssignedRoute() throws Exception {
|
||||
// Build intersecting routes, place aircraft 150m off the route, then expect one event.
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -pl qaup-collision -Dtest=PathConflictDetectionDirectionalTest#shouldStillEvaluateConflictWhenAircraftIsWithinTwoHundredMetersOfAssignedRoute test`
|
||||
Expected: FAIL because no event is published under the current 80m deviation limit
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```java
|
||||
private static final double MAX_ROUTE_DEVIATION_METERS = 200.0;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -pl qaup-collision -Dtest=PathConflictDetectionDirectionalTest#shouldStillEvaluateConflictWhenAircraftIsWithinTwoHundredMetersOfAssignedRoute test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java qaup-collision/src/main/java/com/qaup/collision/pathconflict/service/PathConflictDetectionService.java
|
||||
git commit -m "fix: widen route deviation tolerance for collision detection"
|
||||
```
|
||||
|
||||
### Task 3: Verify Full-Sync Registration Still Removes Omitted Objects
|
||||
|
||||
**Files:**
|
||||
- Modify: `qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldReportRemovedObjectsWhenVehicleRegistryIsResubmitted() {
|
||||
// first update with three objects, second update with one object, then assert removed IDs
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn -pl qaup-collision -Dtest=PlatformRuntimeStateServiceTest#shouldReportRemovedObjectsWhenVehicleRegistryIsResubmitted test`
|
||||
Expected: FAIL because the result does not expose removed IDs yet
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
```java
|
||||
TreeSet<String> previousVehicleIds = new TreeSet<>(vehicleTypes.keySet());
|
||||
vehicleTypes.clear();
|
||||
...
|
||||
previousVehicleIds.removeAll(vehicleTypes.keySet());
|
||||
return new VehicleRegistryUpdateResult(..., new ArrayList<>(previousVehicleIds));
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn -pl qaup-collision -Dtest=PlatformRuntimeStateServiceTest#shouldReportRemovedObjectsWhenVehicleRegistryIsResubmitted test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java qaup-collision/src/main/java/com/qaup/collision/service/PlatformRuntimeStateService.java qaup-collision/src/main/java/com/qaup/collision/controller/PlatformIntegrationController.java
|
||||
git commit -m "feat: surface removed objects on registry full sync"
|
||||
```
|
||||
|
||||
### Task 4: Run Focused Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: None
|
||||
- Test: `qaup-collision/src/test/java/com/qaup/collision/controller/PlatformIntegrationControllerTest.java`
|
||||
- Test: `qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionDirectionalTest.java`
|
||||
- Test: `qaup-collision/src/test/java/com/qaup/collision/pathconflict/service/PathConflictDetectionServiceRuntimeConfigTest.java`
|
||||
- Test: `qaup-collision/src/test/java/com/qaup/collision/service/PlatformRuntimeStateServiceTest.java`
|
||||
|
||||
- [ ] **Step 1: Run focused module tests**
|
||||
|
||||
```bash
|
||||
mvn -pl qaup-collision -Dtest=PlatformIntegrationControllerTest,PathConflictDetectionDirectionalTest,PathConflictDetectionServiceRuntimeConfigTest,PlatformRuntimeStateServiceTest test
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Confirm expected result**
|
||||
|
||||
Expected: BUILD SUCCESS with all targeted tests green
|
||||
|
||||
- [ ] **Step 3: Summarize the operational impact**
|
||||
|
||||
```text
|
||||
1. Preparation status now shows current vehicle/aircraft crossing thresholds.
|
||||
2. Collision detection accepts route offsets up to 200m before dropping the pair.
|
||||
3. Registry re-submission remains full-sync and now reports which objects were removed.
|
||||
```
|
||||
2
mqtt协议验证/demo/.gitattributes
vendored
Normal file
2
mqtt协议验证/demo/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
33
mqtt协议验证/demo/.gitignore
vendored
Normal file
33
mqtt协议验证/demo/.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
3
mqtt协议验证/demo/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
mqtt协议验证/demo/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
295
mqtt协议验证/demo/mvnw
vendored
Normal file
295
mqtt协议验证/demo/mvnw
vendored
Normal file
@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
mqtt协议验证/demo/mvnw.cmd
vendored
Normal file
189
mqtt协议验证/demo/mvnw.cmd
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
62
mqtt协议验证/demo/pom.xml
Normal file
62
mqtt协议验证/demo/pom.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>demo</name>
|
||||
<description>demo</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!--MQTT-->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MqttConfig {
|
||||
|
||||
@Value("${mqtt.username}")
|
||||
private String username;
|
||||
|
||||
@Value("${mqtt.password}")
|
||||
private String password;
|
||||
|
||||
@Value("${mqtt.timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Value("${mqtt.keepalive}")
|
||||
private int keepalive;
|
||||
|
||||
@Bean
|
||||
public MqttConnectOptions mqttConnectOptions() {
|
||||
MqttConnectOptions options = new MqttConnectOptions();
|
||||
options.setCleanSession(true);
|
||||
options.setConnectionTimeout(timeout);
|
||||
options.setKeepAliveInterval(keepalive);
|
||||
|
||||
if (username != null && !username.isEmpty()) {
|
||||
options.setUserName(username);
|
||||
options.setPassword(password.toCharArray());
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.example.demo.subscriber;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.example.demo.subscriber;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.eclipse.paho.client.mqttv3.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Component
|
||||
public class MqttSubscriber {
|
||||
|
||||
@Value("${mqtt.broker}")
|
||||
private String broker;
|
||||
|
||||
@Value("${mqtt.client-id}")
|
||||
private String clientId;
|
||||
|
||||
@Value("${mqtt.topic}")
|
||||
private String topic;
|
||||
|
||||
private final MqttConnectOptions options;
|
||||
|
||||
public MqttSubscriber(MqttConnectOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws MqttException {
|
||||
MqttClient client = new MqttClient(broker, clientId);
|
||||
|
||||
client.setCallback(new MqttCallback() {
|
||||
@Override
|
||||
public void connectionLost(Throwable cause) {
|
||||
System.out.println("❌ MQTT 连接断开:" + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageArrived(String topic, MqttMessage message) {
|
||||
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
|
||||
System.out.println("📥 收到消息");
|
||||
System.out.println("Topic: " + topic);
|
||||
System.out.println("Payload: " + payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliveryComplete(IMqttDeliveryToken token) {
|
||||
// 订阅端一般用不到
|
||||
}
|
||||
});
|
||||
|
||||
client.connect(options);
|
||||
client.subscribe(topic, 0);
|
||||
|
||||
System.out.println("✅ MQTT 已连接,订阅 Topic:" + topic);
|
||||
}
|
||||
}
|
||||
10
mqtt协议验证/demo/src/main/resources/application.yaml
Normal file
10
mqtt协议验证/demo/src/main/resources/application.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
mqtt:
|
||||
broker: ${MQTT_BROKER:tcp://127.0.0.1:8082}
|
||||
client-id: ${MQTT_CLIENT_ID:springboot-mqtt-client}
|
||||
username: ${MQTT_USERNAME:}
|
||||
password: ${MQTT_PASSWORD:}
|
||||
topic: ${MQTT_TOPIC:cusc/v2/SF053/QingDrsu001/data}
|
||||
timeout: ${MQTT_TIMEOUT:10}
|
||||
keepalive: ${MQTT_KEEPALIVE:20}
|
||||
server:
|
||||
port: ${SERVER_PORT:8087}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class DemoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
50
mqtt协议验证/publish.py
Normal file
50
mqtt协议验证/publish.py
Normal file
@ -0,0 +1,50 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
# ===== MQTT TCP 连接信息 =====
|
||||
BROKER_HOST = os.getenv("MQTT_HOST", "127.0.0.1")
|
||||
BROKER_PORT = int(os.getenv("MQTT_PORT", "8082"))
|
||||
USERNAME = os.getenv("MQTT_USERNAME", "")
|
||||
PASSWORD = os.getenv("MQTT_PASSWORD", "")
|
||||
TOPIC = os.getenv("MQTT_TOPIC", "cusc/v2/SF053/QingDrsu001/data")
|
||||
|
||||
payload = {
|
||||
"msgType": "deviceReq",
|
||||
"encryptFlag": 0,
|
||||
"seriesNumber": "tcp-test-001",
|
||||
"reportTime": int(time.time() * 1000),
|
||||
"serviceType": "rsu-traffic-lights",
|
||||
"serviceData": {
|
||||
"collectTime": int(time.time() * 1000),
|
||||
"trafficLightsId": "TL001",
|
||||
"longitude": 120.1,
|
||||
"latitude": 31.1,
|
||||
"intersection": "TCP测试路口",
|
||||
"generateTime": int(time.time() * 1000),
|
||||
"trafficLightsStatus": 1,
|
||||
"phases": [
|
||||
{
|
||||
"phaseId": "1",
|
||||
"phasePosition": 0,
|
||||
"phaseColor": 1,
|
||||
"trafficLightsType": 1,
|
||||
"countDown": 10,
|
||||
"redDuration": 30,
|
||||
"greenDuration": 20,
|
||||
"yellowDuration": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
client = mqtt.Client(client_id="python-tcp-client")
|
||||
if USERNAME:
|
||||
client.username_pw_set(USERNAME, PASSWORD)
|
||||
client.connect(BROKER_HOST, BROKER_PORT, 60)
|
||||
|
||||
client.publish(TOPIC, json.dumps(payload), qos=1)
|
||||
print("消息已发送(MQTT over TCP)")
|
||||
|
||||
client.disconnect()
|
||||
@ -0,0 +1,55 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.qaup.collision.dto.AircraftRouteQueryRequest;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.service.AircraftRouteQueryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Frontend query endpoint for aircraft taxi routes.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/api/aircraft-routes", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequiredArgsConstructor
|
||||
public class AircraftRouteQueryController {
|
||||
|
||||
private final AircraftRouteQueryService aircraftRouteQueryService;
|
||||
|
||||
@PostMapping(path = "/query", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> queryRoute(@RequestBody AircraftRouteQueryRequest request) {
|
||||
try {
|
||||
AircraftRouteDTO data = aircraftRouteQueryService.queryRoute(request);
|
||||
if (data == null) {
|
||||
return buildResponse(HttpStatus.NOT_FOUND, 404, "No route data found", null);
|
||||
}
|
||||
return buildResponse(HttpStatus.OK, 200, "success", data);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, 400, e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Internal server error", null);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> buildResponse(
|
||||
HttpStatus status,
|
||||
int code,
|
||||
String msg,
|
||||
Object data) {
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("code", code);
|
||||
body.put("msg", msg);
|
||||
body.put("data", data);
|
||||
return ResponseEntity.status(status).contentType(MediaType.APPLICATION_JSON).body(body);
|
||||
}
|
||||
}
|
||||
@ -67,6 +67,13 @@ public class PlatformIntegrationController {
|
||||
payload.put("controllableCount", result.controllableCount());
|
||||
payload.put("typesCount", result.typesCount());
|
||||
payload.put("controllableVehicleIDs", result.controllableVehicleIDs());
|
||||
payload.put("removedVehicleIDs", result.removedVehicleIDs());
|
||||
if (result.testSessionId() != null) {
|
||||
payload.put("testSessionId", result.testSessionId());
|
||||
}
|
||||
if (result.endedTestSessionId() != null) {
|
||||
payload.put("endedTestSessionId", result.endedTestSessionId());
|
||||
}
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
} catch (JsonProcessingException e) {
|
||||
return invalidJson(e);
|
||||
@ -75,18 +82,43 @@ public class PlatformIntegrationController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the legacy aircraft warning radius used by older runway-based flows.
|
||||
* This endpoint is no longer the primary threshold source for path crossing alerts.
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the legacy aircraft alert radius used by older runway-based flows.
|
||||
* This endpoint is no longer the primary threshold source for path crossing alerts.
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates path crossing distance thresholds.
|
||||
*
|
||||
* Supported payloads:
|
||||
* - {"value": 40}
|
||||
* Applies the same threshold to both vehicle and aircraft.
|
||||
* - {"vehicleDistance": 50, "aircraftDistance": 80}
|
||||
* Updates each threshold independently.
|
||||
*
|
||||
* Current semantics:
|
||||
* - vehicleDistance: vehicle distance to the crossing point
|
||||
* - aircraftDistance: aircraft distance to the crossing point
|
||||
*
|
||||
* Alert evaluation:
|
||||
* - both sides inside their thresholds -> alert
|
||||
* - only one side inside its threshold -> warning
|
||||
*/
|
||||
@PostMapping(path = "/config/collision/diverging_release_distance", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> updateCollisionDivergingReleaseDistance(@RequestBody String requestBody) {
|
||||
return updateCollisionDistanceConfig(requestBody);
|
||||
@ -169,6 +201,30 @@ public class PlatformIntegrationController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(path = "/api/collision/preparation/status")
|
||||
public ResponseEntity<Map<String, Object>> collisionPreparationStatus() {
|
||||
try {
|
||||
PlatformRuntimeStateService.CollisionPreparationStatus result =
|
||||
platformRuntimeStateService.getCollisionPreparationStatus();
|
||||
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "success");
|
||||
if (result.testSessionId() != null) {
|
||||
payload.put("testSessionId", result.testSessionId());
|
||||
}
|
||||
payload.put("ready", result.ready());
|
||||
payload.put("summary", result.summary());
|
||||
payload.put("objects", result.objects());
|
||||
Map<String, Object> thresholds = new LinkedHashMap<>();
|
||||
thresholds.put("vehicleDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
|
||||
thresholds.put("aircraftDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft());
|
||||
payload.put("thresholds", thresholds);
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
} catch (Exception e) {
|
||||
return internalError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVehicleRegistryItem(
|
||||
JsonNode item,
|
||||
List<String> errors,
|
||||
@ -318,7 +374,7 @@ public class PlatformIntegrationController {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "success");
|
||||
payload.put("area", "collision");
|
||||
payload.put("field", "collision.diverging_release_distance");
|
||||
payload.put("field", "crossing_distance_threshold");
|
||||
payload.put("old", oldValue);
|
||||
payload.put("new", newValue);
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
@ -336,7 +392,7 @@ public class PlatformIntegrationController {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "success");
|
||||
payload.put("area", "collision");
|
||||
payload.put("field", "collision.diverging_release_distance");
|
||||
payload.put("field", "crossing_distance_threshold");
|
||||
payload.put("oldVehicleDistance", oldVehicleDistance);
|
||||
payload.put("newVehicleDistance", newVehicleDistance != null ? newVehicleDistance : platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
|
||||
payload.put("oldAircraftDistance", oldAircraftDistance);
|
||||
@ -357,6 +413,7 @@ public class PlatformIntegrationController {
|
||||
}
|
||||
String normalized = objectTypeValue.trim().toUpperCase(Locale.ROOT);
|
||||
return switch (normalized) {
|
||||
case "AIRCRAFT", "HANGKONG", "AC" -> ObjectRouteAssignment.ObjectType.AIRCRAFT;
|
||||
case "UNMANNED_VEHICLE", "UNMANNED", "UV", "WUREN" -> ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE;
|
||||
case "SPECIAL_VEHICLE", "SPECIAL", "TEQIN", "SPECIALVEHICLE" -> ObjectRouteAssignment.ObjectType.SPECIAL_VEHICLE;
|
||||
default -> null;
|
||||
|
||||
@ -2,8 +2,8 @@ package com.qaup.collision.datacollector.server;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.pathconflict.service.VehicleCommandService;
|
||||
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
|
||||
import com.qaup.collision.websocket.handler.VehicleCommandInfoWebSocketHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -40,7 +40,7 @@ public class TrafficLightMqttSubscriber {
|
||||
|
||||
private final MqttConnectOptions mqttConnectOptions;
|
||||
private final CollisionWebSocketHandler collisionWebSocketHandler;
|
||||
private final VehicleCommandInfoWebSocketHandler vehicleCommandInfoWebSocketHandler;
|
||||
private final VehicleCommandService vehicleCommandService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private MqttClient mqttClient;
|
||||
@ -55,17 +55,17 @@ public class TrafficLightMqttSubscriber {
|
||||
*
|
||||
* @param mqttConnectOptions MQTT 连接选项
|
||||
* @param collisionWebSocketHandler WebSocket 处理器,用于向前端广播消息
|
||||
* @param vehicleCommandInfoWebSocketHandler 车辆控制指令 WebSocket 处理器
|
||||
* @param vehicleCommandService 统一车辆控制服务
|
||||
* @param objectMapper JSON 解析器
|
||||
*/
|
||||
@Autowired
|
||||
public TrafficLightMqttSubscriber(MqttConnectOptions mqttConnectOptions,
|
||||
CollisionWebSocketHandler collisionWebSocketHandler,
|
||||
VehicleCommandInfoWebSocketHandler vehicleCommandInfoWebSocketHandler,
|
||||
VehicleCommandService vehicleCommandService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.mqttConnectOptions = mqttConnectOptions;
|
||||
this.collisionWebSocketHandler = collisionWebSocketHandler;
|
||||
this.vehicleCommandInfoWebSocketHandler = vehicleCommandInfoWebSocketHandler;
|
||||
this.vehicleCommandService = vehicleCommandService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@ -104,59 +104,7 @@ public class TrafficLightMqttSubscriber {
|
||||
public void messageArrived(String topic, MqttMessage message) {
|
||||
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
|
||||
totalMessages.incrementAndGet();
|
||||
|
||||
log.info("🚦 [MQTT] 收到红绿灯消息");
|
||||
log.info("🚦 [MQTT] Topic: {}", topic);
|
||||
log.info("🚦 [MQTT] Payload: {}", payload);
|
||||
|
||||
// 通过 WebSocket 广播消息给前端
|
||||
try {
|
||||
String websocketMessage = String.format(
|
||||
"{\"type\":\"trafficLight\",\"source\":\"mqtt\",\"topic\":\"%s\",\"payload\":%s,\"timestamp\":%d}",
|
||||
topic, payload, System.currentTimeMillis()
|
||||
);
|
||||
collisionWebSocketHandler.broadcastMessage(websocketMessage);
|
||||
log.debug("🚦 [MQTT] 消息已通过 WebSocket 广播到前端");
|
||||
} catch (Exception e) {
|
||||
log.error("🚦 [MQTT] WebSocket 广播失败: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 解析 phaseColor 并发送车辆控制指令
|
||||
try {
|
||||
JsonNode jsonNode = objectMapper.readTree(payload);
|
||||
|
||||
// 从 serviceData.phases 数组中获取 phaseColor
|
||||
JsonNode serviceDataNode = jsonNode.get("serviceData");
|
||||
if (serviceDataNode != null) {
|
||||
JsonNode phasesNode = serviceDataNode.get("phases");
|
||||
if (phasesNode != null && phasesNode.isArray() && phasesNode.size() > 0) {
|
||||
// 获取第一个相位的 phaseColor
|
||||
JsonNode firstPhase = phasesNode.get(0);
|
||||
JsonNode phaseColorNode = firstPhase.get("phaseColor");
|
||||
|
||||
if (phaseColorNode != null && phaseColorNode.isInt()) {
|
||||
int phaseColor = phaseColorNode.asInt();
|
||||
log.info("🚦 [MQTT] 解析到 phaseColor: {}", phaseColor);
|
||||
|
||||
// phaseColor 为 1 时发送 GREEN,为 3 时发送 RED
|
||||
if (phaseColor == 1 || phaseColor == 3) {
|
||||
vehicleCommandInfoWebSocketHandler.sendSignalState(phaseColor);
|
||||
log.info("🚦 [MQTT] 已发送车辆控制指令: phaseColor={}", phaseColor);
|
||||
} else {
|
||||
log.warn("🚦 [MQTT] 未知的 phaseColor 值: {},仅支持 1 (GREEN) 或 3 (RED)", phaseColor);
|
||||
}
|
||||
} else {
|
||||
log.warn("🚦 [MQTT] phases[0] 中未找到 phaseColor 字段或格式不正确");
|
||||
}
|
||||
} else {
|
||||
log.warn("🚦 [MQTT] serviceData 中未找到 phases 数组或数组为空");
|
||||
}
|
||||
} else {
|
||||
log.warn("🚦 [MQTT] 消息中未找到 serviceData 字段");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("🚦 [MQTT] 解析 phaseColor 或发送控制指令失败: {}", e.getMessage(), e);
|
||||
}
|
||||
handleTrafficLightMessage(topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -217,4 +165,75 @@ public class TrafficLightMqttSubscriber {
|
||||
return String.format("MQTT订阅状态 - 连接:%s, 总消息:%d, 错误:%d",
|
||||
connected.get() ? "是" : "否", totalMessages.get(), errorCount.get());
|
||||
}
|
||||
|
||||
void handleTrafficLightMessage(String topic, String payload) {
|
||||
log.info("🚦 [MQTT] 收到红绿灯消息");
|
||||
log.info("🚦 [MQTT] Topic: {}", topic);
|
||||
log.info("🚦 [MQTT] Payload: {}", payload);
|
||||
|
||||
try {
|
||||
String websocketMessage = String.format(
|
||||
"{\"type\":\"trafficLight\",\"source\":\"mqtt\",\"topic\":\"%s\",\"payload\":%s,\"timestamp\":%d}",
|
||||
topic, payload, System.currentTimeMillis()
|
||||
);
|
||||
collisionWebSocketHandler.broadcastMessage(websocketMessage);
|
||||
log.debug("🚦 [MQTT] 消息已通过 WebSocket 广播到前端");
|
||||
} catch (Exception e) {
|
||||
log.error("🚦 [MQTT] WebSocket 广播失败: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode jsonNode = objectMapper.readTree(payload);
|
||||
JsonNode serviceDataNode = jsonNode.get("serviceData");
|
||||
if (serviceDataNode == null) {
|
||||
log.warn("🚦 [MQTT] 消息中未找到 serviceData 字段");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonNode phasesNode = serviceDataNode.get("phases");
|
||||
if (phasesNode == null || !phasesNode.isArray() || phasesNode.isEmpty()) {
|
||||
log.warn("🚦 [MQTT] serviceData 中未找到 phases 数组或数组为空");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonNode phaseColorNode = phasesNode.get(0).get("phaseColor");
|
||||
if (phaseColorNode == null || !phaseColorNode.isInt()) {
|
||||
log.warn("🚦 [MQTT] phases[0] 中未找到 phaseColor 字段或格式不正确");
|
||||
return;
|
||||
}
|
||||
|
||||
int phaseColor = phaseColorNode.asInt();
|
||||
log.info("🚦 [MQTT] 解析到 phaseColor: {}", phaseColor);
|
||||
|
||||
if (phaseColor != 1 && phaseColor != 2 && phaseColor != 3) {
|
||||
log.warn("🚦 [MQTT] 未知的 phaseColor 值: {},仅支持 1 (GREEN) / 2 (YELLOW) / 3 (RED)", phaseColor);
|
||||
return;
|
||||
}
|
||||
|
||||
String intersectionId = extractIntersectionId(serviceDataNode);
|
||||
vehicleCommandService.sendTrafficLightSignalCommands(phaseColor, intersectionId);
|
||||
log.info("🚦 [MQTT] 已通过统一 HTTP 接口发送车辆控制指令: phaseColor={}, intersectionId={}",
|
||||
phaseColor, intersectionId);
|
||||
} catch (Exception e) {
|
||||
log.error("🚦 [MQTT] 解析 phaseColor 或发送控制指令失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractIntersectionId(JsonNode serviceDataNode) {
|
||||
String intersectionId = textOrEmpty(serviceDataNode.get("intersection"));
|
||||
if (!intersectionId.isBlank()) {
|
||||
return intersectionId;
|
||||
}
|
||||
|
||||
String trafficLightsId = textOrEmpty(serviceDataNode.get("trafficLightsId"));
|
||||
if (!trafficLightsId.isBlank()) {
|
||||
return trafficLightsId;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private String textOrEmpty(JsonNode node) {
|
||||
return node == null || node.isNull() ? "" : node.asText("");
|
||||
}
|
||||
}
|
||||
@ -53,6 +53,7 @@ import org.locationtech.jts.geom.PrecisionModel;
|
||||
public class DataCollectorService {
|
||||
|
||||
private static final boolean VEHICLE_MANAGER_ROUTE_INGEST_ENABLED = false;
|
||||
private static final boolean VEHICLE_MANAGER_HTTP_POLLING_ENABLED = false;
|
||||
private static final java.util.concurrent.atomic.AtomicBoolean ROUTE_INGEST_DISABLED_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
// 机场数据源相关配置
|
||||
@ -71,10 +72,6 @@ public class DataCollectorService {
|
||||
@Value("${data.collector.detection.interval:1000}")
|
||||
private long detectionInterval;
|
||||
|
||||
@Value("${data.collector.vehicle-manager.http.polling-enabled:true}")
|
||||
private boolean vehicleManagerHttpPollingEnabled;
|
||||
|
||||
|
||||
@Value("${data.collector.route.periodic-collection-enabled:false}")
|
||||
private boolean periodicRouteCollectionEnabled;
|
||||
|
||||
@ -240,6 +237,9 @@ public class DataCollectorService {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
}
|
||||
if (!VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> ids = dataCollectorDao.getVehicleManagerVehicleDetails();
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
@ -689,6 +689,9 @@ public class DataCollectorService {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
}
|
||||
if (!VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 丢弃旧轮次:上一轮未完成时跳过,避免“多车循环HTTP + fixedRate + @Async”堆积
|
||||
try {
|
||||
@ -745,6 +748,7 @@ public class DataCollectorService {
|
||||
.currentPosition(currentPosition)
|
||||
.currentSpeed(null) // 不在采集阶段计算速度
|
||||
.currentHeading(null) // 不在采集阶段计算方向
|
||||
.sourceTimestampMs(resolveUnmannedVehicleSourceTimestampMs(statusData))
|
||||
.altitude(0.0); // 默认高度
|
||||
|
||||
// Task context is disabled for route ingestion.
|
||||
@ -854,6 +858,19 @@ public class DataCollectorService {
|
||||
}
|
||||
}
|
||||
|
||||
private long resolveUnmannedVehicleSourceTimestampMs(
|
||||
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData) {
|
||||
|
||||
if (statusData != null
|
||||
&& statusData.getSensorStatus() != null
|
||||
&& statusData.getSensorStatus().getGps() != null
|
||||
&& statusData.getSensorStatus().getGps().getLastUpdate() != null
|
||||
&& statusData.getSensorStatus().getGps().getLastUpdate() > 0L) {
|
||||
return statusData.getSensorStatus().getGps().getLastUpdate();
|
||||
}
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已知的无人车ID列表
|
||||
* 可以从配置、缓存或其他数据源获取
|
||||
@ -876,7 +893,7 @@ public class DataCollectorService {
|
||||
@Async // 异步执行
|
||||
public void collectUniversalVehicleStatus() {
|
||||
// 无人车状态已在 collectUnmannedVehicleData() 中通过 HTTP 拉取并缓存,无需重复采集
|
||||
if (vehicleManagerHttpPollingEnabled) {
|
||||
if (VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -369,6 +369,8 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
||||
return handleTISFLIGHT(root);
|
||||
case "ADXP_NAOMS_O_DYN_DFDE":
|
||||
return handleDFDE(root);
|
||||
case "ADXP_WURENCHE_O_DYN_CONTACTTAXIWAY":
|
||||
return handleCONTACTTAXIWAY(root);
|
||||
default:
|
||||
log.debug("未知的服务代码: {}", serviceCode);
|
||||
return null;
|
||||
@ -454,6 +456,17 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getLastTextContent(org.w3c.dom.Element parent, String tagName) {
|
||||
org.w3c.dom.NodeList nodeList = parent.getElementsByTagName(tagName);
|
||||
for (int i = nodeList.getLength() - 1; i >= 0; i--) {
|
||||
String value = nodeList.item(i).getTextContent();
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一航班号归一化规则,避免同一航班参数写入不同 Redis Key,导致路由参数难以凑齐。
|
||||
*
|
||||
@ -1121,7 +1134,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
||||
String flightNumberRaw = getTextContent(root, "FlNo");
|
||||
String flightNumber = normalizeTisFlightNo(flightNumberRaw);
|
||||
String flightId = normalizeTisFlightNo(flightIdRaw);
|
||||
String contactCross = getTextContent(root, "ContactCross");
|
||||
String type = getTextContent(root, "Type");
|
||||
long nowMs = System.currentTimeMillis();
|
||||
|
||||
@ -1157,9 +1169,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
||||
FlightNotificationDTO dto = new FlightNotificationDTO();
|
||||
dto.setFlightNo(resolvedFlightNo != null ? resolvedFlightNo : flightNumberRaw);
|
||||
dto.setType(routeType);
|
||||
if ("IN".equalsIgnoreCase(routeType)) {
|
||||
dto.setContactCross(contactCross);
|
||||
}
|
||||
|
||||
String normalizedBizKey = resolveTisFlightBizKey(bizKey, resolvedFlightNo);
|
||||
String flightKey = buildFlightRedisKey(resolvedFlightNo);
|
||||
@ -1169,12 +1178,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
||||
try {
|
||||
redisCache.setCacheMapValue(flightKey, "flightNumber", resolvedFlightNo);
|
||||
updateActiveBizKey(flightKey, normalizedBizKey, nowMs);
|
||||
// contactCross 仅对进港有效,避免进出港参数串用
|
||||
if ("IN".equalsIgnoreCase(routeType)
|
||||
&& contactCross != null && !contactCross.isBlank()) {
|
||||
setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "contactCross", contactCross);
|
||||
setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "contactCrossTs", String.valueOf(nowMs));
|
||||
}
|
||||
log.info("TISFLIGHT航班号解析(已应用三字转二字): flNoRaw={}, flightIdRaw={}, fuId={}, bizKey={}, resolvedFlightNo={}",
|
||||
flightNumberRaw, flightIdRaw, fuId, normalizedBizKey, resolvedFlightNo);
|
||||
} catch (Exception e) {
|
||||
@ -1187,6 +1190,32 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
||||
return dto;
|
||||
}
|
||||
|
||||
private FlightNotificationDTO handleCONTACTTAXIWAY(org.w3c.dom.Element root) {
|
||||
String flightNoRaw = getTextContent(root, "FlightNo");
|
||||
String normalizedFlightNo = normalizeTisFlightNo(flightNoRaw);
|
||||
String contactCross = getLastTextContent(root, "TaxiwayIntersection");
|
||||
long nowMs = System.currentTimeMillis();
|
||||
|
||||
String flightKey = buildFlightRedisKey(normalizedFlightNo);
|
||||
if (flightKey == null || contactCross == null || contactCross.isBlank()) {
|
||||
log.warn("CONTACTTAXIWAY事件缺少路由参数,跳过Redis写入: flightNoRaw={}, normalizedFlightNo={}, contactCross={}",
|
||||
flightNoRaw, normalizedFlightNo, contactCross);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
redisCache.setCacheMapValue(flightKey, "flightNumber", normalizedFlightNo);
|
||||
redisCache.setCacheMapValue(flightKey, "contactCross", contactCross.trim());
|
||||
redisCache.setCacheMapValue(flightKey, "contactCrossTs", String.valueOf(nowMs));
|
||||
log.info("成功将CONTACTTAXIWAY道口数据存储到Redis: flightNumber={}, contactCross={}",
|
||||
normalizedFlightNo, contactCross);
|
||||
} catch (Exception e) {
|
||||
log.error("存储CONTACTTAXIWAY道口数据到Redis失败: flightNumber={}, contactCross={}",
|
||||
normalizedFlightNo, contactCross, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FlightNotificationDTO handleAXOT(org.w3c.dom.Element root) {
|
||||
// BizKey 常见格式:FLIGHTNO-进出港标识-时间(yyyyMMddHHmmss) 或其他分段
|
||||
String bizKey = getTextContent(root, "BizKey");
|
||||
|
||||
@ -30,11 +30,13 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -65,6 +67,9 @@ public class DataProcessingService {
|
||||
@Value("${data.collector.disabled:false}")
|
||||
private boolean collectorDisabled;
|
||||
|
||||
@Value("${qaup.collision.diagnostic-log.enabled:true}")
|
||||
private boolean collisionDiagnosticLogEnabled;
|
||||
|
||||
@Autowired
|
||||
private SpeedCalculationService speedCalculationService;
|
||||
|
||||
@ -98,6 +103,7 @@ public class DataProcessingService {
|
||||
|
||||
// 从DataCollectorService获取缓存的引用
|
||||
private Map<String, MovingObject> activeMovingObjectsCache;
|
||||
private final Map<String, Long> lastPublishedPositionSampleTimestamps = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 防止@Scheduled任务重入导致并发执行、消息风暴与数据库压力。
|
||||
@ -146,7 +152,7 @@ public class DataProcessingService {
|
||||
calculateSpeedAndDirectionForAllObjects(currentActiveObjects);
|
||||
|
||||
// 第二步:发送WebSocket位置更新消息
|
||||
sendPositionUpdatesForActiveObjects(currentActiveObjects);
|
||||
PositionDispatchSummary positionDispatchSummary = sendPositionUpdatesForActiveObjects(currentActiveObjects);
|
||||
|
||||
// 第三步:处理通用车辆状态数据并发送WebSocket更新
|
||||
processUniversalVehicleStatusUpdates();
|
||||
@ -155,7 +161,8 @@ public class DataProcessingService {
|
||||
// 注意:采集侧只缓存;发送侧在处理完成后会清理缓存,避免同一通知被每秒重复推送(刷屏)。
|
||||
processFlightNotificationUpdates();
|
||||
|
||||
List<MovingObject> collisionManagedObjects = filterCollisionManagedObjects(currentActiveObjects);
|
||||
CollisionManagedObjectsResult collisionManagedResult = filterCollisionManagedObjects(currentActiveObjects);
|
||||
List<MovingObject> collisionManagedObjects = collisionManagedResult.objects();
|
||||
|
||||
// 第五步:将注册的无人车任务路径准备为正式路线输入
|
||||
if (routePreparationService != null) {
|
||||
@ -163,6 +170,7 @@ public class DataProcessingService {
|
||||
}
|
||||
|
||||
// 第六步:执行路径冲突检测
|
||||
PathConflictDetectionService.ConflictDetectionSummary conflictDetectionSummary =
|
||||
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
|
||||
|
||||
// 第七步:执行违规检测
|
||||
@ -171,25 +179,63 @@ public class DataProcessingService {
|
||||
// 第八步:保存无人车数据到数据库
|
||||
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
||||
|
||||
logCycleDiagnostics(currentActiveObjects.size(), collisionManagedObjects.size(),
|
||||
positionDispatchSummary, collisionManagedResult, conflictDetectionSummary);
|
||||
|
||||
log.info("周期性数据处理完成");
|
||||
}
|
||||
|
||||
private List<MovingObject> filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
||||
private CollisionManagedObjectsResult filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
||||
if (activeObjects == null || activeObjects.isEmpty()) {
|
||||
return List.of();
|
||||
return new CollisionManagedObjectsResult(List.of(), 0, 0);
|
||||
}
|
||||
|
||||
List<MovingObject> filteredObjects = activeObjects.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(object -> platformRuntimeStateService == null
|
||||
|| platformRuntimeStateService.isRegisteredForCollision(object.getObjectId(), object.getObjectType()))
|
||||
.collect(Collectors.toList());
|
||||
List<MovingObject> filteredObjects = new ArrayList<>();
|
||||
int registrationFilteredCount = 0;
|
||||
int typeOverrideCount = 0;
|
||||
|
||||
for (MovingObject object : activeObjects) {
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
if (platformRuntimeStateService == null) {
|
||||
filteredObjects.add(object);
|
||||
continue;
|
||||
}
|
||||
|
||||
MovingObjectType registeredType =
|
||||
platformRuntimeStateService.getRegisteredCollisionObjectType(object.getObjectId());
|
||||
if (registeredType == null) {
|
||||
registrationFilteredCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (registeredType != object.getObjectType()) {
|
||||
typeOverrideCount++;
|
||||
filteredObjects.add(copyWithCollisionType(object, registeredType));
|
||||
continue;
|
||||
}
|
||||
filteredObjects.add(object);
|
||||
}
|
||||
|
||||
if (filteredObjects.size() != activeObjects.size()) {
|
||||
log.debug("碰撞检测对象已按注册表过滤: 原始数量={}, 过滤后数量={}",
|
||||
activeObjects.size(), filteredObjects.size());
|
||||
}
|
||||
return filteredObjects;
|
||||
return new CollisionManagedObjectsResult(filteredObjects, registrationFilteredCount, typeOverrideCount);
|
||||
}
|
||||
|
||||
private MovingObject copyWithCollisionType(MovingObject source, MovingObjectType collisionType) {
|
||||
return MovingObject.builder()
|
||||
.objectId(source.getObjectId())
|
||||
.objectType(collisionType)
|
||||
.objectName(source.getObjectName())
|
||||
.currentPosition(source.getCurrentPosition())
|
||||
.currentSpeed(source.getCurrentSpeed())
|
||||
.currentHeading(source.getCurrentHeading())
|
||||
.altitude(source.getAltitude())
|
||||
.sourceTimestampMs(source.getSourceTimestampMs())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -237,17 +283,21 @@ public class DataProcessingService {
|
||||
/**
|
||||
* 发送WebSocket位置更新消息
|
||||
*/
|
||||
private void sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
|
||||
private PositionDispatchSummary sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
|
||||
if (activeObjects.isEmpty()) {
|
||||
log.debug("没有活跃对象,跳过位置更新消息发送");
|
||||
return;
|
||||
return new PositionDispatchSummary(0, 0, 0);
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
log.debug("发送位置更新消息,对象数量: {}", activeObjects.size());
|
||||
int publishedCount = 0;
|
||||
int duplicateSampleSkippedCount = 0;
|
||||
Set<String> currentObjectIds = new HashSet<>();
|
||||
|
||||
for (MovingObject movingObject : activeObjects) {
|
||||
try {
|
||||
currentObjectIds.add(movingObject.getObjectId());
|
||||
// 创建位置更新消息负载
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(movingObject.getCurrentPosition().getY())
|
||||
@ -261,6 +311,11 @@ public class DataProcessingService {
|
||||
? movingObject.getSourceTimestampMs()
|
||||
: currentTime;
|
||||
|
||||
if (!shouldPublishPositionUpdate(movingObject.getObjectId(), payloadTimestamp)) {
|
||||
duplicateSampleSkippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(movingObject.getObjectId())
|
||||
.objectType(movingObject.getObjectType().name())
|
||||
@ -272,6 +327,7 @@ public class DataProcessingService {
|
||||
|
||||
// 发布WebSocket事件
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
publishedCount++;
|
||||
|
||||
log.debug("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
|
||||
movingObject.getObjectId(),
|
||||
@ -285,7 +341,65 @@ public class DataProcessingService {
|
||||
}
|
||||
}
|
||||
|
||||
log.info("位置更新消息发送完成,发送数量: {}", activeObjects.size());
|
||||
cleanupPublishedPositionState(currentObjectIds);
|
||||
log.info("位置更新消息发送完成,发送数量: {}, 跳过重复采样数量: {}", publishedCount, duplicateSampleSkippedCount);
|
||||
return new PositionDispatchSummary(activeObjects.size(), publishedCount, duplicateSampleSkippedCount);
|
||||
}
|
||||
|
||||
private boolean shouldPublishPositionUpdate(String objectId, long payloadTimestamp) {
|
||||
Long previousTimestamp = lastPublishedPositionSampleTimestamps.get(objectId);
|
||||
if (previousTimestamp != null && payloadTimestamp <= previousTimestamp) {
|
||||
return false;
|
||||
}
|
||||
lastPublishedPositionSampleTimestamps.put(objectId, payloadTimestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void cleanupPublishedPositionState(Set<String> currentObjectIds) {
|
||||
lastPublishedPositionSampleTimestamps.entrySet()
|
||||
.removeIf(entry -> !currentObjectIds.contains(entry.getKey()));
|
||||
}
|
||||
|
||||
private void logCycleDiagnostics(
|
||||
int activeObjectCount,
|
||||
int collisionManagedObjectCount,
|
||||
PositionDispatchSummary positionDispatchSummary,
|
||||
CollisionManagedObjectsResult collisionManagedResult,
|
||||
PathConflictDetectionService.ConflictDetectionSummary conflictDetectionSummary) {
|
||||
|
||||
if (!collisionDiagnosticLogEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
String testSessionId = platformRuntimeStateService != null
|
||||
? platformRuntimeStateService.getCurrentTestSessionId()
|
||||
: null;
|
||||
|
||||
log.info(
|
||||
"[test-session={}] [collision-diagnostic] activeObjects={}, collisionManagedObjects={}, positionPublished={}, "
|
||||
+ "positionDuplicateSkipped={}, registrationFiltered={}, typeOverrides={}, pairsTotal={}, pairsSupported={}, missingRoute={}, missingPosition={}, "
|
||||
+ "routeDeviation={}, speedTooLow={}, noIntersection={}, intersectionBehind={}, headingMismatch={}, "
|
||||
+ "thresholdNotReached={}, detectionErrors={}, eventsPublished={}",
|
||||
testSessionId != null ? testSessionId : "none",
|
||||
activeObjectCount,
|
||||
collisionManagedObjectCount,
|
||||
positionDispatchSummary.publishedCount(),
|
||||
positionDispatchSummary.duplicateSampleSkippedCount(),
|
||||
collisionManagedResult.registrationFilteredCount(),
|
||||
collisionManagedResult.typeOverrideCount(),
|
||||
conflictDetectionSummary.totalPairs(),
|
||||
conflictDetectionSummary.supportedPairs(),
|
||||
conflictDetectionSummary.missingRoutePairs(),
|
||||
conflictDetectionSummary.missingPositionPairs(),
|
||||
conflictDetectionSummary.routeDeviationPairs(),
|
||||
conflictDetectionSummary.speedTooLowPairs(),
|
||||
conflictDetectionSummary.noIntersectionPairs(),
|
||||
conflictDetectionSummary.intersectionBehindPairs(),
|
||||
conflictDetectionSummary.headingMismatchPairs(),
|
||||
conflictDetectionSummary.thresholdNotReachedPairs(),
|
||||
conflictDetectionSummary.errorPairs(),
|
||||
conflictDetectionSummary.eventsPublished()
|
||||
);
|
||||
}
|
||||
|
||||
private List<MovingObject> createProcessingSnapshot(java.util.Collection<MovingObject> sourceObjects) {
|
||||
@ -321,6 +435,18 @@ public class DataProcessingService {
|
||||
return Math.round(value * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
private record PositionDispatchSummary(
|
||||
int totalObjects,
|
||||
int publishedCount,
|
||||
int duplicateSampleSkippedCount) {
|
||||
}
|
||||
|
||||
private record CollisionManagedObjectsResult(
|
||||
List<MovingObject> objects,
|
||||
int registrationFilteredCount,
|
||||
int typeOverrideCount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行违规检测
|
||||
*/
|
||||
@ -1500,8 +1626,6 @@ public class DataProcessingService {
|
||||
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
|
||||
|
||||
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
|
||||
// 可选:简化路径以减少冗余点(容差:1米)
|
||||
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
|
||||
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
|
||||
lineStringSegments.size(), mergedGeometry.getNumPoints());
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package com.qaup.collision.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Request payload for aircraft taxi route query.
|
||||
*/
|
||||
@Data
|
||||
public class AircraftRouteQueryRequest {
|
||||
|
||||
/**
|
||||
* Registered collision object id.
|
||||
*/
|
||||
private String objectId;
|
||||
|
||||
/**
|
||||
* Compatibility alias for frontend vehicle id payloads.
|
||||
*/
|
||||
private String vehicleID;
|
||||
|
||||
/**
|
||||
* Compatibility alias for flight number payloads.
|
||||
*/
|
||||
private String flightNo;
|
||||
|
||||
/**
|
||||
* Compatibility alias when frontend only knows object name.
|
||||
*/
|
||||
private String objectName;
|
||||
|
||||
/**
|
||||
* Route type. Supported values: IN, OUT.
|
||||
*/
|
||||
private String routeType;
|
||||
|
||||
/**
|
||||
* Arrival runway.
|
||||
*/
|
||||
private String inRunway;
|
||||
|
||||
/**
|
||||
* Departure runway.
|
||||
*/
|
||||
private String outRunway;
|
||||
|
||||
/**
|
||||
* Contact cross identifier.
|
||||
*/
|
||||
private String contactCross;
|
||||
|
||||
/**
|
||||
* Stand identifier for arrival query.
|
||||
*/
|
||||
private String seat;
|
||||
|
||||
/**
|
||||
* Start stand identifier for departure query.
|
||||
*/
|
||||
private String startSeat;
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.qaup.collision.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Response payload for aircraft taxi route query.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AircraftRouteQueryResponse {
|
||||
|
||||
private String routeType;
|
||||
|
||||
private QueryParams queryParams;
|
||||
|
||||
private String routeCodes;
|
||||
|
||||
private String routeStatus;
|
||||
|
||||
private String routeGeometry;
|
||||
|
||||
private List<RouteSegment> routeSegments;
|
||||
|
||||
private Integer pointCount;
|
||||
|
||||
private String source;
|
||||
|
||||
private String bindingObjectId;
|
||||
|
||||
private Boolean routeBound;
|
||||
|
||||
private String bindingMessage;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class QueryParams {
|
||||
private String inRunway;
|
||||
private String outRunway;
|
||||
private String contactCross;
|
||||
private String seat;
|
||||
private String startSeat;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RouteSegment {
|
||||
private String code;
|
||||
private String name;
|
||||
}
|
||||
}
|
||||
@ -44,26 +44,32 @@ public class PathConflictDetectionService {
|
||||
private final VehicleCommandService vehicleCommandService;
|
||||
|
||||
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
||||
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
||||
|
||||
// Strong accuracy guards
|
||||
private static final double MIN_FORWARD_DISTANCE_METERS = 3.0;
|
||||
private static final double MAX_ROUTE_DEVIATION_METERS = 80.0;
|
||||
private static final double MAX_ROUTE_DEVIATION_METERS = 200.0;
|
||||
private static final double HEADING_ALIGNMENT_MIN_COS = 0.0;
|
||||
private static final double HEADING_CHECK_MIN_SPEED_KPH = 3.0;
|
||||
private static final double MIN_EFFECTIVE_SPEED_KPH = 1.0;
|
||||
|
||||
public void detectPathConflicts(List<MovingObject> activeObjects) {
|
||||
public ConflictDetectionSummary detectPathConflicts(List<MovingObject> activeObjects) {
|
||||
if (activeObjects == null) {
|
||||
return new ConflictDetectionSummary(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
log.debug("Starting path conflict detection for {} active objects", activeObjects.size());
|
||||
|
||||
List<ConflictAlertEvent> detectedAlertEvents = new ArrayList<>();
|
||||
DetectionCounters counters = new DetectionCounters();
|
||||
|
||||
for (int i = 0; i < activeObjects.size(); i++) {
|
||||
for (int j = i + 1; j < activeObjects.size(); j++) {
|
||||
counters.totalPairs++;
|
||||
MovingObject obj1 = activeObjects.get(i);
|
||||
MovingObject obj2 = activeObjects.get(j);
|
||||
Optional<ConflictAlertEvent> alertEventOptional = detectConflictBetweenObjects(obj1, obj2);
|
||||
alertEventOptional.ifPresent(detectedAlertEvents::add);
|
||||
PairDetectionResult detectionResult = detectConflictBetweenObjects(obj1, obj2);
|
||||
counters.record(detectionResult.reason());
|
||||
detectionResult.alertEvent().ifPresent(detectedAlertEvents::add);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,45 +86,46 @@ public class PathConflictDetectionService {
|
||||
}
|
||||
|
||||
log.info("Path conflict detection completed, {} events published", detectedAlertEvents.size());
|
||||
return counters.toSummary(detectedAlertEvents.size());
|
||||
}
|
||||
|
||||
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
|
||||
private PairDetectionResult detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
|
||||
if (!isSupportedConflictPair(obj1.getObjectType(), obj2.getObjectType())) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.UNSUPPORTED_PAIR);
|
||||
}
|
||||
|
||||
try {
|
||||
if (obj1.getCurrentPosition() == null || obj2.getCurrentPosition() == null) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.MISSING_POSITION);
|
||||
}
|
||||
|
||||
TransportRoute route1 = getObjectRoute(obj1);
|
||||
TransportRoute route2 = getObjectRoute(obj2);
|
||||
|
||||
if (route1 == null || route2 == null || route1.getRouteGeometry() == null || route2.getRouteGeometry() == null) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
|
||||
}
|
||||
|
||||
LocalRoute localRoute1 = toLocalRoute(route1);
|
||||
LocalRoute localRoute2 = toLocalRoute(route2);
|
||||
if (localRoute1 == null || localRoute2 == null) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
|
||||
}
|
||||
|
||||
Point localObjPos1 = toLocalPoint(obj1.getCurrentPosition());
|
||||
Point localObjPos2 = toLocalPoint(obj2.getCurrentPosition());
|
||||
if (localObjPos1 == null || localObjPos2 == null) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.MISSING_POSITION);
|
||||
}
|
||||
|
||||
ProjectedPosition projected1 = projectToRoute(localObjPos1, localRoute1.localLine);
|
||||
ProjectedPosition projected2 = projectToRoute(localObjPos2, localRoute2.localLine);
|
||||
if (projected1 == null || projected2 == null) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
|
||||
}
|
||||
|
||||
if (projected1.lateralDistanceMeters > MAX_ROUTE_DEVIATION_METERS || projected2.lateralDistanceMeters > MAX_ROUTE_DEVIATION_METERS) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.ROUTE_DEVIATION_TOO_LARGE);
|
||||
}
|
||||
|
||||
double speed1Kph = normalizeSpeedKph(obj1.getCurrentSpeed());
|
||||
@ -126,10 +133,17 @@ public class PathConflictDetectionService {
|
||||
|
||||
// Ignore pairs that are effectively static.
|
||||
if (speed1Kph < MIN_EFFECTIVE_SPEED_KPH || speed2Kph < MIN_EFFECTIVE_SPEED_KPH) {
|
||||
return Optional.empty();
|
||||
return PairDetectionResult.of(PairOutcomeReason.SPEED_TOO_LOW);
|
||||
}
|
||||
|
||||
List<Point> intersectionPoints = calculateRouteIntersections(route1, route2);
|
||||
if (intersectionPoints.isEmpty()) {
|
||||
return PairDetectionResult.of(PairOutcomeReason.NO_INTERSECTION);
|
||||
}
|
||||
|
||||
boolean intersectionBehind = false;
|
||||
boolean headingMismatch = false;
|
||||
boolean thresholdNotReached = false;
|
||||
for (Point intersectionPoint : intersectionPoints) {
|
||||
Point localIntersection = toLocalPoint(intersectionPoint);
|
||||
if (localIntersection == null) {
|
||||
@ -147,12 +161,14 @@ public class PathConflictDetectionService {
|
||||
|
||||
// Only future conflicts are valid.
|
||||
if (forwardDistance1 <= MIN_FORWARD_DISTANCE_METERS || forwardDistance2 <= MIN_FORWARD_DISTANCE_METERS) {
|
||||
intersectionBehind = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure current heading points to the intersection direction when speed is meaningful.
|
||||
if (!isHeadingAligned(obj1, localObjPos1, localIntersection, speed1Kph)
|
||||
|| !isHeadingAligned(obj2, localObjPos2, localIntersection, speed2Kph)) {
|
||||
headingMismatch = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -160,6 +176,34 @@ public class PathConflictDetectionService {
|
||||
obj1, obj2, forwardDistance1, forwardDistance2, speed1Kph, speed2Kph);
|
||||
|
||||
if (result != null && isSignificantConflict(result)) {
|
||||
return PairDetectionResult.of(buildConflictAlertEvent(obj1, obj2, route1, route2, intersectionPoint, result));
|
||||
}
|
||||
thresholdNotReached = true;
|
||||
}
|
||||
if (thresholdNotReached) {
|
||||
return PairDetectionResult.of(PairOutcomeReason.THRESHOLD_NOT_REACHED);
|
||||
}
|
||||
if (headingMismatch) {
|
||||
return PairDetectionResult.of(PairOutcomeReason.HEADING_MISMATCH);
|
||||
}
|
||||
if (intersectionBehind) {
|
||||
return PairDetectionResult.of(PairOutcomeReason.INTERSECTION_BEHIND);
|
||||
}
|
||||
return PairDetectionResult.of(PairOutcomeReason.NO_INTERSECTION);
|
||||
} catch (Exception e) {
|
||||
log.error("Error detecting conflict between {} and {}", obj1.getObjectName(), obj2.getObjectName(), e);
|
||||
return PairDetectionResult.of(PairOutcomeReason.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private ConflictAlertEvent buildConflictAlertEvent(
|
||||
MovingObject obj1,
|
||||
MovingObject obj2,
|
||||
TransportRoute route1,
|
||||
TransportRoute route2,
|
||||
Point intersectionPoint,
|
||||
ConflictCalculationResult result) {
|
||||
|
||||
String description = generateConflictDescription(obj1, obj2, route1, route2);
|
||||
|
||||
ConflictAlertLog alertLog = ConflictAlertLog.builder()
|
||||
@ -171,10 +215,10 @@ public class PathConflictDetectionService {
|
||||
.minimumDistance(Math.min(result.getDistance1(), result.getDistance2()))
|
||||
.build();
|
||||
|
||||
conflictAlertLogRepository.save(alertLog);
|
||||
ConflictAlertLog savedAlertLog = conflictAlertLogRepository.save(alertLog);
|
||||
|
||||
return Optional.of(ConflictAlertEvent.builder()
|
||||
.conflictId(Optional.of(alertLog.getId()))
|
||||
return ConflictAlertEvent.builder()
|
||||
.conflictId(Optional.ofNullable(savedAlertLog.getId()))
|
||||
.alertType(result.getAlertType())
|
||||
.alertLevel(result.getAlertLevel())
|
||||
.message(description)
|
||||
@ -188,22 +232,19 @@ public class PathConflictDetectionService {
|
||||
.estimatedTimeToConflictObj1(result.getTimeToConflict1())
|
||||
.estimatedTimeToConflictObj2(result.getTimeToConflict2())
|
||||
.timeGapSeconds(result.getTimeGap())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error detecting conflict between {} and {}", obj1.getObjectName(), obj2.getObjectName(), e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
.build();
|
||||
}
|
||||
|
||||
private TransportRoute getObjectRoute(MovingObject obj) {
|
||||
Optional<ObjectRouteAssignment> assignmentOptional = objectRouteAssignmentRepository
|
||||
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
obj.getObjectName(),
|
||||
ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name())
|
||||
);
|
||||
ObjectRouteAssignment.ObjectType objectType = ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name());
|
||||
Optional<ObjectRouteAssignment> assignmentOptional = findLatestAssignment(obj.getObjectId(), objectType);
|
||||
|
||||
if (assignmentOptional.isEmpty()
|
||||
&& obj.getObjectName() != null
|
||||
&& !obj.getObjectName().isBlank()
|
||||
&& !obj.getObjectName().equals(obj.getObjectId())) {
|
||||
assignmentOptional = findLatestAssignment(obj.getObjectName(), objectType);
|
||||
}
|
||||
|
||||
return assignmentOptional
|
||||
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
|
||||
@ -211,6 +252,13 @@ public class PathConflictDetectionService {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Optional<ObjectRouteAssignment> findLatestAssignment(String identifier, ObjectRouteAssignment.ObjectType objectType) {
|
||||
if (identifier == null || identifier.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(identifier, objectType);
|
||||
}
|
||||
|
||||
private List<Point> calculateRouteIntersections(TransportRoute route1, TransportRoute route2) {
|
||||
List<Point> intersections = new ArrayList<>();
|
||||
|
||||
@ -260,15 +308,13 @@ public class PathConflictDetectionService {
|
||||
return null;
|
||||
}
|
||||
|
||||
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
|
||||
|
||||
Optional<ConflictAlertLog.AlertLevel> alertLevelOptional = evaluateAlertLevel(
|
||||
distance1,
|
||||
obj1.getObjectType(),
|
||||
distance2,
|
||||
obj2.getObjectType(),
|
||||
timeGap
|
||||
obj2.getObjectType()
|
||||
);
|
||||
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
|
||||
Optional<ConflictAlertLog.AlertType> alertTypeOptional = alertLevelOptional.isPresent()
|
||||
? evaluateAlertType(alertLevelOptional.get())
|
||||
: Optional.empty();
|
||||
@ -294,27 +340,34 @@ public class PathConflictDetectionService {
|
||||
double distance1,
|
||||
MovingObjectType obj1Type,
|
||||
double distance2,
|
||||
MovingObjectType obj2Type,
|
||||
double timeGap) {
|
||||
MovingObjectType obj2Type) {
|
||||
if (!isSupportedConflictPair(obj1Type, obj2Type)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
double distanceToEvaluate;
|
||||
Double vehicleDistance = null;
|
||||
Double aircraftDistance = null;
|
||||
if (obj1Type == MovingObjectType.AIRCRAFT) {
|
||||
distanceToEvaluate = distance2;
|
||||
aircraftDistance = distance1;
|
||||
vehicleDistance = distance2;
|
||||
} else if (obj2Type == MovingObjectType.AIRCRAFT) {
|
||||
distanceToEvaluate = distance1;
|
||||
} else {
|
||||
aircraftDistance = distance2;
|
||||
vehicleDistance = distance1;
|
||||
}
|
||||
|
||||
if (vehicleDistance == null || aircraftDistance == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
double alertDistanceThreshold = platformRuntimeStateService.getRunwayAlertZoneRadiusAircraft();
|
||||
double warningDistanceThreshold = platformRuntimeStateService.getRunwayWarningZoneRadiusAircraft();
|
||||
double vehicleDistanceThreshold = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle();
|
||||
double aircraftDistanceThreshold = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft();
|
||||
|
||||
if (distanceToEvaluate <= alertDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS) {
|
||||
boolean vehicleInZone = vehicleDistance <= vehicleDistanceThreshold;
|
||||
boolean aircraftInZone = aircraftDistance <= aircraftDistanceThreshold;
|
||||
|
||||
if (vehicleInZone && aircraftInZone) {
|
||||
return Optional.of(ConflictAlertLog.AlertLevel.CRITICAL);
|
||||
} else if (distanceToEvaluate <= warningDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
|
||||
} else if (vehicleInZone || aircraftInZone) {
|
||||
return Optional.of(ConflictAlertLog.AlertLevel.WARNING);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
@ -484,6 +537,95 @@ public class PathConflictDetectionService {
|
||||
|
||||
private record ProjectedPosition(double distanceAlongRouteMeters, double lateralDistanceMeters) {}
|
||||
|
||||
public record ConflictDetectionSummary(
|
||||
int totalPairs,
|
||||
int supportedPairs,
|
||||
int missingPositionPairs,
|
||||
int missingRoutePairs,
|
||||
int routeDeviationPairs,
|
||||
int speedTooLowPairs,
|
||||
int noIntersectionPairs,
|
||||
int intersectionBehindPairs,
|
||||
int headingMismatchPairs,
|
||||
int thresholdNotReachedPairs,
|
||||
int errorPairs,
|
||||
int eventsPublished) {
|
||||
}
|
||||
|
||||
private record PairDetectionResult(Optional<ConflictAlertEvent> alertEvent, PairOutcomeReason reason) {
|
||||
private static PairDetectionResult of(PairOutcomeReason reason) {
|
||||
return new PairDetectionResult(Optional.empty(), reason);
|
||||
}
|
||||
|
||||
private static PairDetectionResult of(ConflictAlertEvent event) {
|
||||
return new PairDetectionResult(Optional.of(event), PairOutcomeReason.EVENT_PUBLISHED);
|
||||
}
|
||||
}
|
||||
|
||||
private enum PairOutcomeReason {
|
||||
UNSUPPORTED_PAIR,
|
||||
MISSING_POSITION,
|
||||
MISSING_ROUTE,
|
||||
ROUTE_DEVIATION_TOO_LARGE,
|
||||
SPEED_TOO_LOW,
|
||||
NO_INTERSECTION,
|
||||
INTERSECTION_BEHIND,
|
||||
HEADING_MISMATCH,
|
||||
THRESHOLD_NOT_REACHED,
|
||||
EVENT_PUBLISHED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private static final class DetectionCounters {
|
||||
private int totalPairs;
|
||||
private int supportedPairs;
|
||||
private int missingPositionPairs;
|
||||
private int missingRoutePairs;
|
||||
private int routeDeviationPairs;
|
||||
private int speedTooLowPairs;
|
||||
private int noIntersectionPairs;
|
||||
private int intersectionBehindPairs;
|
||||
private int headingMismatchPairs;
|
||||
private int thresholdNotReachedPairs;
|
||||
private int errorPairs;
|
||||
|
||||
private void record(PairOutcomeReason reason) {
|
||||
if (reason != PairOutcomeReason.UNSUPPORTED_PAIR) {
|
||||
supportedPairs++;
|
||||
}
|
||||
switch (reason) {
|
||||
case MISSING_POSITION -> missingPositionPairs++;
|
||||
case MISSING_ROUTE -> missingRoutePairs++;
|
||||
case ROUTE_DEVIATION_TOO_LARGE -> routeDeviationPairs++;
|
||||
case SPEED_TOO_LOW -> speedTooLowPairs++;
|
||||
case NO_INTERSECTION -> noIntersectionPairs++;
|
||||
case INTERSECTION_BEHIND -> intersectionBehindPairs++;
|
||||
case HEADING_MISMATCH -> headingMismatchPairs++;
|
||||
case THRESHOLD_NOT_REACHED -> thresholdNotReachedPairs++;
|
||||
case ERROR -> errorPairs++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ConflictDetectionSummary toSummary(int eventsPublished) {
|
||||
return new ConflictDetectionSummary(
|
||||
totalPairs,
|
||||
supportedPairs,
|
||||
missingPositionPairs,
|
||||
missingRoutePairs,
|
||||
routeDeviationPairs,
|
||||
speedTooLowPairs,
|
||||
noIntersectionPairs,
|
||||
intersectionBehindPairs,
|
||||
headingMismatchPairs,
|
||||
thresholdNotReachedPairs,
|
||||
errorPairs,
|
||||
eventsPublished
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConflictCalculationResult {
|
||||
private final double distance1;
|
||||
private final double distance2;
|
||||
|
||||
@ -39,6 +39,7 @@ public class VehicleCommandService {
|
||||
|
||||
private final Map<String, ActiveVehicleCommandState> activeCommandStates = new ConcurrentHashMap<>();
|
||||
private final AtomicLong transIdSequence = new AtomicLong();
|
||||
private static final String UNIFIED_VEHICLE_COMMAND_URL = "http://10.232.18.23:8020/api/VehicleCommandInfo";
|
||||
|
||||
@Value("${data.collector.vehicle-api.base-url:}")
|
||||
private String vehicleApiBaseUrl;
|
||||
@ -213,12 +214,6 @@ public class VehicleCommandService {
|
||||
VehicleConflictCommand command,
|
||||
CommandType commandType,
|
||||
CommandReason commandReason) {
|
||||
|
||||
if (!isCommandEndpointConfigured()) {
|
||||
log.warn("Skipping vehicle command because vehicle API endpoint is not configured: vehicleId={}", command.vehicleId());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
|
||||
.transId(nextTransId())
|
||||
@ -240,7 +235,7 @@ public class VehicleCommandService {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
vehicleApiBaseUrl + vehicleCommandEndpoint,
|
||||
getUnifiedVehicleCommandUrl(),
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(request, headers),
|
||||
String.class
|
||||
@ -299,6 +294,40 @@ public class VehicleCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendTrafficLightSignalCommands(int phaseColor, String intersectionId) {
|
||||
String signalState = toSignalState(phaseColor);
|
||||
if (signalState == null) {
|
||||
log.warn("Skipping traffic light command because phaseColor is unsupported: {}", phaseColor);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> vehicleIds = platformRuntimeStateService.getTrafficLightRecipientVehicleIds();
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("Skipping traffic light command because no controllable vehicles are registered");
|
||||
return;
|
||||
}
|
||||
|
||||
for (String vehicleId : vehicleIds) {
|
||||
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
|
||||
.transId(nextTransId())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.vehicleId(vehicleId)
|
||||
.commandType(CommandType.SIGNAL)
|
||||
.commandReason(CommandReason.TRAFFIC_LIGHT)
|
||||
.latitude(0.0)
|
||||
.longitude(0.0)
|
||||
.signalState(signalState)
|
||||
.intersectionId(intersectionId == null ? "" : intersectionId)
|
||||
.relativeSpeed(0.0)
|
||||
.relativeMotionX(0.0)
|
||||
.relativeMotionY(0.0)
|
||||
.minDistance(0.0)
|
||||
.build();
|
||||
|
||||
sendExternalVehicleCommand(request, vehicleId, CommandType.SIGNAL, CommandReason.TRAFFIC_LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode parseResponseBody(String body) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return null;
|
||||
@ -312,11 +341,87 @@ public class VehicleCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCommandEndpointConfigured() {
|
||||
return vehicleApiBaseUrl != null
|
||||
&& !vehicleApiBaseUrl.isBlank()
|
||||
&& vehicleCommandEndpoint != null
|
||||
&& !vehicleCommandEndpoint.isBlank();
|
||||
private boolean sendExternalVehicleCommand(
|
||||
ExternalVehicleCommandRequest request,
|
||||
String vehicleId,
|
||||
CommandType commandType,
|
||||
CommandReason commandReason) {
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
getUnifiedVehicleCommandUrl(),
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(request, headers),
|
||||
String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode().value() != 200) {
|
||||
log.warn(
|
||||
"Vehicle command rejected by HTTP status: vehicleId={}, commandType={}, status={}",
|
||||
vehicleId,
|
||||
commandType,
|
||||
response.getStatusCode().value()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonNode responseBody = parseResponseBody(response.getBody());
|
||||
if (responseBody == null || !responseBody.has("code") || !responseBody.has("msg")) {
|
||||
log.warn(
|
||||
"Vehicle command response missing required fields: vehicleId={}, commandType={}, body={}",
|
||||
vehicleId,
|
||||
commandType,
|
||||
response.getBody()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
int responseCode = responseBody.path("code").asInt(Integer.MIN_VALUE);
|
||||
if (responseCode != 200) {
|
||||
log.warn(
|
||||
"Vehicle command rejected by upstream response code: vehicleId={}, commandType={}, code={}, msg={}",
|
||||
vehicleId,
|
||||
commandType,
|
||||
responseCode,
|
||||
responseBody.path("msg").asText()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Vehicle command sent successfully: vehicleId={}, registryType={}, commandType={}, reason={}",
|
||||
vehicleId,
|
||||
resolveRegistryType(vehicleId),
|
||||
commandType,
|
||||
commandReason
|
||||
);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to send vehicle command: vehicleId={}, commandType={}, reason={}",
|
||||
vehicleId,
|
||||
commandType,
|
||||
commandReason,
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getUnifiedVehicleCommandUrl() {
|
||||
return UNIFIED_VEHICLE_COMMAND_URL;
|
||||
}
|
||||
|
||||
private String toSignalState(int phaseColor) {
|
||||
return switch (phaseColor) {
|
||||
case 1 -> "GREEN";
|
||||
case 2 -> "YELLOW";
|
||||
case 3 -> "RED";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private VehicleRegistryType resolveRegistryType(String vehicleId) {
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import com.qaup.collision.common.model.AircraftRoute;
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.datacollector.service.RoutePersistenceService;
|
||||
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||
import com.qaup.collision.dto.AircraftRouteQueryRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Service for querying aircraft taxi routes for frontend clients.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AircraftRouteQueryService {
|
||||
private final DataCollectorDao dataCollectorDao;
|
||||
private final RoutePersistenceService routePersistenceService;
|
||||
private final RouteGeometryProcessor routeGeometryProcessor;
|
||||
|
||||
public AircraftRouteDTO queryRoute(AircraftRouteQueryRequest request) {
|
||||
String routeType = normalize(request.getRouteType());
|
||||
if (routeType == null) {
|
||||
throw new IllegalArgumentException("Missing field: routeType");
|
||||
}
|
||||
|
||||
String normalizedRouteType = routeType.toUpperCase(Locale.ROOT);
|
||||
return switch (normalizedRouteType) {
|
||||
case "IN" -> queryArrivalRoute(request, normalizedRouteType);
|
||||
case "OUT" -> queryDepartureRoute(request, normalizedRouteType);
|
||||
default -> throw new IllegalArgumentException("Field 'routeType' must be IN or OUT");
|
||||
};
|
||||
}
|
||||
|
||||
private AircraftRouteDTO queryArrivalRoute(AircraftRouteQueryRequest request, String routeType) {
|
||||
String inRunway = required(request.getInRunway(), "inRunway");
|
||||
String contactCross = required(request.getContactCross(), "contactCross");
|
||||
String seat = required(request.getSeat(), "seat");
|
||||
String outRunway = normalize(request.getOutRunway());
|
||||
|
||||
AircraftRouteDTO route = dataCollectorDao.getArrivalRoute(inRunway, outRunway, contactCross, seat);
|
||||
if (route == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
bindRouteIfPossible(request, routeType, route);
|
||||
return route;
|
||||
}
|
||||
|
||||
private AircraftRouteDTO queryDepartureRoute(AircraftRouteQueryRequest request, String routeType) {
|
||||
String outRunway = required(request.getOutRunway(), "outRunway");
|
||||
String inRunway = normalize(request.getInRunway());
|
||||
String startSeat = normalize(request.getStartSeat());
|
||||
if (startSeat == null) {
|
||||
startSeat = required(request.getSeat(), "startSeat or seat");
|
||||
}
|
||||
|
||||
AircraftRouteDTO route = dataCollectorDao.getDepartureRoute(inRunway, outRunway, startSeat);
|
||||
if (route == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
bindRouteIfPossible(request, routeType, route);
|
||||
return route;
|
||||
}
|
||||
|
||||
private void bindRouteIfPossible(
|
||||
AircraftRouteQueryRequest request,
|
||||
String routeType,
|
||||
AircraftRouteDTO route) {
|
||||
|
||||
String bindingObjectId = resolveBindingObjectId(request);
|
||||
if (bindingObjectId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LineString lineString = extractLineString(route);
|
||||
if (lineString == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
AircraftRoute aircraftRoute = AircraftRoute.builder()
|
||||
.type(routeType)
|
||||
.status(normalize(route.getStatus()) != null ? normalize(route.getStatus()) : "ACTIVE")
|
||||
.codes(normalize(route.getCodes()))
|
||||
.geometry(lineString)
|
||||
.routeSegments(extractRouteSegmentCodes(route).stream()
|
||||
.map(code -> AircraftRoute.RouteSegment.builder().code(code).segmentType("TAXIWAY").build())
|
||||
.toList())
|
||||
.build();
|
||||
|
||||
routePersistenceService.saveAircraftRoute(bindingObjectId, aircraftRoute);
|
||||
}
|
||||
|
||||
private String resolveBindingObjectId(AircraftRouteQueryRequest request) {
|
||||
String objectId = normalize(request.getObjectId());
|
||||
if (objectId != null) {
|
||||
return objectId;
|
||||
}
|
||||
objectId = normalize(request.getVehicleID());
|
||||
if (objectId != null) {
|
||||
return objectId;
|
||||
}
|
||||
objectId = normalize(request.getFlightNo());
|
||||
if (objectId != null) {
|
||||
return objectId;
|
||||
}
|
||||
return normalize(request.getObjectName());
|
||||
}
|
||||
|
||||
private List<String> extractRouteSegmentCodes(AircraftRouteDTO route) {
|
||||
Set<String> codes = new LinkedHashSet<>();
|
||||
if (route.getGeoPath() != null && route.getGeoPath().getFeatures() != null) {
|
||||
for (AircraftRouteDTO.Feature feature : route.getGeoPath().getFeatures()) {
|
||||
if (feature == null || feature.getProperties() == null) {
|
||||
continue;
|
||||
}
|
||||
String code = normalize(feature.getProperties().getCodeAsString());
|
||||
if (code != null) {
|
||||
codes.add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (codes.isEmpty()) {
|
||||
String routeCodes = normalize(route.getCodes());
|
||||
if (routeCodes != null) {
|
||||
for (String code : routeCodes.split(",")) {
|
||||
String normalizedCode = normalize(code);
|
||||
if (normalizedCode != null) {
|
||||
codes.add(normalizedCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(codes);
|
||||
}
|
||||
|
||||
private LineString extractLineString(AircraftRouteDTO route) {
|
||||
if (route.getGeoPath() == null || route.getGeoPath().getFeatures() == null || route.getGeoPath().getFeatures().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<LineString> lineStrings = new ArrayList<>();
|
||||
org.locationtech.jts.geom.GeometryFactory geometryFactory =
|
||||
new org.locationtech.jts.geom.GeometryFactory(new PrecisionModel(), 4326);
|
||||
|
||||
for (AircraftRouteDTO.Feature feature : route.getGeoPath().getFeatures()) {
|
||||
if (feature == null || feature.getGeometry() == null || feature.getGeometry().getCoordinates() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Coordinate> coordinates = feature.getGeometry().getCoordinates().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(item -> item.size() >= 2 && item.get(0) != null && item.get(1) != null)
|
||||
.map(item -> new Coordinate(item.get(0), item.get(1)))
|
||||
.toList();
|
||||
|
||||
if (coordinates.size() >= 2) {
|
||||
lineStrings.add(geometryFactory.createLineString(coordinates.toArray(new Coordinate[0])));
|
||||
}
|
||||
}
|
||||
|
||||
if (lineStrings.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LineString mergedLine = routeGeometryProcessor.mergeLineStrings(lineStrings);
|
||||
if (mergedLine != null && routeGeometryProcessor.isValidLineString(mergedLine)) {
|
||||
return mergedLine;
|
||||
}
|
||||
|
||||
if (lineStrings.size() == 1) {
|
||||
LineString singleLine = lineStrings.get(0);
|
||||
return routeGeometryProcessor.isValidLineString(singleLine) ? singleLine : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String required(String value, String fieldName) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized == null) {
|
||||
throw new IllegalArgumentException("Missing field: " + fieldName);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,10 @@ import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.system.domain.SysConfig;
|
||||
import com.qaup.system.service.ISysConfigService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
@ -24,15 +28,30 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PlatformRuntimeStateService {
|
||||
private static final String CONFIG_KEY_CROSSING_VEHICLE_DISTANCE = "qaup.collision.crossing.vehicleDistance";
|
||||
private static final String CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE = "qaup.collision.crossing.aircraftDistance";
|
||||
private static final String CONFIG_NAME_CROSSING_VEHICLE_DISTANCE = "Path crossing threshold for vehicle";
|
||||
private static final String CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE = "Path crossing threshold for aircraft";
|
||||
private static final String CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE = "Vehicle distance threshold to the route crossing point";
|
||||
private static final String CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE = "Aircraft distance threshold to the route crossing point";
|
||||
private static final String CONFIG_UPDATER = "system";
|
||||
|
||||
private final ConcurrentHashMap<String, VehicleRegistryType> vehicleTypes = new ConcurrentHashMap<>();
|
||||
private final AtomicLong testSessionSequence = new AtomicLong();
|
||||
private volatile String currentTestSessionId;
|
||||
|
||||
private volatile double runwayWarningZoneRadiusAircraft;
|
||||
private volatile double runwayAlertZoneRadiusAircraft;
|
||||
|
||||
// Threshold used when the vehicle approaches a route crossing point.
|
||||
private volatile double collisionDivergingReleaseDistanceForVehicle;
|
||||
|
||||
// Threshold used when the aircraft approaches a route crossing point.
|
||||
private volatile double collisionDivergingReleaseDistanceForAircraft;
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||
private static final String DEFAULT_ROUTE_SOURCE = "VehicleRouteIngestion";
|
||||
@ -44,6 +63,9 @@ public class PlatformRuntimeStateService {
|
||||
@Autowired
|
||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ISysConfigService sysConfigService;
|
||||
|
||||
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,
|
||||
@ -55,9 +77,27 @@ public class PlatformRuntimeStateService {
|
||||
this.collisionDivergingReleaseDistanceForAircraft = collisionDivergingReleaseDistance;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void loadPersistedCrossingThresholds() {
|
||||
if (sysConfigService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Double persistedVehicleDistance = readConfigDouble(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE);
|
||||
if (persistedVehicleDistance != null) {
|
||||
collisionDivergingReleaseDistanceForVehicle = persistedVehicleDistance;
|
||||
}
|
||||
|
||||
Double persistedAircraftDistance = readConfigDouble(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE);
|
||||
if (persistedAircraftDistance != null) {
|
||||
collisionDivergingReleaseDistanceForAircraft = persistedAircraftDistance;
|
||||
}
|
||||
}
|
||||
|
||||
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
|
||||
Objects.requireNonNull(entries, "entries");
|
||||
|
||||
TreeSet<String> previousVehicleIds = new TreeSet<>(vehicleTypes.keySet());
|
||||
vehicleTypes.clear();
|
||||
|
||||
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
|
||||
@ -65,6 +105,25 @@ public class PlatformRuntimeStateService {
|
||||
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
|
||||
requestTypeCounts.merge(entry.vehicleType(), 1, Integer::sum);
|
||||
}
|
||||
previousVehicleIds.removeAll(vehicleTypes.keySet());
|
||||
|
||||
String endedTestSessionId = null;
|
||||
String activeTestSessionId = null;
|
||||
if (entries.isEmpty()) {
|
||||
endedTestSessionId = currentTestSessionId;
|
||||
if (endedTestSessionId != null) {
|
||||
log.info("[test-session={}] collision test session ended by empty registry update", endedTestSessionId);
|
||||
}
|
||||
currentTestSessionId = null;
|
||||
} else {
|
||||
endedTestSessionId = currentTestSessionId;
|
||||
if (endedTestSessionId != null) {
|
||||
log.info("[test-session={}] collision test session replaced by new registry update", endedTestSessionId);
|
||||
}
|
||||
activeTestSessionId = createTestSessionId(entries);
|
||||
currentTestSessionId = activeTestSessionId;
|
||||
log.info("[test-session={}] collision test session started: objects={}", activeTestSessionId, entries);
|
||||
}
|
||||
|
||||
Set<String> controllableVehicleIds = new TreeSet<>();
|
||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||
@ -86,7 +145,14 @@ public class PlatformRuntimeStateService {
|
||||
entries.size(),
|
||||
controllableVehicleIds.size(),
|
||||
responseTypeCounts,
|
||||
new ArrayList<>(controllableVehicleIds));
|
||||
new ArrayList<>(controllableVehicleIds),
|
||||
new ArrayList<>(previousVehicleIds),
|
||||
activeTestSessionId,
|
||||
endedTestSessionId);
|
||||
}
|
||||
|
||||
public String getCurrentTestSessionId() {
|
||||
return currentTestSessionId;
|
||||
}
|
||||
|
||||
public List<String> getControllableVehicleIds() {
|
||||
@ -130,6 +196,56 @@ public class PlatformRuntimeStateService {
|
||||
};
|
||||
}
|
||||
|
||||
public MovingObject.MovingObjectType getRegisteredCollisionObjectType(String objectId) {
|
||||
return mapToMovingObjectType(getVehicleRegistryType(objectId));
|
||||
}
|
||||
|
||||
public CollisionPreparationStatus getCollisionPreparationStatus() {
|
||||
List<CollisionPreparationObjectStatus> objects = new ArrayList<>();
|
||||
int missingRouteCount = 0;
|
||||
|
||||
for (String objectId : new TreeSet<>(vehicleTypes.keySet())) {
|
||||
VehicleRegistryType registryType = vehicleTypes.get(objectId);
|
||||
MovingObject.MovingObjectType objectType = mapToMovingObjectType(registryType);
|
||||
if (objectType == null || !isRegisteredForCollision(objectId, objectType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ObjectRouteAssignment.ObjectType assignmentType = ObjectRouteAssignment.ObjectType.valueOf(objectType.name());
|
||||
boolean routeBound = hasActiveRouteAssignment(objectId, assignmentType);
|
||||
if (!routeBound) {
|
||||
missingRouteCount++;
|
||||
}
|
||||
|
||||
objects.add(new CollisionPreparationObjectStatus(
|
||||
objectId,
|
||||
registryType.name(),
|
||||
objectType.name(),
|
||||
routeBound,
|
||||
routeBound ? "READY" : "MISSING_ROUTE"
|
||||
));
|
||||
}
|
||||
|
||||
return new CollisionPreparationStatus(
|
||||
currentTestSessionId,
|
||||
missingRouteCount == 0,
|
||||
new CollisionPreparationSummary(objects.size(), missingRouteCount),
|
||||
objects
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hasActiveRouteAssignment(String objectId, ObjectRouteAssignment.ObjectType objectType) {
|
||||
if (objectRouteAssignmentRepository == null || transportRouteRepository == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return objectRouteAssignmentRepository
|
||||
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(objectId, objectType)
|
||||
.flatMap(assignment -> transportRouteRepository.findById(assignment.getAssignedRouteId()))
|
||||
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
public double getRunwayWarningZoneRadiusAircraft() {
|
||||
return runwayWarningZoneRadiusAircraft;
|
||||
}
|
||||
@ -150,37 +266,106 @@ public class PlatformRuntimeStateService {
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared crossing threshold when both vehicle and aircraft use the same value.
|
||||
* The value mirrors the vehicle threshold for backward compatibility.
|
||||
*/
|
||||
public double getCollisionDivergingReleaseDistance() {
|
||||
return collisionDivergingReleaseDistanceForVehicle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates both vehicle and aircraft crossing thresholds to the same value.
|
||||
* This keeps the existing API contract that accepts a single numeric value.
|
||||
*/
|
||||
public double updateCollisionDivergingReleaseDistance(double newValue) {
|
||||
double oldValue = collisionDivergingReleaseDistanceForVehicle;
|
||||
collisionDivergingReleaseDistanceForVehicle = newValue;
|
||||
collisionDivergingReleaseDistanceForAircraft = newValue;
|
||||
persistCrossingThreshold(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE, CONFIG_NAME_CROSSING_VEHICLE_DISTANCE, newValue, CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE);
|
||||
persistCrossingThreshold(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE, CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE, newValue, CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the vehicle crossing threshold used by path crossing alerts.
|
||||
*/
|
||||
public double getCollisionDivergingReleaseDistanceForVehicle() {
|
||||
return collisionDivergingReleaseDistanceForVehicle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the aircraft crossing threshold used by path crossing alerts.
|
||||
*/
|
||||
public double getCollisionDivergingReleaseDistanceForAircraft() {
|
||||
return collisionDivergingReleaseDistanceForAircraft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the vehicle crossing threshold used by path crossing alerts.
|
||||
*/
|
||||
public double updateCollisionDivergingReleaseDistanceForVehicle(double newValue) {
|
||||
double oldValue = collisionDivergingReleaseDistanceForVehicle;
|
||||
collisionDivergingReleaseDistanceForVehicle = newValue;
|
||||
persistCrossingThreshold(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE, CONFIG_NAME_CROSSING_VEHICLE_DISTANCE, newValue, CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the aircraft crossing threshold used by path crossing alerts.
|
||||
*/
|
||||
public double updateCollisionDivergingReleaseDistanceForAircraft(double newValue) {
|
||||
double oldValue = collisionDivergingReleaseDistanceForAircraft;
|
||||
collisionDivergingReleaseDistanceForAircraft = newValue;
|
||||
persistCrossingThreshold(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE, CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE, newValue, CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
private Double readConfigDouble(String configKey) {
|
||||
try {
|
||||
String value = sysConfigService.selectConfigByKey(configKey);
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
double parsed = Double.parseDouble(value.trim());
|
||||
return Double.isFinite(parsed) && parsed > 0.0 ? parsed : null;
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistCrossingThreshold(String configKey, String configName, double value, String remark) {
|
||||
if (sysConfigService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SysConfig existing = findConfigByKey(configKey);
|
||||
if (existing == null) {
|
||||
SysConfig config = new SysConfig();
|
||||
config.setConfigName(configName);
|
||||
config.setConfigKey(configKey);
|
||||
config.setConfigValue(Double.toString(value));
|
||||
config.setConfigType("N");
|
||||
config.setCreateBy(CONFIG_UPDATER);
|
||||
config.setRemark(remark);
|
||||
sysConfigService.insertConfig(config);
|
||||
return;
|
||||
}
|
||||
|
||||
existing.setConfigName(configName);
|
||||
existing.setConfigValue(Double.toString(value));
|
||||
existing.setUpdateBy(CONFIG_UPDATER);
|
||||
existing.setRemark(remark);
|
||||
sysConfigService.updateConfig(existing);
|
||||
}
|
||||
|
||||
private SysConfig findConfigByKey(String configKey) {
|
||||
SysConfig query = new SysConfig();
|
||||
query.setConfigKey(configKey);
|
||||
List<SysConfig> configs = sysConfigService.selectConfigList(query);
|
||||
return configs == null || configs.isEmpty() ? null : configs.get(0);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleRouteSubmitResult submitVehicleRoute(
|
||||
String objectId,
|
||||
@ -277,14 +462,43 @@ public class PlatformRuntimeStateService {
|
||||
return "MANUAL_" + routeType.name() + "_" + safeObjectId;
|
||||
}
|
||||
|
||||
private String createTestSessionId(List<VehicleRegistryEntry> entries) {
|
||||
String suffix = entries.stream()
|
||||
.map(VehicleRegistryEntry::vehicleID)
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isEmpty())
|
||||
.sorted()
|
||||
.limit(3)
|
||||
.reduce((left, right) -> left + "-" + right)
|
||||
.orElse("unknown");
|
||||
String normalizedSuffix = suffix.replaceAll("[^A-Za-z0-9_-]", "_");
|
||||
return "collision-test-" + System.currentTimeMillis()
|
||||
+ "-" + testSessionSequence.incrementAndGet()
|
||||
+ "-" + normalizedSuffix;
|
||||
}
|
||||
|
||||
private static TransportRoute.RouteType mapToRouteType(ObjectRouteAssignment.ObjectType objectType) {
|
||||
return switch (objectType) {
|
||||
case AIRCRAFT -> TransportRoute.RouteType.AIRCRAFT;
|
||||
case UNMANNED_VEHICLE -> TransportRoute.RouteType.UNMANNED_VEHICLE;
|
||||
case SPECIAL_VEHICLE -> TransportRoute.RouteType.SPECIAL_VEHICLE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static MovingObject.MovingObjectType mapToMovingObjectType(VehicleRegistryType registryType) {
|
||||
if (registryType == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (registryType) {
|
||||
case WUREN -> MovingObject.MovingObjectType.UNMANNED_VEHICLE;
|
||||
case TEQIN -> MovingObject.MovingObjectType.SPECIAL_VEHICLE;
|
||||
case HANGKONG -> MovingObject.MovingObjectType.AIRCRAFT;
|
||||
case PUTONG, JIUYUAN -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public record VehicleRegistryEntry(String vehicleID, VehicleRegistryType vehicleType) {
|
||||
}
|
||||
|
||||
@ -293,7 +507,10 @@ public class PlatformRuntimeStateService {
|
||||
int updated,
|
||||
int controllableCount,
|
||||
Map<String, Integer> typesCount,
|
||||
List<String> controllableVehicleIDs) {
|
||||
List<String> controllableVehicleIDs,
|
||||
List<String> removedVehicleIDs,
|
||||
String testSessionId,
|
||||
String endedTestSessionId) {
|
||||
}
|
||||
|
||||
public record VehicleRouteSubmitResult(
|
||||
@ -305,6 +522,26 @@ public class PlatformRuntimeStateService {
|
||||
String routeType) {
|
||||
}
|
||||
|
||||
public record CollisionPreparationStatus(
|
||||
String testSessionId,
|
||||
boolean ready,
|
||||
CollisionPreparationSummary summary,
|
||||
List<CollisionPreparationObjectStatus> objects) {
|
||||
}
|
||||
|
||||
public record CollisionPreparationSummary(
|
||||
int registeredCount,
|
||||
int missingRouteCount) {
|
||||
}
|
||||
|
||||
public record CollisionPreparationObjectStatus(
|
||||
String objectId,
|
||||
String registryType,
|
||||
String objectType,
|
||||
boolean routeBound,
|
||||
String status) {
|
||||
}
|
||||
|
||||
public enum VehicleRegistryType {
|
||||
WUREN,
|
||||
TEQIN,
|
||||
|
||||
@ -429,161 +429,14 @@ public class WebSocketMessageBroadcaster {
|
||||
}
|
||||
|
||||
long payloadTimestamp = payload.getTimestamp() != null ? payload.getTimestamp() : System.currentTimeMillis();
|
||||
long filterTimestampMs = payloadTimestamp;
|
||||
PositionTrackState state = positionTrackStates.computeIfAbsent(payload.getObjectId(), k -> new PositionTrackState());
|
||||
|
||||
double filteredLatitude = latitude;
|
||||
double filteredLongitude = longitude;
|
||||
|
||||
synchronized (state) {
|
||||
if (!state.initialized) {
|
||||
initializeTrackState(state, latitude, longitude, filterTimestampMs);
|
||||
} else {
|
||||
if (filterTimestampMs <= state.lastPayloadTimestampMs) {
|
||||
// Use local monotonic time to avoid dropping legitimate points on equal/out-of-order source timestamps.
|
||||
filterTimestampMs = System.currentTimeMillis();
|
||||
if (filterTimestampMs <= state.lastPayloadTimestampMs) {
|
||||
filterTimestampMs = state.lastPayloadTimestampMs + 1;
|
||||
}
|
||||
}
|
||||
|
||||
long sourceDtMs = filterTimestampMs - state.lastPayloadTimestampMs;
|
||||
long arrivalDtMs = System.currentTimeMillis() - state.lastAcceptedAtMs;
|
||||
long boundedArrivalDtMs = clampLong(arrivalDtMs, 0L, Math.max(maxEffectiveDtMs, 50L));
|
||||
long boundedSourceDtMs = clampLong(sourceDtMs, 0L, Math.max(maxEffectiveDtMs, 50L));
|
||||
long effectiveDtMs = Math.max(boundedSourceDtMs, boundedArrivalDtMs);
|
||||
double dtSec = Math.max(effectiveDtMs / 1000.0, MIN_DT_SECONDS);
|
||||
double rawDistance = calculateDistanceMeters(
|
||||
state.lastRawLatitude,
|
||||
state.lastRawLongitude,
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
double rawSpeedMps = rawDistance / Math.max(dtSec, MIN_DT_SECONDS);
|
||||
AdaptiveFilterParams adaptive = resolveAdaptiveFilterParams(payload.getObjectType(), rawSpeedMps);
|
||||
boolean aircraft = isAircraftType(payload.getObjectType());
|
||||
double effectiveSpeedCapMps = resolveEffectiveSpeedCapMps(payload, adaptive.maxSpeedMps(), aircraft);
|
||||
double maxAllowedJump = resolveMaxAllowedJumpMeters(effectiveSpeedCapMps, adaptive.jumpMarginMeter(), dtSec);
|
||||
boolean isOutlierJump = rawDistance > maxAllowedJump;
|
||||
if (isOutlierJump) {
|
||||
if (aircraft) {
|
||||
double nearThreshold = Math.max(maxAllowedJump * clamp(aircraftReacquireNearFactor, 0.2, 2.0), adaptive.jitterMeter());
|
||||
if (state.aircraftPendingReacquire) {
|
||||
double pendingDistance = calculateDistanceMeters(
|
||||
state.aircraftPendingLatitude,
|
||||
state.aircraftPendingLongitude,
|
||||
latitude,
|
||||
longitude
|
||||
);
|
||||
if (pendingDistance <= nearThreshold) {
|
||||
state.aircraftReacquireCount++;
|
||||
} else {
|
||||
state.aircraftReacquireCount = 1;
|
||||
state.aircraftPendingLatitude = latitude;
|
||||
state.aircraftPendingLongitude = longitude;
|
||||
}
|
||||
} else {
|
||||
state.aircraftPendingReacquire = true;
|
||||
state.aircraftReacquireCount = 1;
|
||||
state.aircraftPendingLatitude = latitude;
|
||||
state.aircraftPendingLongitude = longitude;
|
||||
}
|
||||
|
||||
if (state.aircraftReacquireCount < AIRCRAFT_REACQUIRE_CONFIRM_POINTS) {
|
||||
// During reacquire confirmation, keep moving with a bounded step instead of freezing.
|
||||
double holdStepLimit = Math.max(maxAllowedJump, adaptive.jitterMeter() * 2.0);
|
||||
if (rawDistance > holdStepLimit) {
|
||||
double ratio = holdStepLimit / rawDistance;
|
||||
filteredLatitude = state.lastFilteredLatitude + ratio * (latitude - state.lastFilteredLatitude);
|
||||
filteredLongitude = state.lastFilteredLongitude + ratio * (longitude - state.lastFilteredLongitude);
|
||||
} else {
|
||||
filteredLatitude = latitude;
|
||||
filteredLongitude = longitude;
|
||||
}
|
||||
log.warn("[TMP-POS-FIX] reason=aircraft_outlier_blend objectId={} rawLatitude={} rawLongitude={} jumpMeter={} stepMeter={} confirm={}/{}",
|
||||
safeObjectId(payload), rawLatitude, rawLongitude, rawDistance, holdStepLimit,
|
||||
state.aircraftReacquireCount, AIRCRAFT_REACQUIRE_CONFIRM_POINTS);
|
||||
} else {
|
||||
state.aircraftPendingReacquire = false;
|
||||
state.aircraftReacquireCount = 0;
|
||||
|
||||
double transitionMaxStep = Math.max(maxAllowedJump * clamp(aircraftMaxStepFactor, 1.0, 4.0), adaptive.jitterMeter() * 2.0);
|
||||
if (rawDistance > transitionMaxStep) {
|
||||
double ratio = transitionMaxStep / rawDistance;
|
||||
filteredLatitude = state.lastFilteredLatitude + ratio * (latitude - state.lastFilteredLatitude);
|
||||
filteredLongitude = state.lastFilteredLongitude + ratio * (longitude - state.lastFilteredLongitude);
|
||||
log.warn("[TMP-POS-FIX] reason=aircraft_jump_step_limit objectId={} rawLatitude={} rawLongitude={} jumpMeter={} allowedStepMeter={}",
|
||||
safeObjectId(payload), rawLatitude, rawLongitude, rawDistance, transitionMaxStep);
|
||||
} else {
|
||||
filteredLatitude = latitude;
|
||||
filteredLongitude = longitude;
|
||||
}
|
||||
}
|
||||
} else if (state.rejectedCount < REACQUIRE_AFTER_REJECTS - 1) {
|
||||
state.rejectedCount++;
|
||||
logDroppedPosition(
|
||||
"outlier_jump_rejected",
|
||||
payload,
|
||||
rawLatitude,
|
||||
rawLongitude,
|
||||
String.format("jumpMeter=%.3f,allowedMeter=%.3f,rejectedCount=%d", rawDistance, maxAllowedJump, state.rejectedCount)
|
||||
);
|
||||
log.debug("Drop outlier position update: objectId={}, jumpMeter={}, allowedMeter={}",
|
||||
payload.getObjectId(), rawDistance, maxAllowedJump);
|
||||
return null;
|
||||
} else {
|
||||
state.rejectedCount = 0;
|
||||
double transitionStepLimit = Math.max(maxAllowedJump, adaptive.jitterMeter() * 2.0);
|
||||
if (rawDistance > transitionStepLimit) {
|
||||
double ratio = transitionStepLimit / rawDistance;
|
||||
filteredLatitude = state.lastFilteredLatitude + ratio * (latitude - state.lastFilteredLatitude);
|
||||
filteredLongitude = state.lastFilteredLongitude + ratio * (longitude - state.lastFilteredLongitude);
|
||||
log.warn("[TMP-POS-FIX] reason=non_aircraft_jump_step_limit objectId={} rawLatitude={} rawLongitude={} jumpMeter={} allowedStepMeter={}",
|
||||
safeObjectId(payload), rawLatitude, rawLongitude, rawDistance, transitionStepLimit);
|
||||
} else {
|
||||
filteredLatitude = latitude;
|
||||
filteredLongitude = longitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOutlierJump) {
|
||||
state.rejectedCount = 0;
|
||||
state.aircraftPendingReacquire = false;
|
||||
state.aircraftReacquireCount = 0;
|
||||
if (aircraft) {
|
||||
if (rawDistance <= adaptive.jitterMeter()) {
|
||||
filteredLatitude = state.lastFilteredLatitude;
|
||||
filteredLongitude = state.lastFilteredLongitude;
|
||||
} else {
|
||||
double alpha = resolveAircraftDynamicAlpha(adaptive.emaAlpha(), rawDistance, maxAllowedJump);
|
||||
filteredLatitude = state.lastFilteredLatitude + alpha * (latitude - state.lastFilteredLatitude);
|
||||
filteredLongitude = state.lastFilteredLongitude + alpha * (longitude - state.lastFilteredLongitude);
|
||||
}
|
||||
} else {
|
||||
if (rawDistance <= adaptive.jitterMeter()) {
|
||||
filteredLatitude = state.lastFilteredLatitude;
|
||||
filteredLongitude = state.lastFilteredLongitude;
|
||||
} else {
|
||||
double alpha = clamp(adaptive.emaAlpha(), 0.05, 1.0);
|
||||
filteredLatitude = state.lastFilteredLatitude + alpha * (latitude - state.lastFilteredLatitude);
|
||||
filteredLongitude = state.lastFilteredLongitude + alpha * (longitude - state.lastFilteredLongitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.lastRawLatitude = latitude;
|
||||
state.lastRawLongitude = longitude;
|
||||
state.lastPayloadTimestampMs = filterTimestampMs;
|
||||
state.lastFilteredLatitude = filteredLatitude;
|
||||
state.lastFilteredLongitude = filteredLongitude;
|
||||
state.lastAcceptedAtMs = System.currentTimeMillis();
|
||||
}
|
||||
initializeTrackState(state, latitude, longitude, payloadTimestamp);
|
||||
}
|
||||
|
||||
PositionUpdatePayload.Position filteredPosition = PositionUpdatePayload.Position.builder()
|
||||
.latitude(filteredLatitude)
|
||||
.longitude(filteredLongitude)
|
||||
.latitude(latitude)
|
||||
.longitude(longitude)
|
||||
.build();
|
||||
|
||||
return PositionUpdatePayload.builder()
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.dto.AircraftRouteQueryRequest;
|
||||
import com.qaup.collision.service.AircraftRouteQueryService;
|
||||
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.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
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;
|
||||
|
||||
class AircraftRouteQueryControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
AircraftRouteQueryService aircraftRouteQueryService = mock(AircraftRouteQueryService.class);
|
||||
AircraftRouteDTO response = new AircraftRouteDTO();
|
||||
response.setType("IN");
|
||||
response.setStatus("ACTIVE");
|
||||
response.setCodes("F1");
|
||||
when(aircraftRouteQueryService.queryRoute(any(AircraftRouteQueryRequest.class))).thenReturn(response);
|
||||
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(new AircraftRouteQueryController(aircraftRouteQueryService)).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnRawAircraftRouteForAircraftRouteQuery() throws Exception {
|
||||
mockMvc.perform(post("/api/aircraft-routes/query")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(new ObjectMapper().writeValueAsString(buildRequest())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.type").value("IN"))
|
||||
.andExpect(jsonPath("$.data.status").value("ACTIVE"))
|
||||
.andExpect(jsonPath("$.data.codes").value("F1"));
|
||||
}
|
||||
|
||||
private AircraftRouteQueryRequest buildRequest() {
|
||||
AircraftRouteQueryRequest request = new AircraftRouteQueryRequest();
|
||||
request.setRouteType("IN");
|
||||
request.setInRunway("35");
|
||||
request.setContactCross("F1");
|
||||
request.setSeat("138");
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,18 @@ package com.qaup.collision.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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 org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
@ -16,10 +23,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
class PlatformIntegrationControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private PlatformRuntimeStateService runtimeStateService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
ReflectionTestUtils.setField(runtimeStateService, "objectRouteAssignmentRepository", mock(ObjectRouteAssignmentRepository.class));
|
||||
ReflectionTestUtils.setField(runtimeStateService, "transportRouteRepository", mock(TransportRouteRepository.class));
|
||||
PlatformIntegrationController controller =
|
||||
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
@ -40,6 +50,7 @@ class PlatformIntegrationControllerTest {
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.status").value("success"))
|
||||
.andExpect(jsonPath("$.updated").value(3))
|
||||
.andExpect(jsonPath("$.testSessionId").isNotEmpty())
|
||||
.andExpect(jsonPath("$.controllableCount").value(2))
|
||||
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
|
||||
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
|
||||
@ -119,6 +130,7 @@ class PlatformIntegrationControllerTest {
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"vehicleDistance\":500}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.field").value("crossing_distance_threshold"))
|
||||
.andExpect(jsonPath("$.oldVehicleDistance").value(40.0))
|
||||
.andExpect(jsonPath("$.newVehicleDistance").value(500.0));
|
||||
}
|
||||
@ -129,6 +141,7 @@ class PlatformIntegrationControllerTest {
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"vehicleDistance\":500,\"aircraftDistance\":600}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.field").value("crossing_distance_threshold"))
|
||||
.andExpect(jsonPath("$.newVehicleDistance").value(500.0))
|
||||
.andExpect(jsonPath("$.newAircraftDistance").value(600.0))
|
||||
.andExpect(jsonPath("$.updatedVehicle").value(true))
|
||||
@ -143,4 +156,51 @@ class PlatformIntegrationControllerTest {
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value("Invalid JSON"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportRegisteredObjectsMissingRouteBindings() throws Exception {
|
||||
runtimeStateService.updateVehicleRegistry(java.util.List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN)
|
||||
));
|
||||
|
||||
mockMvc.perform(post("/api/collision/preparation/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.testSessionId").isNotEmpty())
|
||||
.andExpect(jsonPath("$.ready").value(false))
|
||||
.andExpect(jsonPath("$.summary.registeredCount").value(3))
|
||||
.andExpect(jsonPath("$.summary.missingRouteCount").value(3))
|
||||
.andExpect(jsonPath("$.objects[0].routeBound").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeCurrentCrossingThresholdsInPreparationStatus() throws Exception {
|
||||
mockMvc.perform(post("/api/collision/preparation/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.testSessionId").doesNotExist())
|
||||
.andExpect(jsonPath("$.thresholds.vehicleDistance").value(40.0))
|
||||
.andExpect(jsonPath("$.thresholds.aircraftDistance").value(40.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEndCurrentTestSessionWhenRegistryIsEmpty() throws Exception {
|
||||
mockMvc.perform(post("/api/VehicleRegistry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
[
|
||||
{ "vehicleID": "WR01", "vehicleType": "WUREN" },
|
||||
{ "vehicleID": "CD423", "vehicleType": "HANGKONG" }
|
||||
]
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.testSessionId").isNotEmpty());
|
||||
|
||||
mockMvc.perform(post("/api/VehicleRegistry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("[]"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.testSessionId").doesNotExist())
|
||||
.andExpect(jsonPath("$.endedTestSessionId").isNotEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,6 @@ 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)
|
||||
@ -71,7 +70,10 @@ class PlatformIntegrationSecurityTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
Mockito.when(permitAllUrlProperties.getUrls()).thenReturn(List.of());
|
||||
Mockito.when(permitAllUrlProperties.getUrls()).thenReturn(List.of(
|
||||
"/config/collision/diverging_release_distance",
|
||||
"/api/collision/preparation/status"
|
||||
));
|
||||
Mockito.when(userDetailsService.loadUserByUsername(Mockito.anyString()))
|
||||
.thenReturn(User.withUsername("user").password("{noop}pwd").authorities("ROLE_USER").build());
|
||||
|
||||
@ -101,6 +103,13 @@ class PlatformIntegrationSecurityTest {
|
||||
|
||||
Mockito.when(platformRuntimeStateService.updateCollisionDivergingReleaseDistance(Mockito.anyDouble()))
|
||||
.thenReturn(40.0);
|
||||
Mockito.when(platformRuntimeStateService.getCollisionPreparationStatus())
|
||||
.thenReturn(new PlatformRuntimeStateService.CollisionPreparationStatus(
|
||||
null,
|
||||
true,
|
||||
new PlatformRuntimeStateService.CollisionPreparationSummary(0, 0),
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -108,9 +117,9 @@ class PlatformIntegrationSecurityTest {
|
||||
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));
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(post("/api/collision/preparation/status"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
package com.qaup.collision.datacollector.server;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.pathconflict.service.VehicleCommandService;
|
||||
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class TrafficLightMqttSubscriberTest {
|
||||
|
||||
private CollisionWebSocketHandler collisionWebSocketHandler;
|
||||
private VehicleCommandService vehicleCommandService;
|
||||
private TrafficLightMqttSubscriber subscriber;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
collisionWebSocketHandler = mock(CollisionWebSocketHandler.class);
|
||||
vehicleCommandService = mock(VehicleCommandService.class);
|
||||
subscriber = new TrafficLightMqttSubscriber(
|
||||
new MqttConnectOptions(),
|
||||
collisionWebSocketHandler,
|
||||
vehicleCommandService,
|
||||
new ObjectMapper()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRouteTrafficLightPhaseToUnifiedHttpVehicleCommandService() {
|
||||
String payload = """
|
||||
{
|
||||
"serviceData": {
|
||||
"trafficLightsId": "3",
|
||||
"intersection": "T2",
|
||||
"phases": [
|
||||
{
|
||||
"phaseColor": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
subscriber.handleTrafficLightMessage("cusc/v2/SF053/QingDrsu001/data", payload);
|
||||
|
||||
verify(collisionWebSocketHandler).broadcastMessage(contains("\"source\":\"mqtt\""));
|
||||
verify(vehicleCommandService).sendTrafficLightSignalCommands(eq(3), eq("T2"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.qaup.collision.datacollector.service;
|
||||
|
||||
import com.qaup.collision.datacollector.config.VehicleManagerProperties;
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.datacollector.filter.VehicleLocationFilter;
|
||||
import com.qaup.collision.datacollector.model.dto.SensorStatusDTO;
|
||||
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
|
||||
import com.qaup.collision.datacollector.sdk.AdxpFlightServiceHttpClient;
|
||||
import com.qaup.collision.dataprocessing.service.DataProcessingService;
|
||||
import com.qaup.collision.common.service.VehicleLocationService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DataCollectorServicePositionTimestampTest {
|
||||
|
||||
@Mock
|
||||
private DataCollectorDao dataCollectorDao;
|
||||
@Mock
|
||||
private VehicleLocationService vehicleLocationService;
|
||||
@Mock
|
||||
private DataProcessingService dataProcessingService;
|
||||
@Mock
|
||||
private VehicleLocationFilter vehicleLocationFilter;
|
||||
@Mock
|
||||
private TrafficLightDataCollector trafficLightDataCollector;
|
||||
@Mock
|
||||
private AdxpFlightServiceHttpClient adxpFlightServiceClient;
|
||||
@Mock
|
||||
private com.qaup.collision.datacollector.websocket.AdxpFlightServiceWebSocketClient adxpFlightServiceWebSocketClient;
|
||||
@Mock
|
||||
private com.qaup.collision.datacollector.websocket.VehicleManagerWebSocketClient vehicleManagerWebSocketClient;
|
||||
@Mock
|
||||
private VehicleManagerProperties vehicleManagerProperties;
|
||||
@Mock
|
||||
private VehicleManagerCacheService vehicleManagerCacheService;
|
||||
@Mock
|
||||
private VehicleStatusAggregationService vehicleStatusAggregationService;
|
||||
|
||||
@InjectMocks
|
||||
private DataCollectorService dataCollectorService;
|
||||
|
||||
@Test
|
||||
void shouldUseGpsLastUpdateAsUnmannedVehicleSourceTimestamp() throws Exception {
|
||||
UniversalVehicleStatusDTO statusDTO = UniversalVehicleStatusDTO.builder()
|
||||
.sensorStatus(SensorStatusDTO.builder()
|
||||
.gps(SensorStatusDTO.GpsStatusDTO.builder()
|
||||
.lastUpdate(1_710_000_123_000L)
|
||||
.build())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Method method = DataCollectorService.class.getDeclaredMethod(
|
||||
"resolveUnmannedVehicleSourceTimestampMs",
|
||||
UniversalVehicleStatusDTO.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertEquals(1_710_000_123_000L, method.invoke(dataCollectorService, statusDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCallVehicleManagerHttpPollingEndpoints() {
|
||||
ReflectionTestUtils.setField(dataCollectorService, "collectorDisabled", false);
|
||||
|
||||
dataCollectorService.refreshUnmannedVehicleList();
|
||||
dataCollectorService.collectUnmannedVehicleData();
|
||||
|
||||
verify(dataCollectorDao, never()).getVehicleManagerVehicleDetails();
|
||||
verify(dataCollectorDao, never()).getVehicleManagerStatus(org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
}
|
||||
@ -307,10 +307,10 @@ class VehicleStatusUpdateIntegrationTest {
|
||||
.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);
|
||||
when(platformRuntimeStateService.getRegisteredCollisionObjectType(registeredUvId))
|
||||
.thenReturn(MovingObjectType.UNMANNED_VEHICLE);
|
||||
when(platformRuntimeStateService.getRegisteredCollisionObjectType(unregisteredUvId))
|
||||
.thenReturn(null);
|
||||
|
||||
dataProcessingService.performPeriodicDataProcessing();
|
||||
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
package com.qaup.collision.datacollector.websocket;
|
||||
|
||||
import com.qaup.collision.datacollector.config.FlightSdkProperties;
|
||||
import com.qaup.collision.dataprocessing.service.DataProcessingService;
|
||||
import com.qaup.common.core.redis.RedisCache;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class AdxpFlightServiceWebSocketClientContactCrossTest {
|
||||
|
||||
private AdxpFlightServiceWebSocketClient client;
|
||||
private RedisCache redisCache;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
client = new AdxpFlightServiceWebSocketClient(mock(WebSocketClient.class), new FlightSdkProperties());
|
||||
redisCache = mock(RedisCache.class);
|
||||
ReflectionTestUtils.setField(client, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(client, "dataProcessingService", mock(DataProcessingService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contactTaxiwayEventStoresTaxiwayIntersectionAsContactCross() throws Exception {
|
||||
client.handleTextMessage(mock(WebSocketSession.class), flightMessagesJson(
|
||||
"ADXP_WURENCHE_O_DYN_CONTACTTAXIWAY",
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
+ "<Msg><Head>"
|
||||
+ "<Svc_ServiceCode>ADXP_WURENCHE_O_DYN_CONTACTTAXIWAY</Svc_ServiceCode>"
|
||||
+ "</Head><Body><TaxiwayIntersection>"
|
||||
+ "<FlightNo>CQH6325</FlightNo>"
|
||||
+ "<TaxiwayIntersection>F5</TaxiwayIntersection>"
|
||||
+ "</TaxiwayIntersection></Body></Msg>"));
|
||||
|
||||
verify(redisCache).setCacheMapValue("flight:9C6325", "flightNumber", "9C6325");
|
||||
verify(redisCache).setCacheMapValue("flight:9C6325", "contactCross", "F5");
|
||||
verify(redisCache).setCacheMapValue(eq("flight:9C6325"), eq("contactCrossTs"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tisFlightNoLongerStoresContactCross() throws Exception {
|
||||
client.handleTextMessage(mock(WebSocketSession.class), flightMessagesJson(
|
||||
"ADXP_NAOMS_O_DYN_TISFLIGHT",
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
+ "<Msg><Body><TisFlight>"
|
||||
+ "<FlNo>9C6325</FlNo>"
|
||||
+ "<Type>ARR</Type>"
|
||||
+ "<ContactCross>F1</ContactCross>"
|
||||
+ "</TisFlight></Body></Msg>"));
|
||||
|
||||
verify(redisCache, never()).setCacheMapValue(anyString(), eq("contactCross"), anyString());
|
||||
verify(redisCache, never()).setCacheMapValue(anyString(), eq("contactCrossTs"), anyString());
|
||||
}
|
||||
|
||||
private String flightMessagesJson(String serviceCode, String xml) {
|
||||
return "[{\"serviceCode\":\"" + serviceCode + "\","
|
||||
+ "\"actionCode\":null,"
|
||||
+ "\"content\":\"" + xml.replace("\"", "\\\"") + "\"}]";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.qaup.collision.dataprocessing.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DataProcessingServiceCollisionRegistrationTest {
|
||||
|
||||
@Test
|
||||
void shouldUseRegisteredCollisionTypeInsteadOfCollectedSourceType() throws Exception {
|
||||
DataProcessingService service = new DataProcessingService();
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("WR01", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("CD423", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
ReflectionTestUtils.setField(service, "platformRuntimeStateService", runtimeStateService);
|
||||
|
||||
List<MovingObject> activeObjects = List.of(
|
||||
MovingObject.builder()
|
||||
.objectId("WR01")
|
||||
.objectName("WR01")
|
||||
.objectType(MovingObject.MovingObjectType.AIRCRAFT)
|
||||
.currentPosition(new org.locationtech.jts.geom.GeometryFactory()
|
||||
.createPoint(new org.locationtech.jts.geom.Coordinate(120.09704, 36.34834)))
|
||||
.build(),
|
||||
MovingObject.builder()
|
||||
.objectId("CD423")
|
||||
.objectName("CD423")
|
||||
.objectType(MovingObject.MovingObjectType.AIRCRAFT)
|
||||
.currentPosition(new org.locationtech.jts.geom.GeometryFactory()
|
||||
.createPoint(new org.locationtech.jts.geom.Coordinate(120.09914, 36.34943)))
|
||||
.build()
|
||||
);
|
||||
|
||||
Method method = DataProcessingService.class.getDeclaredMethod("filterCollisionManagedObjects", List.class);
|
||||
method.setAccessible(true);
|
||||
Object result = method.invoke(service, activeObjects);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<MovingObject> objects = (List<MovingObject>) ReflectionTestUtils.getField(result, "objects");
|
||||
|
||||
assertEquals(2, objects.size());
|
||||
MovingObject wr01 = objects.stream().filter(item -> "WR01".equals(item.getObjectId())).findFirst().orElse(null);
|
||||
MovingObject cd423 = objects.stream().filter(item -> "CD423".equals(item.getObjectId())).findFirst().orElse(null);
|
||||
assertNotNull(wr01);
|
||||
assertNotNull(cd423);
|
||||
assertEquals(MovingObject.MovingObjectType.UNMANNED_VEHICLE, wr01.getObjectType());
|
||||
assertEquals(MovingObject.MovingObjectType.AIRCRAFT, cd423.getObjectType());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.qaup.collision.dataprocessing.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class DataProcessingServicePositionDispatchTest {
|
||||
|
||||
@Test
|
||||
void shouldSkipPublishingWhenSourceSampleTimestampDidNotChange() throws Exception {
|
||||
DataProcessingService service = new DataProcessingService();
|
||||
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
|
||||
ReflectionTestUtils.setField(service, "eventPublisher", eventPublisher);
|
||||
|
||||
GeometryFactory geometryFactory = new GeometryFactory();
|
||||
MovingObject movingObject = MovingObject.builder()
|
||||
.objectId("UV-001")
|
||||
.objectName("UV-001")
|
||||
.objectType(MovingObject.MovingObjectType.UNMANNED_VEHICLE)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(120.0834, 36.3540)))
|
||||
.currentSpeed(12.0)
|
||||
.currentHeading(90.0)
|
||||
.sourceTimestampMs(1_710_000_123_000L)
|
||||
.build();
|
||||
|
||||
Object firstSummary = invokeSendPositionUpdates(service, List.of(movingObject));
|
||||
Object secondSummary = invokeSendPositionUpdates(service, List.of(movingObject));
|
||||
|
||||
assertEquals(1, ReflectionTestUtils.getField(firstSummary, "publishedCount"));
|
||||
assertEquals(0, ReflectionTestUtils.getField(firstSummary, "duplicateSampleSkippedCount"));
|
||||
assertEquals(0, ReflectionTestUtils.getField(secondSummary, "publishedCount"));
|
||||
assertEquals(1, ReflectionTestUtils.getField(secondSummary, "duplicateSampleSkippedCount"));
|
||||
verify(eventPublisher, times(1)).publishEvent(org.mockito.ArgumentMatchers.any(PositionUpdateEvent.class));
|
||||
}
|
||||
|
||||
private Object invokeSendPositionUpdates(DataProcessingService service,
|
||||
List<MovingObject> activeObjects) throws Exception {
|
||||
Method method = DataProcessingService.class.getDeclaredMethod("sendPositionUpdatesForActiveObjects", List.class);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(service, activeObjects);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.qaup.collision.dataprocessing.service;
|
||||
|
||||
import com.qaup.collision.common.model.AircraftRoute;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DataProcessingServiceRouteGeometryTest {
|
||||
|
||||
@Test
|
||||
void convertToAircraftRouteShouldKeepAllRoutePoints() throws Exception {
|
||||
DataProcessingService service = new DataProcessingService();
|
||||
ApplicationContext applicationContext = Mockito.mock(ApplicationContext.class);
|
||||
Mockito.when(applicationContext.getBean(RouteGeometryProcessor.class)).thenReturn(new RouteGeometryProcessor());
|
||||
setField(service, "applicationContext", applicationContext);
|
||||
|
||||
AircraftRouteDTO routeDTO = new AircraftRouteDTO();
|
||||
routeDTO.setType("IN");
|
||||
routeDTO.setStatus("COMPLETE");
|
||||
routeDTO.setCodes("A,B");
|
||||
routeDTO.setGeoPath(new AircraftRouteDTO.GeoPath(
|
||||
"FeatureCollection",
|
||||
List.of(new AircraftRouteDTO.Feature(
|
||||
"Feature",
|
||||
new AircraftRouteDTO.Geometry(
|
||||
"LineString",
|
||||
List.of(
|
||||
List.of(120.0000, 36.0000),
|
||||
List.of(120.0001, 36.0001),
|
||||
List.of(120.0002, 36.0002)
|
||||
)
|
||||
),
|
||||
new AircraftRouteDTO.Properties("A")
|
||||
))
|
||||
));
|
||||
|
||||
AircraftRoute route = invokeConvertToAircraftRoute(service, routeDTO);
|
||||
|
||||
assertNotNull(route);
|
||||
assertNotNull(route.getGeometry());
|
||||
assertEquals(3, route.getGeometry().getNumPoints());
|
||||
}
|
||||
|
||||
private AircraftRoute invokeConvertToAircraftRoute(DataProcessingService service,
|
||||
AircraftRouteDTO routeDTO) throws Exception {
|
||||
Method method = DataProcessingService.class.getDeclaredMethod("convertToAircraftRoute", AircraftRouteDTO.class);
|
||||
method.setAccessible(true);
|
||||
return (AircraftRoute) method.invoke(service, routeDTO);
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = DataProcessingService.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package com.qaup.collision.pathconflict.service;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
@ -25,6 +26,7 @@ import java.util.Optional;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@ -50,6 +52,9 @@ class PathConflictDetectionDirectionalTest {
|
||||
@Mock
|
||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
@Mock
|
||||
private VehicleCommandService vehicleCommandService;
|
||||
|
||||
@InjectMocks
|
||||
private PathConflictDetectionService pathConflictDetectionService;
|
||||
|
||||
@ -130,10 +135,13 @@ class PathConflictDetectionDirectionalTest {
|
||||
when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble()))
|
||||
.thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)});
|
||||
|
||||
PathConflictDetectionService.ConflictDetectionSummary summary =
|
||||
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
||||
|
||||
verify(conflictAlertLogRepository, never()).save(any());
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, summary.supportedPairs());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, summary.intersectionBehindPairs());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -160,9 +168,95 @@ class PathConflictDetectionDirectionalTest {
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
PathConflictDetectionService.ConflictDetectionSummary summary =
|
||||
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
||||
|
||||
verify(conflictAlertLogRepository, never()).save(any());
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, summary.supportedPairs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStillEvaluateConflictWhenAircraftIsWithinTwoHundredMetersOfAssignedRoute() throws Exception {
|
||||
GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
LineString vehicleRoute = geometryFactory.createLineString(new Coordinate[]{
|
||||
new Coordinate(0.0, 0.0),
|
||||
new Coordinate(300.0, 0.0)
|
||||
});
|
||||
LineString aircraftRoute = geometryFactory.createLineString(new Coordinate[]{
|
||||
new Coordinate(150.0, -300.0),
|
||||
new Coordinate(150.0, 300.0)
|
||||
});
|
||||
|
||||
MovingObject vehicle = MovingObject.builder()
|
||||
.objectId("UV-1")
|
||||
.objectName("UV-1")
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(30.0, 0.0)))
|
||||
.currentSpeed(18.0)
|
||||
.currentHeading(90.0)
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
MovingObject aircraft = MovingObject.builder()
|
||||
.objectId("AC-1")
|
||||
.objectName("AC-1")
|
||||
.objectType(MovingObjectType.AIRCRAFT)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(0.0, -150.0)))
|
||||
.currentSpeed(20.0)
|
||||
.currentHeading(0.0)
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
ObjectRouteAssignment vehicleAssignment = ObjectRouteAssignment.builder()
|
||||
.objectName("UV-1")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.assignedRouteId(1L)
|
||||
.build();
|
||||
ObjectRouteAssignment aircraftAssignment = ObjectRouteAssignment.builder()
|
||||
.objectName("AC-1")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.AIRCRAFT)
|
||||
.assignedRouteId(2L)
|
||||
.build();
|
||||
|
||||
TransportRoute route1 = TransportRoute.builder()
|
||||
.id(1L)
|
||||
.routeName("UV_ROUTE")
|
||||
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.routeGeometry(vehicleRoute)
|
||||
.build();
|
||||
TransportRoute route2 = TransportRoute.builder()
|
||||
.id(2L)
|
||||
.routeName("AC_ROUTE")
|
||||
.routeType(TransportRoute.RouteType.AIRCRAFT)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.routeGeometry(aircraftRoute)
|
||||
.build();
|
||||
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc("UV-1", ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE))
|
||||
.thenReturn(Optional.of(vehicleAssignment));
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc("AC-1", ObjectRouteAssignment.ObjectType.AIRCRAFT))
|
||||
.thenReturn(Optional.of(aircraftAssignment));
|
||||
when(routeRepository.findById(1L)).thenReturn(Optional.of(route1));
|
||||
when(routeRepository.findById(2L)).thenReturn(Optional.of(route2));
|
||||
when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble()))
|
||||
.thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)});
|
||||
when(platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle()).thenReturn(200.0);
|
||||
when(platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()).thenReturn(200.0);
|
||||
when(conflictAlertLogRepository.save(any())).thenAnswer(invocation -> {
|
||||
com.qaup.collision.pathconflict.model.entity.ConflictAlertLog logEntry = invocation.getArgument(0);
|
||||
logEntry.setId(1L);
|
||||
return logEntry;
|
||||
});
|
||||
|
||||
PathConflictDetectionService.ConflictDetectionSummary summary =
|
||||
pathConflictDetectionService.detectPathConflicts(List.of(vehicle, aircraft));
|
||||
|
||||
verify(conflictAlertLogRepository, atLeastOnce()).save(any());
|
||||
verify(eventPublisher, atLeastOnce()).publishEvent(any(ConflictAlertEvent.class));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, summary.supportedPairs());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, summary.eventsPublished());
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,7 @@ package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -20,12 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 测试PathConflictDetectionService处理null速度的情况
|
||||
@ -33,12 +24,6 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PathConflictDetectionServiceNullSpeedTest {
|
||||
|
||||
@Mock
|
||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
@Mock
|
||||
private TransportRouteRepository routeRepository;
|
||||
|
||||
@Mock
|
||||
private ConflictAlertLogRepository conflictAlertLogRepository;
|
||||
|
||||
@ -84,29 +69,6 @@ class PathConflictDetectionServiceNullSpeedTest {
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
// Mock路径分配和路径数据
|
||||
ObjectRouteAssignment assignment1 = new ObjectRouteAssignment();
|
||||
assignment1.setAssignedRouteId(1L);
|
||||
|
||||
ObjectRouteAssignment assignment2 = new ObjectRouteAssignment();
|
||||
assignment2.setAssignedRouteId(2L);
|
||||
|
||||
TransportRoute route1 = new TransportRoute();
|
||||
route1.setId(1L);
|
||||
route1.setRouteName("测试路径1");
|
||||
|
||||
TransportRoute route2 = new TransportRoute();
|
||||
route2.setId(2L);
|
||||
route2.setRouteName("测试路径2");
|
||||
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
anyString(), any(ObjectRouteAssignment.ObjectType.class)))
|
||||
.thenReturn(Optional.of(assignment1))
|
||||
.thenReturn(Optional.of(assignment2));
|
||||
|
||||
when(routeRepository.findById(1L)).thenReturn(Optional.of(route1));
|
||||
when(routeRepository.findById(2L)).thenReturn(Optional.of(route2));
|
||||
|
||||
// 测试:调用detectPathConflicts不应该抛出NullPointerException
|
||||
assertDoesNotThrow(() -> {
|
||||
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));
|
||||
@ -139,29 +101,6 @@ class PathConflictDetectionServiceNullSpeedTest {
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
// Mock路径分配和路径数据
|
||||
ObjectRouteAssignment assignment1 = new ObjectRouteAssignment();
|
||||
assignment1.setAssignedRouteId(1L);
|
||||
|
||||
ObjectRouteAssignment assignment2 = new ObjectRouteAssignment();
|
||||
assignment2.setAssignedRouteId(2L);
|
||||
|
||||
TransportRoute route1 = new TransportRoute();
|
||||
route1.setId(1L);
|
||||
route1.setRouteName("测试路径1");
|
||||
|
||||
TransportRoute route2 = new TransportRoute();
|
||||
route2.setId(2L);
|
||||
route2.setRouteName("测试路径2");
|
||||
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
anyString(), any(ObjectRouteAssignment.ObjectType.class)))
|
||||
.thenReturn(Optional.of(assignment1))
|
||||
.thenReturn(Optional.of(assignment2));
|
||||
|
||||
when(routeRepository.findById(1L)).thenReturn(Optional.of(route1));
|
||||
when(routeRepository.findById(2L)).thenReturn(Optional.of(route2));
|
||||
|
||||
// 测试:调用detectPathConflicts不应该抛出NullPointerException
|
||||
assertDoesNotThrow(() -> {
|
||||
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));
|
||||
|
||||
@ -3,6 +3,8 @@ 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.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
@ -14,12 +16,14 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
|
||||
@Test
|
||||
void shouldUseRuntimeThresholdsWhenEvaluatingAlertLevel() {
|
||||
void shouldUseCollisionDistanceThresholdsWhenEvaluatingAlertLevel() {
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
PathConflictDetectionService service = new PathConflictDetectionService(
|
||||
mock(TransportRouteRepository.class),
|
||||
@ -36,30 +40,106 @@ class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"evaluateAlertLevel",
|
||||
140.0,
|
||||
35.0,
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
500.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT,
|
||||
20.0
|
||||
30.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL), beforeUpdate);
|
||||
|
||||
runtimeStateService.updateRunwayAlertZoneRadiusAircraft(100.0);
|
||||
runtimeStateService.updateRunwayWarningZoneRadiusAircraft(130.0);
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistanceForVehicle(20.0);
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(25.0);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Optional<ConflictAlertLog.AlertLevel> afterUpdate =
|
||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"evaluateAlertLevel",
|
||||
140.0,
|
||||
35.0,
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
500.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT,
|
||||
20.0
|
||||
30.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT
|
||||
);
|
||||
|
||||
assertEquals(Optional.empty(), afterUpdate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnWarningWhenOnlyOneSideEntersThreshold() {
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistanceForVehicle(50.0);
|
||||
runtimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(20.0);
|
||||
|
||||
PathConflictDetectionService service = new PathConflictDetectionService(
|
||||
mock(TransportRouteRepository.class),
|
||||
mock(ObjectRouteAssignmentRepository.class),
|
||||
mock(ConflictAlertLogRepository.class),
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService,
|
||||
mock(VehicleCommandService.class)
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Optional<ConflictAlertLog.AlertLevel> result =
|
||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"evaluateAlertLevel",
|
||||
30.0,
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
25.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.WARNING), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveAssignedRouteUsingObjectIdWhenObjectNameDiffers() {
|
||||
TransportRouteRepository routeRepository = mock(TransportRouteRepository.class);
|
||||
ObjectRouteAssignmentRepository assignmentRepository = mock(ObjectRouteAssignmentRepository.class);
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
|
||||
PathConflictDetectionService service = new PathConflictDetectionService(
|
||||
routeRepository,
|
||||
assignmentRepository,
|
||||
mock(ConflictAlertLogRepository.class),
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService,
|
||||
mock(VehicleCommandService.class)
|
||||
);
|
||||
|
||||
MovingObject aircraft = MovingObject.builder()
|
||||
.objectId("AC001")
|
||||
.objectName("CA3456")
|
||||
.objectType(MovingObject.MovingObjectType.AIRCRAFT)
|
||||
.build();
|
||||
|
||||
ObjectRouteAssignment assignment = ObjectRouteAssignment.builder()
|
||||
.id(1L)
|
||||
.objectType(ObjectRouteAssignment.ObjectType.AIRCRAFT)
|
||||
.objectName("AC001")
|
||||
.assignedRouteId(99L)
|
||||
.build();
|
||||
|
||||
TransportRoute route = TransportRoute.builder()
|
||||
.id(99L)
|
||||
.routeName("TEST_AIRCRAFT_ROUTE")
|
||||
.routeType(TransportRoute.RouteType.AIRCRAFT)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.build();
|
||||
|
||||
when(assignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"AC001",
|
||||
ObjectRouteAssignment.ObjectType.AIRCRAFT
|
||||
)).thenReturn(Optional.of(assignment));
|
||||
when(routeRepository.findById(99L)).thenReturn(Optional.of(route));
|
||||
|
||||
TransportRoute resolved = (TransportRoute) ReflectionTestUtils.invokeMethod(service, "getObjectRoute", aircraft);
|
||||
|
||||
assertNotNull(resolved);
|
||||
assertEquals(99L, resolved.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
@ -30,7 +29,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
|
||||
|
||||
class VehicleCommandServiceTest {
|
||||
|
||||
private static final String COMMAND_URL = "http://vehicle.example/api/VehicleCommandInfo";
|
||||
private static final String COMMAND_URL = "http://10.232.18.23:8020/api/VehicleCommandInfo";
|
||||
|
||||
private VehicleCommandService vehicleCommandService;
|
||||
private MockRestServiceServer server;
|
||||
@ -43,8 +42,6 @@ class VehicleCommandServiceTest {
|
||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
||||
ReflectionTestUtils.setField(vehicleCommandService, "vehicleApiBaseUrl", "http://vehicle.example");
|
||||
ReflectionTestUtils.setField(vehicleCommandService, "vehicleCommandEndpoint", "/api/VehicleCommandInfo");
|
||||
geometryFactory = new GeometryFactory();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
package com.qaup.collision.pathconflict.service;
|
||||
|
||||
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.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.client.ExpectedCount.once;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
class VehicleCommandServiceTrafficLightTest {
|
||||
|
||||
private static final String COMMAND_URL = "http://10.232.18.23:8020/api/VehicleCommandInfo";
|
||||
|
||||
private VehicleCommandService vehicleCommandService;
|
||||
private MockRestServiceServer server;
|
||||
private PlatformRuntimeStateService runtimeStateService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSendTrafficLightSignalCommandsToFixedUnifiedHttpEndpoint() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.vehicleID").value("QN001"))
|
||||
.andExpect(jsonPath("$.commandType").value("SIGNAL"))
|
||||
.andExpect(jsonPath("$.commandReason").value("TRAFFIC_LIGHT"))
|
||||
.andExpect(jsonPath("$.signalState").value("RED"))
|
||||
.andExpect(jsonPath("$.intersectionId").value("T2"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("TQ001"))
|
||||
.andExpect(jsonPath("$.commandType").value("SIGNAL"))
|
||||
.andExpect(jsonPath("$.commandReason").value("TRAFFIC_LIGHT"))
|
||||
.andExpect(jsonPath("$.signalState").value("RED"))
|
||||
.andExpect(jsonPath("$.intersectionId").value("T2"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
vehicleCommandService.sendTrafficLightSignalCommands(3, "T2");
|
||||
|
||||
server.verify();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.datacollector.service.RoutePersistenceService;
|
||||
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||
import com.qaup.collision.dto.AircraftRouteQueryRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AircraftRouteQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldPersistAndBindAircraftRouteWhenFrontendProvidesObjectId() {
|
||||
DataCollectorDao dataCollectorDao = mock(DataCollectorDao.class);
|
||||
RoutePersistenceService routePersistenceService = mock(RoutePersistenceService.class);
|
||||
when(routePersistenceService.saveAircraftRoute(eq("AC001"), any())).thenReturn(true);
|
||||
when(dataCollectorDao.getArrivalRoute("35", null, "F1", "138")).thenReturn(buildRouteDto());
|
||||
|
||||
AircraftRouteQueryService service = new AircraftRouteQueryService(
|
||||
dataCollectorDao,
|
||||
routePersistenceService,
|
||||
new RouteGeometryProcessor()
|
||||
);
|
||||
|
||||
AircraftRouteQueryRequest request = new AircraftRouteQueryRequest();
|
||||
request.setObjectId("AC001");
|
||||
request.setRouteType("IN");
|
||||
request.setInRunway("35");
|
||||
request.setContactCross("F1");
|
||||
request.setSeat("138");
|
||||
|
||||
AircraftRouteDTO response = service.queryRoute(request);
|
||||
|
||||
assertEquals("ACTIVE", response.getStatus());
|
||||
assertEquals("F1", response.getCodes());
|
||||
verify(routePersistenceService).saveAircraftRoute(eq("AC001"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMergeReversedRouteSegmentsIntoSingleContinuousLine() {
|
||||
DataCollectorDao dataCollectorDao = mock(DataCollectorDao.class);
|
||||
RoutePersistenceService routePersistenceService = mock(RoutePersistenceService.class);
|
||||
when(routePersistenceService.saveAircraftRoute(eq("AC001"), any())).thenReturn(true);
|
||||
when(dataCollectorDao.getArrivalRoute("35", null, "F1", "138")).thenReturn(buildRouteDtoWithReversedSegments());
|
||||
|
||||
AircraftRouteQueryService service = new AircraftRouteQueryService(
|
||||
dataCollectorDao,
|
||||
routePersistenceService,
|
||||
new RouteGeometryProcessor()
|
||||
);
|
||||
|
||||
AircraftRouteQueryRequest request = new AircraftRouteQueryRequest();
|
||||
request.setObjectId("AC001");
|
||||
request.setRouteType("IN");
|
||||
request.setInRunway("35");
|
||||
request.setContactCross("F1");
|
||||
request.setSeat("138");
|
||||
|
||||
AircraftRouteDTO response = service.queryRoute(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals("A,B", response.getCodes());
|
||||
verify(routePersistenceService).saveAircraftRoute(eq("AC001"), argThat(route -> {
|
||||
LineString geometry = route.getGeometry();
|
||||
return geometry != null
|
||||
&& "LINESTRING (120 36, 120.1 36.1, 120.2 36.2)".equals(geometry.toText());
|
||||
}));
|
||||
}
|
||||
|
||||
private AircraftRouteDTO buildRouteDto() {
|
||||
AircraftRouteDTO.Geometry geometry = new AircraftRouteDTO.Geometry();
|
||||
geometry.setCoordinates(List.of(
|
||||
List.of(120.1, 36.1),
|
||||
List.of(120.2, 36.2)
|
||||
));
|
||||
|
||||
AircraftRouteDTO.Properties properties = new AircraftRouteDTO.Properties();
|
||||
properties.setCode("F1");
|
||||
|
||||
AircraftRouteDTO.Feature feature = new AircraftRouteDTO.Feature();
|
||||
feature.setGeometry(geometry);
|
||||
feature.setProperties(properties);
|
||||
|
||||
AircraftRouteDTO.GeoPath geoPath = new AircraftRouteDTO.GeoPath();
|
||||
geoPath.setFeatures(List.of(feature));
|
||||
|
||||
AircraftRouteDTO dto = new AircraftRouteDTO();
|
||||
dto.setCodes("F1");
|
||||
dto.setStatus("ACTIVE");
|
||||
dto.setGeoPath(geoPath);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private AircraftRouteDTO buildRouteDtoWithReversedSegments() {
|
||||
AircraftRouteDTO.Geometry segment1 = new AircraftRouteDTO.Geometry();
|
||||
segment1.setCoordinates(List.of(
|
||||
List.of(120.1, 36.1),
|
||||
List.of(120.2, 36.2)
|
||||
));
|
||||
|
||||
AircraftRouteDTO.Geometry segment2 = new AircraftRouteDTO.Geometry();
|
||||
segment2.setCoordinates(List.of(
|
||||
List.of(120.1, 36.1),
|
||||
List.of(120.0, 36.0)
|
||||
));
|
||||
|
||||
AircraftRouteDTO.Properties properties1 = new AircraftRouteDTO.Properties();
|
||||
properties1.setCode("A");
|
||||
AircraftRouteDTO.Properties properties2 = new AircraftRouteDTO.Properties();
|
||||
properties2.setCode("B");
|
||||
|
||||
AircraftRouteDTO.Feature feature1 = new AircraftRouteDTO.Feature();
|
||||
feature1.setGeometry(segment1);
|
||||
feature1.setProperties(properties1);
|
||||
|
||||
AircraftRouteDTO.Feature feature2 = new AircraftRouteDTO.Feature();
|
||||
feature2.setGeometry(segment2);
|
||||
feature2.setProperties(properties2);
|
||||
|
||||
AircraftRouteDTO.GeoPath geoPath = new AircraftRouteDTO.GeoPath();
|
||||
geoPath.setFeatures(List.of(feature1, feature2));
|
||||
|
||||
AircraftRouteDTO dto = new AircraftRouteDTO();
|
||||
dto.setCodes("A,B");
|
||||
dto.setStatus("ACTIVE");
|
||||
dto.setGeoPath(geoPath);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,21 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.system.domain.SysConfig;
|
||||
import com.qaup.system.service.ISysConfigService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
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;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PlatformRuntimeStateServiceTest {
|
||||
|
||||
@ -55,6 +63,33 @@ class PlatformRuntimeStateServiceTest {
|
||||
assertEquals(55.0, service.getCollisionDivergingReleaseDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistCrossingThresholdsWhenUpdated() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
ISysConfigService sysConfigService = mock(ISysConfigService.class);
|
||||
when(sysConfigService.selectConfigList(any(SysConfig.class))).thenReturn(List.of());
|
||||
ReflectionTestUtils.setField(service, "sysConfigService", sysConfigService);
|
||||
|
||||
service.updateCollisionDivergingReleaseDistanceForVehicle(60.0);
|
||||
service.updateCollisionDivergingReleaseDistanceForAircraft(80.0);
|
||||
|
||||
verify(sysConfigService, times(2)).insertConfig(any(SysConfig.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoadPersistedCrossingThresholdsOnStartup() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
ISysConfigService sysConfigService = mock(ISysConfigService.class);
|
||||
when(sysConfigService.selectConfigByKey("qaup.collision.crossing.vehicleDistance")).thenReturn("65");
|
||||
when(sysConfigService.selectConfigByKey("qaup.collision.crossing.aircraftDistance")).thenReturn("85");
|
||||
ReflectionTestUtils.setField(service, "sysConfigService", sysConfigService);
|
||||
|
||||
service.loadPersistedCrossingThresholds();
|
||||
|
||||
assertEquals(65.0, service.getCollisionDivergingReleaseDistanceForVehicle());
|
||||
assertEquals(85.0, service.getCollisionDivergingReleaseDistanceForAircraft());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyAllowRegisteredCollisionTypesToParticipate() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
@ -75,4 +110,42 @@ class PlatformRuntimeStateServiceTest {
|
||||
assertEquals(List.of("QN001", "TQ001"), service.getControllableVehicleIds());
|
||||
assertEquals(List.of("QN001", "TQ001"), service.getTrafficLightRecipientVehicleIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportRemovedObjectsWhenVehicleRegistryIsResubmitted() {
|
||||
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)
|
||||
));
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult second = service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
assertEquals(List.of("AC001", "TQ001"), second.removedVehicleIDs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAndEndCollisionTestSessionFromRegistryUpdates() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult started = service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("WR01", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("CD423", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
assertTrue(started.testSessionId().startsWith("collision-test-"));
|
||||
assertEquals(started.testSessionId(), service.getCurrentTestSessionId());
|
||||
|
||||
PlatformRuntimeStateService.CollisionPreparationStatus status = service.getCollisionPreparationStatus();
|
||||
assertEquals(started.testSessionId(), status.testSessionId());
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult ended = service.updateVehicleRegistry(List.of());
|
||||
|
||||
assertEquals(started.testSessionId(), ended.endedTestSessionId());
|
||||
assertEquals(null, ended.testSessionId());
|
||||
assertEquals(null, service.getCurrentTestSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
package com.qaup.collision.websocket.broadcaster;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.websocket.cache.MessageCacheService;
|
||||
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
|
||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class WebSocketMessageBroadcasterTest {
|
||||
|
||||
@Test
|
||||
void sanitizePositionPayloadShouldKeepAircraftCoordinatesUnchanged() throws Exception {
|
||||
WebSocketMessageBroadcaster broadcaster = new WebSocketMessageBroadcaster(
|
||||
mock(MessageCacheService.class),
|
||||
mock(CollisionWebSocketHandler.class),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
PositionUpdatePayload firstPayload = payload("AC-1", 36.100000, 120.100000, 1_000L);
|
||||
PositionUpdatePayload secondPayload = payload("AC-1", 36.100100, 120.100100, 2_000L);
|
||||
|
||||
invokeSanitize(broadcaster, firstPayload);
|
||||
PositionUpdatePayload sanitized = invokeSanitize(broadcaster, secondPayload);
|
||||
|
||||
assertNotNull(sanitized);
|
||||
assertEquals(36.100100, sanitized.getPosition().getLatitude());
|
||||
assertEquals(120.100100, sanitized.getPosition().getLongitude());
|
||||
}
|
||||
|
||||
private PositionUpdatePayload invokeSanitize(WebSocketMessageBroadcaster broadcaster,
|
||||
PositionUpdatePayload payload) throws Exception {
|
||||
Method method = WebSocketMessageBroadcaster.class.getDeclaredMethod("sanitizePositionPayload", PositionUpdatePayload.class);
|
||||
method.setAccessible(true);
|
||||
return (PositionUpdatePayload) method.invoke(broadcaster, payload);
|
||||
}
|
||||
|
||||
private PositionUpdatePayload payload(String objectId, double latitude, double longitude, long timestamp) {
|
||||
return PositionUpdatePayload.builder()
|
||||
.objectId(objectId)
|
||||
.objectType("AIRCRAFT")
|
||||
.position(PositionUpdatePayload.Position.builder()
|
||||
.latitude(latitude)
|
||||
.longitude(longitude)
|
||||
.build())
|
||||
.timestamp(timestamp)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user