From d647ab17bc6db50d0cbc884ef934f15d140f7c85 Mon Sep 17 00:00:00 2001 From: Tian jianyong <11429339@qq.com> Date: Thu, 25 Sep 2025 14:12:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dmock=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E4=B8=AD=E5=9D=90=E6=A0=87=E8=BD=AC=E6=8D=A2=EF=BC=88=E7=94=A8?= =?UTF-8?q?=E6=A0=87=E5=87=86=E5=BA=93=E6=9D=A5=E8=BF=9B=E8=A1=8CCGCS2000?= =?UTF-8?q?=E5=92=8CWGS84=E7=9A=84=E8=BD=AC=E6=8D=A2=EF=BC=8C=E5=92=8C?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E6=96=B9=E5=90=91=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 21 + doc/design/CA1234.json | 50 + doc/design/MU5123.json | 54 + doc/design/坐标系.md | 28 + ...ollectorServiceFlightNotificationTest.java | 4 +- test_trigger_route_collection.md | 70 - .../__pycache__/mock_airport.cpython-313.pyc | Bin 0 -> 83296 bytes tools/mock_airport.py | 1631 ++++++++++------- 8 files changed, 1124 insertions(+), 734 deletions(-) create mode 100644 doc/design/CA1234.json create mode 100644 doc/design/MU5123.json create mode 100644 doc/design/坐标系.md delete mode 100644 test_trigger_route_collection.md create mode 100644 tools/__pycache__/mock_airport.cpython-313.pyc diff --git a/CLAUDE.md b/CLAUDE.md index 9a9abd61..c38366b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,6 +131,27 @@ tail -f qaup-admin/app.log ### Data Access Always use `QuapDataAdapter` for accessing vehicle/driver data in collision module to maintain clean separation between collision detection and system management. +### **CRITICAL: Serena MCP Token Management** +**Claude MUST follow these rules to prevent excessive token usage:** + +#### **Mandatory Parameters** +- **Always set max_answer_chars=2000** +- **Always set depth=0** +- **Always set include_body=false** +- **Always specify exact relative_path** (never use ".") + +#### **Simple Usage Pattern** +```bash +# Good - controlled usage +mcp__serena__find_symbol(name_path="ClassName", relative_path="path/to/file.java", include_body=false, depth=0, max_answer_chars=2000) + +# Bad - excessive tokens +mcp__serena__find_symbol(depth=1) # Never use depth=1 +mcp__serena__search_for_pattern(relative_path=".") # Never search entire project +``` + +**This reduces token usage from 9930+ characters to under 500 characters per query.** + ### Real-time Communication - WebSocket endpoints use `/topic` prefix for broadcasting - PostGIS handles geometric calculations with airport center at (120.0834104, 36.35406879) diff --git a/doc/design/CA1234.json b/doc/design/CA1234.json new file mode 100644 index 00000000..afade0ae --- /dev/null +++ b/doc/design/CA1234.json @@ -0,0 +1,50 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 120.08508640012495, + 36.36182498963186 + ], + [ + 120.08090199425916, + 36.362577136200784 + ], + [ + 120.08077110864791, + 36.36212595046586 + ], + [ + 120.08004241896676, + 36.36188154498729 + ], + [ + 120.07859116299593, + 36.365372400901556 + ], + [ + 120.07757535108954, + 36.36533179518197 + ], + [ + 120.07649732717964, + 36.36814042245338 + ], + [ + 120.07441715579117, + 36.36757367925402 + ] + ] + }, + "properties": { + "type": "route", + "length": 1267.4987536104536, + "vertices": 8 + } + } + ] +} \ No newline at end of file diff --git a/doc/design/MU5123.json b/doc/design/MU5123.json new file mode 100644 index 00000000..dfcbf2b5 --- /dev/null +++ b/doc/design/MU5123.json @@ -0,0 +1,54 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 120.09353070859456, + 36.376457497442686 + ], + [ + 120.09118060340178, + 36.37587688907353 + ], + [ + 120.09370775664254, + 36.3694933442094 + ], + [ + 120.09309129621212, + 36.36913901651289 + ], + [ + 120.09421002369645, + 36.365617677245275 + ], + [ + 120.09345006300696, + 36.36536527657874 + ], + [ + 120.09260145183363, + 36.36542372582119 + ], + [ + 120.09143729509786, + 36.3644546644055 + ], + [ + 120.09000868390461, + 36.363111104274545 + ] + ] + }, + "properties": { + "type": "route", + "length": 1600.4912629822934, + "vertices": 9 + } + } + ] +} \ No newline at end of file diff --git a/doc/design/坐标系.md b/doc/design/坐标系.md new file mode 100644 index 00000000..62598859 --- /dev/null +++ b/doc/design/坐标系.md @@ -0,0 +1,28 @@ +## 坐标系 + +### 机场坐标系: +基于CGCS2000椭球的横轴墨卡托投影,特点: + +-CGCS2000坐标系详细内容 + + 1. 基本定义 + + - 全称: China Geodetic Coordinate System 2000(中国大地坐标系2000) + - 性质: 地心坐标系(与WGS84在定义上一致,差异仅0.11mm,相容至cm级) + - 启用时间: 2008年7月1日 + - 法定地位: 中国国家标准坐标系 + + 2. 椭球参数 + + - 长半轴: a = 6,378,137米 + - 扁率倒数: f⁻¹ = 298.257222101 + - 椭球: 基于GRS80椭球 + + 3. 横轴墨卡托投影参数(120°中央经线) + + 根据搜索结果,标准的CGCS2000 / 3-degree Gauss-Kruger zone 40参数: + - 中央经线: 120°E + - 比例因子: 1.0 + - 东偏移: 40,500,000米(其中40代表带号,500,000是标准偏移) + - 北偏移: 0米 + - 投影方式: 横轴墨卡托投影(高斯-克吕格投影) \ No newline at end of file diff --git a/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceFlightNotificationTest.java b/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceFlightNotificationTest.java index d81ea275..8699720d 100644 --- a/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceFlightNotificationTest.java +++ b/qaup-collision/src/test/java/com/qaup/collision/datacollector/service/DataCollectorServiceFlightNotificationTest.java @@ -59,7 +59,7 @@ class DataCollectorServiceFlightNotificationTest { @Test void testCollectFlightNotificationData_Success() { // 准备测试数据 - FlightNotificationDTO dto1 = createTestFlightNotificationDTO("CA3456", "IN", "02R", "A01", System.currentTimeMillis()); + FlightNotificationDTO dto1 = createTestFlightNotificationDTO("CA1234", "IN", "02R", "A01", System.currentTimeMillis()); FlightNotificationDTO dto2 = createTestFlightNotificationDTO("MU5678", "OUT", "02L", "B15", System.currentTimeMillis()); List mockNotifications = Arrays.asList(dto1, dto2); @@ -96,7 +96,7 @@ class DataCollectorServiceFlightNotificationTest { @Test void testCollectFlightNotificationData_WithInvalidData() { // 准备测试数据(包含无效数据) - FlightNotificationDTO validDto = createTestFlightNotificationDTO("CA3456", "IN", "02R", "A01", System.currentTimeMillis()); + FlightNotificationDTO validDto = createTestFlightNotificationDTO("CA1234", "IN", "02R", "A01", System.currentTimeMillis()); FlightNotificationDTO invalidDto = createTestFlightNotificationDTO(null, "INVALID", null, null, null); // 无效数据 List mockNotifications = Arrays.asList(validDto, invalidDto); diff --git a/test_trigger_route_collection.md b/test_trigger_route_collection.md deleted file mode 100644 index 9bca0784..00000000 --- a/test_trigger_route_collection.md +++ /dev/null @@ -1,70 +0,0 @@ -# 航空器路由触发式采集测试 - -## 修改摘要 - -已成功将航空器路由采集从周期性访问改为触发式访问: - -### 主要变更 - -1. **新增缓存机制** - - 添加了 `routeRetrievalCache` 缓存Map - - 缓存键格式:`{flightNo}:{type}:{time}` - - 配置项:路由缓存过期时间(默认2小时) - -2. **修改触发逻辑** - - 在 `triggerRouteQueryByFlightNotification()` 开始处添加重复检查 - - 成功获取路由后标记为已获取 - - 基于航班进出港通知的时间戳确保精确去重 - -3. **禁用周期性采集** - - `collectAircraftRouteAndStatus()` 默认禁用 - - 通过配置项 `data.collector.route.periodic-collection-enabled` 控制 - - 保留原有方法作为应急备用 - -4. **新增缓存管理** - - 定时清理过期缓存记录(每小时执行一次) - - 详细日志记录便于监控 - -### 配置说明 - -在 `application.yml` 中可添加以下配置: - -```yaml -data: - collector: - route: - cache-expiry-hours: 2 # 路由缓存过期时间(小时) - periodic-collection-enabled: false # 是否启用周期性采集(默认禁用) -``` - -### 预期效果 - -1. **避免重复API调用**:同一航班事件(相同航班号+类型+时间戳)的路由只会获取一次 -2. **事件驱动响应**:完全基于航班进出港通知触发,响应更及时 -3. **减少系统负载**:取消周期性轮询,减少不必要的API调用 -4. **保持数据完整性**:每个航班事件都能准确获取和存储路由信息 - -### 关键日志示例 - -- **首次获取**:`🛫 航班通知触发路由查询: 航班号=CA8901, 类型=OUT, 时间=1732783090000` -- **重复跳过**:`🔄 航班路由已获取过,跳过重复查询: 航班号=CA8901, 类型=OUT, 时间=1732783090000` -- **成功完成**:`🚀 事件驱动的路由更新完成: 航班号=CA8901, 路由类型=OUT, 时间=1732783090000` -- **缓存清理**:`清理过期路由缓存完成:清理前5条,清理后2条,清理了3条记录` - -## 测试验证步骤 - -1. **启动系统**:确保 mock_airport.py 正在运行(提供航班进出港通知) -2. **观察日志**:查看是否有 "周期性路由采集已禁用" 的调试日志 -3. **监控触发**:查看航班进出港通知是否正确触发路由查询 -4. **验证去重**:同一航班事件应该只触发一次路由获取 -5. **检查缓存**:验证缓存清理机制是否正常工作 - -## 回滚方案 - -如需恢复周期性采集,在配置文件中设置: -```yaml -data: - collector: - route: - periodic-collection-enabled: true -``` \ No newline at end of file diff --git a/tools/__pycache__/mock_airport.cpython-313.pyc b/tools/__pycache__/mock_airport.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ef6dc80ef7b675c5b9aa84ca572b429b83cee56 GIT binary patch literal 83296 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 literal 0 HcmV?d00001 diff --git a/tools/mock_airport.py b/tools/mock_airport.py index 022d3db9..e9077abe 100644 --- a/tools/mock_airport.py +++ b/tools/mock_airport.py @@ -4,6 +4,8 @@ import math import logging import os import uuid +import threading +import atexit from typing import Any, Literal, final, TypedDict # 创建 logs 目录(如果不存在) @@ -105,27 +107,27 @@ VEHICLE_SIZE_M = 10.0 # 修改为与路由参数配置一致的航班 current_time = time.time() flight_data = { - "CA1234": { # 出港航班 - 与路由参数配置一致 + "CA1234": { # 出港航班 "flightNo": "CA1234", "type": "OUT", - "runway": "17", # 使用路由参数中的inRunway - "contactCross": "A2", # 使用路由参数中的contactCross - "seat": "201", # 使用路由参数中的seat + "runway": "17", + "contactCross": "A2", + "seat": "201", "cycle_start": current_time, - "active_duration": 600, # 10分钟活跃期 - "gap_duration": 10, # 10秒间隔期 + "active_duration": 60, # 1分钟活跃期 + "gap_duration": 5, # 5秒间隔期 "fixed_time": int(current_time * 1000), # 固定的滑出时间 "status": "active" }, - "MU5123": { # 进港航班 - 与路由参数配置一致 + "MU5123": { # 进港航班 - 使用真实路由 "flightNo": "MU5123", "type": "IN", - "runway": "35", # 使用路由参数中的inRunway - "contactCross": "B3", # 使用路由参数中的contactCross - "seat": "156", # 使用路由参数中的seat + "runway": "35", + "contactCross": "F1", # F1滑行道 + "seat": "138", # 138机位 "cycle_start": current_time, - "active_duration": 900, # 15分钟活跃期 - "gap_duration": 10, # 10秒间隔期 + "active_duration": 60, # 1分钟活跃期 + "gap_duration": 5, # 5秒间隔期 "fixed_time": int(current_time * 1000), # 固定的落地时间 "status": "active" } @@ -133,19 +135,6 @@ flight_data = { # 航空器路由参数配置 aircraft_route_params = { - "CA3456": { - "arrival": { # 进港参数 - "inRunway": "35", - "outRunway": "34", - "contactCross": "F1", - "seat": "138" - }, - "departure": { # 出港参数 - "inRunway": "35", - "outRunway": "34", - "startSeat": "138" - } - }, "CA1234": { "arrival": { "inRunway": "17", @@ -162,14 +151,14 @@ aircraft_route_params = { "MU5123": { "arrival": { "inRunway": "35", - "outRunway": "17", - "contactCross": "B3", - "seat": "156" + "outRunway": "34", + "contactCross": "F1", + "seat": "138" }, "departure": { "inRunway": "35", - "outRunway": "17", - "startSeat": "156" + "outRunway": "34", + "startSeat": "138" } } } @@ -262,29 +251,54 @@ def get_aircraft_route_params(): } }) -ca3456_status = { - "flightNo": "CA3456", - "type": "IN", # IN: 进港, ARRIVED: 停留, OUT: 出港 - "inRunway": "35", - "outRunway": "34", - "contactCross": "F1", - "seat": "138", - "timestamp": int(time.time() * 1000), - "status_start_time": time.time(), - "cycle_duration": 77, # 总循环时间: 30+15+30+2=77秒 - "arrival_duration": 30, # 进港阶段持续时间 - "wait_duration": 15, # 停留时间(15秒,方便测试) - "departure_duration": 30 # 出港阶段持续时间 -} -# 航空器路由数据 - 使用API文档中的完整示例数据 +# 航空器路由数据 - 每架飞机使用不同的路由 aircraft_routes = { - "arrival": { - "type": "IN", - "status": "COMPLETE", - "codes": "F1,L4,138", - "geometry": None, - "geoPath": { + + # CA1234的路由 - 简化的出港路由(17号跑道到201机位区域) + "CA1234": { + "departure": { + "type": "OUT", + "status": "COMPLETE", + "codes": "201,A2,17", + "geometry": None, + "coordinateSystem": "WGS84", + "geoPath": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [120.08508640012495, 36.36182498963186], + [120.08090199425916, 36.362577136200784], + [120.08077110864791, 36.36212595046586], + [120.08004241896676, 36.36188154498729], + [120.07859116299593, 36.365372400901556], + [120.07757535108954, 36.36533179518197], + [120.07649732717964, 36.36814042245338], + [120.07441715579117, 36.36757367925402] + ] + }, + "properties": { + "code": "A2" + } + } + ] + } + } + }, + + # MU5123的路由 - 真实完整进港路由(35号跑道经F1滑行道到138机位) + "MU5123": { + "arrival": { + "type": "IN", + "status": "COMPLETE", + "codes": "F1,L4,138", + "geometry": None, + "coordinateSystem": "CGCS2000", + "geoPath": { "type": "FeatureCollection", "features": [ { @@ -748,506 +762,12 @@ aircraft_routes = { } ] } - }, - "departure": { - "type": "OUT", - "status": "COMPLETE", - "codes": "138,L4,F1", - "geometry": None, - "geoPath": { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050742275893088E7, 4026164.644604296], - [4.050742342874898E7, 4026162.545793306] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050743615407222E7, 4026122.672208275], - [4.050743684026714E7, 4026120.146600441], - [4.050743730372977E7, 4026117.570797326], - [4.050743754093282E7, 4026114.964402468], - [4.050743757419489E7, 4026113.602043673], - [4.050743755007106E7, 4026112.347252104], - [4.050743733107493E7, 4026109.739264329], - [4.050743688561112E7, 4026107.160287504] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050717462298063E7, 4026091.904402129], - [4.050716820216861E7, 4026089.855066455] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050722536188381E7, 4026108.097315812], - [4.050720821283463E7, 4026102.624334418] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050727144214725E7, 4026112.527790001], - [4.050726278505515E7, 4026114.415332655] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050731882638656E7, 4026102.196402456], - [4.050727312768086E7, 4026112.160285922] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050738651815705E7, 4026087.437277401], - [4.050734647450486E7, 4026096.168165339] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050714461981621E7, 4026082.328947974], - [4.05071119278174E7, 4026071.895744022] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050734647450486E7, 4026096.168165339], - [4.050733913391775E7, 4026097.768664928] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050689454491971E7, 4026002.519737061], - [4.050693265139649E7, 4026014.681113256], - [4.050697075787329E7, 4026026.842489458] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050741162298967E7, 4026083.825606086], - [4.050741416963529E7, 4026084.285112275], - [4.050741669524226E7, 4026084.971307588], - [4.050741915143272E7, 4026085.875012957], - [4.050742151951354E7, 4026086.989350639], - [4.050742378146222E7, 4026088.305839852], - [4.050742592006397E7, 4026089.814461317], - [4.050742791904272E7, 4026091.503733515], - [4.050742976318505E7, 4026093.360800063], - [4.050743143845592E7, 4026095.371527565], - [4.050743293210549E7, 4026097.52061317], - [4.050743423276621E7, 4026099.791701039], - [4.050743533053925E7, 4026102.167506821], - [4.05074362170699E7, 4026104.629949201], - [4.050743683431807E7, 4026106.966150228], - [4.050743688561112E7, 4026107.160287504] - ] - }, - "properties": { - "code": "" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050697552118288E7, 4026028.362661481], - [4.050697075787329E7, 4026026.842489458] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050704221346159E7, 4026049.646966941], - [4.050703137036284E7, 4026046.18647901] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050708746004742E7, 4026064.087051627], - [4.050704840096232E7, 4026051.621473066] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05071119278174E7, 4026071.895744022], - [4.050710556055213E7, 4026069.863682419] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050741939071107E7, 4026175.198599438], - [4.05074216811156E7, 4026168.021835575], - [4.050742275893088E7, 4026164.644604296] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050753774654577E7, 4026246.448261945], - [4.050749515081406E7, 4026236.848849251], - [4.050744870329395E7, 4026226.381394062] - ] - }, - "properties": { - "code": "138" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050753774654577E7, 4026246.448261945], - [4.0507613391983E7, 4026263.495786141], - [4.05076192451935E7, 4026264.814870958], - [4.050762119626365E7, 4026265.254565894] - ] - }, - "properties": { - "code": "138" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050742342874898E7, 4026162.545793306], - [4.050743615407222E7, 4026122.672208275] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050716820216861E7, 4026089.855066455], - [4.050714461981621E7, 4026082.328947974] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050720821283463E7, 4026102.624334418], - [4.050717462298063E7, 4026091.904402129] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050726278505515E7, 4026114.415332655], - [4.050725934642086E7, 4026115.009285077], - [4.050725586910526E7, 4026115.301280484], - [4.050725237957282E7, 4026115.289096617], - [4.050724890438099E7, 4026114.9728262], - [4.050724546997807E7, 4026114.354876244], - [4.050724210250195E7, 4026113.43994972], - [4.050722536188381E7, 4026108.097315812] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050727312768086E7, 4026112.160285922], - [4.050727144214725E7, 4026112.527790001] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050733913391775E7, 4026097.768664928], - [4.050731882638656E7, 4026102.196402456] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05074011833275E7, 4026084.239767811], - [4.050738651815705E7, 4026087.437277401] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05074011833275E7, 4026084.239767811], - [4.050740376230329E7, 4026083.794303922], - [4.050740637029002E7, 4026083.5753078], - [4.050740898743934E7, 4026083.584446135], - [4.050741162298967E7, 4026083.825606086] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050744870329395E7, 4026226.381394062], - [4.050744533581797E7, 4026225.466467002], - [4.050744206089737E7, 4026224.261526533], - [4.050743890345625E7, 4026222.775742978], - [4.050743588752466E7, 4026221.020424048], - [4.050743303605565E7, 4026219.00892878], - [4.050743037075064E7, 4026216.756565868], - [4.050742791189419E7, 4026214.280477153], - [4.050742567819968E7, 4026211.599507165], - [4.050742368666689E7, 4026208.734059705], - [4.050742195245258E7, 4026205.705942559], - [4.050742048875517E7, 4026202.538201526], - [4.050741930671428E7, 4026199.254945029], - [4.050741841532595E7, 4026195.88116063], - [4.050741782137419E7, 4026192.442524868], - [4.050741752937933E7, 4026188.965207836], - [4.050741754156362E7, 4026185.475674017], - [4.050741785783435E7, 4026182.000480871], - [4.050741847578449E7, 4026178.566076715], - [4.050741939071107E7, 4026175.198599438] - ] - }, - "properties": { - "code": "" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050684675534101E7, 4025987.268076611], - [4.050685643844293E7, 4025990.358360866], - [4.050689454491971E7, 4026002.519737061] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050703137036284E7, 4026046.18647901], - [4.05070295640735E7, 4026045.610016264], - [4.050701981996215E7, 4026042.500261312], - [4.050697623399237E7, 4026028.590148907], - [4.050697552118288E7, 4026028.362661481] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050704840096232E7, 4026051.621473066], - [4.050704221346159E7, 4026049.646966941] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050710556055213E7, 4026069.863682419], - [4.050708746004742E7, 4026064.087051627] - ] - }, - "properties": { - "code": "F1" - } - } - ] - } + } } } -# 飞机数据 - 根据 route.md 配置 -aircraft_data = [ - { - "flightNo": "CA1234", # 根据route.md更新航班号 - "longitude": CA_START["longitude"], - "latitude": CA_START["latitude"], - "time": int(time.time() * 1000), - "altitude": 0.0, - "trackNumber": 1001, - "speed": 50.0, # 根据route.md设置为50km/h - "start_point": CA_START, # 起点 - "end_point": CA_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - }, - { - "flightNo": "MU5123", # 根据route.md更新航班号 - "longitude": MU_START["longitude"], - "latitude": MU_START["latitude"], - "time": int(time.time() * 1000), - "altitude": 0.0, - "trackNumber": 1002, - "speed": 50.0, # 根据route.md设置为50km/h - "start_point": MU_START, # 起点 - "end_point": MU_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - } -] +# 飞机数据将在路径跟随器创建后初始化 +aircraft_data = [] # 车辆数据,根据 route.md 配置 DEFAULT_VEHICLE_SPEED = 30.0 # km/h,默认速度 @@ -1415,6 +935,743 @@ vehicle_states = {} for vehicle in airport_vehicle_data: vehicle_states[str(vehicle["vehicleNo"])] = VehicleState(str(vehicle["vehicleNo"])) +# 坐标转换系统 - 使用pyproj进行精确转换 +class AirportCoordinateSystem: + """机场坐标转换系统,使用pyproj进行精确的CGCS2000横轴墨卡托投影转换""" + + def __init__(self): + # 导入pyproj库 + try: + from pyproj import CRS, Transformer + import warnings + warnings.filterwarnings('ignore', category=UserWarning, module='pyproj') + + # 定义CGCS2000坐标系(横轴墨卡托投影,120°E中央经线) + cgcs2000_proj4 = '+proj=tmerc +ellps=GRS80 +lon_0=120 +x_0=40500000 +y_0=0 +k_0=1.0 +units=m +no_defs' + self.cgcs2000_crs = CRS.from_proj4(cgcs2000_proj4) + self.wgs84_crs = CRS.from_epsg(4326) # WGS84 + + # 创建转换器 + self.transformer_to_wgs84 = Transformer.from_crs(self.cgcs2000_crs, self.wgs84_crs, always_xy=True) + self.transformer_to_cgcs2000 = Transformer.from_crs(self.wgs84_crs, self.cgcs2000_crs, always_xy=True) + + # 机场中心坐标(基于CGCS2000投影坐标) + self.utm_origin_x = 40507423 # 机场中心的CGCS2000 X坐标 + self.utm_origin_y = 4026164 # 机场中心的CGCS2000 Y坐标 + + # 计算机场中心的WGS84坐标 + center_lon, center_lat = self.transformer_to_wgs84.transform(self.utm_origin_x, self.utm_origin_y) + self.center_lon = center_lon + self.center_lat = center_lat + + print(f"机场中心CGCS2000坐标: X={self.utm_origin_x}, Y={self.utm_origin_y}") + print(f"机场中心WGS84坐标: lon={self.center_lon:.10f}, lat={self.center_lat:.10f}") + + except ImportError: + print("警告: pyproj库未安装,将回退到简化转换算法") + self.use_pyproj = False + # 回退到原来的参数 + self.center_lon = 120.0834104 + self.center_lat = 36.35406879 + self.utm_origin_x = 40507423 + self.utm_origin_y = 4026164 + self.meters_per_degree_lon = 89932 + self.meters_per_degree_lat = 111320 + except Exception as e: + print(f"pyproj初始化失败: {str(e)},将回退到简化转换算法") + self.use_pyproj = False + else: + self.use_pyproj = True + + def cgcs2000_to_wgs84(self, x: float, y: float) -> tuple[float, float]: + """ + 将CGCS2000投影坐标转换为WGS84地理坐标 + 使用pyproj进行精确转换 + + Args: + x: CGCS2000 X坐标(东向) + y: CGCS2000 Y坐标(北向) + + Returns: + tuple: (纬度, 经度) WGS84坐标 + """ + try: + if self.use_pyproj: + # 使用pyproj进行精确转换 + longitude, latitude = self.transformer_to_wgs84.transform(float(x), float(y)) + return latitude, longitude + else: + # 回退到简化算法 + return self._fallback_cgcs2000_to_wgs84(x, y) + except Exception as e: + print(f"坐标转换错误: {str(e)}, x={x}, y={y}") + return self.center_lat, self.center_lon + + def wgs84_to_cgcs2000(self, latitude: float, longitude: float) -> tuple[float, float]: + """ + 将WGS84地理坐标转换为CGCS2000投影坐标 + 使用pyproj进行精确转换 + + Args: + latitude: WGS84纬度 + longitude: WGS84经度 + + Returns: + tuple: (CGCS2000 X坐标, CGCS2000 Y坐标) + """ + try: + if self.use_pyproj: + # 使用pyproj进行精确转换 + x, y = self.transformer_to_cgcs2000.transform(float(longitude), float(latitude)) + return x, y + else: + # 回退到简化算法 + return self._fallback_wgs84_to_cgcs2000(latitude, longitude) + except Exception as e: + print(f"WGS84到CGCS2000坐标转换错误: {str(e)}, lat={latitude}, lon={longitude}") + return self.utm_origin_x, self.utm_origin_y + + def _fallback_cgcs2000_to_wgs84(self, x: float, y: float) -> tuple[float, float]: + """回退的简化转换算法""" + offset_x = float(x) - self.utm_origin_x + offset_y = float(y) - self.utm_origin_y + lon_offset = offset_x / self.meters_per_degree_lon + lat_offset = offset_y / self.meters_per_degree_lat + longitude = self.center_lon + lon_offset + latitude = self.center_lat + lat_offset + return latitude, longitude + + def _fallback_wgs84_to_cgcs2000(self, latitude: float, longitude: float) -> tuple[float, float]: + """回退的简化转换算法""" + lon_offset = float(longitude) - self.center_lon + lat_offset = float(latitude) - self.center_lat + offset_x = lon_offset * self.meters_per_degree_lon + offset_y = lat_offset * self.meters_per_degree_lat + x = self.utm_origin_x + offset_x + y = self.utm_origin_y + offset_y + return x, y + + def batch_convert_coordinates(self, coordinates: list[list[float]]) -> list[tuple[float, float]]: + """批量转换坐标列表""" + converted = [] + for coord in coordinates: + if len(coord) >= 2: + lat, lon = self.cgcs2000_to_wgs84(coord[0], coord[1]) + converted.append((lat, lon)) + return converted + +# 全局坐标转换系统实例 +coordinate_system = AirportCoordinateSystem() + +def convert_route_to_cgcs2000(route_data: dict) -> dict: + """ + 将路由数据转换为CGCS2000坐标系格式 + + Args: + route_data: 原始路由数据 + + Returns: + dict: 转换后的路由数据(统一使用CGCS2000坐标系) + """ + import copy + + # 深度复制路由数据,避免修改原始数据 + converted_route = copy.deepcopy(route_data) + + try: + # 检查当前坐标系类型 + current_coordinate_system = route_data.get("coordinateSystem", "WGS84") + logging.info(f"转换路由数据坐标系: 当前={current_coordinate_system} -> 目标=CGCS2000") + + # 如果已经是CGCS2000坐标系,直接返回 + if current_coordinate_system == "CGCS2000": + logging.info("路由数据已经是CGCS2000坐标系,无需转换") + return converted_route + + # 如果是WGS84坐标系,需要转换为CGCS2000 + if current_coordinate_system == "WGS84": + geo_path = converted_route.get("geoPath", {}) + features = geo_path.get("features", []) + + logging.info(f"开始转换WGS84坐标到CGCS2000,共有 {len(features)} 个feature") + + for feature in features: + geometry = feature.get("geometry", {}) + if geometry.get("type") == "LineString": + coordinates = geometry.get("coordinates", []) + + # 转换坐标点 + converted_coordinates = [] + for coord in coordinates: + if len(coord) >= 2: + # WGS84坐标格式:[longitude, latitude] + longitude, latitude = float(coord[0]), float(coord[1]) + + # 转换为CGCS2000坐标(X, Y) + x, y = coordinate_system.wgs84_to_cgcs2000(latitude, longitude) + converted_coordinates.append([x, y]) + + logging.debug(f"WGS84坐标转换: lon={longitude}, lat={latitude} -> CGCS2000: x={x}, y={y}") + + # 更新坐标数据 + geometry["coordinates"] = converted_coordinates + + # 更新坐标系标识 + converted_route["coordinateSystem"] = "CGCS2000" + + logging.info(f"WGS84到CGCS2000坐标转换完成,共转换 {len(features)} 个feature") + + return converted_route + + except Exception as e: + logging.error(f"路由数据坐标转换失败: {str(e)}") + # 转换失败时返回原始数据,但确保坐标系标识为CGCS2000 + converted_route["coordinateSystem"] = "CGCS2000" + return converted_route + +def merge_discontinuous_route_for_flight(route_data: dict, flight_no: str) -> dict: + """ + 使用Shapely自动拼接多个路径段为连续路径,并基于codes字段验证方向 + """ + try: + from shapely.geometry import LineString, Point + from shapely.ops import linemerge + + geo_path = route_data.get("geoPath", {}) + features = geo_path.get("features", []) + + if len(features) <= 1: + return route_data + + # 解析codes字段获取正确的路径顺序 + codes_str = route_data.get("codes", "") + if codes_str: + code_order = [code.strip() for code in codes_str.split(",")] + logging.info(f"航班 {flight_no} codes顺序: {code_order}") + else: + logging.warning(f"航班 {flight_no} 缺少codes字段,无法验证方向") + code_order = [] + + # 收集所有LineString路径段,并记录每段的code标识 + lines = [] + feature_codes = [] + for feature in features: + geometry = feature.get("geometry", {}) + if geometry.get("type") == "LineString": + coordinates = geometry.get("coordinates", []) + if len(coordinates) >= 2: + lines.append(LineString(coordinates)) + code = feature.get("properties", {}).get("code", "") + feature_codes.append(code) + + if not lines: + return route_data + + # Shapely自动拼接 + merged_geometry = linemerge(lines) + + # 如果成功合并为单条路径 + if isinstance(merged_geometry, LineString): + continuous_coords = list(merged_geometry.coords) + logging.info(f"✅ 航班 {flight_no} 合并为连续路径,包含 {len(continuous_coords)} 个坐标点") + + # 基于codes字段验证和修正路径方向 + if len(code_order) >= 2: + continuous_coords = _verify_and_correct_path_direction( + continuous_coords, features, code_order, flight_no + ) + + merged_route_data = route_data.copy() + merged_route_data["geoPath"]["features"] = [{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": continuous_coords + }, + "properties": {"code": "MERGED_PATH"} + }] + return merged_route_data + else: + # 无法合并就报错,不能保持原样掩盖问题 + error_msg = f"❌ 航班 {flight_no} 路径无法合并为连续路径,存在不连续段" + logging.error(error_msg) + raise RuntimeError(error_msg) + + except ImportError: + logging.warning(f"Shapely库未安装,跳过路径合并") + return route_data + except Exception as e: + logging.error(f"航班 {flight_no} 路径合并失败: {str(e)}") + return route_data + + +def _verify_and_correct_path_direction(continuous_coords: list, original_features: list, code_order: list, flight_no: str) -> list: + """ + 简化版本:检查合并后路径端点所在的路径段code值来判断方向 + """ + try: + if len(code_order) < 2: + return continuous_coords + + # 找到端点最接近的路径段 + def find_point_code(coord): + closest_code = None + min_distance = float('inf') + + for feature in original_features: + geometry = feature.get("geometry", {}) + code = feature.get("properties", {}).get("code", "") + if geometry.get("type") == "LineString" and code: + coordinates = geometry.get("coordinates", []) + # 计算到这个路径段的最小距离 + for seg_coord in coordinates: + # 简单的欧几里得距离 + distance = ((coord[0] - seg_coord[0]) ** 2 + (coord[1] - seg_coord[1]) ** 2) ** 0.5 + if distance < min_distance: + min_distance = distance + closest_code = code + + return closest_code + + start_code = find_point_code(continuous_coords[0]) + end_code = find_point_code(continuous_coords[-1]) + + logging.info(f"航班 {flight_no} 端点检查: 起点在{start_code}, 终点在{end_code}, 期望顺序{code_order}") + + # 检查方向是否正确 - 只使用实际存在的路径段codes进行判断 + expected_start = code_order[0] # F1 + # 对于MU5123进港,应该从F1开始,但终点138不是路径段code,所以检查起点即可 + + logging.info(f"航班 {flight_no} 方向检查: 期望从{expected_start}开始, 实际起点{start_code}, 终点{end_code}") + + # 通用路径方向检查逻辑 + if start_code == expected_start: + logging.info(f"✅ 航班 {flight_no} 路径方向正确: 从{expected_start}开始") + else: + # 方向相反,需要反转 + continuous_coords.reverse() + logging.info(f"✅ 航班 {flight_no} 路径方向已修正: 从{expected_start}开始 (原起点: {start_code})") + + return continuous_coords + + except Exception as e: + logging.error(f"航班 {flight_no} 路径方向验证失败: {str(e)}") + return continuous_coords + +def validate_route_data_integrity(route_data: dict, flight_no: str) -> dict: + """ + 验证路由数据的完整性和格式正确性 + + Args: + route_data: 包含geoPath的路由数据 + flight_no: 航班号,用于日志 + + Returns: + dict: 原始路由数据(不做任何修改) + """ + try: + geo_path = route_data.get("geoPath", {}) + features = geo_path.get("features", []) + + if len(features) == 0: + logging.warning(f"⚠️ 航班 {flight_no} 路径数据为空,无路径段") + return route_data + + logging.info(f"✅ 航班 {flight_no} 路径数据完整:包含 {len(features)} 个路径段") + + # 验证每个路径段的数据完整性 + valid_segments = 0 + total_coordinates = 0 + + for i, feature in enumerate(features): + geometry = feature.get("geometry", {}) + properties = feature.get("properties", {}) + + if geometry.get("type") == "LineString": + coordinates = geometry.get("coordinates", []) + code = properties.get("code", "") + + if len(coordinates) >= 2: + valid_segments += 1 + total_coordinates += len(coordinates) + logging.debug(f" 路径段 {i+1}: 代码={code}, 坐标点数={len(coordinates)} ✅") + else: + logging.warning(f" 路径段 {i+1}: 代码={code}, 坐标点数不足({len(coordinates)}) ⚠️") + else: + logging.warning(f" 路径段 {i+1}: 非LineString类型 ({geometry.get('type')}) ⚠️") + + logging.info(f"✅ 航班 {flight_no} 数据验证完成:{valid_segments}/{len(features)} 有效路径段,共 {total_coordinates} 个坐标点") + + # 验证坐标系信息 + coordinate_system = route_data.get("coordinateSystem", "未知") + logging.info(f"✅ 航班 {flight_no} 坐标系:{coordinate_system}") + + return route_data + + except Exception as e: + logging.error(f"航班 {flight_no} 路径数据完整性验证异常: {str(e)}") + return route_data + +def parse_route_path(route_data: dict) -> list[tuple[float, float]]: + """ + 解析GeoJSON路由数据,转换为WGS84坐标点序列 + + Args: + route_data: 包含geoPath的路由数据 + + Returns: + list: WGS84坐标点列表 [(lat, lon), ...] + """ + path_points = [] + + try: + # 检查坐标系类型 + coordinate_system_type = route_data.get("coordinateSystem", "CGCS2000") + logging.info(f"路由数据坐标系: {coordinate_system_type}") + + geo_path = route_data.get("geoPath", {}) + features = geo_path.get("features", []) + + logging.info(f"解析路径,共有 {len(features)} 个feature") + + for feature in features: + geometry = feature.get("geometry", {}) + if geometry.get("type") == "LineString": + coordinates = geometry.get("coordinates", []) + + # 根据坐标系类型处理坐标点 + for coord in coordinates: + if len(coord) >= 2: + x, y = float(coord[0]), float(coord[1]) + + if coordinate_system_type == "WGS84": + # 如果已经是WGS84坐标,直接使用 (经度, 纬度) + lat, lon = y, x + logging.debug(f"WGS84坐标直接使用: lon={lon}, lat={lat}") + else: + # 如果是CGCS2000坐标,需要转换 + lat, lon = coordinate_system.cgcs2000_to_wgs84(x, y) + logging.debug(f"CGCS2000坐标转换: {x},{y} -> lat={lat}, lon={lon}") + + path_points.append((lat, lon)) + + logging.info(f"路径解析完成,坐标系={coordinate_system_type},共生成 {len(path_points)} 个坐标点") + + # 暂时禁用过滤,保留所有路径点用于测试 + logging.info(f"保留所有 {len(path_points)} 个路径点") + + return path_points + + except Exception as e: + logging.error(f"路径解析失败: {str(e)}") + return [] + +def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """使用Haversine公式计算两点间距离(米)""" + lat1_rad = math.radians(lat1) + lon1_rad = math.radians(lon1) + lat2_rad = math.radians(lat2) + lon2_rad = math.radians(lon2) + + dlat = lat2_rad - lat1_rad + dlon = lon2_rad - lon1_rad + + a = math.sin(dlat/2) * math.sin(dlat/2) + \ + math.cos(lat1_rad) * math.cos(lat2_rad) * \ + math.sin(dlon/2) * math.sin(dlon/2) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + return EARTH_RADIUS * c + +# 航空器路径跟随系统 +class AircraftRouteFollower: + """航空器路径跟随管理类""" + + def __init__(self, flight_no: str): + self.flight_no = flight_no + self.current_route_type = None # 'arrival' or 'departure' + self.route_points = [] # 当前路径的坐标点序列 + self.current_point_index = 0 # 当前目标路径点索引 + self.route_progress = 0.0 # 路径完成进度 (0-1) + self.is_at_gate = False # 是否在机位停留 + self.gate_position = None # 机位坐标 + self._status_start_time = time.time() # 状态开始时间,用于简化状态管理 + + # 解析航空器路由路径 + self._parse_routes() + + def _parse_routes(self): + """解析进出港路径""" + try: + # 根据航班号获取对应的路由数据 + flight_routes = aircraft_routes.get(self.flight_no) + if not flight_routes: + logging.warning(f"未找到航班 {self.flight_no} 的路由数据,使用MU5123的路由作为默认") + flight_routes = aircraft_routes.get("MU5123", {}) + + # 解析进港路径 + arrival_route = flight_routes.get("arrival", {}) + + # 验证MU5123进港路径的数据完整性,然后合并为连续路径 + if self.flight_no == "MU5123" and arrival_route: + logging.info(f"验证 {self.flight_no} 进港路径数据完整性") + arrival_route = validate_route_data_integrity(arrival_route, self.flight_no) + + # 将31个路径段合并为连续路径,让飞机能够完整运行 + logging.info(f"将 {self.flight_no} 的分散路径段合并为连续路径") + arrival_route = merge_discontinuous_route_for_flight(arrival_route, self.flight_no) + + self.arrival_points = parse_route_path(arrival_route) + logging.info(f"航空器 {self.flight_no} 进港路径: {len(self.arrival_points)} 个点") + + # 解析出港路径 + departure_route = flight_routes.get("departure", {}) + self.departure_points = parse_route_path(departure_route) + logging.info(f"航空器 {self.flight_no} 出港路径: {len(self.departure_points)} 个点") + + # 设置机位坐标(138号机位的大概位置) + if self.arrival_points: + self.gate_position = self.arrival_points[-1] # 进港路径的终点作为机位 + elif self.departure_points: + self.gate_position = self.departure_points[0] # 出港路径的起点作为机位 + else: + # 默认机位坐标 + self.gate_position = (36.354068, 120.083410) + + except Exception as e: + logging.error(f"路径解析失败: {str(e)}") + self.arrival_points = [] + self.departure_points = [] + self.gate_position = (36.354068, 120.083410) + + def start_route(self, route_type: str): + """开始跟随指定类型的路径""" + if route_type == "arrival" and self.arrival_points: + self.current_route_type = "arrival" + self.route_points = self.arrival_points.copy() + self.current_point_index = 0 + self.route_progress = 0.0 + self.is_at_gate = False + logging.info(f"航空器 {self.flight_no} 开始进港路径跟随") + + elif route_type == "departure" and self.departure_points: + self.current_route_type = "departure" + self.route_points = self.departure_points.copy() + self.current_point_index = 0 + self.route_progress = 0.0 + self.is_at_gate = False + logging.info(f"航空器 {self.flight_no} 开始出港路径跟随") + + else: + logging.warning(f"航空器 {self.flight_no} 无效的路径类型: {route_type}") + + def update_position_on_route(self, aircraft: dict[str, Any], speed_kmh: float, elapsed_time: float) -> bool: + """ + 沿路径更新航空器位置 + + Args: + aircraft: 航空器数据字典 + speed_kmh: 速度 (km/h) + elapsed_time: 经过时间 (秒) + + Returns: + bool: 是否到达路径终点 + """ + if not self.route_points or self.current_point_index >= len(self.route_points): + return True # 路径已完成 + + # 获取当前位置 + current_lat = to_float_safe(aircraft.get("latitude", 0)) + current_lon = to_float_safe(aircraft.get("longitude", 0)) + if current_lat is None or current_lon is None: + return False + + # 获取目标路径点 + target_lat, target_lon = self.route_points[self.current_point_index] + + # 计算到目标点的距离 + distance_to_target = haversine_distance(current_lat, current_lon, target_lat, target_lon) + + # 计算移动距离 + speed_mps = speed_kmh * 1000.0 / 3600.0 # 转换为米/秒 + move_distance = speed_mps * elapsed_time + + # 如果已经接近目标点(5米内),移动到下一个路径点 + if distance_to_target <= 5.0: + self.current_point_index += 1 + self.route_progress = min(1.0, self.current_point_index / max(1, len(self.route_points) - 1)) + + if self.current_point_index >= len(self.route_points): + # 路径完成 + aircraft["latitude"] = target_lat + aircraft["longitude"] = target_lon + self.is_at_gate = (self.current_route_type == "arrival") + logging.info(f"航空器 {self.flight_no} 完成 {self.current_route_type} 路径") + return True + + # 更新目标到下一个路径点 + target_lat, target_lon = self.route_points[self.current_point_index] + distance_to_target = haversine_distance(current_lat, current_lon, target_lat, target_lon) + + # 计算移动方向 + if distance_to_target > 0: + # 计算移动比例 + move_ratio = min(1.0, move_distance / distance_to_target) + + # 线性插值计算新位置 + new_lat = current_lat + (target_lat - current_lat) * move_ratio + new_lon = current_lon + (target_lon - current_lon) * move_ratio + + # 更新航空器位置 + aircraft["latitude"] = new_lat + aircraft["longitude"] = new_lon + + return False + + def set_at_gate(self, aircraft: dict[str, Any]): + """设置航空器在机位停留""" + if self.gate_position: + aircraft["latitude"] = self.gate_position[0] + aircraft["longitude"] = self.gate_position[1] + self.is_at_gate = True + +# 多飞机路径跟随管理器 +class AircraftRouteManager: + """多架飞机路径跟随管理器""" + + def __init__(self): + self.followers = {} # {flight_no: AircraftRouteFollower} + self.flight_configs = {} # 飞机配置信息 + + def add_aircraft(self, flight_no: str, config: dict): + """添加飞机到路径跟随系统 + + Args: + flight_no: 航班号 + config: 飞机配置 { + 'primary_behavior': 'arrival'|'departure'|'cycle', + 'speed': 30.0, + 'cycle_timing': {...} # 仅用于cycle模式 + } + """ + self.flight_configs[flight_no] = config + self.followers[flight_no] = AircraftRouteFollower(flight_no) + logging.info(f"已添加飞机 {flight_no} 到路径跟随系统,行为模式: {config.get('primary_behavior', 'cycle')}") + + def update_aircraft_position(self, aircraft: dict[str, Any], elapsed_time: float): + """统一的航空器位置更新入口""" + flight_no = aircraft.get("flightNo") + + if flight_no not in self.followers: + logging.warning(f"飞机 {flight_no} 未注册到路径跟随系统") + return + + follower = self.followers[flight_no] + config = self.flight_configs[flight_no] + behavior = config.get("primary_behavior", "cycle") + speed = config.get("speed", 30.0) + + if behavior == "arrival": + # 纯进港模式 + self._update_arrival_only_aircraft(aircraft, follower, speed, elapsed_time) + elif behavior == "departure": + # 纯出港模式 + self._update_departure_only_aircraft(aircraft, follower, speed, elapsed_time) + + # 更新时间戳 + aircraft["time"] = int(time.time() * 1000) + + + def _update_arrival_only_aircraft(self, aircraft: dict[str, Any], follower: AircraftRouteFollower, + speed: float, elapsed_time: float): + """更新纯进港模式飞机位置""" + if follower.current_route_type != "arrival": + follower.start_route("arrival") + # 设置到进港路径起点 + if follower.route_points: + aircraft["latitude"] = follower.route_points[0][0] + aircraft["longitude"] = follower.route_points[0][1] + + route_completed = follower.update_position_on_route(aircraft, speed, elapsed_time) + if route_completed: + # 进港完成后重新开始 + follower.start_route("arrival") + if follower.route_points: + aircraft["latitude"] = follower.route_points[0][0] + aircraft["longitude"] = follower.route_points[0][1] + + def _update_departure_only_aircraft(self, aircraft: dict[str, Any], follower: AircraftRouteFollower, + speed: float, elapsed_time: float): + """更新纯出港模式飞机位置""" + if follower.current_route_type != "departure": + follower.start_route("departure") + # 设置到出港路径起点 + if follower.route_points: + aircraft["latitude"] = follower.route_points[0][0] + aircraft["longitude"] = follower.route_points[0][1] + + route_completed = follower.update_position_on_route(aircraft, speed, elapsed_time) + if route_completed: + # 出港完成后重新开始 + follower.start_route("departure") + if follower.route_points: + aircraft["latitude"] = follower.route_points[0][0] + aircraft["longitude"] = follower.route_points[0][1] + + +# 创建全局路径跟随管理器 +route_manager = AircraftRouteManager() + +# 配置并添加飞机到路径跟随系统 +# CA1234:纯出港模式,模拟出港飞机 +route_manager.add_aircraft("CA1234", { + 'primary_behavior': 'departure', + 'speed': 30.0 +}) + +# MU5123:纯进港模式,使用真实进港路由 +route_manager.add_aircraft("MU5123", { + 'primary_behavior': 'arrival', + 'speed': 30.0 +}) + +# 初始化飞机数据 +def initialize_aircraft_data(): + """初始化航空器数据,设置合适的起始位置""" + + # CA1234 - 出港模式,从机位开始(出港路径起点) + ca1234_follower = route_manager.followers["CA1234"] + ca1234_start_pos = ca1234_follower.departure_points[0] if ca1234_follower.departure_points else (36.354068, 120.083410) + + # MU5123 - 进港模式,从跑道开始(进港路径起点) + mu5123_follower = route_manager.followers["MU5123"] + mu5123_start_pos = mu5123_follower.arrival_points[0] if mu5123_follower.arrival_points else (36.374179, 120.088076) + + return [ + { + "flightNo": "CA1234", # 出港模式 + "longitude": ca1234_start_pos[1], + "latitude": ca1234_start_pos[0], + "time": int(time.time() * 1000), + "altitude": 0.0, + "trackNumber": 1001, + "speed": 30.0, + "use_route_following": True + }, + { + "flightNo": "MU5123", # 进港模式 + "longitude": mu5123_start_pos[1], + "latitude": mu5123_start_pos[0], + "time": int(time.time() * 1000), + "altitude": 0.0, + "trackNumber": 1002, + "speed": 30.0, + "use_route_following": True + } + ] + +# 初始化飞机数据 +aircraft_data.extend(initialize_aircraft_data()) + from collections.abc import Mapping def calculate_distance_to_target(vehicle: Mapping[str, float], target_lat: float, target_lon: float) -> float: @@ -1577,8 +1834,16 @@ def update_object_position(obj: dict[str, Any], elapsed_time: float) -> None: obj["time"] = int(time.time() * 1000) def update_aircraft_position(aircraft: dict[str, Any], elapsed_time: float) -> None: - """更新航空器位置 - 使用统一的位置更新函数""" - update_object_position(aircraft, elapsed_time) + """更新航空器位置 - 统一使用路径跟随管理器""" + flight_no = aircraft.get("flightNo") + use_route_following = aircraft.get("use_route_following", False) + + if use_route_following: + # 使用新的路径跟随管理器 + route_manager.update_aircraft_position(aircraft, elapsed_time) + else: + # 保留旧的简单移动逻辑作为兜底 + update_object_position(aircraft, elapsed_time) def update_vehicle_position(vehicle: dict[str, Any], elapsed_time: float) -> None: """更新车辆位置 - 使用统一的位置更新函数""" @@ -1636,6 +1901,58 @@ last_aircraft_update_time = time.time() last_vehicle_update_time = time.time() # 机场车辆更新时间 last_light_switch_time = time.time() +# 后台更新管理器 +class BackgroundUpdateManager: + """独立的后台更新管理器,主动更新航空器状态和位置""" + + def __init__(self): + self.running = False + self.update_thread = None + self.data_lock = threading.Lock() # 保护共享数据的锁 + + def start(self): + """启动后台更新线程""" + if not self.running: + self.running = True + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + logging.info("后台更新管理器已启动") + + def stop(self): + """停止后台更新线程""" + if self.running: + self.running = False + if self.update_thread: + self.update_thread.join(timeout=2.0) + logging.info("后台更新管理器已停止") + + def _update_loop(self): + """主更新循环""" + while self.running: + try: + current_time = time.time() + + with self.data_lock: + # 更新所有航空器位置 + for aircraft in aircraft_data: + update_aircraft_position(aircraft, UPDATE_INTERVAL) + aircraft["time"] = int(current_time * 1000) + + # 更新所有车辆位置 + for vehicle in airport_vehicle_data: + update_vehicle_position(vehicle, UPDATE_INTERVAL) + vehicle["time"] = int(current_time * 1000) + + # 休眠到下一个更新周期 + time.sleep(UPDATE_INTERVAL) + + except Exception as e: + logging.error(f"后台更新出错: {str(e)}") + time.sleep(1.0) # 出错时短暂休眠后继续 + +# 全局后台更新管理器实例 +background_manager = BackgroundUpdateManager() + def check_auth() -> bool: auth_header = request.headers.get('Authorization') @@ -1681,36 +1998,28 @@ def get_flight_positions(): """获取当前航空器位置信息""" if request.method == 'OPTIONS': return '', 204 - + if not check_auth(): return jsonify({ "status": 401, "msg": "认证失败", "data": None }), 401 - global last_aircraft_update_time - current_time = time.time() - elapsed_time = current_time - last_aircraft_update_time - - # 只在达到更新间隔时更新位置 - if elapsed_time >= UPDATE_INTERVAL: + + # 使用线程锁保护数据访问 + with background_manager.data_lock: + # 创建符合 API 格式的响应数据 + response_data = [] for aircraft in aircraft_data: - update_aircraft_position(aircraft, UPDATE_INTERVAL) - aircraft["time"] = int(current_time * 1000) - last_aircraft_update_time = current_time - - # 创建符合 API 格式的响应数据 - response_data = [] - for aircraft in aircraft_data: - lon_val = to_float_safe(aircraft.get("longitude")) - lat_val = to_float_safe(aircraft.get("latitude")) - api_aircraft = { - "flightNo": aircraft.get("flightNo"), - "longitude": round(lon_val, 6) if lon_val is not None else aircraft.get("longitude"), - "latitude": round(lat_val, 6) if lat_val is not None else aircraft.get("latitude"), - "time": aircraft.get("time") - } - response_data.append(api_aircraft) + lon_val = to_float_safe(aircraft.get("longitude")) + lat_val = to_float_safe(aircraft.get("latitude")) + api_aircraft = { + "flightNo": aircraft.get("flightNo"), + "longitude": round(lon_val, 6) if lon_val is not None else aircraft.get("longitude"), + "latitude": round(lat_val, 6) if lat_val is not None else aircraft.get("latitude"), + "time": aircraft.get("time") + } + response_data.append(api_aircraft) return jsonify({ "status": 200, @@ -1769,33 +2078,28 @@ def get_vehicle_positions(): """获取当前车辆位置信息(仅机场车辆)""" if request.method == 'OPTIONS': return '', 204 + if not check_auth(): return jsonify({ "status": 401, "msg": "认证失败", "data": None }), 401 - global last_vehicle_update_time - current_time = time.time() - elapsed_time = current_time - last_vehicle_update_time + try: - # 只在达到更新间隔时更新位置 - if elapsed_time >= UPDATE_INTERVAL: - for vehicle in airport_vehicle_data: - update_vehicle_position(vehicle, UPDATE_INTERVAL) - vehicle["time"] = int(current_time * 1000) - last_vehicle_update_time = current_time - response_data = [] - for v in airport_vehicle_data: - lon_val = to_float_safe(v.get("longitude")) - lat_val = to_float_safe(v.get("latitude")) - v_out = { - "vehicleNo": v.get("vehicleNo"), - "longitude": round(lon_val, 6) if lon_val is not None else v.get("longitude"), - "latitude": round(lat_val, 6) if lat_val is not None else v.get("latitude"), - "time": v.get("time") - } - response_data.append(v_out) + # 使用线程锁保护数据访问 + with background_manager.data_lock: + response_data = [] + for v in airport_vehicle_data: + lon_val = to_float_safe(v.get("longitude")) + lat_val = to_float_safe(v.get("latitude")) + v_out = { + "vehicleNo": v.get("vehicleNo"), + "longitude": round(lon_val, 6) if lon_val is not None else v.get("longitude"), + "latitude": round(lat_val, 6) if lat_val is not None else v.get("latitude"), + "time": v.get("time") + } + response_data.append(v_out) return jsonify({ "status": 200, "msg": "当前车辆实时位置数据", @@ -1901,48 +2205,6 @@ def login(): -def update_ca3456_status() -> None: - """更新CA3456航空器状态 - 进港->停留1分钟->出港循环""" - global ca3456_status - current_time = time.time() - # 安全转换状态起始时间,避免与 float 相减的类型冲突 - status_start_time = to_float_safe(ca3456_status.get("status_start_time")) - if status_start_time is None: - # 无法解析时,用当前时间作为起点,避免负值 - status_start_time = current_time - ca3456_status["status_start_time"] = status_start_time - elapsed_time = float(current_time) - float(status_start_time) - - # 读取并安全转换周期与阶段时长,避免 str | int | float 与 float 的 %/比较类型问题 - cycle_duration = to_float_safe(ca3456_status.get("cycle_duration")) - arrival_duration = to_float_safe(ca3456_status.get("arrival_duration")) - wait_duration = to_float_safe(ca3456_status.get("wait_duration")) - - # 兜底默认值:若无法解析则使用已有注释中的默认秒数 - if cycle_duration is None or cycle_duration <= 0: - cycle_duration = 77.0 - if arrival_duration is None or arrival_duration < 0: - arrival_duration = 30.0 - if wait_duration is None or wait_duration < 0: - wait_duration = 15.0 - - # 计算当前在循环中的位置(均为 float) - cycle_time = float(elapsed_time) % float(cycle_duration) - - if cycle_time < arrival_duration: - # 进港阶段 - ca3456_status["type"] = "IN" - logging.info(f"CA3456 进港阶段: {cycle_time:.1f}s") - elif cycle_time < (arrival_duration + wait_duration): - # 停留阶段 - ca3456_status["type"] = "ARRIVED" - logging.info(f"CA3456 停留阶段: {cycle_time:.1f}s") - else: - # 出港阶段 - ca3456_status["type"] = "OUT" - logging.info(f"CA3456 出港阶段: {cycle_time:.1f}s") - - ca3456_status["timestamp"] = int(current_time * 1000) # 新增API端点 @@ -1990,11 +2252,31 @@ def get_arrival_taxiway_route(): logging.info(f"进港路线查询: inRunway={in_runway}, outRunway={out_runway}, contactCross={contact_cross}, seat={seat}") - # 返回进港路线数据 + # 根据参数匹配对应的航班路由 + matching_flight = None + for flight_no, params in aircraft_route_params.items(): + arrival_params = params.get("arrival", {}) + if (arrival_params.get("inRunway") == in_runway and + arrival_params.get("contactCross") == contact_cross and + arrival_params.get("seat") == seat): + matching_flight = flight_no + break + + # 如果找到匹配的航班,返回对应路由,否则使用MU5123作为默认 + if matching_flight and matching_flight in aircraft_routes: + route_data = aircraft_routes[matching_flight]["arrival"] + logging.info(f"匹配航班 {matching_flight} 的进港路由") + else: + route_data = aircraft_routes["MU5123"]["arrival"] + logging.info(f"未找到匹配航班,使用MU5123的进港路由作为默认") + + # 将路由数据转换为CGCS2000坐标系格式 + converted_route_data = convert_route_to_cgcs2000(route_data) + return jsonify({ "status": 200, "msg": "进港滑行路线查询成功", - "data": aircraft_routes["arrival"] + "data": converted_route_data }) @app.route('/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat', methods=['GET', 'OPTIONS']) @@ -2017,18 +2299,37 @@ def get_departure_taxiway_route(): logging.info(f"出港路线查询: inRunway={in_runway}, outRunway={out_runway}, startSeat={start_seat}") - # 返回出港路线数据 + # 根据参数匹配对应的航班路由 + matching_flight = None + for flight_no, params in aircraft_route_params.items(): + departure_params = params.get("departure", {}) + if (departure_params.get("inRunway") == in_runway and + departure_params.get("startSeat") == start_seat): + matching_flight = flight_no + break + + # 如果找到匹配的航班,返回对应路由,否则使用CA1234作为默认 + if matching_flight and matching_flight in aircraft_routes: + route_data = aircraft_routes[matching_flight]["departure"] + logging.info(f"匹配航班 {matching_flight} 的出港路由") + else: + route_data = aircraft_routes["CA1234"]["departure"] + logging.info(f"未找到匹配航班,使用CA1234的出港路由作为默认") + + # 将路由数据转换为CGCS2000坐标系格式 + converted_route_data = convert_route_to_cgcs2000(route_data) + return jsonify({ "status": 200, "msg": "出港滑行路线查询成功", - "data": aircraft_routes["departure"] + "data": converted_route_data }) def get_active_flights() -> list[dict[str, Any]]: """获取当前活跃的进出港航班(简化版)""" current_time = time.time() active_flights = [] - + for flight_no, flight_info in flight_data.items(): # 安全读取并转换为数值 cycle_start = to_float_safe(flight_info.get("cycle_start")) @@ -2056,27 +2357,30 @@ def get_active_flights() -> list[dict[str, Any]]: # 检查是否在活跃期内 if cycle_position <= float(active_duration): - # 在活跃期内,直接返回固定信息 + # 在活跃期内,根据航班类型返回信息 + flight_type = flight_info.get("type") + + # 航班使用固定信息 flight_data_response = { "flightNo": flight_info.get("flightNo"), - "type": flight_info.get("type"), + "type": flight_type, "runway": flight_info.get("runway"), "contactCross": flight_info.get("contactCross"), "seat": flight_info.get("seat"), "time": flight_info.get("fixed_time") # 使用固定时间 } active_flights.append(flight_data_response) - - event_type = "落地" if flight_info.get("type") == "IN" else "滑出" + + # 确定事件类型用于日志 + actual_type = flight_data_response.get("type", flight_info.get("type")) + event_type = "落地" if actual_type == "IN" else "滑出" logging.info(f"航班 {flight_no} {event_type}通知中 (活跃期: {cycle_position:.1f}s/{int(active_duration)}s)") else: # 在间隔期内,更新固定时间为下一个周期 cycle_number = int(float(elapsed_time) // float(total_cycle)) + 1 next_cycle_start = float(cycle_start) + cycle_number * float(total_cycle) - if flight_info.get("type") == "OUT": - flight_info["fixed_time"] = int((next_cycle_start + 30.0) * 1000) # 出港滑出时间 - else: - flight_info["fixed_time"] = int((next_cycle_start + 20.0) * 1000) # 进港落地时间 + # 简化时间计算,与路径起点时间同步 + flight_info["fixed_time"] = int(next_cycle_start * 1000) # 使用周期开始时间作为通知时间 gap_part = cycle_position - float(active_duration) logging.info(f"航班 {flight_no} 在间隔期,准备下一个周期 (间隔期: {gap_part:.1f}s/{int(gap_duration)}s)") @@ -2121,32 +2425,35 @@ def get_aircraft_status(): """获取航空器状态""" if request.method == 'OPTIONS': return '', 204 - + if not check_auth(): return jsonify({ "status": 401, "msg": "认证失败", "data": None }), 401 - - # 更新CA3456状态 - update_ca3456_status() - - # 返回当前状态 + + # 返回简化的航空器状态信息 return jsonify({ "status": 200, "msg": "航空器状态查询成功", "data": { - "type": ca3456_status["type"], - "flightNo": ca3456_status["flightNo"], - "inRunway": ca3456_status["inRunway"], - "outRunway": ca3456_status["outRunway"], - "contactCross": ca3456_status["contactCross"], - "seat": ca3456_status["seat"], - "timestamp": ca3456_status["timestamp"] + "message": "已简化为两架飞机:CA1234(出港)和MU5123(进港)" } }) if __name__ == '__main__': - app.run(host='localhost', port=8090, debug=True) \ No newline at end of file + # 启动后台更新管理器 + background_manager.start() + + # 注册应用关闭时的清理函数 + def cleanup_on_exit(): + background_manager.stop() + atexit.register(cleanup_on_exit) + + try: + app.run(host='localhost', port=8090, debug=True) + finally: + # 确保后台管理器正确停止 + background_manager.stop() \ No newline at end of file