Clean up collision flow changes
This commit is contained in:
parent
20af9b8288
commit
1cac6fb811
4
.gitignore
vendored
4
.gitignore
vendored
@ -55,6 +55,8 @@ Users/
|
|||||||
|
|
||||||
qaup-deploy/
|
qaup-deploy/
|
||||||
deploy/offline_packages/
|
deploy/offline_packages/
|
||||||
|
*.lck
|
||||||
|
mqtt协议验证/账户信息.txt
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Python
|
# Python
|
||||||
@ -95,4 +97,4 @@ yarn-error.log*
|
|||||||
.npm
|
.npm
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
dist/
|
dist/
|
||||||
.cache/
|
.cache/
|
||||||
|
|||||||
@ -6,3 +6,9 @@
|
|||||||
这是开发环境,运行环境在centos7的容器里,分别为
|
这是开发环境,运行环境在centos7的容器里,分别为
|
||||||
qaup-app
|
qaup-app
|
||||||
qaup-redis
|
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("controllableCount", result.controllableCount());
|
||||||
payload.put("typesCount", result.typesCount());
|
payload.put("typesCount", result.typesCount());
|
||||||
payload.put("controllableVehicleIDs", result.controllableVehicleIDs());
|
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);
|
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
return invalidJson(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)
|
@PostMapping(path = "/config/runway/warning_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public ResponseEntity<Map<String, Object>> updateRunwayWarningZoneRadiusAircraft(@RequestBody String requestBody) {
|
public ResponseEntity<Map<String, Object>> updateRunwayWarningZoneRadiusAircraft(@RequestBody String requestBody) {
|
||||||
return updateNumericConfig(requestBody, "runway", "warning_zone_radius.aircraft",
|
return updateNumericConfig(requestBody, "runway", "warning_zone_radius.aircraft",
|
||||||
platformRuntimeStateService::updateRunwayWarningZoneRadiusAircraft);
|
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)
|
@PostMapping(path = "/config/runway/alert_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public ResponseEntity<Map<String, Object>> updateRunwayAlertZoneRadiusAircraft(@RequestBody String requestBody) {
|
public ResponseEntity<Map<String, Object>> updateRunwayAlertZoneRadiusAircraft(@RequestBody String requestBody) {
|
||||||
return updateNumericConfig(requestBody, "runway", "alert_zone_radius.aircraft",
|
return updateNumericConfig(requestBody, "runway", "alert_zone_radius.aircraft",
|
||||||
platformRuntimeStateService::updateRunwayAlertZoneRadiusAircraft);
|
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)
|
@PostMapping(path = "/config/collision/diverging_release_distance", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public ResponseEntity<Map<String, Object>> updateCollisionDivergingReleaseDistance(@RequestBody String requestBody) {
|
public ResponseEntity<Map<String, Object>> updateCollisionDivergingReleaseDistance(@RequestBody String requestBody) {
|
||||||
return updateCollisionDistanceConfig(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(
|
private void validateVehicleRegistryItem(
|
||||||
JsonNode item,
|
JsonNode item,
|
||||||
List<String> errors,
|
List<String> errors,
|
||||||
@ -318,7 +374,7 @@ public class PlatformIntegrationController {
|
|||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("status", "success");
|
payload.put("status", "success");
|
||||||
payload.put("area", "collision");
|
payload.put("area", "collision");
|
||||||
payload.put("field", "collision.diverging_release_distance");
|
payload.put("field", "crossing_distance_threshold");
|
||||||
payload.put("old", oldValue);
|
payload.put("old", oldValue);
|
||||||
payload.put("new", newValue);
|
payload.put("new", newValue);
|
||||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||||
@ -336,7 +392,7 @@ public class PlatformIntegrationController {
|
|||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("status", "success");
|
payload.put("status", "success");
|
||||||
payload.put("area", "collision");
|
payload.put("area", "collision");
|
||||||
payload.put("field", "collision.diverging_release_distance");
|
payload.put("field", "crossing_distance_threshold");
|
||||||
payload.put("oldVehicleDistance", oldVehicleDistance);
|
payload.put("oldVehicleDistance", oldVehicleDistance);
|
||||||
payload.put("newVehicleDistance", newVehicleDistance != null ? newVehicleDistance : platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
|
payload.put("newVehicleDistance", newVehicleDistance != null ? newVehicleDistance : platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
|
||||||
payload.put("oldAircraftDistance", oldAircraftDistance);
|
payload.put("oldAircraftDistance", oldAircraftDistance);
|
||||||
@ -357,6 +413,7 @@ public class PlatformIntegrationController {
|
|||||||
}
|
}
|
||||||
String normalized = objectTypeValue.trim().toUpperCase(Locale.ROOT);
|
String normalized = objectTypeValue.trim().toUpperCase(Locale.ROOT);
|
||||||
return switch (normalized) {
|
return switch (normalized) {
|
||||||
|
case "AIRCRAFT", "HANGKONG", "AC" -> ObjectRouteAssignment.ObjectType.AIRCRAFT;
|
||||||
case "UNMANNED_VEHICLE", "UNMANNED", "UV", "WUREN" -> ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE;
|
case "UNMANNED_VEHICLE", "UNMANNED", "UV", "WUREN" -> ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE;
|
||||||
case "SPECIAL_VEHICLE", "SPECIAL", "TEQIN", "SPECIALVEHICLE" -> ObjectRouteAssignment.ObjectType.SPECIAL_VEHICLE;
|
case "SPECIAL_VEHICLE", "SPECIAL", "TEQIN", "SPECIALVEHICLE" -> ObjectRouteAssignment.ObjectType.SPECIAL_VEHICLE;
|
||||||
default -> null;
|
default -> null;
|
||||||
|
|||||||
@ -2,8 +2,8 @@ package com.qaup.collision.datacollector.server;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.CollisionWebSocketHandler;
|
||||||
import com.qaup.collision.websocket.handler.VehicleCommandInfoWebSocketHandler;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.paho.client.mqttv3.*;
|
import org.eclipse.paho.client.mqttv3.*;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -40,7 +40,7 @@ public class TrafficLightMqttSubscriber {
|
|||||||
|
|
||||||
private final MqttConnectOptions mqttConnectOptions;
|
private final MqttConnectOptions mqttConnectOptions;
|
||||||
private final CollisionWebSocketHandler collisionWebSocketHandler;
|
private final CollisionWebSocketHandler collisionWebSocketHandler;
|
||||||
private final VehicleCommandInfoWebSocketHandler vehicleCommandInfoWebSocketHandler;
|
private final VehicleCommandService vehicleCommandService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private MqttClient mqttClient;
|
private MqttClient mqttClient;
|
||||||
@ -55,17 +55,17 @@ public class TrafficLightMqttSubscriber {
|
|||||||
*
|
*
|
||||||
* @param mqttConnectOptions MQTT 连接选项
|
* @param mqttConnectOptions MQTT 连接选项
|
||||||
* @param collisionWebSocketHandler WebSocket 处理器,用于向前端广播消息
|
* @param collisionWebSocketHandler WebSocket 处理器,用于向前端广播消息
|
||||||
* @param vehicleCommandInfoWebSocketHandler 车辆控制指令 WebSocket 处理器
|
* @param vehicleCommandService 统一车辆控制服务
|
||||||
* @param objectMapper JSON 解析器
|
* @param objectMapper JSON 解析器
|
||||||
*/
|
*/
|
||||||
@Autowired
|
@Autowired
|
||||||
public TrafficLightMqttSubscriber(MqttConnectOptions mqttConnectOptions,
|
public TrafficLightMqttSubscriber(MqttConnectOptions mqttConnectOptions,
|
||||||
CollisionWebSocketHandler collisionWebSocketHandler,
|
CollisionWebSocketHandler collisionWebSocketHandler,
|
||||||
VehicleCommandInfoWebSocketHandler vehicleCommandInfoWebSocketHandler,
|
VehicleCommandService vehicleCommandService,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
this.mqttConnectOptions = mqttConnectOptions;
|
this.mqttConnectOptions = mqttConnectOptions;
|
||||||
this.collisionWebSocketHandler = collisionWebSocketHandler;
|
this.collisionWebSocketHandler = collisionWebSocketHandler;
|
||||||
this.vehicleCommandInfoWebSocketHandler = vehicleCommandInfoWebSocketHandler;
|
this.vehicleCommandService = vehicleCommandService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,59 +104,7 @@ public class TrafficLightMqttSubscriber {
|
|||||||
public void messageArrived(String topic, MqttMessage message) {
|
public void messageArrived(String topic, MqttMessage message) {
|
||||||
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
|
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
|
||||||
totalMessages.incrementAndGet();
|
totalMessages.incrementAndGet();
|
||||||
|
handleTrafficLightMessage(topic, payload);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -217,4 +165,75 @@ public class TrafficLightMqttSubscriber {
|
|||||||
return String.format("MQTT订阅状态 - 连接:%s, 总消息:%d, 错误:%d",
|
return String.format("MQTT订阅状态 - 连接:%s, 总消息:%d, 错误:%d",
|
||||||
connected.get() ? "是" : "否", totalMessages.get(), errorCount.get());
|
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 {
|
public class DataCollectorService {
|
||||||
|
|
||||||
private static final boolean VEHICLE_MANAGER_ROUTE_INGEST_ENABLED = false;
|
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);
|
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}")
|
@Value("${data.collector.detection.interval:1000}")
|
||||||
private long detectionInterval;
|
private long detectionInterval;
|
||||||
|
|
||||||
@Value("${data.collector.vehicle-manager.http.polling-enabled:true}")
|
|
||||||
private boolean vehicleManagerHttpPollingEnabled;
|
|
||||||
|
|
||||||
|
|
||||||
@Value("${data.collector.route.periodic-collection-enabled:false}")
|
@Value("${data.collector.route.periodic-collection-enabled:false}")
|
||||||
private boolean periodicRouteCollectionEnabled;
|
private boolean periodicRouteCollectionEnabled;
|
||||||
|
|
||||||
@ -240,6 +237,9 @@ public class DataCollectorService {
|
|||||||
if (collectorDisabled) {
|
if (collectorDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> ids = dataCollectorDao.getVehicleManagerVehicleDetails();
|
List<String> ids = dataCollectorDao.getVehicleManagerVehicleDetails();
|
||||||
if (ids == null || ids.isEmpty()) {
|
if (ids == null || ids.isEmpty()) {
|
||||||
@ -689,6 +689,9 @@ public class DataCollectorService {
|
|||||||
if (collectorDisabled) {
|
if (collectorDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 丢弃旧轮次:上一轮未完成时跳过,避免“多车循环HTTP + fixedRate + @Async”堆积
|
// 丢弃旧轮次:上一轮未完成时跳过,避免“多车循环HTTP + fixedRate + @Async”堆积
|
||||||
try {
|
try {
|
||||||
@ -745,6 +748,7 @@ public class DataCollectorService {
|
|||||||
.currentPosition(currentPosition)
|
.currentPosition(currentPosition)
|
||||||
.currentSpeed(null) // 不在采集阶段计算速度
|
.currentSpeed(null) // 不在采集阶段计算速度
|
||||||
.currentHeading(null) // 不在采集阶段计算方向
|
.currentHeading(null) // 不在采集阶段计算方向
|
||||||
|
.sourceTimestampMs(resolveUnmannedVehicleSourceTimestampMs(statusData))
|
||||||
.altitude(0.0); // 默认高度
|
.altitude(0.0); // 默认高度
|
||||||
|
|
||||||
// Task context is disabled for route ingestion.
|
// 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列表
|
* 获取已知的无人车ID列表
|
||||||
* 可以从配置、缓存或其他数据源获取
|
* 可以从配置、缓存或其他数据源获取
|
||||||
@ -876,7 +893,7 @@ public class DataCollectorService {
|
|||||||
@Async // 异步执行
|
@Async // 异步执行
|
||||||
public void collectUniversalVehicleStatus() {
|
public void collectUniversalVehicleStatus() {
|
||||||
// 无人车状态已在 collectUnmannedVehicleData() 中通过 HTTP 拉取并缓存,无需重复采集
|
// 无人车状态已在 collectUnmannedVehicleData() 中通过 HTTP 拉取并缓存,无需重复采集
|
||||||
if (vehicleManagerHttpPollingEnabled) {
|
if (VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -369,6 +369,8 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
|||||||
return handleTISFLIGHT(root);
|
return handleTISFLIGHT(root);
|
||||||
case "ADXP_NAOMS_O_DYN_DFDE":
|
case "ADXP_NAOMS_O_DYN_DFDE":
|
||||||
return handleDFDE(root);
|
return handleDFDE(root);
|
||||||
|
case "ADXP_WURENCHE_O_DYN_CONTACTTAXIWAY":
|
||||||
|
return handleCONTACTTAXIWAY(root);
|
||||||
default:
|
default:
|
||||||
log.debug("未知的服务代码: {}", serviceCode);
|
log.debug("未知的服务代码: {}", serviceCode);
|
||||||
return null;
|
return null;
|
||||||
@ -454,6 +456,17 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
|||||||
return null;
|
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,导致路由参数难以凑齐。
|
* 统一航班号归一化规则,避免同一航班参数写入不同 Redis Key,导致路由参数难以凑齐。
|
||||||
*
|
*
|
||||||
@ -1121,7 +1134,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
|||||||
String flightNumberRaw = getTextContent(root, "FlNo");
|
String flightNumberRaw = getTextContent(root, "FlNo");
|
||||||
String flightNumber = normalizeTisFlightNo(flightNumberRaw);
|
String flightNumber = normalizeTisFlightNo(flightNumberRaw);
|
||||||
String flightId = normalizeTisFlightNo(flightIdRaw);
|
String flightId = normalizeTisFlightNo(flightIdRaw);
|
||||||
String contactCross = getTextContent(root, "ContactCross");
|
|
||||||
String type = getTextContent(root, "Type");
|
String type = getTextContent(root, "Type");
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
|
|
||||||
@ -1157,9 +1169,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
|||||||
FlightNotificationDTO dto = new FlightNotificationDTO();
|
FlightNotificationDTO dto = new FlightNotificationDTO();
|
||||||
dto.setFlightNo(resolvedFlightNo != null ? resolvedFlightNo : flightNumberRaw);
|
dto.setFlightNo(resolvedFlightNo != null ? resolvedFlightNo : flightNumberRaw);
|
||||||
dto.setType(routeType);
|
dto.setType(routeType);
|
||||||
if ("IN".equalsIgnoreCase(routeType)) {
|
|
||||||
dto.setContactCross(contactCross);
|
|
||||||
}
|
|
||||||
|
|
||||||
String normalizedBizKey = resolveTisFlightBizKey(bizKey, resolvedFlightNo);
|
String normalizedBizKey = resolveTisFlightBizKey(bizKey, resolvedFlightNo);
|
||||||
String flightKey = buildFlightRedisKey(resolvedFlightNo);
|
String flightKey = buildFlightRedisKey(resolvedFlightNo);
|
||||||
@ -1169,12 +1178,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
|||||||
try {
|
try {
|
||||||
redisCache.setCacheMapValue(flightKey, "flightNumber", resolvedFlightNo);
|
redisCache.setCacheMapValue(flightKey, "flightNumber", resolvedFlightNo);
|
||||||
updateActiveBizKey(flightKey, normalizedBizKey, nowMs);
|
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={}",
|
log.info("TISFLIGHT航班号解析(已应用三字转二字): flNoRaw={}, flightIdRaw={}, fuId={}, bizKey={}, resolvedFlightNo={}",
|
||||||
flightNumberRaw, flightIdRaw, fuId, normalizedBizKey, resolvedFlightNo);
|
flightNumberRaw, flightIdRaw, fuId, normalizedBizKey, resolvedFlightNo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -1187,6 +1190,32 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
|
|||||||
return dto;
|
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) {
|
private FlightNotificationDTO handleAXOT(org.w3c.dom.Element root) {
|
||||||
// BizKey 常见格式:FLIGHTNO-进出港标识-时间(yyyyMMddHHmmss) 或其他分段
|
// BizKey 常见格式:FLIGHTNO-进出港标识-时间(yyyyMMddHHmmss) 或其他分段
|
||||||
String bizKey = getTextContent(root, "BizKey");
|
String bizKey = getTextContent(root, "BizKey");
|
||||||
|
|||||||
@ -30,11 +30,13 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -65,6 +67,9 @@ public class DataProcessingService {
|
|||||||
@Value("${data.collector.disabled:false}")
|
@Value("${data.collector.disabled:false}")
|
||||||
private boolean collectorDisabled;
|
private boolean collectorDisabled;
|
||||||
|
|
||||||
|
@Value("${qaup.collision.diagnostic-log.enabled:true}")
|
||||||
|
private boolean collisionDiagnosticLogEnabled;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SpeedCalculationService speedCalculationService;
|
private SpeedCalculationService speedCalculationService;
|
||||||
|
|
||||||
@ -98,6 +103,7 @@ public class DataProcessingService {
|
|||||||
|
|
||||||
// 从DataCollectorService获取缓存的引用
|
// 从DataCollectorService获取缓存的引用
|
||||||
private Map<String, MovingObject> activeMovingObjectsCache;
|
private Map<String, MovingObject> activeMovingObjectsCache;
|
||||||
|
private final Map<String, Long> lastPublishedPositionSampleTimestamps = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 防止@Scheduled任务重入导致并发执行、消息风暴与数据库压力。
|
* 防止@Scheduled任务重入导致并发执行、消息风暴与数据库压力。
|
||||||
@ -146,7 +152,7 @@ public class DataProcessingService {
|
|||||||
calculateSpeedAndDirectionForAllObjects(currentActiveObjects);
|
calculateSpeedAndDirectionForAllObjects(currentActiveObjects);
|
||||||
|
|
||||||
// 第二步:发送WebSocket位置更新消息
|
// 第二步:发送WebSocket位置更新消息
|
||||||
sendPositionUpdatesForActiveObjects(currentActiveObjects);
|
PositionDispatchSummary positionDispatchSummary = sendPositionUpdatesForActiveObjects(currentActiveObjects);
|
||||||
|
|
||||||
// 第三步:处理通用车辆状态数据并发送WebSocket更新
|
// 第三步:处理通用车辆状态数据并发送WebSocket更新
|
||||||
processUniversalVehicleStatusUpdates();
|
processUniversalVehicleStatusUpdates();
|
||||||
@ -155,7 +161,8 @@ public class DataProcessingService {
|
|||||||
// 注意:采集侧只缓存;发送侧在处理完成后会清理缓存,避免同一通知被每秒重复推送(刷屏)。
|
// 注意:采集侧只缓存;发送侧在处理完成后会清理缓存,避免同一通知被每秒重复推送(刷屏)。
|
||||||
processFlightNotificationUpdates();
|
processFlightNotificationUpdates();
|
||||||
|
|
||||||
List<MovingObject> collisionManagedObjects = filterCollisionManagedObjects(currentActiveObjects);
|
CollisionManagedObjectsResult collisionManagedResult = filterCollisionManagedObjects(currentActiveObjects);
|
||||||
|
List<MovingObject> collisionManagedObjects = collisionManagedResult.objects();
|
||||||
|
|
||||||
// 第五步:将注册的无人车任务路径准备为正式路线输入
|
// 第五步:将注册的无人车任务路径准备为正式路线输入
|
||||||
if (routePreparationService != null) {
|
if (routePreparationService != null) {
|
||||||
@ -163,7 +170,8 @@ public class DataProcessingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 第六步:执行路径冲突检测
|
// 第六步:执行路径冲突检测
|
||||||
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
|
PathConflictDetectionService.ConflictDetectionSummary conflictDetectionSummary =
|
||||||
|
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
|
||||||
|
|
||||||
// 第七步:执行违规检测
|
// 第七步:执行违规检测
|
||||||
performViolationDetection(currentActiveObjects);
|
performViolationDetection(currentActiveObjects);
|
||||||
@ -171,25 +179,63 @@ public class DataProcessingService {
|
|||||||
// 第八步:保存无人车数据到数据库
|
// 第八步:保存无人车数据到数据库
|
||||||
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
||||||
|
|
||||||
|
logCycleDiagnostics(currentActiveObjects.size(), collisionManagedObjects.size(),
|
||||||
|
positionDispatchSummary, collisionManagedResult, conflictDetectionSummary);
|
||||||
|
|
||||||
log.info("周期性数据处理完成");
|
log.info("周期性数据处理完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MovingObject> filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
private CollisionManagedObjectsResult filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
||||||
if (activeObjects == null || activeObjects.isEmpty()) {
|
if (activeObjects == null || activeObjects.isEmpty()) {
|
||||||
return List.of();
|
return new CollisionManagedObjectsResult(List.of(), 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MovingObject> filteredObjects = activeObjects.stream()
|
List<MovingObject> filteredObjects = new ArrayList<>();
|
||||||
.filter(Objects::nonNull)
|
int registrationFilteredCount = 0;
|
||||||
.filter(object -> platformRuntimeStateService == null
|
int typeOverrideCount = 0;
|
||||||
|| platformRuntimeStateService.isRegisteredForCollision(object.getObjectId(), object.getObjectType()))
|
|
||||||
.collect(Collectors.toList());
|
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()) {
|
if (filteredObjects.size() != activeObjects.size()) {
|
||||||
log.debug("碰撞检测对象已按注册表过滤: 原始数量={}, 过滤后数量={}",
|
log.debug("碰撞检测对象已按注册表过滤: 原始数量={}, 过滤后数量={}",
|
||||||
activeObjects.size(), filteredObjects.size());
|
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位置更新消息
|
* 发送WebSocket位置更新消息
|
||||||
*/
|
*/
|
||||||
private void sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
|
private PositionDispatchSummary sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
|
||||||
if (activeObjects.isEmpty()) {
|
if (activeObjects.isEmpty()) {
|
||||||
log.debug("没有活跃对象,跳过位置更新消息发送");
|
log.debug("没有活跃对象,跳过位置更新消息发送");
|
||||||
return;
|
return new PositionDispatchSummary(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
log.debug("发送位置更新消息,对象数量: {}", activeObjects.size());
|
log.debug("发送位置更新消息,对象数量: {}", activeObjects.size());
|
||||||
|
int publishedCount = 0;
|
||||||
|
int duplicateSampleSkippedCount = 0;
|
||||||
|
Set<String> currentObjectIds = new HashSet<>();
|
||||||
|
|
||||||
for (MovingObject movingObject : activeObjects) {
|
for (MovingObject movingObject : activeObjects) {
|
||||||
try {
|
try {
|
||||||
|
currentObjectIds.add(movingObject.getObjectId());
|
||||||
// 创建位置更新消息负载
|
// 创建位置更新消息负载
|
||||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||||
.latitude(movingObject.getCurrentPosition().getY())
|
.latitude(movingObject.getCurrentPosition().getY())
|
||||||
@ -261,6 +311,11 @@ public class DataProcessingService {
|
|||||||
? movingObject.getSourceTimestampMs()
|
? movingObject.getSourceTimestampMs()
|
||||||
: currentTime;
|
: currentTime;
|
||||||
|
|
||||||
|
if (!shouldPublishPositionUpdate(movingObject.getObjectId(), payloadTimestamp)) {
|
||||||
|
duplicateSampleSkippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||||
.objectId(movingObject.getObjectId())
|
.objectId(movingObject.getObjectId())
|
||||||
.objectType(movingObject.getObjectType().name())
|
.objectType(movingObject.getObjectType().name())
|
||||||
@ -272,6 +327,7 @@ public class DataProcessingService {
|
|||||||
|
|
||||||
// 发布WebSocket事件
|
// 发布WebSocket事件
|
||||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||||
|
publishedCount++;
|
||||||
|
|
||||||
log.debug("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
|
log.debug("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
|
||||||
movingObject.getObjectId(),
|
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) {
|
private List<MovingObject> createProcessingSnapshot(java.util.Collection<MovingObject> sourceObjects) {
|
||||||
@ -321,6 +435,18 @@ public class DataProcessingService {
|
|||||||
return Math.round(value * 100.0) / 100.0;
|
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);
|
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
|
||||||
|
|
||||||
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
|
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
|
||||||
// 可选:简化路径以减少冗余点(容差:1米)
|
|
||||||
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
|
|
||||||
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
|
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
|
||||||
lineStringSegments.size(), mergedGeometry.getNumPoints());
|
lineStringSegments.size(), mergedGeometry.getNumPoints());
|
||||||
} else {
|
} 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 final VehicleCommandService vehicleCommandService;
|
||||||
|
|
||||||
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
||||||
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
|
||||||
|
|
||||||
// Strong accuracy guards
|
// Strong accuracy guards
|
||||||
private static final double MIN_FORWARD_DISTANCE_METERS = 3.0;
|
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_ALIGNMENT_MIN_COS = 0.0;
|
||||||
private static final double HEADING_CHECK_MIN_SPEED_KPH = 3.0;
|
private static final double HEADING_CHECK_MIN_SPEED_KPH = 3.0;
|
||||||
private static final double MIN_EFFECTIVE_SPEED_KPH = 1.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());
|
log.debug("Starting path conflict detection for {} active objects", activeObjects.size());
|
||||||
|
|
||||||
List<ConflictAlertEvent> detectedAlertEvents = new ArrayList<>();
|
List<ConflictAlertEvent> detectedAlertEvents = new ArrayList<>();
|
||||||
|
DetectionCounters counters = new DetectionCounters();
|
||||||
|
|
||||||
for (int i = 0; i < activeObjects.size(); i++) {
|
for (int i = 0; i < activeObjects.size(); i++) {
|
||||||
for (int j = i + 1; j < activeObjects.size(); j++) {
|
for (int j = i + 1; j < activeObjects.size(); j++) {
|
||||||
|
counters.totalPairs++;
|
||||||
MovingObject obj1 = activeObjects.get(i);
|
MovingObject obj1 = activeObjects.get(i);
|
||||||
MovingObject obj2 = activeObjects.get(j);
|
MovingObject obj2 = activeObjects.get(j);
|
||||||
Optional<ConflictAlertEvent> alertEventOptional = detectConflictBetweenObjects(obj1, obj2);
|
PairDetectionResult detectionResult = detectConflictBetweenObjects(obj1, obj2);
|
||||||
alertEventOptional.ifPresent(detectedAlertEvents::add);
|
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());
|
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())) {
|
if (!isSupportedConflictPair(obj1.getObjectType(), obj2.getObjectType())) {
|
||||||
return Optional.empty();
|
return PairDetectionResult.of(PairOutcomeReason.UNSUPPORTED_PAIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (obj1.getCurrentPosition() == null || obj2.getCurrentPosition() == null) {
|
if (obj1.getCurrentPosition() == null || obj2.getCurrentPosition() == null) {
|
||||||
return Optional.empty();
|
return PairDetectionResult.of(PairOutcomeReason.MISSING_POSITION);
|
||||||
}
|
}
|
||||||
|
|
||||||
TransportRoute route1 = getObjectRoute(obj1);
|
TransportRoute route1 = getObjectRoute(obj1);
|
||||||
TransportRoute route2 = getObjectRoute(obj2);
|
TransportRoute route2 = getObjectRoute(obj2);
|
||||||
|
|
||||||
if (route1 == null || route2 == null || route1.getRouteGeometry() == null || route2.getRouteGeometry() == null) {
|
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 localRoute1 = toLocalRoute(route1);
|
||||||
LocalRoute localRoute2 = toLocalRoute(route2);
|
LocalRoute localRoute2 = toLocalRoute(route2);
|
||||||
if (localRoute1 == null || localRoute2 == null) {
|
if (localRoute1 == null || localRoute2 == null) {
|
||||||
return Optional.empty();
|
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Point localObjPos1 = toLocalPoint(obj1.getCurrentPosition());
|
Point localObjPos1 = toLocalPoint(obj1.getCurrentPosition());
|
||||||
Point localObjPos2 = toLocalPoint(obj2.getCurrentPosition());
|
Point localObjPos2 = toLocalPoint(obj2.getCurrentPosition());
|
||||||
if (localObjPos1 == null || localObjPos2 == null) {
|
if (localObjPos1 == null || localObjPos2 == null) {
|
||||||
return Optional.empty();
|
return PairDetectionResult.of(PairOutcomeReason.MISSING_POSITION);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectedPosition projected1 = projectToRoute(localObjPos1, localRoute1.localLine);
|
ProjectedPosition projected1 = projectToRoute(localObjPos1, localRoute1.localLine);
|
||||||
ProjectedPosition projected2 = projectToRoute(localObjPos2, localRoute2.localLine);
|
ProjectedPosition projected2 = projectToRoute(localObjPos2, localRoute2.localLine);
|
||||||
if (projected1 == null || projected2 == null) {
|
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) {
|
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());
|
double speed1Kph = normalizeSpeedKph(obj1.getCurrentSpeed());
|
||||||
@ -126,10 +133,17 @@ public class PathConflictDetectionService {
|
|||||||
|
|
||||||
// Ignore pairs that are effectively static.
|
// Ignore pairs that are effectively static.
|
||||||
if (speed1Kph < MIN_EFFECTIVE_SPEED_KPH || speed2Kph < MIN_EFFECTIVE_SPEED_KPH) {
|
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);
|
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) {
|
for (Point intersectionPoint : intersectionPoints) {
|
||||||
Point localIntersection = toLocalPoint(intersectionPoint);
|
Point localIntersection = toLocalPoint(intersectionPoint);
|
||||||
if (localIntersection == null) {
|
if (localIntersection == null) {
|
||||||
@ -147,12 +161,14 @@ public class PathConflictDetectionService {
|
|||||||
|
|
||||||
// Only future conflicts are valid.
|
// Only future conflicts are valid.
|
||||||
if (forwardDistance1 <= MIN_FORWARD_DISTANCE_METERS || forwardDistance2 <= MIN_FORWARD_DISTANCE_METERS) {
|
if (forwardDistance1 <= MIN_FORWARD_DISTANCE_METERS || forwardDistance2 <= MIN_FORWARD_DISTANCE_METERS) {
|
||||||
|
intersectionBehind = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure current heading points to the intersection direction when speed is meaningful.
|
// Ensure current heading points to the intersection direction when speed is meaningful.
|
||||||
if (!isHeadingAligned(obj1, localObjPos1, localIntersection, speed1Kph)
|
if (!isHeadingAligned(obj1, localObjPos1, localIntersection, speed1Kph)
|
||||||
|| !isHeadingAligned(obj2, localObjPos2, localIntersection, speed2Kph)) {
|
|| !isHeadingAligned(obj2, localObjPos2, localIntersection, speed2Kph)) {
|
||||||
|
headingMismatch = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,50 +176,75 @@ public class PathConflictDetectionService {
|
|||||||
obj1, obj2, forwardDistance1, forwardDistance2, speed1Kph, speed2Kph);
|
obj1, obj2, forwardDistance1, forwardDistance2, speed1Kph, speed2Kph);
|
||||||
|
|
||||||
if (result != null && isSignificantConflict(result)) {
|
if (result != null && isSignificantConflict(result)) {
|
||||||
String description = generateConflictDescription(obj1, obj2, route1, route2);
|
return PairDetectionResult.of(buildConflictAlertEvent(obj1, obj2, route1, route2, intersectionPoint, result));
|
||||||
|
|
||||||
ConflictAlertLog alertLog = ConflictAlertLog.builder()
|
|
||||||
.alertType(result.getAlertType().orElse(null))
|
|
||||||
.alertLevel(result.getAlertLevel().orElse(null))
|
|
||||||
.alertMessage(description)
|
|
||||||
.object1Distance(result.getDistance1())
|
|
||||||
.object2Distance(result.getDistance2())
|
|
||||||
.minimumDistance(Math.min(result.getDistance1(), result.getDistance2()))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
conflictAlertLogRepository.save(alertLog);
|
|
||||||
|
|
||||||
return Optional.of(ConflictAlertEvent.builder()
|
|
||||||
.conflictId(Optional.of(alertLog.getId()))
|
|
||||||
.alertType(result.getAlertType())
|
|
||||||
.alertLevel(result.getAlertLevel())
|
|
||||||
.message(description)
|
|
||||||
.object1Name(obj1.getObjectName())
|
|
||||||
.object1Type(obj1.getObjectType())
|
|
||||||
.object2Name(obj2.getObjectName())
|
|
||||||
.object2Type(obj2.getObjectType())
|
|
||||||
.conflictPoint(intersectionPoint)
|
|
||||||
.object1Distance(result.getDistance1())
|
|
||||||
.object2Distance(result.getDistance2())
|
|
||||||
.estimatedTimeToConflictObj1(result.getTimeToConflict1())
|
|
||||||
.estimatedTimeToConflictObj2(result.getTimeToConflict2())
|
|
||||||
.timeGapSeconds(result.getTimeGap())
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("Error detecting conflict between {} and {}", obj1.getObjectName(), obj2.getObjectName(), e);
|
log.error("Error detecting conflict between {} and {}", obj1.getObjectName(), obj2.getObjectName(), e);
|
||||||
|
return PairDetectionResult.of(PairOutcomeReason.ERROR);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Optional.empty();
|
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()
|
||||||
|
.alertType(result.getAlertType().orElse(null))
|
||||||
|
.alertLevel(result.getAlertLevel().orElse(null))
|
||||||
|
.alertMessage(description)
|
||||||
|
.object1Distance(result.getDistance1())
|
||||||
|
.object2Distance(result.getDistance2())
|
||||||
|
.minimumDistance(Math.min(result.getDistance1(), result.getDistance2()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ConflictAlertLog savedAlertLog = conflictAlertLogRepository.save(alertLog);
|
||||||
|
|
||||||
|
return ConflictAlertEvent.builder()
|
||||||
|
.conflictId(Optional.ofNullable(savedAlertLog.getId()))
|
||||||
|
.alertType(result.getAlertType())
|
||||||
|
.alertLevel(result.getAlertLevel())
|
||||||
|
.message(description)
|
||||||
|
.object1Name(obj1.getObjectName())
|
||||||
|
.object1Type(obj1.getObjectType())
|
||||||
|
.object2Name(obj2.getObjectName())
|
||||||
|
.object2Type(obj2.getObjectType())
|
||||||
|
.conflictPoint(intersectionPoint)
|
||||||
|
.object1Distance(result.getDistance1())
|
||||||
|
.object2Distance(result.getDistance2())
|
||||||
|
.estimatedTimeToConflictObj1(result.getTimeToConflict1())
|
||||||
|
.estimatedTimeToConflictObj2(result.getTimeToConflict2())
|
||||||
|
.timeGapSeconds(result.getTimeGap())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TransportRoute getObjectRoute(MovingObject obj) {
|
private TransportRoute getObjectRoute(MovingObject obj) {
|
||||||
Optional<ObjectRouteAssignment> assignmentOptional = objectRouteAssignmentRepository
|
ObjectRouteAssignment.ObjectType objectType = ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name());
|
||||||
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
Optional<ObjectRouteAssignment> assignmentOptional = findLatestAssignment(obj.getObjectId(), objectType);
|
||||||
obj.getObjectName(),
|
|
||||||
ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name())
|
if (assignmentOptional.isEmpty()
|
||||||
);
|
&& obj.getObjectName() != null
|
||||||
|
&& !obj.getObjectName().isBlank()
|
||||||
|
&& !obj.getObjectName().equals(obj.getObjectId())) {
|
||||||
|
assignmentOptional = findLatestAssignment(obj.getObjectName(), objectType);
|
||||||
|
}
|
||||||
|
|
||||||
return assignmentOptional
|
return assignmentOptional
|
||||||
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
|
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
|
||||||
@ -211,6 +252,13 @@ public class PathConflictDetectionService {
|
|||||||
.orElse(null);
|
.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) {
|
private List<Point> calculateRouteIntersections(TransportRoute route1, TransportRoute route2) {
|
||||||
List<Point> intersections = new ArrayList<>();
|
List<Point> intersections = new ArrayList<>();
|
||||||
|
|
||||||
@ -260,15 +308,13 @@ public class PathConflictDetectionService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
|
|
||||||
|
|
||||||
Optional<ConflictAlertLog.AlertLevel> alertLevelOptional = evaluateAlertLevel(
|
Optional<ConflictAlertLog.AlertLevel> alertLevelOptional = evaluateAlertLevel(
|
||||||
distance1,
|
distance1,
|
||||||
obj1.getObjectType(),
|
obj1.getObjectType(),
|
||||||
distance2,
|
distance2,
|
||||||
obj2.getObjectType(),
|
obj2.getObjectType()
|
||||||
timeGap
|
|
||||||
);
|
);
|
||||||
|
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
|
||||||
Optional<ConflictAlertLog.AlertType> alertTypeOptional = alertLevelOptional.isPresent()
|
Optional<ConflictAlertLog.AlertType> alertTypeOptional = alertLevelOptional.isPresent()
|
||||||
? evaluateAlertType(alertLevelOptional.get())
|
? evaluateAlertType(alertLevelOptional.get())
|
||||||
: Optional.empty();
|
: Optional.empty();
|
||||||
@ -294,27 +340,34 @@ public class PathConflictDetectionService {
|
|||||||
double distance1,
|
double distance1,
|
||||||
MovingObjectType obj1Type,
|
MovingObjectType obj1Type,
|
||||||
double distance2,
|
double distance2,
|
||||||
MovingObjectType obj2Type,
|
MovingObjectType obj2Type) {
|
||||||
double timeGap) {
|
|
||||||
if (!isSupportedConflictPair(obj1Type, obj2Type)) {
|
if (!isSupportedConflictPair(obj1Type, obj2Type)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
double distanceToEvaluate;
|
Double vehicleDistance = null;
|
||||||
|
Double aircraftDistance = null;
|
||||||
if (obj1Type == MovingObjectType.AIRCRAFT) {
|
if (obj1Type == MovingObjectType.AIRCRAFT) {
|
||||||
distanceToEvaluate = distance2;
|
aircraftDistance = distance1;
|
||||||
|
vehicleDistance = distance2;
|
||||||
} else if (obj2Type == MovingObjectType.AIRCRAFT) {
|
} else if (obj2Type == MovingObjectType.AIRCRAFT) {
|
||||||
distanceToEvaluate = distance1;
|
aircraftDistance = distance2;
|
||||||
} else {
|
vehicleDistance = distance1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vehicleDistance == null || aircraftDistance == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
double alertDistanceThreshold = platformRuntimeStateService.getRunwayAlertZoneRadiusAircraft();
|
double vehicleDistanceThreshold = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle();
|
||||||
double warningDistanceThreshold = platformRuntimeStateService.getRunwayWarningZoneRadiusAircraft();
|
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);
|
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);
|
return Optional.of(ConflictAlertLog.AlertLevel.WARNING);
|
||||||
} else {
|
} else {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@ -484,6 +537,95 @@ public class PathConflictDetectionService {
|
|||||||
|
|
||||||
private record ProjectedPosition(double distanceAlongRouteMeters, double lateralDistanceMeters) {}
|
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 static class ConflictCalculationResult {
|
||||||
private final double distance1;
|
private final double distance1;
|
||||||
private final double distance2;
|
private final double distance2;
|
||||||
|
|||||||
@ -39,6 +39,7 @@ public class VehicleCommandService {
|
|||||||
|
|
||||||
private final Map<String, ActiveVehicleCommandState> activeCommandStates = new ConcurrentHashMap<>();
|
private final Map<String, ActiveVehicleCommandState> activeCommandStates = new ConcurrentHashMap<>();
|
||||||
private final AtomicLong transIdSequence = new AtomicLong();
|
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:}")
|
@Value("${data.collector.vehicle-api.base-url:}")
|
||||||
private String vehicleApiBaseUrl;
|
private String vehicleApiBaseUrl;
|
||||||
@ -213,12 +214,6 @@ public class VehicleCommandService {
|
|||||||
VehicleConflictCommand command,
|
VehicleConflictCommand command,
|
||||||
CommandType commandType,
|
CommandType commandType,
|
||||||
CommandReason commandReason) {
|
CommandReason commandReason) {
|
||||||
|
|
||||||
if (!isCommandEndpointConfigured()) {
|
|
||||||
log.warn("Skipping vehicle command because vehicle API endpoint is not configured: vehicleId={}", command.vehicleId());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
|
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
|
||||||
.transId(nextTransId())
|
.transId(nextTransId())
|
||||||
@ -240,7 +235,7 @@ public class VehicleCommandService {
|
|||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.exchange(
|
ResponseEntity<String> response = restTemplate.exchange(
|
||||||
vehicleApiBaseUrl + vehicleCommandEndpoint,
|
getUnifiedVehicleCommandUrl(),
|
||||||
HttpMethod.POST,
|
HttpMethod.POST,
|
||||||
new HttpEntity<>(request, headers),
|
new HttpEntity<>(request, headers),
|
||||||
String.class
|
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) {
|
private JsonNode parseResponseBody(String body) {
|
||||||
if (body == null || body.isBlank()) {
|
if (body == null || body.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
@ -312,11 +341,87 @@ public class VehicleCommandService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCommandEndpointConfigured() {
|
private boolean sendExternalVehicleCommand(
|
||||||
return vehicleApiBaseUrl != null
|
ExternalVehicleCommandRequest request,
|
||||||
&& !vehicleApiBaseUrl.isBlank()
|
String vehicleId,
|
||||||
&& vehicleCommandEndpoint != null
|
CommandType commandType,
|
||||||
&& !vehicleCommandEndpoint.isBlank();
|
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) {
|
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.ObjectRouteAssignmentRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||||
import com.qaup.collision.common.model.MovingObject;
|
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.Coordinate;
|
||||||
import org.locationtech.jts.geom.LineString;
|
import org.locationtech.jts.geom.LineString;
|
||||||
import org.locationtech.jts.geom.PrecisionModel;
|
import org.locationtech.jts.geom.PrecisionModel;
|
||||||
@ -24,15 +28,30 @@ import java.util.Objects;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class PlatformRuntimeStateService {
|
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 ConcurrentHashMap<String, VehicleRegistryType> vehicleTypes = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicLong testSessionSequence = new AtomicLong();
|
||||||
|
private volatile String currentTestSessionId;
|
||||||
|
|
||||||
private volatile double runwayWarningZoneRadiusAircraft;
|
private volatile double runwayWarningZoneRadiusAircraft;
|
||||||
private volatile double runwayAlertZoneRadiusAircraft;
|
private volatile double runwayAlertZoneRadiusAircraft;
|
||||||
|
|
||||||
|
// Threshold used when the vehicle approaches a route crossing point.
|
||||||
private volatile double collisionDivergingReleaseDistanceForVehicle;
|
private volatile double collisionDivergingReleaseDistanceForVehicle;
|
||||||
|
|
||||||
|
// Threshold used when the aircraft approaches a route crossing point.
|
||||||
private volatile double collisionDivergingReleaseDistanceForAircraft;
|
private volatile double collisionDivergingReleaseDistanceForAircraft;
|
||||||
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||||
private static final String DEFAULT_ROUTE_SOURCE = "VehicleRouteIngestion";
|
private static final String DEFAULT_ROUTE_SOURCE = "VehicleRouteIngestion";
|
||||||
@ -44,6 +63,9 @@ public class PlatformRuntimeStateService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private ISysConfigService sysConfigService;
|
||||||
|
|
||||||
public PlatformRuntimeStateService(
|
public PlatformRuntimeStateService(
|
||||||
@Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft,
|
@Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft,
|
||||||
@Value("${qaup.runtime-config.runway.alert-zone-radius.aircraft:100.0}") double runwayAlertZoneRadiusAircraft,
|
@Value("${qaup.runtime-config.runway.alert-zone-radius.aircraft:100.0}") double runwayAlertZoneRadiusAircraft,
|
||||||
@ -55,9 +77,27 @@ public class PlatformRuntimeStateService {
|
|||||||
this.collisionDivergingReleaseDistanceForAircraft = collisionDivergingReleaseDistance;
|
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) {
|
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
|
||||||
Objects.requireNonNull(entries, "entries");
|
Objects.requireNonNull(entries, "entries");
|
||||||
|
|
||||||
|
TreeSet<String> previousVehicleIds = new TreeSet<>(vehicleTypes.keySet());
|
||||||
vehicleTypes.clear();
|
vehicleTypes.clear();
|
||||||
|
|
||||||
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
|
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
|
||||||
@ -65,6 +105,25 @@ public class PlatformRuntimeStateService {
|
|||||||
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
|
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
|
||||||
requestTypeCounts.merge(entry.vehicleType(), 1, Integer::sum);
|
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<>();
|
Set<String> controllableVehicleIds = new TreeSet<>();
|
||||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||||
@ -86,7 +145,14 @@ public class PlatformRuntimeStateService {
|
|||||||
entries.size(),
|
entries.size(),
|
||||||
controllableVehicleIds.size(),
|
controllableVehicleIds.size(),
|
||||||
responseTypeCounts,
|
responseTypeCounts,
|
||||||
new ArrayList<>(controllableVehicleIds));
|
new ArrayList<>(controllableVehicleIds),
|
||||||
|
new ArrayList<>(previousVehicleIds),
|
||||||
|
activeTestSessionId,
|
||||||
|
endedTestSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentTestSessionId() {
|
||||||
|
return currentTestSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getControllableVehicleIds() {
|
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() {
|
public double getRunwayWarningZoneRadiusAircraft() {
|
||||||
return runwayWarningZoneRadiusAircraft;
|
return runwayWarningZoneRadiusAircraft;
|
||||||
}
|
}
|
||||||
@ -150,37 +266,106 @@ public class PlatformRuntimeStateService {
|
|||||||
return oldValue;
|
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() {
|
public double getCollisionDivergingReleaseDistance() {
|
||||||
return collisionDivergingReleaseDistanceForVehicle;
|
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) {
|
public double updateCollisionDivergingReleaseDistance(double newValue) {
|
||||||
double oldValue = collisionDivergingReleaseDistanceForVehicle;
|
double oldValue = collisionDivergingReleaseDistanceForVehicle;
|
||||||
collisionDivergingReleaseDistanceForVehicle = newValue;
|
collisionDivergingReleaseDistanceForVehicle = newValue;
|
||||||
collisionDivergingReleaseDistanceForAircraft = 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;
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the vehicle crossing threshold used by path crossing alerts.
|
||||||
|
*/
|
||||||
public double getCollisionDivergingReleaseDistanceForVehicle() {
|
public double getCollisionDivergingReleaseDistanceForVehicle() {
|
||||||
return collisionDivergingReleaseDistanceForVehicle;
|
return collisionDivergingReleaseDistanceForVehicle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the aircraft crossing threshold used by path crossing alerts.
|
||||||
|
*/
|
||||||
public double getCollisionDivergingReleaseDistanceForAircraft() {
|
public double getCollisionDivergingReleaseDistanceForAircraft() {
|
||||||
return collisionDivergingReleaseDistanceForAircraft;
|
return collisionDivergingReleaseDistanceForAircraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the vehicle crossing threshold used by path crossing alerts.
|
||||||
|
*/
|
||||||
public double updateCollisionDivergingReleaseDistanceForVehicle(double newValue) {
|
public double updateCollisionDivergingReleaseDistanceForVehicle(double newValue) {
|
||||||
double oldValue = collisionDivergingReleaseDistanceForVehicle;
|
double oldValue = collisionDivergingReleaseDistanceForVehicle;
|
||||||
collisionDivergingReleaseDistanceForVehicle = newValue;
|
collisionDivergingReleaseDistanceForVehicle = newValue;
|
||||||
|
persistCrossingThreshold(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE, CONFIG_NAME_CROSSING_VEHICLE_DISTANCE, newValue, CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE);
|
||||||
return oldValue;
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the aircraft crossing threshold used by path crossing alerts.
|
||||||
|
*/
|
||||||
public double updateCollisionDivergingReleaseDistanceForAircraft(double newValue) {
|
public double updateCollisionDivergingReleaseDistanceForAircraft(double newValue) {
|
||||||
double oldValue = collisionDivergingReleaseDistanceForAircraft;
|
double oldValue = collisionDivergingReleaseDistanceForAircraft;
|
||||||
collisionDivergingReleaseDistanceForAircraft = newValue;
|
collisionDivergingReleaseDistanceForAircraft = newValue;
|
||||||
|
persistCrossingThreshold(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE, CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE, newValue, CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE);
|
||||||
return oldValue;
|
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
|
@Transactional
|
||||||
public VehicleRouteSubmitResult submitVehicleRoute(
|
public VehicleRouteSubmitResult submitVehicleRoute(
|
||||||
String objectId,
|
String objectId,
|
||||||
@ -277,14 +462,43 @@ public class PlatformRuntimeStateService {
|
|||||||
return "MANUAL_" + routeType.name() + "_" + safeObjectId;
|
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) {
|
private static TransportRoute.RouteType mapToRouteType(ObjectRouteAssignment.ObjectType objectType) {
|
||||||
return switch (objectType) {
|
return switch (objectType) {
|
||||||
|
case AIRCRAFT -> TransportRoute.RouteType.AIRCRAFT;
|
||||||
case UNMANNED_VEHICLE -> TransportRoute.RouteType.UNMANNED_VEHICLE;
|
case UNMANNED_VEHICLE -> TransportRoute.RouteType.UNMANNED_VEHICLE;
|
||||||
case SPECIAL_VEHICLE -> TransportRoute.RouteType.SPECIAL_VEHICLE;
|
case SPECIAL_VEHICLE -> TransportRoute.RouteType.SPECIAL_VEHICLE;
|
||||||
default -> null;
|
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) {
|
public record VehicleRegistryEntry(String vehicleID, VehicleRegistryType vehicleType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +507,10 @@ public class PlatformRuntimeStateService {
|
|||||||
int updated,
|
int updated,
|
||||||
int controllableCount,
|
int controllableCount,
|
||||||
Map<String, Integer> typesCount,
|
Map<String, Integer> typesCount,
|
||||||
List<String> controllableVehicleIDs) {
|
List<String> controllableVehicleIDs,
|
||||||
|
List<String> removedVehicleIDs,
|
||||||
|
String testSessionId,
|
||||||
|
String endedTestSessionId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record VehicleRouteSubmitResult(
|
public record VehicleRouteSubmitResult(
|
||||||
@ -305,6 +522,26 @@ public class PlatformRuntimeStateService {
|
|||||||
String routeType) {
|
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 {
|
public enum VehicleRegistryType {
|
||||||
WUREN,
|
WUREN,
|
||||||
TEQIN,
|
TEQIN,
|
||||||
|
|||||||
@ -429,161 +429,14 @@ public class WebSocketMessageBroadcaster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long payloadTimestamp = payload.getTimestamp() != null ? payload.getTimestamp() : System.currentTimeMillis();
|
long payloadTimestamp = payload.getTimestamp() != null ? payload.getTimestamp() : System.currentTimeMillis();
|
||||||
long filterTimestampMs = payloadTimestamp;
|
|
||||||
PositionTrackState state = positionTrackStates.computeIfAbsent(payload.getObjectId(), k -> new PositionTrackState());
|
PositionTrackState state = positionTrackStates.computeIfAbsent(payload.getObjectId(), k -> new PositionTrackState());
|
||||||
|
|
||||||
double filteredLatitude = latitude;
|
|
||||||
double filteredLongitude = longitude;
|
|
||||||
|
|
||||||
synchronized (state) {
|
synchronized (state) {
|
||||||
if (!state.initialized) {
|
initializeTrackState(state, latitude, longitude, payloadTimestamp);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PositionUpdatePayload.Position filteredPosition = PositionUpdatePayload.Position.builder()
|
PositionUpdatePayload.Position filteredPosition = PositionUpdatePayload.Position.builder()
|
||||||
.latitude(filteredLatitude)
|
.latitude(latitude)
|
||||||
.longitude(filteredLongitude)
|
.longitude(longitude)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return PositionUpdatePayload.builder()
|
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.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
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.request.MockMvcRequestBuilders.post;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
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 {
|
class PlatformIntegrationControllerTest {
|
||||||
|
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
private PlatformRuntimeStateService runtimeStateService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
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 =
|
PlatformIntegrationController controller =
|
||||||
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
|
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
|
||||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||||
@ -40,6 +50,7 @@ class PlatformIntegrationControllerTest {
|
|||||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(jsonPath("$.status").value("success"))
|
.andExpect(jsonPath("$.status").value("success"))
|
||||||
.andExpect(jsonPath("$.updated").value(3))
|
.andExpect(jsonPath("$.updated").value(3))
|
||||||
|
.andExpect(jsonPath("$.testSessionId").isNotEmpty())
|
||||||
.andExpect(jsonPath("$.controllableCount").value(2))
|
.andExpect(jsonPath("$.controllableCount").value(2))
|
||||||
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
|
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
|
||||||
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
|
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
|
||||||
@ -119,6 +130,7 @@ class PlatformIntegrationControllerTest {
|
|||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"vehicleDistance\":500}"))
|
.content("{\"vehicleDistance\":500}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.field").value("crossing_distance_threshold"))
|
||||||
.andExpect(jsonPath("$.oldVehicleDistance").value(40.0))
|
.andExpect(jsonPath("$.oldVehicleDistance").value(40.0))
|
||||||
.andExpect(jsonPath("$.newVehicleDistance").value(500.0));
|
.andExpect(jsonPath("$.newVehicleDistance").value(500.0));
|
||||||
}
|
}
|
||||||
@ -129,6 +141,7 @@ class PlatformIntegrationControllerTest {
|
|||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"vehicleDistance\":500,\"aircraftDistance\":600}"))
|
.content("{\"vehicleDistance\":500,\"aircraftDistance\":600}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.field").value("crossing_distance_threshold"))
|
||||||
.andExpect(jsonPath("$.newVehicleDistance").value(500.0))
|
.andExpect(jsonPath("$.newVehicleDistance").value(500.0))
|
||||||
.andExpect(jsonPath("$.newAircraftDistance").value(600.0))
|
.andExpect(jsonPath("$.newAircraftDistance").value(600.0))
|
||||||
.andExpect(jsonPath("$.updatedVehicle").value(true))
|
.andExpect(jsonPath("$.updatedVehicle").value(true))
|
||||||
@ -143,4 +156,51 @@ class PlatformIntegrationControllerTest {
|
|||||||
.andExpect(status().isBadRequest())
|
.andExpect(status().isBadRequest())
|
||||||
.andExpect(jsonPath("$.message").value("Invalid JSON"));
|
.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 java.util.List;
|
||||||
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
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;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@WebMvcTest(controllers = PlatformIntegrationController.class)
|
@WebMvcTest(controllers = PlatformIntegrationController.class)
|
||||||
@ -71,7 +70,10 @@ class PlatformIntegrationSecurityTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
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()))
|
Mockito.when(userDetailsService.loadUserByUsername(Mockito.anyString()))
|
||||||
.thenReturn(User.withUsername("user").password("{noop}pwd").authorities("ROLE_USER").build());
|
.thenReturn(User.withUsername("user").password("{noop}pwd").authorities("ROLE_USER").build());
|
||||||
|
|
||||||
@ -101,6 +103,13 @@ class PlatformIntegrationSecurityTest {
|
|||||||
|
|
||||||
Mockito.when(platformRuntimeStateService.updateCollisionDivergingReleaseDistance(Mockito.anyDouble()))
|
Mockito.when(platformRuntimeStateService.updateCollisionDivergingReleaseDistance(Mockito.anyDouble()))
|
||||||
.thenReturn(40.0);
|
.thenReturn(40.0);
|
||||||
|
Mockito.when(platformRuntimeStateService.getCollisionPreparationStatus())
|
||||||
|
.thenReturn(new PlatformRuntimeStateService.CollisionPreparationStatus(
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
new PlatformRuntimeStateService.CollisionPreparationSummary(0, 0),
|
||||||
|
List.of()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -108,9 +117,9 @@ class PlatformIntegrationSecurityTest {
|
|||||||
mockMvc.perform(post("/config/collision/diverging_release_distance")
|
mockMvc.perform(post("/config/collision/diverging_release_distance")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"value\":50}"))
|
.content("{\"value\":50}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk());
|
||||||
.andExpect(jsonPath("$.status").value("success"))
|
|
||||||
.andExpect(jsonPath("$.old").value(40.0))
|
mockMvc.perform(post("/api/collision/preparation/status"))
|
||||||
.andExpect(jsonPath("$.new").value(50.0));
|
.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)
|
.altitude(0.0)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
when(platformRuntimeStateService.isRegisteredForCollision(eq(registeredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
|
when(platformRuntimeStateService.getRegisteredCollisionObjectType(registeredUvId))
|
||||||
.thenReturn(true);
|
.thenReturn(MovingObjectType.UNMANNED_VEHICLE);
|
||||||
when(platformRuntimeStateService.isRegisteredForCollision(eq(unregisteredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
|
when(platformRuntimeStateService.getRegisteredCollisionObjectType(unregisteredUvId))
|
||||||
.thenReturn(false);
|
.thenReturn(null);
|
||||||
|
|
||||||
dataProcessingService.performPeriodicDataProcessing();
|
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;
|
||||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||||
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
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.ObjectRouteAssignment;
|
||||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@ -50,6 +52,9 @@ class PathConflictDetectionDirectionalTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private VehicleCommandService vehicleCommandService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PathConflictDetectionService pathConflictDetectionService;
|
private PathConflictDetectionService pathConflictDetectionService;
|
||||||
|
|
||||||
@ -130,10 +135,13 @@ class PathConflictDetectionDirectionalTest {
|
|||||||
when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble()))
|
when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble()))
|
||||||
.thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)});
|
.thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)});
|
||||||
|
|
||||||
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
PathConflictDetectionService.ConflictDetectionSummary summary =
|
||||||
|
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
||||||
|
|
||||||
verify(conflictAlertLogRepository, never()).save(any());
|
verify(conflictAlertLogRepository, never()).save(any());
|
||||||
verify(eventPublisher, never()).publishEvent(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
|
@Test
|
||||||
@ -160,9 +168,95 @@ class PathConflictDetectionDirectionalTest {
|
|||||||
.altitude(0.0)
|
.altitude(0.0)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
PathConflictDetectionService.ConflictDetectionSummary summary =
|
||||||
|
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
||||||
|
|
||||||
verify(conflictAlertLogRepository, never()).save(any());
|
verify(conflictAlertLogRepository, never()).save(any());
|
||||||
verify(eventPublisher, never()).publishEvent(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;
|
||||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
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.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 com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -20,12 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
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速度的情况
|
* 测试PathConflictDetectionService处理null速度的情况
|
||||||
@ -33,12 +24,6 @@ import static org.mockito.Mockito.when;
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class PathConflictDetectionServiceNullSpeedTest {
|
class PathConflictDetectionServiceNullSpeedTest {
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private TransportRouteRepository routeRepository;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ConflictAlertLogRepository conflictAlertLogRepository;
|
private ConflictAlertLogRepository conflictAlertLogRepository;
|
||||||
|
|
||||||
@ -84,29 +69,6 @@ class PathConflictDetectionServiceNullSpeedTest {
|
|||||||
.altitude(0.0)
|
.altitude(0.0)
|
||||||
.build();
|
.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
|
// 测试:调用detectPathConflicts不应该抛出NullPointerException
|
||||||
assertDoesNotThrow(() -> {
|
assertDoesNotThrow(() -> {
|
||||||
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));
|
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));
|
||||||
@ -139,29 +101,6 @@ class PathConflictDetectionServiceNullSpeedTest {
|
|||||||
.altitude(0.0)
|
.altitude(0.0)
|
||||||
.build();
|
.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
|
// 测试:调用detectPathConflicts不应该抛出NullPointerException
|
||||||
assertDoesNotThrow(() -> {
|
assertDoesNotThrow(() -> {
|
||||||
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));
|
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.common.model.MovingObject;
|
||||||
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
||||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
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.ConflictAlertLogRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||||
@ -14,12 +16,14 @@ import org.springframework.test.util.ReflectionTestUtils;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class PathConflictDetectionServiceRuntimeConfigTest {
|
class PathConflictDetectionServiceRuntimeConfigTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUseRuntimeThresholdsWhenEvaluatingAlertLevel() {
|
void shouldUseCollisionDistanceThresholdsWhenEvaluatingAlertLevel() {
|
||||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||||
PathConflictDetectionService service = new PathConflictDetectionService(
|
PathConflictDetectionService service = new PathConflictDetectionService(
|
||||||
mock(TransportRouteRepository.class),
|
mock(TransportRouteRepository.class),
|
||||||
@ -36,30 +40,106 @@ class PathConflictDetectionServiceRuntimeConfigTest {
|
|||||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||||
service,
|
service,
|
||||||
"evaluateAlertLevel",
|
"evaluateAlertLevel",
|
||||||
140.0,
|
35.0,
|
||||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||||
500.0,
|
30.0,
|
||||||
MovingObject.MovingObjectType.AIRCRAFT,
|
MovingObject.MovingObjectType.AIRCRAFT
|
||||||
20.0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL), beforeUpdate);
|
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL), beforeUpdate);
|
||||||
|
|
||||||
runtimeStateService.updateRunwayAlertZoneRadiusAircraft(100.0);
|
runtimeStateService.updateCollisionDivergingReleaseDistanceForVehicle(20.0);
|
||||||
runtimeStateService.updateRunwayWarningZoneRadiusAircraft(130.0);
|
runtimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(25.0);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Optional<ConflictAlertLog.AlertLevel> afterUpdate =
|
Optional<ConflictAlertLog.AlertLevel> afterUpdate =
|
||||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||||
service,
|
service,
|
||||||
"evaluateAlertLevel",
|
"evaluateAlertLevel",
|
||||||
140.0,
|
35.0,
|
||||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||||
500.0,
|
30.0,
|
||||||
MovingObject.MovingObjectType.AIRCRAFT,
|
MovingObject.MovingObjectType.AIRCRAFT
|
||||||
20.0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(Optional.empty(), afterUpdate);
|
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.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.test.web.client.MockRestServiceServer;
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
|
|||||||
|
|
||||||
class VehicleCommandServiceTest {
|
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 VehicleCommandService vehicleCommandService;
|
||||||
private MockRestServiceServer server;
|
private MockRestServiceServer server;
|
||||||
@ -43,8 +42,6 @@ class VehicleCommandServiceTest {
|
|||||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
||||||
ReflectionTestUtils.setField(vehicleCommandService, "vehicleApiBaseUrl", "http://vehicle.example");
|
|
||||||
ReflectionTestUtils.setField(vehicleCommandService, "vehicleCommandEndpoint", "/api/VehicleCommandInfo");
|
|
||||||
geometryFactory = new GeometryFactory();
|
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;
|
package com.qaup.collision.service;
|
||||||
|
|
||||||
import com.qaup.collision.common.model.MovingObject;
|
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.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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 {
|
class PlatformRuntimeStateServiceTest {
|
||||||
|
|
||||||
@ -55,6 +63,33 @@ class PlatformRuntimeStateServiceTest {
|
|||||||
assertEquals(55.0, service.getCollisionDivergingReleaseDistance());
|
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
|
@Test
|
||||||
void shouldOnlyAllowRegisteredCollisionTypesToParticipate() {
|
void shouldOnlyAllowRegisteredCollisionTypesToParticipate() {
|
||||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
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.getControllableVehicleIds());
|
||||||
assertEquals(List.of("QN001", "TQ001"), service.getTrafficLightRecipientVehicleIds());
|
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