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 00000000..1ef6dc80 Binary files /dev/null and b/tools/__pycache__/mock_airport.cpython-313.pyc differ 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