Clean up collision flow changes

This commit is contained in:
sladro 2026-04-27 09:24:20 +08:00
parent 20af9b8288
commit 1cac6fb811
49 changed files with 3615 additions and 409 deletions

4
.gitignore vendored
View File

@ -55,6 +55,8 @@ Users/
qaup-deploy/
deploy/offline_packages/
*.lck
mqtt协议验证/账户信息.txt
######################################################################
# Python
@ -95,4 +97,4 @@ yarn-error.log*
.npm
.yarn-integrity
dist/
.cache/
.cache/

View File

@ -6,3 +6,9 @@
这是开发环境运行环境在centos7的容器里分别为
qaup-app
qaup-redis
### 红绿灯接入说明
- 正式环境中qaup-app 的红绿灯接入方式为 MQTT不是 HTTP。
- 正式环境配置中 `traffic.light.tcp.enabled=false``traffic.light.mqtt.enabled=true`。
- MQTT Broker 地址示例:`tcp://10.64.58.228:8082`,订阅 Topic 示例:`cusc/v2/SF053/QingDrsu001/data`。
- 注意区分 MQTT Broker 的 TCP 连接地址与业务 TCP 直连服务,前者不是“红绿灯走 TCP 直连”的意思。

View 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`.

View 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 "$@"

View File

@ -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
View File

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
mqtt协议验证/demo/.gitignore vendored Normal file
View 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/

View 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
View 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
View 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"

View 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>

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View 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}

View File

@ -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() {
}
}

View 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()

View File

@ -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);
}
}

View File

@ -67,6 +67,13 @@ public class PlatformIntegrationController {
payload.put("controllableCount", result.controllableCount());
payload.put("typesCount", result.typesCount());
payload.put("controllableVehicleIDs", result.controllableVehicleIDs());
payload.put("removedVehicleIDs", result.removedVehicleIDs());
if (result.testSessionId() != null) {
payload.put("testSessionId", result.testSessionId());
}
if (result.endedTestSessionId() != null) {
payload.put("endedTestSessionId", result.endedTestSessionId());
}
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
} catch (JsonProcessingException e) {
return invalidJson(e);
@ -75,18 +82,43 @@ public class PlatformIntegrationController {
}
}
/**
* Updates the legacy aircraft warning radius used by older runway-based flows.
* This endpoint is no longer the primary threshold source for path crossing alerts.
*/
@PostMapping(path = "/config/runway/warning_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> updateRunwayWarningZoneRadiusAircraft(@RequestBody String requestBody) {
return updateNumericConfig(requestBody, "runway", "warning_zone_radius.aircraft",
platformRuntimeStateService::updateRunwayWarningZoneRadiusAircraft);
}
/**
* Updates the legacy aircraft alert radius used by older runway-based flows.
* This endpoint is no longer the primary threshold source for path crossing alerts.
*/
@PostMapping(path = "/config/runway/alert_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> updateRunwayAlertZoneRadiusAircraft(@RequestBody String requestBody) {
return updateNumericConfig(requestBody, "runway", "alert_zone_radius.aircraft",
platformRuntimeStateService::updateRunwayAlertZoneRadiusAircraft);
}
/**
* Updates path crossing distance thresholds.
*
* Supported payloads:
* - {"value": 40}
* Applies the same threshold to both vehicle and aircraft.
* - {"vehicleDistance": 50, "aircraftDistance": 80}
* Updates each threshold independently.
*
* Current semantics:
* - vehicleDistance: vehicle distance to the crossing point
* - aircraftDistance: aircraft distance to the crossing point
*
* Alert evaluation:
* - both sides inside their thresholds -> alert
* - only one side inside its threshold -> warning
*/
@PostMapping(path = "/config/collision/diverging_release_distance", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> updateCollisionDivergingReleaseDistance(@RequestBody String requestBody) {
return updateCollisionDistanceConfig(requestBody);
@ -169,6 +201,30 @@ public class PlatformIntegrationController {
}
}
@PostMapping(path = "/api/collision/preparation/status")
public ResponseEntity<Map<String, Object>> collisionPreparationStatus() {
try {
PlatformRuntimeStateService.CollisionPreparationStatus result =
platformRuntimeStateService.getCollisionPreparationStatus();
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("status", "success");
if (result.testSessionId() != null) {
payload.put("testSessionId", result.testSessionId());
}
payload.put("ready", result.ready());
payload.put("summary", result.summary());
payload.put("objects", result.objects());
Map<String, Object> thresholds = new LinkedHashMap<>();
thresholds.put("vehicleDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
thresholds.put("aircraftDistance", platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft());
payload.put("thresholds", thresholds);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
} catch (Exception e) {
return internalError(e);
}
}
private void validateVehicleRegistryItem(
JsonNode item,
List<String> errors,
@ -318,7 +374,7 @@ public class PlatformIntegrationController {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("status", "success");
payload.put("area", "collision");
payload.put("field", "collision.diverging_release_distance");
payload.put("field", "crossing_distance_threshold");
payload.put("old", oldValue);
payload.put("new", newValue);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
@ -336,7 +392,7 @@ public class PlatformIntegrationController {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("status", "success");
payload.put("area", "collision");
payload.put("field", "collision.diverging_release_distance");
payload.put("field", "crossing_distance_threshold");
payload.put("oldVehicleDistance", oldVehicleDistance);
payload.put("newVehicleDistance", newVehicleDistance != null ? newVehicleDistance : platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle());
payload.put("oldAircraftDistance", oldAircraftDistance);
@ -357,6 +413,7 @@ public class PlatformIntegrationController {
}
String normalized = objectTypeValue.trim().toUpperCase(Locale.ROOT);
return switch (normalized) {
case "AIRCRAFT", "HANGKONG", "AC" -> ObjectRouteAssignment.ObjectType.AIRCRAFT;
case "UNMANNED_VEHICLE", "UNMANNED", "UV", "WUREN" -> ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE;
case "SPECIAL_VEHICLE", "SPECIAL", "TEQIN", "SPECIALVEHICLE" -> ObjectRouteAssignment.ObjectType.SPECIAL_VEHICLE;
default -> null;

View File

@ -2,8 +2,8 @@ package com.qaup.collision.datacollector.server;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qaup.collision.pathconflict.service.VehicleCommandService;
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
import com.qaup.collision.websocket.handler.VehicleCommandInfoWebSocketHandler;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.springframework.beans.factory.annotation.Autowired;
@ -40,7 +40,7 @@ public class TrafficLightMqttSubscriber {
private final MqttConnectOptions mqttConnectOptions;
private final CollisionWebSocketHandler collisionWebSocketHandler;
private final VehicleCommandInfoWebSocketHandler vehicleCommandInfoWebSocketHandler;
private final VehicleCommandService vehicleCommandService;
private final ObjectMapper objectMapper;
private MqttClient mqttClient;
@ -55,17 +55,17 @@ public class TrafficLightMqttSubscriber {
*
* @param mqttConnectOptions MQTT 连接选项
* @param collisionWebSocketHandler WebSocket 处理器用于向前端广播消息
* @param vehicleCommandInfoWebSocketHandler 车辆控制指令 WebSocket 处理器
* @param vehicleCommandService 统一车辆控制服务
* @param objectMapper JSON 解析器
*/
@Autowired
public TrafficLightMqttSubscriber(MqttConnectOptions mqttConnectOptions,
CollisionWebSocketHandler collisionWebSocketHandler,
VehicleCommandInfoWebSocketHandler vehicleCommandInfoWebSocketHandler,
VehicleCommandService vehicleCommandService,
ObjectMapper objectMapper) {
this.mqttConnectOptions = mqttConnectOptions;
this.collisionWebSocketHandler = collisionWebSocketHandler;
this.vehicleCommandInfoWebSocketHandler = vehicleCommandInfoWebSocketHandler;
this.vehicleCommandService = vehicleCommandService;
this.objectMapper = objectMapper;
}
@ -104,59 +104,7 @@ public class TrafficLightMqttSubscriber {
public void messageArrived(String topic, MqttMessage message) {
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
totalMessages.incrementAndGet();
log.info("🚦 [MQTT] 收到红绿灯消息");
log.info("🚦 [MQTT] Topic: {}", topic);
log.info("🚦 [MQTT] Payload: {}", payload);
// 通过 WebSocket 广播消息给前端
try {
String websocketMessage = String.format(
"{\"type\":\"trafficLight\",\"source\":\"mqtt\",\"topic\":\"%s\",\"payload\":%s,\"timestamp\":%d}",
topic, payload, System.currentTimeMillis()
);
collisionWebSocketHandler.broadcastMessage(websocketMessage);
log.debug("🚦 [MQTT] 消息已通过 WebSocket 广播到前端");
} catch (Exception e) {
log.error("🚦 [MQTT] WebSocket 广播失败: {}", e.getMessage(), e);
}
// 解析 phaseColor 并发送车辆控制指令
try {
JsonNode jsonNode = objectMapper.readTree(payload);
// serviceData.phases 数组中获取 phaseColor
JsonNode serviceDataNode = jsonNode.get("serviceData");
if (serviceDataNode != null) {
JsonNode phasesNode = serviceDataNode.get("phases");
if (phasesNode != null && phasesNode.isArray() && phasesNode.size() > 0) {
// 获取第一个相位的 phaseColor
JsonNode firstPhase = phasesNode.get(0);
JsonNode phaseColorNode = firstPhase.get("phaseColor");
if (phaseColorNode != null && phaseColorNode.isInt()) {
int phaseColor = phaseColorNode.asInt();
log.info("🚦 [MQTT] 解析到 phaseColor: {}", phaseColor);
// phaseColor 1 时发送 GREEN 3 时发送 RED
if (phaseColor == 1 || phaseColor == 3) {
vehicleCommandInfoWebSocketHandler.sendSignalState(phaseColor);
log.info("🚦 [MQTT] 已发送车辆控制指令: phaseColor={}", phaseColor);
} else {
log.warn("🚦 [MQTT] 未知的 phaseColor 值: {},仅支持 1 (GREEN) 或 3 (RED)", phaseColor);
}
} else {
log.warn("🚦 [MQTT] phases[0] 中未找到 phaseColor 字段或格式不正确");
}
} else {
log.warn("🚦 [MQTT] serviceData 中未找到 phases 数组或数组为空");
}
} else {
log.warn("🚦 [MQTT] 消息中未找到 serviceData 字段");
}
} catch (Exception e) {
log.error("🚦 [MQTT] 解析 phaseColor 或发送控制指令失败: {}", e.getMessage(), e);
}
handleTrafficLightMessage(topic, payload);
}
@Override
@ -217,4 +165,75 @@ public class TrafficLightMqttSubscriber {
return String.format("MQTT订阅状态 - 连接:%s, 总消息:%d, 错误:%d",
connected.get() ? "" : "", totalMessages.get(), errorCount.get());
}
}
void handleTrafficLightMessage(String topic, String payload) {
log.info("🚦 [MQTT] 收到红绿灯消息");
log.info("🚦 [MQTT] Topic: {}", topic);
log.info("🚦 [MQTT] Payload: {}", payload);
try {
String websocketMessage = String.format(
"{\"type\":\"trafficLight\",\"source\":\"mqtt\",\"topic\":\"%s\",\"payload\":%s,\"timestamp\":%d}",
topic, payload, System.currentTimeMillis()
);
collisionWebSocketHandler.broadcastMessage(websocketMessage);
log.debug("🚦 [MQTT] 消息已通过 WebSocket 广播到前端");
} catch (Exception e) {
log.error("🚦 [MQTT] WebSocket 广播失败: {}", e.getMessage(), e);
}
try {
JsonNode jsonNode = objectMapper.readTree(payload);
JsonNode serviceDataNode = jsonNode.get("serviceData");
if (serviceDataNode == null) {
log.warn("🚦 [MQTT] 消息中未找到 serviceData 字段");
return;
}
JsonNode phasesNode = serviceDataNode.get("phases");
if (phasesNode == null || !phasesNode.isArray() || phasesNode.isEmpty()) {
log.warn("🚦 [MQTT] serviceData 中未找到 phases 数组或数组为空");
return;
}
JsonNode phaseColorNode = phasesNode.get(0).get("phaseColor");
if (phaseColorNode == null || !phaseColorNode.isInt()) {
log.warn("🚦 [MQTT] phases[0] 中未找到 phaseColor 字段或格式不正确");
return;
}
int phaseColor = phaseColorNode.asInt();
log.info("🚦 [MQTT] 解析到 phaseColor: {}", phaseColor);
if (phaseColor != 1 && phaseColor != 2 && phaseColor != 3) {
log.warn("🚦 [MQTT] 未知的 phaseColor 值: {},仅支持 1 (GREEN) / 2 (YELLOW) / 3 (RED)", phaseColor);
return;
}
String intersectionId = extractIntersectionId(serviceDataNode);
vehicleCommandService.sendTrafficLightSignalCommands(phaseColor, intersectionId);
log.info("🚦 [MQTT] 已通过统一 HTTP 接口发送车辆控制指令: phaseColor={}, intersectionId={}",
phaseColor, intersectionId);
} catch (Exception e) {
log.error("🚦 [MQTT] 解析 phaseColor 或发送控制指令失败: {}", e.getMessage(), e);
}
}
private String extractIntersectionId(JsonNode serviceDataNode) {
String intersectionId = textOrEmpty(serviceDataNode.get("intersection"));
if (!intersectionId.isBlank()) {
return intersectionId;
}
String trafficLightsId = textOrEmpty(serviceDataNode.get("trafficLightsId"));
if (!trafficLightsId.isBlank()) {
return trafficLightsId;
}
return "";
}
private String textOrEmpty(JsonNode node) {
return node == null || node.isNull() ? "" : node.asText("");
}
}

View File

@ -53,6 +53,7 @@ import org.locationtech.jts.geom.PrecisionModel;
public class DataCollectorService {
private static final boolean VEHICLE_MANAGER_ROUTE_INGEST_ENABLED = false;
private static final boolean VEHICLE_MANAGER_HTTP_POLLING_ENABLED = false;
private static final java.util.concurrent.atomic.AtomicBoolean ROUTE_INGEST_DISABLED_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
// 机场数据源相关配置
@ -71,10 +72,6 @@ public class DataCollectorService {
@Value("${data.collector.detection.interval:1000}")
private long detectionInterval;
@Value("${data.collector.vehicle-manager.http.polling-enabled:true}")
private boolean vehicleManagerHttpPollingEnabled;
@Value("${data.collector.route.periodic-collection-enabled:false}")
private boolean periodicRouteCollectionEnabled;
@ -240,6 +237,9 @@ public class DataCollectorService {
if (collectorDisabled) {
return;
}
if (!VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
return;
}
List<String> ids = dataCollectorDao.getVehicleManagerVehicleDetails();
if (ids == null || ids.isEmpty()) {
@ -689,6 +689,9 @@ public class DataCollectorService {
if (collectorDisabled) {
return;
}
if (!VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
return;
}
// 丢弃旧轮次上一轮未完成时跳过避免多车循环HTTP + fixedRate + @Async堆积
try {
@ -745,6 +748,7 @@ public class DataCollectorService {
.currentPosition(currentPosition)
.currentSpeed(null) // 不在采集阶段计算速度
.currentHeading(null) // 不在采集阶段计算方向
.sourceTimestampMs(resolveUnmannedVehicleSourceTimestampMs(statusData))
.altitude(0.0); // 默认高度
// Task context is disabled for route ingestion.
@ -854,6 +858,19 @@ public class DataCollectorService {
}
}
private long resolveUnmannedVehicleSourceTimestampMs(
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData) {
if (statusData != null
&& statusData.getSensorStatus() != null
&& statusData.getSensorStatus().getGps() != null
&& statusData.getSensorStatus().getGps().getLastUpdate() != null
&& statusData.getSensorStatus().getGps().getLastUpdate() > 0L) {
return statusData.getSensorStatus().getGps().getLastUpdate();
}
return System.currentTimeMillis();
}
/**
* 获取已知的无人车ID列表
* 可以从配置缓存或其他数据源获取
@ -876,7 +893,7 @@ public class DataCollectorService {
@Async // 异步执行
public void collectUniversalVehicleStatus() {
// 无人车状态已在 collectUnmannedVehicleData() 中通过 HTTP 拉取并缓存无需重复采集
if (vehicleManagerHttpPollingEnabled) {
if (VEHICLE_MANAGER_HTTP_POLLING_ENABLED) {
return;
}

View File

@ -369,6 +369,8 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
return handleTISFLIGHT(root);
case "ADXP_NAOMS_O_DYN_DFDE":
return handleDFDE(root);
case "ADXP_WURENCHE_O_DYN_CONTACTTAXIWAY":
return handleCONTACTTAXIWAY(root);
default:
log.debug("未知的服务代码: {}", serviceCode);
return null;
@ -454,6 +456,17 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
return null;
}
private String getLastTextContent(org.w3c.dom.Element parent, String tagName) {
org.w3c.dom.NodeList nodeList = parent.getElementsByTagName(tagName);
for (int i = nodeList.getLength() - 1; i >= 0; i--) {
String value = nodeList.item(i).getTextContent();
if (value != null && !value.isBlank()) {
return value.trim();
}
}
return null;
}
/**
* 统一航班号归一化规则避免同一航班参数写入不同 Redis Key导致路由参数难以凑齐
*
@ -1121,7 +1134,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
String flightNumberRaw = getTextContent(root, "FlNo");
String flightNumber = normalizeTisFlightNo(flightNumberRaw);
String flightId = normalizeTisFlightNo(flightIdRaw);
String contactCross = getTextContent(root, "ContactCross");
String type = getTextContent(root, "Type");
long nowMs = System.currentTimeMillis();
@ -1157,9 +1169,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
FlightNotificationDTO dto = new FlightNotificationDTO();
dto.setFlightNo(resolvedFlightNo != null ? resolvedFlightNo : flightNumberRaw);
dto.setType(routeType);
if ("IN".equalsIgnoreCase(routeType)) {
dto.setContactCross(contactCross);
}
String normalizedBizKey = resolveTisFlightBizKey(bizKey, resolvedFlightNo);
String flightKey = buildFlightRedisKey(resolvedFlightNo);
@ -1169,12 +1178,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
try {
redisCache.setCacheMapValue(flightKey, "flightNumber", resolvedFlightNo);
updateActiveBizKey(flightKey, normalizedBizKey, nowMs);
// contactCross 仅对进港有效避免进出港参数串用
if ("IN".equalsIgnoreCase(routeType)
&& contactCross != null && !contactCross.isBlank()) {
setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "contactCross", contactCross);
setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "contactCrossTs", String.valueOf(nowMs));
}
log.info("TISFLIGHT航班号解析(已应用三字转二字): flNoRaw={}, flightIdRaw={}, fuId={}, bizKey={}, resolvedFlightNo={}",
flightNumberRaw, flightIdRaw, fuId, normalizedBizKey, resolvedFlightNo);
} catch (Exception e) {
@ -1187,6 +1190,32 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
return dto;
}
private FlightNotificationDTO handleCONTACTTAXIWAY(org.w3c.dom.Element root) {
String flightNoRaw = getTextContent(root, "FlightNo");
String normalizedFlightNo = normalizeTisFlightNo(flightNoRaw);
String contactCross = getLastTextContent(root, "TaxiwayIntersection");
long nowMs = System.currentTimeMillis();
String flightKey = buildFlightRedisKey(normalizedFlightNo);
if (flightKey == null || contactCross == null || contactCross.isBlank()) {
log.warn("CONTACTTAXIWAY事件缺少路由参数跳过Redis写入: flightNoRaw={}, normalizedFlightNo={}, contactCross={}",
flightNoRaw, normalizedFlightNo, contactCross);
return null;
}
try {
redisCache.setCacheMapValue(flightKey, "flightNumber", normalizedFlightNo);
redisCache.setCacheMapValue(flightKey, "contactCross", contactCross.trim());
redisCache.setCacheMapValue(flightKey, "contactCrossTs", String.valueOf(nowMs));
log.info("成功将CONTACTTAXIWAY道口数据存储到Redis: flightNumber={}, contactCross={}",
normalizedFlightNo, contactCross);
} catch (Exception e) {
log.error("存储CONTACTTAXIWAY道口数据到Redis失败: flightNumber={}, contactCross={}",
normalizedFlightNo, contactCross, e);
}
return null;
}
private FlightNotificationDTO handleAXOT(org.w3c.dom.Element root) {
// BizKey 常见格式FLIGHTNO-进出港标识-时间(yyyyMMddHHmmss) 或其他分段
String bizKey = getTextContent(root, "BizKey");

View File

@ -30,11 +30,13 @@ import org.springframework.context.ApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@ -65,6 +67,9 @@ public class DataProcessingService {
@Value("${data.collector.disabled:false}")
private boolean collectorDisabled;
@Value("${qaup.collision.diagnostic-log.enabled:true}")
private boolean collisionDiagnosticLogEnabled;
@Autowired
private SpeedCalculationService speedCalculationService;
@ -98,6 +103,7 @@ public class DataProcessingService {
// 从DataCollectorService获取缓存的引用
private Map<String, MovingObject> activeMovingObjectsCache;
private final Map<String, Long> lastPublishedPositionSampleTimestamps = new ConcurrentHashMap<>();
/**
* 防止@Scheduled任务重入导致并发执行消息风暴与数据库压力
@ -146,7 +152,7 @@ public class DataProcessingService {
calculateSpeedAndDirectionForAllObjects(currentActiveObjects);
// 第二步发送WebSocket位置更新消息
sendPositionUpdatesForActiveObjects(currentActiveObjects);
PositionDispatchSummary positionDispatchSummary = sendPositionUpdatesForActiveObjects(currentActiveObjects);
// 第三步处理通用车辆状态数据并发送WebSocket更新
processUniversalVehicleStatusUpdates();
@ -155,7 +161,8 @@ public class DataProcessingService {
// 注意采集侧只缓存发送侧在处理完成后会清理缓存避免同一通知被每秒重复推送刷屏
processFlightNotificationUpdates();
List<MovingObject> collisionManagedObjects = filterCollisionManagedObjects(currentActiveObjects);
CollisionManagedObjectsResult collisionManagedResult = filterCollisionManagedObjects(currentActiveObjects);
List<MovingObject> collisionManagedObjects = collisionManagedResult.objects();
// 第五步将注册的无人车任务路径准备为正式路线输入
if (routePreparationService != null) {
@ -163,7 +170,8 @@ public class DataProcessingService {
}
// 第六步执行路径冲突检测
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
PathConflictDetectionService.ConflictDetectionSummary conflictDetectionSummary =
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
// 第七步执行违规检测
performViolationDetection(currentActiveObjects);
@ -171,25 +179,63 @@ public class DataProcessingService {
// 第八步保存无人车数据到数据库
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
logCycleDiagnostics(currentActiveObjects.size(), collisionManagedObjects.size(),
positionDispatchSummary, collisionManagedResult, conflictDetectionSummary);
log.info("周期性数据处理完成");
}
private List<MovingObject> filterCollisionManagedObjects(List<MovingObject> activeObjects) {
private CollisionManagedObjectsResult filterCollisionManagedObjects(List<MovingObject> activeObjects) {
if (activeObjects == null || activeObjects.isEmpty()) {
return List.of();
return new CollisionManagedObjectsResult(List.of(), 0, 0);
}
List<MovingObject> filteredObjects = activeObjects.stream()
.filter(Objects::nonNull)
.filter(object -> platformRuntimeStateService == null
|| platformRuntimeStateService.isRegisteredForCollision(object.getObjectId(), object.getObjectType()))
.collect(Collectors.toList());
List<MovingObject> filteredObjects = new ArrayList<>();
int registrationFilteredCount = 0;
int typeOverrideCount = 0;
for (MovingObject object : activeObjects) {
if (object == null) {
continue;
}
if (platformRuntimeStateService == null) {
filteredObjects.add(object);
continue;
}
MovingObjectType registeredType =
platformRuntimeStateService.getRegisteredCollisionObjectType(object.getObjectId());
if (registeredType == null) {
registrationFilteredCount++;
continue;
}
if (registeredType != object.getObjectType()) {
typeOverrideCount++;
filteredObjects.add(copyWithCollisionType(object, registeredType));
continue;
}
filteredObjects.add(object);
}
if (filteredObjects.size() != activeObjects.size()) {
log.debug("碰撞检测对象已按注册表过滤: 原始数量={}, 过滤后数量={}",
activeObjects.size(), filteredObjects.size());
}
return filteredObjects;
return new CollisionManagedObjectsResult(filteredObjects, registrationFilteredCount, typeOverrideCount);
}
private MovingObject copyWithCollisionType(MovingObject source, MovingObjectType collisionType) {
return MovingObject.builder()
.objectId(source.getObjectId())
.objectType(collisionType)
.objectName(source.getObjectName())
.currentPosition(source.getCurrentPosition())
.currentSpeed(source.getCurrentSpeed())
.currentHeading(source.getCurrentHeading())
.altitude(source.getAltitude())
.sourceTimestampMs(source.getSourceTimestampMs())
.build();
}
/**
@ -237,17 +283,21 @@ public class DataProcessingService {
/**
* 发送WebSocket位置更新消息
*/
private void sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
private PositionDispatchSummary sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
if (activeObjects.isEmpty()) {
log.debug("没有活跃对象,跳过位置更新消息发送");
return;
return new PositionDispatchSummary(0, 0, 0);
}
long currentTime = System.currentTimeMillis();
log.debug("发送位置更新消息,对象数量: {}", activeObjects.size());
int publishedCount = 0;
int duplicateSampleSkippedCount = 0;
Set<String> currentObjectIds = new HashSet<>();
for (MovingObject movingObject : activeObjects) {
try {
currentObjectIds.add(movingObject.getObjectId());
// 创建位置更新消息负载
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
.latitude(movingObject.getCurrentPosition().getY())
@ -261,6 +311,11 @@ public class DataProcessingService {
? movingObject.getSourceTimestampMs()
: currentTime;
if (!shouldPublishPositionUpdate(movingObject.getObjectId(), payloadTimestamp)) {
duplicateSampleSkippedCount++;
continue;
}
PositionUpdatePayload payload = PositionUpdatePayload.builder()
.objectId(movingObject.getObjectId())
.objectType(movingObject.getObjectType().name())
@ -272,6 +327,7 @@ public class DataProcessingService {
// 发布WebSocket事件
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
publishedCount++;
log.debug("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
movingObject.getObjectId(),
@ -285,7 +341,65 @@ public class DataProcessingService {
}
}
log.info("位置更新消息发送完成,发送数量: {}", activeObjects.size());
cleanupPublishedPositionState(currentObjectIds);
log.info("位置更新消息发送完成,发送数量: {}, 跳过重复采样数量: {}", publishedCount, duplicateSampleSkippedCount);
return new PositionDispatchSummary(activeObjects.size(), publishedCount, duplicateSampleSkippedCount);
}
private boolean shouldPublishPositionUpdate(String objectId, long payloadTimestamp) {
Long previousTimestamp = lastPublishedPositionSampleTimestamps.get(objectId);
if (previousTimestamp != null && payloadTimestamp <= previousTimestamp) {
return false;
}
lastPublishedPositionSampleTimestamps.put(objectId, payloadTimestamp);
return true;
}
private void cleanupPublishedPositionState(Set<String> currentObjectIds) {
lastPublishedPositionSampleTimestamps.entrySet()
.removeIf(entry -> !currentObjectIds.contains(entry.getKey()));
}
private void logCycleDiagnostics(
int activeObjectCount,
int collisionManagedObjectCount,
PositionDispatchSummary positionDispatchSummary,
CollisionManagedObjectsResult collisionManagedResult,
PathConflictDetectionService.ConflictDetectionSummary conflictDetectionSummary) {
if (!collisionDiagnosticLogEnabled) {
return;
}
String testSessionId = platformRuntimeStateService != null
? platformRuntimeStateService.getCurrentTestSessionId()
: null;
log.info(
"[test-session={}] [collision-diagnostic] activeObjects={}, collisionManagedObjects={}, positionPublished={}, "
+ "positionDuplicateSkipped={}, registrationFiltered={}, typeOverrides={}, pairsTotal={}, pairsSupported={}, missingRoute={}, missingPosition={}, "
+ "routeDeviation={}, speedTooLow={}, noIntersection={}, intersectionBehind={}, headingMismatch={}, "
+ "thresholdNotReached={}, detectionErrors={}, eventsPublished={}",
testSessionId != null ? testSessionId : "none",
activeObjectCount,
collisionManagedObjectCount,
positionDispatchSummary.publishedCount(),
positionDispatchSummary.duplicateSampleSkippedCount(),
collisionManagedResult.registrationFilteredCount(),
collisionManagedResult.typeOverrideCount(),
conflictDetectionSummary.totalPairs(),
conflictDetectionSummary.supportedPairs(),
conflictDetectionSummary.missingRoutePairs(),
conflictDetectionSummary.missingPositionPairs(),
conflictDetectionSummary.routeDeviationPairs(),
conflictDetectionSummary.speedTooLowPairs(),
conflictDetectionSummary.noIntersectionPairs(),
conflictDetectionSummary.intersectionBehindPairs(),
conflictDetectionSummary.headingMismatchPairs(),
conflictDetectionSummary.thresholdNotReachedPairs(),
conflictDetectionSummary.errorPairs(),
conflictDetectionSummary.eventsPublished()
);
}
private List<MovingObject> createProcessingSnapshot(java.util.Collection<MovingObject> sourceObjects) {
@ -321,6 +435,18 @@ public class DataProcessingService {
return Math.round(value * 100.0) / 100.0;
}
private record PositionDispatchSummary(
int totalObjects,
int publishedCount,
int duplicateSampleSkippedCount) {
}
private record CollisionManagedObjectsResult(
List<MovingObject> objects,
int registrationFilteredCount,
int typeOverrideCount) {
}
/**
* 执行违规检测
*/
@ -1500,8 +1626,6 @@ public class DataProcessingService {
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
// 可选简化路径以减少冗余点容差1米
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
lineStringSegments.size(), mergedGeometry.getNumPoints());
} else {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -44,26 +44,32 @@ public class PathConflictDetectionService {
private final VehicleCommandService vehicleCommandService;
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
private static final double MIN_TIME_GAP_SECONDS = 30.0;
// Strong accuracy guards
private static final double MIN_FORWARD_DISTANCE_METERS = 3.0;
private static final double MAX_ROUTE_DEVIATION_METERS = 80.0;
private static final double MAX_ROUTE_DEVIATION_METERS = 200.0;
private static final double HEADING_ALIGNMENT_MIN_COS = 0.0;
private static final double HEADING_CHECK_MIN_SPEED_KPH = 3.0;
private static final double MIN_EFFECTIVE_SPEED_KPH = 1.0;
public void detectPathConflicts(List<MovingObject> activeObjects) {
public ConflictDetectionSummary detectPathConflicts(List<MovingObject> activeObjects) {
if (activeObjects == null) {
return new ConflictDetectionSummary(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
log.debug("Starting path conflict detection for {} active objects", activeObjects.size());
List<ConflictAlertEvent> detectedAlertEvents = new ArrayList<>();
DetectionCounters counters = new DetectionCounters();
for (int i = 0; i < activeObjects.size(); i++) {
for (int j = i + 1; j < activeObjects.size(); j++) {
counters.totalPairs++;
MovingObject obj1 = activeObjects.get(i);
MovingObject obj2 = activeObjects.get(j);
Optional<ConflictAlertEvent> alertEventOptional = detectConflictBetweenObjects(obj1, obj2);
alertEventOptional.ifPresent(detectedAlertEvents::add);
PairDetectionResult detectionResult = detectConflictBetweenObjects(obj1, obj2);
counters.record(detectionResult.reason());
detectionResult.alertEvent().ifPresent(detectedAlertEvents::add);
}
}
@ -80,45 +86,46 @@ public class PathConflictDetectionService {
}
log.info("Path conflict detection completed, {} events published", detectedAlertEvents.size());
return counters.toSummary(detectedAlertEvents.size());
}
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
private PairDetectionResult detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
if (!isSupportedConflictPair(obj1.getObjectType(), obj2.getObjectType())) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.UNSUPPORTED_PAIR);
}
try {
if (obj1.getCurrentPosition() == null || obj2.getCurrentPosition() == null) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.MISSING_POSITION);
}
TransportRoute route1 = getObjectRoute(obj1);
TransportRoute route2 = getObjectRoute(obj2);
if (route1 == null || route2 == null || route1.getRouteGeometry() == null || route2.getRouteGeometry() == null) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
}
LocalRoute localRoute1 = toLocalRoute(route1);
LocalRoute localRoute2 = toLocalRoute(route2);
if (localRoute1 == null || localRoute2 == null) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
}
Point localObjPos1 = toLocalPoint(obj1.getCurrentPosition());
Point localObjPos2 = toLocalPoint(obj2.getCurrentPosition());
if (localObjPos1 == null || localObjPos2 == null) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.MISSING_POSITION);
}
ProjectedPosition projected1 = projectToRoute(localObjPos1, localRoute1.localLine);
ProjectedPosition projected2 = projectToRoute(localObjPos2, localRoute2.localLine);
if (projected1 == null || projected2 == null) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.MISSING_ROUTE);
}
if (projected1.lateralDistanceMeters > MAX_ROUTE_DEVIATION_METERS || projected2.lateralDistanceMeters > MAX_ROUTE_DEVIATION_METERS) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.ROUTE_DEVIATION_TOO_LARGE);
}
double speed1Kph = normalizeSpeedKph(obj1.getCurrentSpeed());
@ -126,10 +133,17 @@ public class PathConflictDetectionService {
// Ignore pairs that are effectively static.
if (speed1Kph < MIN_EFFECTIVE_SPEED_KPH || speed2Kph < MIN_EFFECTIVE_SPEED_KPH) {
return Optional.empty();
return PairDetectionResult.of(PairOutcomeReason.SPEED_TOO_LOW);
}
List<Point> intersectionPoints = calculateRouteIntersections(route1, route2);
if (intersectionPoints.isEmpty()) {
return PairDetectionResult.of(PairOutcomeReason.NO_INTERSECTION);
}
boolean intersectionBehind = false;
boolean headingMismatch = false;
boolean thresholdNotReached = false;
for (Point intersectionPoint : intersectionPoints) {
Point localIntersection = toLocalPoint(intersectionPoint);
if (localIntersection == null) {
@ -147,12 +161,14 @@ public class PathConflictDetectionService {
// Only future conflicts are valid.
if (forwardDistance1 <= MIN_FORWARD_DISTANCE_METERS || forwardDistance2 <= MIN_FORWARD_DISTANCE_METERS) {
intersectionBehind = true;
continue;
}
// Ensure current heading points to the intersection direction when speed is meaningful.
if (!isHeadingAligned(obj1, localObjPos1, localIntersection, speed1Kph)
|| !isHeadingAligned(obj2, localObjPos2, localIntersection, speed2Kph)) {
headingMismatch = true;
continue;
}
@ -160,50 +176,75 @@ public class PathConflictDetectionService {
obj1, obj2, forwardDistance1, forwardDistance2, speed1Kph, speed2Kph);
if (result != null && isSignificantConflict(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();
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());
return PairDetectionResult.of(buildConflictAlertEvent(obj1, obj2, route1, route2, intersectionPoint, result));
}
thresholdNotReached = true;
}
if (thresholdNotReached) {
return PairDetectionResult.of(PairOutcomeReason.THRESHOLD_NOT_REACHED);
}
if (headingMismatch) {
return PairDetectionResult.of(PairOutcomeReason.HEADING_MISMATCH);
}
if (intersectionBehind) {
return PairDetectionResult.of(PairOutcomeReason.INTERSECTION_BEHIND);
}
return PairDetectionResult.of(PairOutcomeReason.NO_INTERSECTION);
} catch (Exception e) {
log.error("Error detecting conflict between {} and {}", obj1.getObjectName(), obj2.getObjectName(), e);
return PairDetectionResult.of(PairOutcomeReason.ERROR);
}
}
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) {
Optional<ObjectRouteAssignment> assignmentOptional = objectRouteAssignmentRepository
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
obj.getObjectName(),
ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name())
);
ObjectRouteAssignment.ObjectType objectType = ObjectRouteAssignment.ObjectType.valueOf(obj.getObjectType().name());
Optional<ObjectRouteAssignment> assignmentOptional = findLatestAssignment(obj.getObjectId(), objectType);
if (assignmentOptional.isEmpty()
&& obj.getObjectName() != null
&& !obj.getObjectName().isBlank()
&& !obj.getObjectName().equals(obj.getObjectId())) {
assignmentOptional = findLatestAssignment(obj.getObjectName(), objectType);
}
return assignmentOptional
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
@ -211,6 +252,13 @@ public class PathConflictDetectionService {
.orElse(null);
}
private Optional<ObjectRouteAssignment> findLatestAssignment(String identifier, ObjectRouteAssignment.ObjectType objectType) {
if (identifier == null || identifier.isBlank()) {
return Optional.empty();
}
return objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(identifier, objectType);
}
private List<Point> calculateRouteIntersections(TransportRoute route1, TransportRoute route2) {
List<Point> intersections = new ArrayList<>();
@ -260,15 +308,13 @@ public class PathConflictDetectionService {
return null;
}
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
Optional<ConflictAlertLog.AlertLevel> alertLevelOptional = evaluateAlertLevel(
distance1,
obj1.getObjectType(),
distance2,
obj2.getObjectType(),
timeGap
obj2.getObjectType()
);
double timeGap = Math.abs(timeToConflict1 - timeToConflict2);
Optional<ConflictAlertLog.AlertType> alertTypeOptional = alertLevelOptional.isPresent()
? evaluateAlertType(alertLevelOptional.get())
: Optional.empty();
@ -294,27 +340,34 @@ public class PathConflictDetectionService {
double distance1,
MovingObjectType obj1Type,
double distance2,
MovingObjectType obj2Type,
double timeGap) {
MovingObjectType obj2Type) {
if (!isSupportedConflictPair(obj1Type, obj2Type)) {
return Optional.empty();
}
double distanceToEvaluate;
Double vehicleDistance = null;
Double aircraftDistance = null;
if (obj1Type == MovingObjectType.AIRCRAFT) {
distanceToEvaluate = distance2;
aircraftDistance = distance1;
vehicleDistance = distance2;
} else if (obj2Type == MovingObjectType.AIRCRAFT) {
distanceToEvaluate = distance1;
} else {
aircraftDistance = distance2;
vehicleDistance = distance1;
}
if (vehicleDistance == null || aircraftDistance == null) {
return Optional.empty();
}
double alertDistanceThreshold = platformRuntimeStateService.getRunwayAlertZoneRadiusAircraft();
double warningDistanceThreshold = platformRuntimeStateService.getRunwayWarningZoneRadiusAircraft();
double vehicleDistanceThreshold = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle();
double aircraftDistanceThreshold = platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft();
if (distanceToEvaluate <= alertDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS) {
boolean vehicleInZone = vehicleDistance <= vehicleDistanceThreshold;
boolean aircraftInZone = aircraftDistance <= aircraftDistanceThreshold;
if (vehicleInZone && aircraftInZone) {
return Optional.of(ConflictAlertLog.AlertLevel.CRITICAL);
} else if (distanceToEvaluate <= warningDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
} else if (vehicleInZone || aircraftInZone) {
return Optional.of(ConflictAlertLog.AlertLevel.WARNING);
} else {
return Optional.empty();
@ -484,6 +537,95 @@ public class PathConflictDetectionService {
private record ProjectedPosition(double distanceAlongRouteMeters, double lateralDistanceMeters) {}
public record ConflictDetectionSummary(
int totalPairs,
int supportedPairs,
int missingPositionPairs,
int missingRoutePairs,
int routeDeviationPairs,
int speedTooLowPairs,
int noIntersectionPairs,
int intersectionBehindPairs,
int headingMismatchPairs,
int thresholdNotReachedPairs,
int errorPairs,
int eventsPublished) {
}
private record PairDetectionResult(Optional<ConflictAlertEvent> alertEvent, PairOutcomeReason reason) {
private static PairDetectionResult of(PairOutcomeReason reason) {
return new PairDetectionResult(Optional.empty(), reason);
}
private static PairDetectionResult of(ConflictAlertEvent event) {
return new PairDetectionResult(Optional.of(event), PairOutcomeReason.EVENT_PUBLISHED);
}
}
private enum PairOutcomeReason {
UNSUPPORTED_PAIR,
MISSING_POSITION,
MISSING_ROUTE,
ROUTE_DEVIATION_TOO_LARGE,
SPEED_TOO_LOW,
NO_INTERSECTION,
INTERSECTION_BEHIND,
HEADING_MISMATCH,
THRESHOLD_NOT_REACHED,
EVENT_PUBLISHED,
ERROR
}
private static final class DetectionCounters {
private int totalPairs;
private int supportedPairs;
private int missingPositionPairs;
private int missingRoutePairs;
private int routeDeviationPairs;
private int speedTooLowPairs;
private int noIntersectionPairs;
private int intersectionBehindPairs;
private int headingMismatchPairs;
private int thresholdNotReachedPairs;
private int errorPairs;
private void record(PairOutcomeReason reason) {
if (reason != PairOutcomeReason.UNSUPPORTED_PAIR) {
supportedPairs++;
}
switch (reason) {
case MISSING_POSITION -> missingPositionPairs++;
case MISSING_ROUTE -> missingRoutePairs++;
case ROUTE_DEVIATION_TOO_LARGE -> routeDeviationPairs++;
case SPEED_TOO_LOW -> speedTooLowPairs++;
case NO_INTERSECTION -> noIntersectionPairs++;
case INTERSECTION_BEHIND -> intersectionBehindPairs++;
case HEADING_MISMATCH -> headingMismatchPairs++;
case THRESHOLD_NOT_REACHED -> thresholdNotReachedPairs++;
case ERROR -> errorPairs++;
default -> {
}
}
}
private ConflictDetectionSummary toSummary(int eventsPublished) {
return new ConflictDetectionSummary(
totalPairs,
supportedPairs,
missingPositionPairs,
missingRoutePairs,
routeDeviationPairs,
speedTooLowPairs,
noIntersectionPairs,
intersectionBehindPairs,
headingMismatchPairs,
thresholdNotReachedPairs,
errorPairs,
eventsPublished
);
}
}
private static class ConflictCalculationResult {
private final double distance1;
private final double distance2;

View File

@ -39,6 +39,7 @@ public class VehicleCommandService {
private final Map<String, ActiveVehicleCommandState> activeCommandStates = new ConcurrentHashMap<>();
private final AtomicLong transIdSequence = new AtomicLong();
private static final String UNIFIED_VEHICLE_COMMAND_URL = "http://10.232.18.23:8020/api/VehicleCommandInfo";
@Value("${data.collector.vehicle-api.base-url:}")
private String vehicleApiBaseUrl;
@ -213,12 +214,6 @@ public class VehicleCommandService {
VehicleConflictCommand command,
CommandType commandType,
CommandReason commandReason) {
if (!isCommandEndpointConfigured()) {
log.warn("Skipping vehicle command because vehicle API endpoint is not configured: vehicleId={}", command.vehicleId());
return false;
}
try {
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
.transId(nextTransId())
@ -240,7 +235,7 @@ public class VehicleCommandService {
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<String> response = restTemplate.exchange(
vehicleApiBaseUrl + vehicleCommandEndpoint,
getUnifiedVehicleCommandUrl(),
HttpMethod.POST,
new HttpEntity<>(request, headers),
String.class
@ -299,6 +294,40 @@ public class VehicleCommandService {
}
}
public void sendTrafficLightSignalCommands(int phaseColor, String intersectionId) {
String signalState = toSignalState(phaseColor);
if (signalState == null) {
log.warn("Skipping traffic light command because phaseColor is unsupported: {}", phaseColor);
return;
}
List<String> vehicleIds = platformRuntimeStateService.getTrafficLightRecipientVehicleIds();
if (vehicleIds.isEmpty()) {
log.info("Skipping traffic light command because no controllable vehicles are registered");
return;
}
for (String vehicleId : vehicleIds) {
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
.transId(nextTransId())
.timestamp(System.currentTimeMillis())
.vehicleId(vehicleId)
.commandType(CommandType.SIGNAL)
.commandReason(CommandReason.TRAFFIC_LIGHT)
.latitude(0.0)
.longitude(0.0)
.signalState(signalState)
.intersectionId(intersectionId == null ? "" : intersectionId)
.relativeSpeed(0.0)
.relativeMotionX(0.0)
.relativeMotionY(0.0)
.minDistance(0.0)
.build();
sendExternalVehicleCommand(request, vehicleId, CommandType.SIGNAL, CommandReason.TRAFFIC_LIGHT);
}
}
private JsonNode parseResponseBody(String body) {
if (body == null || body.isBlank()) {
return null;
@ -312,11 +341,87 @@ public class VehicleCommandService {
}
}
private boolean isCommandEndpointConfigured() {
return vehicleApiBaseUrl != null
&& !vehicleApiBaseUrl.isBlank()
&& vehicleCommandEndpoint != null
&& !vehicleCommandEndpoint.isBlank();
private boolean sendExternalVehicleCommand(
ExternalVehicleCommandRequest request,
String vehicleId,
CommandType commandType,
CommandReason commandReason) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<String> response = restTemplate.exchange(
getUnifiedVehicleCommandUrl(),
HttpMethod.POST,
new HttpEntity<>(request, headers),
String.class
);
if (response.getStatusCode().value() != 200) {
log.warn(
"Vehicle command rejected by HTTP status: vehicleId={}, commandType={}, status={}",
vehicleId,
commandType,
response.getStatusCode().value()
);
return false;
}
JsonNode responseBody = parseResponseBody(response.getBody());
if (responseBody == null || !responseBody.has("code") || !responseBody.has("msg")) {
log.warn(
"Vehicle command response missing required fields: vehicleId={}, commandType={}, body={}",
vehicleId,
commandType,
response.getBody()
);
return false;
}
int responseCode = responseBody.path("code").asInt(Integer.MIN_VALUE);
if (responseCode != 200) {
log.warn(
"Vehicle command rejected by upstream response code: vehicleId={}, commandType={}, code={}, msg={}",
vehicleId,
commandType,
responseCode,
responseBody.path("msg").asText()
);
return false;
}
log.info(
"Vehicle command sent successfully: vehicleId={}, registryType={}, commandType={}, reason={}",
vehicleId,
resolveRegistryType(vehicleId),
commandType,
commandReason
);
return true;
} catch (Exception e) {
log.error(
"Failed to send vehicle command: vehicleId={}, commandType={}, reason={}",
vehicleId,
commandType,
commandReason,
e
);
return false;
}
}
private String getUnifiedVehicleCommandUrl() {
return UNIFIED_VEHICLE_COMMAND_URL;
}
private String toSignalState(int phaseColor) {
return switch (phaseColor) {
case 1 -> "GREEN";
case 2 -> "YELLOW";
case 3 -> "RED";
default -> null;
};
}
private VehicleRegistryType resolveRegistryType(String vehicleId) {

View File

@ -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;
}
}

View File

@ -5,6 +5,10 @@ import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.system.domain.SysConfig;
import com.qaup.system.service.ISysConfigService;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.PrecisionModel;
@ -24,15 +28,30 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Service
public class PlatformRuntimeStateService {
private static final String CONFIG_KEY_CROSSING_VEHICLE_DISTANCE = "qaup.collision.crossing.vehicleDistance";
private static final String CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE = "qaup.collision.crossing.aircraftDistance";
private static final String CONFIG_NAME_CROSSING_VEHICLE_DISTANCE = "Path crossing threshold for vehicle";
private static final String CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE = "Path crossing threshold for aircraft";
private static final String CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE = "Vehicle distance threshold to the route crossing point";
private static final String CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE = "Aircraft distance threshold to the route crossing point";
private static final String CONFIG_UPDATER = "system";
private final ConcurrentHashMap<String, VehicleRegistryType> vehicleTypes = new ConcurrentHashMap<>();
private final AtomicLong testSessionSequence = new AtomicLong();
private volatile String currentTestSessionId;
private volatile double runwayWarningZoneRadiusAircraft;
private volatile double runwayAlertZoneRadiusAircraft;
// Threshold used when the vehicle approaches a route crossing point.
private volatile double collisionDivergingReleaseDistanceForVehicle;
// Threshold used when the aircraft approaches a route crossing point.
private volatile double collisionDivergingReleaseDistanceForAircraft;
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
private static final String DEFAULT_ROUTE_SOURCE = "VehicleRouteIngestion";
@ -44,6 +63,9 @@ public class PlatformRuntimeStateService {
@Autowired
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
@Autowired(required = false)
private ISysConfigService sysConfigService;
public PlatformRuntimeStateService(
@Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft,
@Value("${qaup.runtime-config.runway.alert-zone-radius.aircraft:100.0}") double runwayAlertZoneRadiusAircraft,
@ -55,9 +77,27 @@ public class PlatformRuntimeStateService {
this.collisionDivergingReleaseDistanceForAircraft = collisionDivergingReleaseDistance;
}
@PostConstruct
public void loadPersistedCrossingThresholds() {
if (sysConfigService == null) {
return;
}
Double persistedVehicleDistance = readConfigDouble(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE);
if (persistedVehicleDistance != null) {
collisionDivergingReleaseDistanceForVehicle = persistedVehicleDistance;
}
Double persistedAircraftDistance = readConfigDouble(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE);
if (persistedAircraftDistance != null) {
collisionDivergingReleaseDistanceForAircraft = persistedAircraftDistance;
}
}
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
Objects.requireNonNull(entries, "entries");
TreeSet<String> previousVehicleIds = new TreeSet<>(vehicleTypes.keySet());
vehicleTypes.clear();
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
@ -65,6 +105,25 @@ public class PlatformRuntimeStateService {
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
requestTypeCounts.merge(entry.vehicleType(), 1, Integer::sum);
}
previousVehicleIds.removeAll(vehicleTypes.keySet());
String endedTestSessionId = null;
String activeTestSessionId = null;
if (entries.isEmpty()) {
endedTestSessionId = currentTestSessionId;
if (endedTestSessionId != null) {
log.info("[test-session={}] collision test session ended by empty registry update", endedTestSessionId);
}
currentTestSessionId = null;
} else {
endedTestSessionId = currentTestSessionId;
if (endedTestSessionId != null) {
log.info("[test-session={}] collision test session replaced by new registry update", endedTestSessionId);
}
activeTestSessionId = createTestSessionId(entries);
currentTestSessionId = activeTestSessionId;
log.info("[test-session={}] collision test session started: objects={}", activeTestSessionId, entries);
}
Set<String> controllableVehicleIds = new TreeSet<>();
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
@ -86,7 +145,14 @@ public class PlatformRuntimeStateService {
entries.size(),
controllableVehicleIds.size(),
responseTypeCounts,
new ArrayList<>(controllableVehicleIds));
new ArrayList<>(controllableVehicleIds),
new ArrayList<>(previousVehicleIds),
activeTestSessionId,
endedTestSessionId);
}
public String getCurrentTestSessionId() {
return currentTestSessionId;
}
public List<String> getControllableVehicleIds() {
@ -130,6 +196,56 @@ public class PlatformRuntimeStateService {
};
}
public MovingObject.MovingObjectType getRegisteredCollisionObjectType(String objectId) {
return mapToMovingObjectType(getVehicleRegistryType(objectId));
}
public CollisionPreparationStatus getCollisionPreparationStatus() {
List<CollisionPreparationObjectStatus> objects = new ArrayList<>();
int missingRouteCount = 0;
for (String objectId : new TreeSet<>(vehicleTypes.keySet())) {
VehicleRegistryType registryType = vehicleTypes.get(objectId);
MovingObject.MovingObjectType objectType = mapToMovingObjectType(registryType);
if (objectType == null || !isRegisteredForCollision(objectId, objectType)) {
continue;
}
ObjectRouteAssignment.ObjectType assignmentType = ObjectRouteAssignment.ObjectType.valueOf(objectType.name());
boolean routeBound = hasActiveRouteAssignment(objectId, assignmentType);
if (!routeBound) {
missingRouteCount++;
}
objects.add(new CollisionPreparationObjectStatus(
objectId,
registryType.name(),
objectType.name(),
routeBound,
routeBound ? "READY" : "MISSING_ROUTE"
));
}
return new CollisionPreparationStatus(
currentTestSessionId,
missingRouteCount == 0,
new CollisionPreparationSummary(objects.size(), missingRouteCount),
objects
);
}
private boolean hasActiveRouteAssignment(String objectId, ObjectRouteAssignment.ObjectType objectType) {
if (objectRouteAssignmentRepository == null || transportRouteRepository == null) {
return false;
}
return objectRouteAssignmentRepository
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(objectId, objectType)
.flatMap(assignment -> transportRouteRepository.findById(assignment.getAssignedRouteId()))
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
.isPresent();
}
public double getRunwayWarningZoneRadiusAircraft() {
return runwayWarningZoneRadiusAircraft;
}
@ -150,37 +266,106 @@ public class PlatformRuntimeStateService {
return oldValue;
}
/**
* Returns the shared crossing threshold when both vehicle and aircraft use the same value.
* The value mirrors the vehicle threshold for backward compatibility.
*/
public double getCollisionDivergingReleaseDistance() {
return collisionDivergingReleaseDistanceForVehicle;
}
/**
* Updates both vehicle and aircraft crossing thresholds to the same value.
* This keeps the existing API contract that accepts a single numeric value.
*/
public double updateCollisionDivergingReleaseDistance(double newValue) {
double oldValue = collisionDivergingReleaseDistanceForVehicle;
collisionDivergingReleaseDistanceForVehicle = newValue;
collisionDivergingReleaseDistanceForAircraft = newValue;
persistCrossingThreshold(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE, CONFIG_NAME_CROSSING_VEHICLE_DISTANCE, newValue, CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE);
persistCrossingThreshold(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE, CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE, newValue, CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE);
return oldValue;
}
/**
* Returns the vehicle crossing threshold used by path crossing alerts.
*/
public double getCollisionDivergingReleaseDistanceForVehicle() {
return collisionDivergingReleaseDistanceForVehicle;
}
/**
* Returns the aircraft crossing threshold used by path crossing alerts.
*/
public double getCollisionDivergingReleaseDistanceForAircraft() {
return collisionDivergingReleaseDistanceForAircraft;
}
/**
* Updates the vehicle crossing threshold used by path crossing alerts.
*/
public double updateCollisionDivergingReleaseDistanceForVehicle(double newValue) {
double oldValue = collisionDivergingReleaseDistanceForVehicle;
collisionDivergingReleaseDistanceForVehicle = newValue;
persistCrossingThreshold(CONFIG_KEY_CROSSING_VEHICLE_DISTANCE, CONFIG_NAME_CROSSING_VEHICLE_DISTANCE, newValue, CONFIG_REMARK_CROSSING_VEHICLE_DISTANCE);
return oldValue;
}
/**
* Updates the aircraft crossing threshold used by path crossing alerts.
*/
public double updateCollisionDivergingReleaseDistanceForAircraft(double newValue) {
double oldValue = collisionDivergingReleaseDistanceForAircraft;
collisionDivergingReleaseDistanceForAircraft = newValue;
persistCrossingThreshold(CONFIG_KEY_CROSSING_AIRCRAFT_DISTANCE, CONFIG_NAME_CROSSING_AIRCRAFT_DISTANCE, newValue, CONFIG_REMARK_CROSSING_AIRCRAFT_DISTANCE);
return oldValue;
}
private Double readConfigDouble(String configKey) {
try {
String value = sysConfigService.selectConfigByKey(configKey);
if (value == null || value.isBlank()) {
return null;
}
double parsed = Double.parseDouble(value.trim());
return Double.isFinite(parsed) && parsed > 0.0 ? parsed : null;
} catch (Exception ignored) {
return null;
}
}
private void persistCrossingThreshold(String configKey, String configName, double value, String remark) {
if (sysConfigService == null) {
return;
}
SysConfig existing = findConfigByKey(configKey);
if (existing == null) {
SysConfig config = new SysConfig();
config.setConfigName(configName);
config.setConfigKey(configKey);
config.setConfigValue(Double.toString(value));
config.setConfigType("N");
config.setCreateBy(CONFIG_UPDATER);
config.setRemark(remark);
sysConfigService.insertConfig(config);
return;
}
existing.setConfigName(configName);
existing.setConfigValue(Double.toString(value));
existing.setUpdateBy(CONFIG_UPDATER);
existing.setRemark(remark);
sysConfigService.updateConfig(existing);
}
private SysConfig findConfigByKey(String configKey) {
SysConfig query = new SysConfig();
query.setConfigKey(configKey);
List<SysConfig> configs = sysConfigService.selectConfigList(query);
return configs == null || configs.isEmpty() ? null : configs.get(0);
}
@Transactional
public VehicleRouteSubmitResult submitVehicleRoute(
String objectId,
@ -277,14 +462,43 @@ public class PlatformRuntimeStateService {
return "MANUAL_" + routeType.name() + "_" + safeObjectId;
}
private String createTestSessionId(List<VehicleRegistryEntry> entries) {
String suffix = entries.stream()
.map(VehicleRegistryEntry::vehicleID)
.filter(Objects::nonNull)
.map(String::trim)
.filter(value -> !value.isEmpty())
.sorted()
.limit(3)
.reduce((left, right) -> left + "-" + right)
.orElse("unknown");
String normalizedSuffix = suffix.replaceAll("[^A-Za-z0-9_-]", "_");
return "collision-test-" + System.currentTimeMillis()
+ "-" + testSessionSequence.incrementAndGet()
+ "-" + normalizedSuffix;
}
private static TransportRoute.RouteType mapToRouteType(ObjectRouteAssignment.ObjectType objectType) {
return switch (objectType) {
case AIRCRAFT -> TransportRoute.RouteType.AIRCRAFT;
case UNMANNED_VEHICLE -> TransportRoute.RouteType.UNMANNED_VEHICLE;
case SPECIAL_VEHICLE -> TransportRoute.RouteType.SPECIAL_VEHICLE;
default -> null;
};
}
private static MovingObject.MovingObjectType mapToMovingObjectType(VehicleRegistryType registryType) {
if (registryType == null) {
return null;
}
return switch (registryType) {
case WUREN -> MovingObject.MovingObjectType.UNMANNED_VEHICLE;
case TEQIN -> MovingObject.MovingObjectType.SPECIAL_VEHICLE;
case HANGKONG -> MovingObject.MovingObjectType.AIRCRAFT;
case PUTONG, JIUYUAN -> null;
};
}
public record VehicleRegistryEntry(String vehicleID, VehicleRegistryType vehicleType) {
}
@ -293,7 +507,10 @@ public class PlatformRuntimeStateService {
int updated,
int controllableCount,
Map<String, Integer> typesCount,
List<String> controllableVehicleIDs) {
List<String> controllableVehicleIDs,
List<String> removedVehicleIDs,
String testSessionId,
String endedTestSessionId) {
}
public record VehicleRouteSubmitResult(
@ -305,6 +522,26 @@ public class PlatformRuntimeStateService {
String routeType) {
}
public record CollisionPreparationStatus(
String testSessionId,
boolean ready,
CollisionPreparationSummary summary,
List<CollisionPreparationObjectStatus> objects) {
}
public record CollisionPreparationSummary(
int registeredCount,
int missingRouteCount) {
}
public record CollisionPreparationObjectStatus(
String objectId,
String registryType,
String objectType,
boolean routeBound,
String status) {
}
public enum VehicleRegistryType {
WUREN,
TEQIN,

View File

@ -429,161 +429,14 @@ public class WebSocketMessageBroadcaster {
}
long payloadTimestamp = payload.getTimestamp() != null ? payload.getTimestamp() : System.currentTimeMillis();
long filterTimestampMs = payloadTimestamp;
PositionTrackState state = positionTrackStates.computeIfAbsent(payload.getObjectId(), k -> new PositionTrackState());
double filteredLatitude = latitude;
double filteredLongitude = longitude;
synchronized (state) {
if (!state.initialized) {
initializeTrackState(state, latitude, longitude, filterTimestampMs);
} else {
if (filterTimestampMs <= state.lastPayloadTimestampMs) {
// Use local monotonic time to avoid dropping legitimate points on equal/out-of-order source timestamps.
filterTimestampMs = System.currentTimeMillis();
if (filterTimestampMs <= state.lastPayloadTimestampMs) {
filterTimestampMs = state.lastPayloadTimestampMs + 1;
}
}
long sourceDtMs = filterTimestampMs - state.lastPayloadTimestampMs;
long arrivalDtMs = System.currentTimeMillis() - state.lastAcceptedAtMs;
long boundedArrivalDtMs = clampLong(arrivalDtMs, 0L, Math.max(maxEffectiveDtMs, 50L));
long boundedSourceDtMs = clampLong(sourceDtMs, 0L, Math.max(maxEffectiveDtMs, 50L));
long effectiveDtMs = Math.max(boundedSourceDtMs, boundedArrivalDtMs);
double dtSec = Math.max(effectiveDtMs / 1000.0, MIN_DT_SECONDS);
double rawDistance = calculateDistanceMeters(
state.lastRawLatitude,
state.lastRawLongitude,
latitude,
longitude
);
double rawSpeedMps = rawDistance / Math.max(dtSec, MIN_DT_SECONDS);
AdaptiveFilterParams adaptive = resolveAdaptiveFilterParams(payload.getObjectType(), rawSpeedMps);
boolean aircraft = isAircraftType(payload.getObjectType());
double effectiveSpeedCapMps = resolveEffectiveSpeedCapMps(payload, adaptive.maxSpeedMps(), aircraft);
double maxAllowedJump = resolveMaxAllowedJumpMeters(effectiveSpeedCapMps, adaptive.jumpMarginMeter(), dtSec);
boolean isOutlierJump = rawDistance > maxAllowedJump;
if (isOutlierJump) {
if (aircraft) {
double nearThreshold = Math.max(maxAllowedJump * clamp(aircraftReacquireNearFactor, 0.2, 2.0), adaptive.jitterMeter());
if (state.aircraftPendingReacquire) {
double pendingDistance = calculateDistanceMeters(
state.aircraftPendingLatitude,
state.aircraftPendingLongitude,
latitude,
longitude
);
if (pendingDistance <= nearThreshold) {
state.aircraftReacquireCount++;
} else {
state.aircraftReacquireCount = 1;
state.aircraftPendingLatitude = latitude;
state.aircraftPendingLongitude = longitude;
}
} else {
state.aircraftPendingReacquire = true;
state.aircraftReacquireCount = 1;
state.aircraftPendingLatitude = latitude;
state.aircraftPendingLongitude = longitude;
}
if (state.aircraftReacquireCount < AIRCRAFT_REACQUIRE_CONFIRM_POINTS) {
// During reacquire confirmation, keep moving with a bounded step instead of freezing.
double holdStepLimit = Math.max(maxAllowedJump, adaptive.jitterMeter() * 2.0);
if (rawDistance > holdStepLimit) {
double ratio = holdStepLimit / rawDistance;
filteredLatitude = state.lastFilteredLatitude + ratio * (latitude - state.lastFilteredLatitude);
filteredLongitude = state.lastFilteredLongitude + ratio * (longitude - state.lastFilteredLongitude);
} else {
filteredLatitude = latitude;
filteredLongitude = longitude;
}
log.warn("[TMP-POS-FIX] reason=aircraft_outlier_blend objectId={} rawLatitude={} rawLongitude={} jumpMeter={} stepMeter={} confirm={}/{}",
safeObjectId(payload), rawLatitude, rawLongitude, rawDistance, holdStepLimit,
state.aircraftReacquireCount, AIRCRAFT_REACQUIRE_CONFIRM_POINTS);
} else {
state.aircraftPendingReacquire = false;
state.aircraftReacquireCount = 0;
double transitionMaxStep = Math.max(maxAllowedJump * clamp(aircraftMaxStepFactor, 1.0, 4.0), adaptive.jitterMeter() * 2.0);
if (rawDistance > transitionMaxStep) {
double ratio = transitionMaxStep / rawDistance;
filteredLatitude = state.lastFilteredLatitude + ratio * (latitude - state.lastFilteredLatitude);
filteredLongitude = state.lastFilteredLongitude + ratio * (longitude - state.lastFilteredLongitude);
log.warn("[TMP-POS-FIX] reason=aircraft_jump_step_limit objectId={} rawLatitude={} rawLongitude={} jumpMeter={} allowedStepMeter={}",
safeObjectId(payload), rawLatitude, rawLongitude, rawDistance, transitionMaxStep);
} else {
filteredLatitude = latitude;
filteredLongitude = longitude;
}
}
} else if (state.rejectedCount < REACQUIRE_AFTER_REJECTS - 1) {
state.rejectedCount++;
logDroppedPosition(
"outlier_jump_rejected",
payload,
rawLatitude,
rawLongitude,
String.format("jumpMeter=%.3f,allowedMeter=%.3f,rejectedCount=%d", rawDistance, maxAllowedJump, state.rejectedCount)
);
log.debug("Drop outlier position update: objectId={}, jumpMeter={}, allowedMeter={}",
payload.getObjectId(), rawDistance, maxAllowedJump);
return null;
} else {
state.rejectedCount = 0;
double transitionStepLimit = Math.max(maxAllowedJump, adaptive.jitterMeter() * 2.0);
if (rawDistance > transitionStepLimit) {
double ratio = transitionStepLimit / rawDistance;
filteredLatitude = state.lastFilteredLatitude + ratio * (latitude - state.lastFilteredLatitude);
filteredLongitude = state.lastFilteredLongitude + ratio * (longitude - state.lastFilteredLongitude);
log.warn("[TMP-POS-FIX] reason=non_aircraft_jump_step_limit objectId={} rawLatitude={} rawLongitude={} jumpMeter={} allowedStepMeter={}",
safeObjectId(payload), rawLatitude, rawLongitude, rawDistance, transitionStepLimit);
} else {
filteredLatitude = latitude;
filteredLongitude = longitude;
}
}
}
if (!isOutlierJump) {
state.rejectedCount = 0;
state.aircraftPendingReacquire = false;
state.aircraftReacquireCount = 0;
if (aircraft) {
if (rawDistance <= adaptive.jitterMeter()) {
filteredLatitude = state.lastFilteredLatitude;
filteredLongitude = state.lastFilteredLongitude;
} else {
double alpha = resolveAircraftDynamicAlpha(adaptive.emaAlpha(), rawDistance, maxAllowedJump);
filteredLatitude = state.lastFilteredLatitude + alpha * (latitude - state.lastFilteredLatitude);
filteredLongitude = state.lastFilteredLongitude + alpha * (longitude - state.lastFilteredLongitude);
}
} else {
if (rawDistance <= adaptive.jitterMeter()) {
filteredLatitude = state.lastFilteredLatitude;
filteredLongitude = state.lastFilteredLongitude;
} else {
double alpha = clamp(adaptive.emaAlpha(), 0.05, 1.0);
filteredLatitude = state.lastFilteredLatitude + alpha * (latitude - state.lastFilteredLatitude);
filteredLongitude = state.lastFilteredLongitude + alpha * (longitude - state.lastFilteredLongitude);
}
}
}
state.lastRawLatitude = latitude;
state.lastRawLongitude = longitude;
state.lastPayloadTimestampMs = filterTimestampMs;
state.lastFilteredLatitude = filteredLatitude;
state.lastFilteredLongitude = filteredLongitude;
state.lastAcceptedAtMs = System.currentTimeMillis();
}
initializeTrackState(state, latitude, longitude, payloadTimestamp);
}
PositionUpdatePayload.Position filteredPosition = PositionUpdatePayload.Position.builder()
.latitude(filteredLatitude)
.longitude(filteredLongitude)
.latitude(latitude)
.longitude(longitude)
.build();
return PositionUpdatePayload.builder()

View File

@ -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;
}
}

View File

@ -2,11 +2,18 @@ package com.qaup.collision.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qaup.collision.service.PlatformRuntimeStateService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.util.ReflectionTestUtils;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@ -16,10 +23,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class PlatformIntegrationControllerTest {
private MockMvc mockMvc;
private PlatformRuntimeStateService runtimeStateService;
@BeforeEach
void setUp() {
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
ReflectionTestUtils.setField(runtimeStateService, "objectRouteAssignmentRepository", mock(ObjectRouteAssignmentRepository.class));
ReflectionTestUtils.setField(runtimeStateService, "transportRouteRepository", mock(TransportRouteRepository.class));
PlatformIntegrationController controller =
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
@ -40,6 +50,7 @@ class PlatformIntegrationControllerTest {
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value("success"))
.andExpect(jsonPath("$.updated").value(3))
.andExpect(jsonPath("$.testSessionId").isNotEmpty())
.andExpect(jsonPath("$.controllableCount").value(2))
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
@ -119,6 +130,7 @@ class PlatformIntegrationControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"vehicleDistance\":500}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.field").value("crossing_distance_threshold"))
.andExpect(jsonPath("$.oldVehicleDistance").value(40.0))
.andExpect(jsonPath("$.newVehicleDistance").value(500.0));
}
@ -129,6 +141,7 @@ class PlatformIntegrationControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"vehicleDistance\":500,\"aircraftDistance\":600}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.field").value("crossing_distance_threshold"))
.andExpect(jsonPath("$.newVehicleDistance").value(500.0))
.andExpect(jsonPath("$.newAircraftDistance").value(600.0))
.andExpect(jsonPath("$.updatedVehicle").value(true))
@ -143,4 +156,51 @@ class PlatformIntegrationControllerTest {
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("Invalid JSON"));
}
@Test
void shouldReportRegisteredObjectsMissingRouteBindings() throws Exception {
runtimeStateService.updateVehicleRegistry(java.util.List.of(
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG),
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN)
));
mockMvc.perform(post("/api/collision/preparation/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.testSessionId").isNotEmpty())
.andExpect(jsonPath("$.ready").value(false))
.andExpect(jsonPath("$.summary.registeredCount").value(3))
.andExpect(jsonPath("$.summary.missingRouteCount").value(3))
.andExpect(jsonPath("$.objects[0].routeBound").value(false));
}
@Test
void shouldExposeCurrentCrossingThresholdsInPreparationStatus() throws Exception {
mockMvc.perform(post("/api/collision/preparation/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.testSessionId").doesNotExist())
.andExpect(jsonPath("$.thresholds.vehicleDistance").value(40.0))
.andExpect(jsonPath("$.thresholds.aircraftDistance").value(40.0));
}
@Test
void shouldEndCurrentTestSessionWhenRegistryIsEmpty() throws Exception {
mockMvc.perform(post("/api/VehicleRegistry")
.contentType(MediaType.APPLICATION_JSON)
.content("""
[
{ "vehicleID": "WR01", "vehicleType": "WUREN" },
{ "vehicleID": "CD423", "vehicleType": "HANGKONG" }
]
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.testSessionId").isNotEmpty());
mockMvc.perform(post("/api/VehicleRegistry")
.contentType(MediaType.APPLICATION_JSON)
.content("[]"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.testSessionId").doesNotExist())
.andExpect(jsonPath("$.endedTestSessionId").isNotEmpty());
}
}

View File

@ -28,7 +28,6 @@ import org.springframework.web.filter.CorsFilter;
import java.util.List;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(controllers = PlatformIntegrationController.class)
@ -71,7 +70,10 @@ class PlatformIntegrationSecurityTest {
@BeforeEach
void setUp() throws Exception {
Mockito.when(permitAllUrlProperties.getUrls()).thenReturn(List.of());
Mockito.when(permitAllUrlProperties.getUrls()).thenReturn(List.of(
"/config/collision/diverging_release_distance",
"/api/collision/preparation/status"
));
Mockito.when(userDetailsService.loadUserByUsername(Mockito.anyString()))
.thenReturn(User.withUsername("user").password("{noop}pwd").authorities("ROLE_USER").build());
@ -101,6 +103,13 @@ class PlatformIntegrationSecurityTest {
Mockito.when(platformRuntimeStateService.updateCollisionDivergingReleaseDistance(Mockito.anyDouble()))
.thenReturn(40.0);
Mockito.when(platformRuntimeStateService.getCollisionPreparationStatus())
.thenReturn(new PlatformRuntimeStateService.CollisionPreparationStatus(
null,
true,
new PlatformRuntimeStateService.CollisionPreparationSummary(0, 0),
List.of()
));
}
@Test
@ -108,9 +117,9 @@ class PlatformIntegrationSecurityTest {
mockMvc.perform(post("/config/collision/diverging_release_distance")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"value\":50}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("success"))
.andExpect(jsonPath("$.old").value(40.0))
.andExpect(jsonPath("$.new").value(50.0));
.andExpect(status().isOk());
mockMvc.perform(post("/api/collision/preparation/status"))
.andExpect(status().isOk());
}
}

View File

@ -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"));
}
}

View File

@ -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());
}
}

View File

@ -307,10 +307,10 @@ class VehicleStatusUpdateIntegrationTest {
.altitude(0.0)
.build());
when(platformRuntimeStateService.isRegisteredForCollision(eq(registeredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
.thenReturn(true);
when(platformRuntimeStateService.isRegisteredForCollision(eq(unregisteredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
.thenReturn(false);
when(platformRuntimeStateService.getRegisteredCollisionObjectType(registeredUvId))
.thenReturn(MovingObjectType.UNMANNED_VEHICLE);
when(platformRuntimeStateService.getRegisteredCollisionObjectType(unregisteredUvId))
.thenReturn(null);
dataProcessingService.performPeriodicDataProcessing();

View File

@ -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("\"", "\\\"") + "\"}]";
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -3,6 +3,7 @@ package com.qaup.collision.pathconflict.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
@ -25,6 +26,7 @@ import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -50,6 +52,9 @@ class PathConflictDetectionDirectionalTest {
@Mock
private PlatformRuntimeStateService platformRuntimeStateService;
@Mock
private VehicleCommandService vehicleCommandService;
@InjectMocks
private PathConflictDetectionService pathConflictDetectionService;
@ -130,10 +135,13 @@ class PathConflictDetectionDirectionalTest {
when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble()))
.thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)});
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
PathConflictDetectionService.ConflictDetectionSummary summary =
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
verify(conflictAlertLogRepository, never()).save(any());
verify(eventPublisher, never()).publishEvent(any());
org.junit.jupiter.api.Assertions.assertEquals(1, summary.supportedPairs());
org.junit.jupiter.api.Assertions.assertEquals(1, summary.intersectionBehindPairs());
}
@Test
@ -160,9 +168,95 @@ class PathConflictDetectionDirectionalTest {
.altitude(0.0)
.build();
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
PathConflictDetectionService.ConflictDetectionSummary summary =
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
verify(conflictAlertLogRepository, never()).save(any());
verify(eventPublisher, never()).publishEvent(any());
org.junit.jupiter.api.Assertions.assertEquals(0, summary.supportedPairs());
}
@Test
void shouldStillEvaluateConflictWhenAircraftIsWithinTwoHundredMetersOfAssignedRoute() throws Exception {
GeometryFactory geometryFactory = new GeometryFactory();
LineString vehicleRoute = geometryFactory.createLineString(new Coordinate[]{
new Coordinate(0.0, 0.0),
new Coordinate(300.0, 0.0)
});
LineString aircraftRoute = geometryFactory.createLineString(new Coordinate[]{
new Coordinate(150.0, -300.0),
new Coordinate(150.0, 300.0)
});
MovingObject vehicle = MovingObject.builder()
.objectId("UV-1")
.objectName("UV-1")
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.currentPosition(geometryFactory.createPoint(new Coordinate(30.0, 0.0)))
.currentSpeed(18.0)
.currentHeading(90.0)
.altitude(0.0)
.build();
MovingObject aircraft = MovingObject.builder()
.objectId("AC-1")
.objectName("AC-1")
.objectType(MovingObjectType.AIRCRAFT)
.currentPosition(geometryFactory.createPoint(new Coordinate(0.0, -150.0)))
.currentSpeed(20.0)
.currentHeading(0.0)
.altitude(0.0)
.build();
ObjectRouteAssignment vehicleAssignment = ObjectRouteAssignment.builder()
.objectName("UV-1")
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
.assignedRouteId(1L)
.build();
ObjectRouteAssignment aircraftAssignment = ObjectRouteAssignment.builder()
.objectName("AC-1")
.objectType(ObjectRouteAssignment.ObjectType.AIRCRAFT)
.assignedRouteId(2L)
.build();
TransportRoute route1 = TransportRoute.builder()
.id(1L)
.routeName("UV_ROUTE")
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
.status(TransportRoute.RouteStatus.ACTIVE)
.routeGeometry(vehicleRoute)
.build();
TransportRoute route2 = TransportRoute.builder()
.id(2L)
.routeName("AC_ROUTE")
.routeType(TransportRoute.RouteType.AIRCRAFT)
.status(TransportRoute.RouteStatus.ACTIVE)
.routeGeometry(aircraftRoute)
.build();
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc("UV-1", ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE))
.thenReturn(Optional.of(vehicleAssignment));
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc("AC-1", ObjectRouteAssignment.ObjectType.AIRCRAFT))
.thenReturn(Optional.of(aircraftAssignment));
when(routeRepository.findById(1L)).thenReturn(Optional.of(route1));
when(routeRepository.findById(2L)).thenReturn(Optional.of(route2));
when(coordinateSystemService.convertToLocalCoordinate(anyDouble(), anyDouble()))
.thenAnswer(invocation -> new double[]{invocation.getArgument(0), invocation.getArgument(1)});
when(platformRuntimeStateService.getCollisionDivergingReleaseDistanceForVehicle()).thenReturn(200.0);
when(platformRuntimeStateService.getCollisionDivergingReleaseDistanceForAircraft()).thenReturn(200.0);
when(conflictAlertLogRepository.save(any())).thenAnswer(invocation -> {
com.qaup.collision.pathconflict.model.entity.ConflictAlertLog logEntry = invocation.getArgument(0);
logEntry.setId(1L);
return logEntry;
});
PathConflictDetectionService.ConflictDetectionSummary summary =
pathConflictDetectionService.detectPathConflicts(List.of(vehicle, aircraft));
verify(conflictAlertLogRepository, atLeastOnce()).save(any());
verify(eventPublisher, atLeastOnce()).publishEvent(any(ConflictAlertEvent.class));
org.junit.jupiter.api.Assertions.assertEquals(1, summary.supportedPairs());
org.junit.jupiter.api.Assertions.assertEquals(1, summary.eventsPublished());
}
}

View File

@ -2,11 +2,7 @@ package com.qaup.collision.pathconflict.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -20,12 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import java.util.Arrays;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* 测试PathConflictDetectionService处理null速度的情况
@ -33,12 +24,6 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PathConflictDetectionServiceNullSpeedTest {
@Mock
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
@Mock
private TransportRouteRepository routeRepository;
@Mock
private ConflictAlertLogRepository conflictAlertLogRepository;
@ -84,29 +69,6 @@ class PathConflictDetectionServiceNullSpeedTest {
.altitude(0.0)
.build();
// Mock路径分配和路径数据
ObjectRouteAssignment assignment1 = new ObjectRouteAssignment();
assignment1.setAssignedRouteId(1L);
ObjectRouteAssignment assignment2 = new ObjectRouteAssignment();
assignment2.setAssignedRouteId(2L);
TransportRoute route1 = new TransportRoute();
route1.setId(1L);
route1.setRouteName("测试路径1");
TransportRoute route2 = new TransportRoute();
route2.setId(2L);
route2.setRouteName("测试路径2");
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
anyString(), any(ObjectRouteAssignment.ObjectType.class)))
.thenReturn(Optional.of(assignment1))
.thenReturn(Optional.of(assignment2));
when(routeRepository.findById(1L)).thenReturn(Optional.of(route1));
when(routeRepository.findById(2L)).thenReturn(Optional.of(route2));
// 测试调用detectPathConflicts不应该抛出NullPointerException
assertDoesNotThrow(() -> {
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));
@ -139,29 +101,6 @@ class PathConflictDetectionServiceNullSpeedTest {
.altitude(0.0)
.build();
// Mock路径分配和路径数据
ObjectRouteAssignment assignment1 = new ObjectRouteAssignment();
assignment1.setAssignedRouteId(1L);
ObjectRouteAssignment assignment2 = new ObjectRouteAssignment();
assignment2.setAssignedRouteId(2L);
TransportRoute route1 = new TransportRoute();
route1.setId(1L);
route1.setRouteName("测试路径1");
TransportRoute route2 = new TransportRoute();
route2.setId(2L);
route2.setRouteName("测试路径2");
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
anyString(), any(ObjectRouteAssignment.ObjectType.class)))
.thenReturn(Optional.of(assignment1))
.thenReturn(Optional.of(assignment2));
when(routeRepository.findById(1L)).thenReturn(Optional.of(route1));
when(routeRepository.findById(2L)).thenReturn(Optional.of(route2));
// 测试调用detectPathConflicts不应该抛出NullPointerException
assertDoesNotThrow(() -> {
pathConflictDetectionService.detectPathConflicts(Arrays.asList(obj1, obj2));

View File

@ -3,6 +3,8 @@ package com.qaup.collision.pathconflict.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
@ -14,12 +16,14 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class PathConflictDetectionServiceRuntimeConfigTest {
@Test
void shouldUseRuntimeThresholdsWhenEvaluatingAlertLevel() {
void shouldUseCollisionDistanceThresholdsWhenEvaluatingAlertLevel() {
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
PathConflictDetectionService service = new PathConflictDetectionService(
mock(TransportRouteRepository.class),
@ -36,30 +40,106 @@ class PathConflictDetectionServiceRuntimeConfigTest {
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
service,
"evaluateAlertLevel",
140.0,
35.0,
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
500.0,
MovingObject.MovingObjectType.AIRCRAFT,
20.0
30.0,
MovingObject.MovingObjectType.AIRCRAFT
);
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL), beforeUpdate);
runtimeStateService.updateRunwayAlertZoneRadiusAircraft(100.0);
runtimeStateService.updateRunwayWarningZoneRadiusAircraft(130.0);
runtimeStateService.updateCollisionDivergingReleaseDistanceForVehicle(20.0);
runtimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(25.0);
@SuppressWarnings("unchecked")
Optional<ConflictAlertLog.AlertLevel> afterUpdate =
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
service,
"evaluateAlertLevel",
140.0,
35.0,
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
500.0,
MovingObject.MovingObjectType.AIRCRAFT,
20.0
30.0,
MovingObject.MovingObjectType.AIRCRAFT
);
assertEquals(Optional.empty(), afterUpdate);
}
@Test
void shouldReturnWarningWhenOnlyOneSideEntersThreshold() {
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
runtimeStateService.updateCollisionDivergingReleaseDistanceForVehicle(50.0);
runtimeStateService.updateCollisionDivergingReleaseDistanceForAircraft(20.0);
PathConflictDetectionService service = new PathConflictDetectionService(
mock(TransportRouteRepository.class),
mock(ObjectRouteAssignmentRepository.class),
mock(ConflictAlertLogRepository.class),
mock(ApplicationEventPublisher.class),
mock(CoordinateSystemService.class),
runtimeStateService,
mock(VehicleCommandService.class)
);
@SuppressWarnings("unchecked")
Optional<ConflictAlertLog.AlertLevel> result =
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
service,
"evaluateAlertLevel",
30.0,
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
25.0,
MovingObject.MovingObjectType.AIRCRAFT
);
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.WARNING), result);
}
@Test
void shouldResolveAssignedRouteUsingObjectIdWhenObjectNameDiffers() {
TransportRouteRepository routeRepository = mock(TransportRouteRepository.class);
ObjectRouteAssignmentRepository assignmentRepository = mock(ObjectRouteAssignmentRepository.class);
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
PathConflictDetectionService service = new PathConflictDetectionService(
routeRepository,
assignmentRepository,
mock(ConflictAlertLogRepository.class),
mock(ApplicationEventPublisher.class),
mock(CoordinateSystemService.class),
runtimeStateService,
mock(VehicleCommandService.class)
);
MovingObject aircraft = MovingObject.builder()
.objectId("AC001")
.objectName("CA3456")
.objectType(MovingObject.MovingObjectType.AIRCRAFT)
.build();
ObjectRouteAssignment assignment = ObjectRouteAssignment.builder()
.id(1L)
.objectType(ObjectRouteAssignment.ObjectType.AIRCRAFT)
.objectName("AC001")
.assignedRouteId(99L)
.build();
TransportRoute route = TransportRoute.builder()
.id(99L)
.routeName("TEST_AIRCRAFT_ROUTE")
.routeType(TransportRoute.RouteType.AIRCRAFT)
.status(TransportRoute.RouteStatus.ACTIVE)
.build();
when(assignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
"AC001",
ObjectRouteAssignment.ObjectType.AIRCRAFT
)).thenReturn(Optional.of(assignment));
when(routeRepository.findById(99L)).thenReturn(Optional.of(route));
TransportRoute resolved = (TransportRoute) ReflectionTestUtils.invokeMethod(service, "getObjectRoute", aircraft);
assertNotNull(resolved);
assertEquals(99L, resolved.getId());
}
}

View File

@ -13,7 +13,6 @@ import org.locationtech.jts.geom.Point;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.test.web.client.MockRestServiceServer;
@ -30,7 +29,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
class VehicleCommandServiceTest {
private static final String COMMAND_URL = "http://vehicle.example/api/VehicleCommandInfo";
private static final String COMMAND_URL = "http://10.232.18.23:8020/api/VehicleCommandInfo";
private VehicleCommandService vehicleCommandService;
private MockRestServiceServer server;
@ -43,8 +42,6 @@ class VehicleCommandServiceTest {
server = MockRestServiceServer.bindTo(restTemplate).build();
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
ReflectionTestUtils.setField(vehicleCommandService, "vehicleApiBaseUrl", "http://vehicle.example");
ReflectionTestUtils.setField(vehicleCommandService, "vehicleCommandEndpoint", "/api/VehicleCommandInfo");
geometryFactory = new GeometryFactory();
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -1,13 +1,21 @@
package com.qaup.collision.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.system.domain.SysConfig;
import com.qaup.system.service.ISysConfigService;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class PlatformRuntimeStateServiceTest {
@ -55,6 +63,33 @@ class PlatformRuntimeStateServiceTest {
assertEquals(55.0, service.getCollisionDivergingReleaseDistance());
}
@Test
void shouldPersistCrossingThresholdsWhenUpdated() {
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
ISysConfigService sysConfigService = mock(ISysConfigService.class);
when(sysConfigService.selectConfigList(any(SysConfig.class))).thenReturn(List.of());
ReflectionTestUtils.setField(service, "sysConfigService", sysConfigService);
service.updateCollisionDivergingReleaseDistanceForVehicle(60.0);
service.updateCollisionDivergingReleaseDistanceForAircraft(80.0);
verify(sysConfigService, times(2)).insertConfig(any(SysConfig.class));
}
@Test
void shouldLoadPersistedCrossingThresholdsOnStartup() {
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
ISysConfigService sysConfigService = mock(ISysConfigService.class);
when(sysConfigService.selectConfigByKey("qaup.collision.crossing.vehicleDistance")).thenReturn("65");
when(sysConfigService.selectConfigByKey("qaup.collision.crossing.aircraftDistance")).thenReturn("85");
ReflectionTestUtils.setField(service, "sysConfigService", sysConfigService);
service.loadPersistedCrossingThresholds();
assertEquals(65.0, service.getCollisionDivergingReleaseDistanceForVehicle());
assertEquals(85.0, service.getCollisionDivergingReleaseDistanceForAircraft());
}
@Test
void shouldOnlyAllowRegisteredCollisionTypesToParticipate() {
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
@ -75,4 +110,42 @@ class PlatformRuntimeStateServiceTest {
assertEquals(List.of("QN001", "TQ001"), service.getControllableVehicleIds());
assertEquals(List.of("QN001", "TQ001"), service.getTrafficLightRecipientVehicleIds());
}
@Test
void shouldReportRemovedObjectsWhenVehicleRegistryIsResubmitted() {
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
service.updateVehicleRegistry(List.of(
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
));
PlatformRuntimeStateService.VehicleRegistryUpdateResult second = service.updateVehicleRegistry(List.of(
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
));
assertEquals(List.of("AC001", "TQ001"), second.removedVehicleIDs());
}
@Test
void shouldCreateAndEndCollisionTestSessionFromRegistryUpdates() {
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
PlatformRuntimeStateService.VehicleRegistryUpdateResult started = service.updateVehicleRegistry(List.of(
new PlatformRuntimeStateService.VehicleRegistryEntry("WR01", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
new PlatformRuntimeStateService.VehicleRegistryEntry("CD423", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
));
assertTrue(started.testSessionId().startsWith("collision-test-"));
assertEquals(started.testSessionId(), service.getCurrentTestSessionId());
PlatformRuntimeStateService.CollisionPreparationStatus status = service.getCollisionPreparationStatus();
assertEquals(started.testSessionId(), status.testSessionId());
PlatformRuntimeStateService.VehicleRegistryUpdateResult ended = service.updateVehicleRegistry(List.of());
assertEquals(started.testSessionId(), ended.endedTestSessionId());
assertEquals(null, ended.testSessionId());
assertEquals(null, service.getCurrentTestSessionId());
}
}

View File

@ -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();
}
}