深入Rust与航空数据:尝试实现高性能航路规划算法库
前言
故事的开始
去年冬天,我接手了一个航空数据相关的Go项目。这个项目本身不复杂,就是从SQLite数据库里读取机场、航点、航路信息,然后提供API给前端调用。但有个需求一直让我头疼: 动态航路查询
用户输入起飞机场(比如北京首都ZBAA)和目的地机场(比如上海浦东ZSPD),系统要自动生成一条合理的飞行航路。听起来简单?但航空航路有它自己的规则:
- 不是随便两点连直线就行,要沿着既定的航路(Airway)走
- 机场附近有SID(标准仪表离场程序)和STAR(标准仪表进场程序)
- 要考虑高度层、航路类型(高/低空)
- 要能解析飞行员写的航路字符串
一开始我用Go硬编码了一些规则(国内一号航路,如果熟悉民航可能会熟悉这个),其余情况通过Go在数据库中进行航路、点的依次检索,生成航路。但很快发现这条路走不通。航空数据太复杂了,全球有上万个航点、数千条航路,计算过于复杂,而且有的空域会有特殊规则,使用这种方法根本无法完成。
如果你飞过模拟飞行或者真实接触过飞行计划,可能会熟悉这样的航路字符串:
ZBAA SID TEPID G212 VYK STAR ZSPD这看起来简单,背后却藏着复杂的航空规则。每个字母都有含义:ZBAA是北京首都机场,TEPID是一个航点,G212是一条航路,VYK是另一个航点,最后落地上海浦东 (ZSPD)
问题是:用户输入起飞机场和目的地,系统要能自动生成这样的航路。这不是简单的两点连线,因为飞机必须沿着既定的空中高速公路——也就是航路(Airway)飞行。
航空数据源是一个SQLite数据库(来自PMDG的数据格式),里面包含了全球的机场、航点、航路、SID/STAR程序。我需要在这个基础上构建一个高性能的航路规划引擎。
为什么是Rust?
说实话,我不是Rust狂热粉。但我需要一个高性能、内存安全、能和Go愉快玩耍的解决方案。
Go的优点是开发快,但在这种图搜索场景下,性能不是它的强项。我需要一个能在内存中构建航路网络图、跑Dijkstra算法的组件。这个组件要足够快,又不能拖累主服务的稳定性。
Rust恰好满足这些:
- 性能接近C/C++,没有GC停顿
- 内存安全,不会因为我的算法bug导致段错误
- 可以通过C FFI和Go无缝集成
最关键的是,Rust有很棒的SQLite绑定(rusqlite)和地理空间计算库,适合做这种数据密集型的计算。
核心挑战:从扁平的数据库到可搜索的图
数据结构之困
打开数据库,看到的是这样的表结构:
-- 航路表的核心字段CREATE TABLE tbl_enroute_airways ( route_identifier TEXT, -- 航路名称,如 "G212" seqno INTEGER, -- 顺序号,1,2,3... waypoint_identifier TEXT, -- 航点标识,如 "TEPID" waypoint_latitude REAL, waypoint_longitude REAL, -- ... 其他字段);数据长这样:
| route_identifier | seqno | waypoint_identifier | latitude | longitude |
|---|---|---|---|---|
| G212 | 1 | TEPID | 40.123 | 116.456 |
| G212 | 2 | VYK | 38.789 | 118.234 |
| G212 | 3 | AND | 37.456 | 119.567 |
| A593 | 1 | TEPID | 40.123 | 116.456 |
| A593 | 2 | P62 | 39.876 | 117.890 |
注意两个关键点:
- 同一条航路由多个连续航点组成
- 同一个航点(如TEPID)可能出现在多条航路中
这意味着,在逻辑上,这是一个多图叠加的网络——每个航路都是一条路径,它们在某些航点交汇,形成复杂的网络。
构建内存中的航路图
最简单的想法:把数据库里的数据直接塞进内存,用邻接表表示图。但问题来了:图里的节点是什么?边又是什么?
节点定义:节点不应该是简单的航点标识符,因为同一个标识符可能对应不同位置的航点!比如”P62”在华北和华东都可能存在。所以必须用标识符+坐标来唯一确定一个节点。
#[derive(Debug, Clone, Hash, Eq, PartialEq)]struct NodeKey { identifier: String, latitude: i32, // 存储为整数,避免浮点精度问题 longitude: i32,}
impl NodeKey { fn from_coord(identifier: &str, coord: &Coordinate) -> Self { // 保留4位小数精度,转换成整数 let lat = (coord.latitude * 10000.0).round() as i32; let lon = (coord.longitude * 10000.0).round() as i32; Self { identifier: identifier.to_string(), latitude: lat, longitude: lon, } }}边定义:每条边应该包含哪些信息?至少要有目标节点、航路名称、距离。距离不是直线距离,而是沿着航路的长度——但在航路数据里,相邻航点间的距离就是直线距离,因为航路就是由这些直线段组成的。
struct Edge { to_idx: usize, // 目标节点索引 airway: String, // 航路名称,如 "G212" distance_nm: f64, // 这段航路的长度(海里) direction: Direction, // 方向限制(单向/双向)}方向限制:航空航路不都是双向的。有的航路规定只能单向飞行(比如为了避免冲突)。数据库里有 direction_restriction字段,取值可能是”EAST”、“WEST”、“BOTH”等。构建图时必须考虑这一点。
图的构建过程
从数据库到内存图的转换,核心代码大致如下:
pub fn build_graph(&self) -> Result<AirwayGraph> { let mut graph = AirwayGraph::new();
// 1. 加载所有航路边(连续航点对) let edges = self.load_all_airway_edges()?;
for (airway, from_id, from_coord, to_id, to_coord, direction) in edges { // 2. 为航点创建或获取节点索引 let from_idx = graph.get_or_create_node(&from_id, from_coord); let to_idx = graph.get_or_create_node(&to_id, to_coord);
// 3. 计算大圆距离 let distance = haversine_distance_nm(&from_coord, &to_coord);
// 4. 根据方向限制添加边 match direction.as_str() { "BOTH" => { graph.add_edge(from_idx, to_idx, &airway, distance); graph.add_edge(to_idx, from_idx, &airway, distance); } "FORWARD" => { graph.add_edge(from_idx, to_idx, &airway, distance); } "BACKWARD" => { graph.add_edge(to_idx, from_idx, &airway, distance); } _ => {} // 忽略无效方向 } }
Ok(graph)}这里有个性能关键点:haversine_distance_nm是三角函数密集的计算,在一个拥有数万条边的图上调用数十万次,开销不小。但好在图只构建一次,后续查询复用,所以可以接受。
路径搜索算法的选择与优化
为什么用Dijkstra不用A*?
航路搜索有几个特点:
- 图中节点数量中等(几万个)
- 边权都是正数(距离)
- 需要保证找到最短路径(相对于启发式搜索)
理论上A*更快,但需要好的启发式函数。在航路网络中,两点间的直线距离是个 admissible heuristic,但效果一般,因为航路往往要绕行。权衡后我选择了Dijkstra,但做了优化。
标准Dijkstra的实现
fn dijkstra( &self, graph: &AirwayGraph, start_idx: usize, end_idx: usize,) -> Option<(Vec<PathEntry>, f64)> { let mut dist = vec![f64::INFINITY; graph.nodes.len()]; let mut prev = vec![None; graph.nodes.len()]; let mut prev_airway = vec![None; graph.nodes.len()];
dist[start_idx] = 0.0;
// 使用BinaryHeap作为优先队列 let mut heap = BinaryHeap::new(); heap.push(State { cost: 0.0, node: start_idx, });
while let Some(State { cost, node }) = heap.pop() { if node == end_idx { // 重建路径 return Some(self.reconstruct_path(&prev, &prev_airway, end_idx)); }
if cost > dist[node] { continue; }
for edge in &graph.adjacency[node] { let next = State { cost: cost + edge.distance_nm, node: edge.to_idx, };
if next.cost < dist[edge.to_idx] { dist[edge.to_idx] = next.cost; prev[edge.to_idx] = Some(node); prev_airway[edge.to_idx] = Some(edge.airway.clone()); heap.push(next); } } }
None}这里用了 BinaryHeap(最大堆)但我们需要最小堆,所以 State需要实现 Ord时反转比较:
impl Ord for State { fn cmp(&self, other: &Self) -> Ordering { other.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal) }}性能瓶颈与优化
实测发现,从北京到上海的搜索耗时约200ms,勉强能用但不够理想。分析后发现几个问题:
- 优先队列操作频繁:每次松弛都要push/pop
- 路径重建开销:最后要用链表重建完整路径
- 搜索空间太大:Dijkstra会遍历大量无关节点
优化1:双向搜索
起点和终点同时向中间搜索,当两个搜索相遇时停止。理论上是单侧搜索的√n倍加速。
fn bidirectional_dijkstra(&self, start: usize, end: usize) -> Option<Path> { let mut forward_dist = vec![f64::INFINITY; self.node_count]; let mut backward_dist = vec![f64::INFINITY; self.node_count]; let mut forward_heap = BinaryHeap::new(); let mut backward_heap = BinaryHeap::new();
forward_dist[start] = 0.0; backward_dist[end] = 0.0;
forward_heap.push(State { cost: 0.0, node: start }); backward_heap.push(State { cost: 0.0, node: end });
let mut best_path_cost = f64::INFINITY; let mut meeting_node = None;
while let (Some(f), Some(b)) = (forward_heap.peek(), backward_heap.peek()) { // 如果两个搜索的最优值之和已经超过已知最优路径,可以提前终止 if f.cost + b.cost >= best_path_cost { break; }
// 正向搜索一步 if let Some(State { cost, node }) = forward_heap.pop() { if cost <= forward_dist[node] { for edge in &self.forward_adj[node] { let new_cost = cost + edge.distance; if new_cost < forward_dist[edge.to] { forward_dist[edge.to] = new_cost; forward_heap.push(State { cost: new_cost, node: edge.to });
// 检查是否与反向搜索相遇 if backward_dist[edge.to] < f64::INFINITY { let total = new_cost + backward_dist[edge.to]; if total < best_path_cost { best_path_cost = total; meeting_node = Some(edge.to); } } } } } }
// 类似的反向搜索... }
// 重建路径...}双向搜索让时间降到了约80ms。
优化2:分层搜索
航空航路有高低空之分。高空航路(High Altitude Airways)通常用”H”标识,适合长距离巡航;低空航路(Low Altitude Airways)用”L”标识,适合短途或终端区。
根据飞行高度层(Flight Level),可以只在高空或低空网络搜索:
fn filter_graph_by_flight_level(&self, graph: &AirwayGraph, level: FlightLevel) -> FilteredGraph { let filtered_adj: Vec<Vec<Edge>> = graph.adjacency .iter() .map(|edges| { edges.iter() .filter(|e| match level { FlightLevel::High => e.route_type == RouteType::High || e.route_type == RouteType::RNAV, FlightLevel::Low => e.route_type == RouteType::Low, _ => true, }) .cloned() .collect() }) .collect();
FilteredGraph { adjacency: filtered_adj, nodes: graph.nodes.clone(), }}优化3:边收缩
有些航点是”死胡同”(只连接一条边),或者在航路网络中只是过渡点(度数为2)。这些点可以在预处理时收缩,减少图规模:
fn contract_graph(&self, graph: &AirwayGraph) -> ContractedGraph { let mut contracted = graph.clone();
loop { let mut contracted_any = false; let nodes_to_remove: Vec<usize> = contracted.nodes .iter() .enumerate() .filter(|(idx, _)| { let degree = contracted.adjacency[*idx].len(); degree == 2 // 度数为2的节点可以收缩 }) .map(|(idx, _)| idx) .collect();
for idx in nodes_to_remove { if contracted.adjacency[idx].len() == 2 { let edge1 = &contracted.adjacency[idx][0]; let edge2 = &contracted.adjacency[idx][1];
// 创建一条新的边,跳过中间节点 let new_distance = edge1.distance_nm + edge2.distance_nm; let new_airway = if edge1.airway == edge2.airway { edge1.airway.clone() // 同一条航路 } else { format!("{}/{}", edge1.airway, edge2.airway) // 不同航路 };
contracted.add_edge(edge1.to_idx, edge2.to_idx, &new_airway, new_distance); contracted.remove_node(idx); contracted_any = true; } }
if !contracted_any { break; } }
contracted}地理空间计算的精度与性能
Haversine公式的实现
计算两点间的大圆距离是基础中的基础。标准Haversine公式:
pub fn haversine_distance_nm(coord1: &Coordinate, coord2: &Coordinate) -> f64 { const EARTH_RADIUS_NM: f64 = 3440.065; // 地球半径(海里)
let lat1 = coord1.latitude.to_radians(); let lat2 = coord2.latitude.to_radians(); let dlat = (coord2.latitude - coord1.latitude).to_radians(); let dlon = (coord2.longitude - coord1.longitude).to_radians();
let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2); let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_NM * c}这个函数被调用的频率极高——每次边权重计算、每次路径评估都要用。微小的优化就能累积成可观的性能提升。
优化方案1:预计算三角函数值
如果同一坐标被多次使用,可以缓存其sin/cos值:
#[derive(Clone)]struct CachedCoord { coord: Coordinate, lat_rad: f64, sin_lat: f64, cos_lat: f64,}
impl CachedCoord { fn from_coord(coord: Coordinate) -> Self { let lat_rad = coord.latitude.to_radians(); Self { coord, lat_rad, sin_lat: lat_rad.sin(), cos_lat: lat_rad.cos(), } }
fn distance_to(&self, other: &CachedCoord) -> f64 { let dlat = (other.coord.latitude - self.coord.latitude).to_radians(); let dlon = (other.coord.longitude - self.coord.longitude).to_radians();
let a = (dlat / 2.0).sin().powi(2) + self.cos_lat * other.cos_lat * (dlon / 2.0).sin().powi(2); let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_NM * c }}优化方案2:近似计算
对于短距离(<100海里),可以用平面近似代替球面计算,误差在可接受范围内:
pub fn fast_distance_nm(coord1: &Coordinate, coord2: &Coordinate) -> f64 { let dlat = (coord2.latitude - coord1.latitude) * 60.0; // 纬度差(海里) let dlon = (coord2.longitude - coord1.longitude) * 60.0 * coord1.latitude.to_radians().cos(); // 经度差(海里)
(dlat.powi(2) + dlon.powi(2)).sqrt()}这个近似函数比Haversine快约5倍。在构建图的边时,可以先用近似计算筛选,再对候选边用精确计算。
空间索引的必要性
当用户输入”ZBAA”时,我知道机场坐标。但Dijkstra搜索需要起点——也就是离机场最近的航路网络入口点。这个入口可能是SID的最后一个航点,也可能是机场附近任意一个航路航点。
要快速找到”附近”的航点,需要空间索引。我用了 rstar库,它实现了R-tree:
use rstar::{RTree, RTreeObject, AABB};
#[derive(Debug, Clone)]struct SpatialWaypoint { waypoint: Waypoint,}
impl RTreeObject for SpatialWaypoint { type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope { let point = [ self.waypoint.coordinate.longitude, self.waypoint.coordinate.latitude, ]; AABB::from_point(point) }}
pub struct SpatialIndex { rtree: RTree<SpatialWaypoint>,}查找最近邻时,不能直接用R-tree的nearest_neighbor,因为它用的是欧氏距离,而我们想要的是大圆距离。所以我实现了一个迭代扩展搜索:
pub fn find_nearest(&self, coord: &Coordinate) -> Option<Waypoint> { let mut radius = 10.0; // 起始半径10海里 let max_radius = 500.0; // 最大500海里
while radius <= max_radius { let candidates = self.find_within_radius(coord, radius); if !candidates.is_empty() { return candidates .into_iter() .min_by(|a, b| { let dist_a = haversine_distance_nm(coord, &a.coordinate); let dist_b = haversine_distance_nm(coord, &b.coordinate); dist_a.partial_cmp(&dist_b).unwrap() }); } radius *= 2.0; // 指数增长,快速扩大范围 }
None}这种策略在大多数情况下能快速返回结果,只在极偏远地区需要搜索大半径。
SID/STAR的处理逻辑
SID(标准仪表离场)和STAR(标准仪表进场)是航路规划中最复杂的部分。它们定义了从机场到航路网络,以及从航路网络到机场的标准化路径。
数据库中的SID表结构:
CREATE TABLE tbl_sids ( airport_identifier TEXT, -- 机场 procedure_identifier TEXT, -- 程序名,如 "TEPID6D" transition_identifier TEXT, -- 过渡段(可为NULL) seqno INTEGER, -- 顺序号 waypoint_identifier TEXT, -- 航点 waypoint_latitude REAL, waypoint_longitude REAL, path_termination TEXT, -- 路径终止码 -- ... 其他字段);一个SID可能有多个过渡段(transition),比如:
TEPID6D (主程序) ├─ TEPID (第一个航点) ├─ AND (第二个航点) └─ ...
TEPID6D G212 Transition (过渡段) ├─ TEPID ├─ VYK └─ ...解析SID时,我需要找到所有在航路网络中的出口点:
pub fn find_sid_exit_waypoints(&self, airport_icao: &str) -> Result<Vec<Waypoint>> { let conn = self.get_connection()?;
// 找出SID中所有在航路网络里的航点 let mut stmt = conn.prepare( "SELECT DISTINCT s.waypoint_identifier, s.waypoint_latitude, s.waypoint_longitude FROM tbl_sids s WHERE s.airport_identifier = ?1 AND s.waypoint_identifier IS NOT NULL AND s.waypoint_identifier IN ( SELECT DISTINCT waypoint_identifier FROM tbl_enroute_airways ) LIMIT 20" )?;
// 返回结果...}这个查询的核心是 IN子句,它确保选出的航点确实属于航路网络,这样Dijkstra搜索才能从这些点开始。
航路字符串解析的算法
用户输入的航路字符串千奇百怪,解析器需要有容错性。我采用了一种有限状态自动机的思路:
状态定义:- Start: 等待起飞机场- ExpectSID: 等待SID程序- ExpectAirway: 等待航路- ExpectWaypoint: 等待航点- ExpectSTAR: 等待STAR程序- End: 等待目的机场状态转换逻辑:
pub fn parse(&mut self, tokens: &[String]) -> Result<ParsedRoute> { let mut state = ParserState::Start; let mut last_waypoint: Option<Waypoint> = None;
for token in tokens { match state { ParserState::Start => { if let Ok(airport) = self.db_pool.load_airport(token) { parsed.departure = Some(airport); state = ParserState::ExpectSID; } else { parsed.warnings.push(format!("未知机场: {}", token)); } }
ParserState::ExpectSID => { if token.contains("SID") || self.is_sid_procedure(token) { parsed.sid = Some(token.clone()); parsed.elements.push(RouteElement::SID(token.clone())); state = ParserState::ExpectAirway; } else if let Ok(wp) = self.db_pool.find_waypoint(token) { // 直接给航点,跳过SID if let Some(wp) = wp { last_waypoint = Some(wp.clone()); parsed.elements.push(RouteElement::Waypoint(wp)); state = ParserState::ExpectAirway; } } }
ParserState::ExpectAirway => { if self.is_airway(token) { // 加载航路信息 if let Ok(segments) = self.db_pool.find_airway_segments(token) { parsed.elements.push(RouteElement::Airway { identifier: token.clone(), segments, }); state = ParserState::ExpectWaypoint; } } else if token == "DCT" { state = ParserState::ExpectWaypoint; // DCT后跟航点 } else if let Ok(wp) = self.db_pool.find_waypoint(token) { // 直接给航点(可能是DCT省略了) if let Some(wp) = wp { last_waypoint = Some(wp.clone()); parsed.elements.push(RouteElement::Direct { from: last_waypoint.clone().unwrap(), to: wp.clone(), }); } } }
// ... 其他状态 } }
Ok(parsed)}为了处理”ZBAA TEPID G212 VYK ZSPD”这种省略SID/STAR的情况,状态机需要有一定的容错性,能够从中间状态恢复。
与Go的桥接技术
最后是Rust库如何被Go调用。这里用了C FFI,Rust编译成静态库,Go通过cgo调用。
Rust端的FFI定义:
#[no_mangle]pub unsafe extern "C" fn routekit_find_routes( handle: *mut c_void, departure: *const c_char, destination: *const c_char, max_routes: usize,) -> *mut c_char { // 参数检查 if handle.is_null() { return ptr::null_mut(); }
let kit = &*(handle as *const RouteKit);
// C字符串转Rust String let dep = match CStr::from_ptr(departure).to_str() { Ok(s) => s, Err(_) => return ptr::null_mut(), };
let dest = match CStr::from_ptr(destination).to_str() { Ok(s) => s, Err(_) => return ptr::null_mut(), };
// 执行搜索 let request = RouteRequest { departure_icao: dep.to_string(), destination_icao: dest.to_string(), max_routes, ..Default::default() };
match kit.find_routes_simple(&request) { Ok(routes) => { // 序列化JSON let json = serde_json::to_string(&routes).unwrap(); // 转换成C字符串,移交所有权给调用者 CString::new(json).unwrap().into_raw() } Err(e) => { // 设置错误信息 set_last_error(e.to_string()); ptr::null_mut() } }}
#[no_mangle]pub unsafe extern "C" fn routekit_free_string(s: *mut c_char) { if !s.is_null() { // 回收C字符串内存 drop(CString::from_raw(s)); }}Go端的调用代码:
/*#cgo LDFLAGS: -L./target/release -lroutekit#include <stdlib.h>#include "routekit.h"*/import "C"import "unsafe"
type RouteKit struct { handle unsafe.Pointer}
func NewRouteKit(dbPath string) (*RouteKit, error) { cPath := C.CString(dbPath) defer C.free(unsafe.Pointer(cPath))
handle := C.routekit_new(cPath) if handle == nil { lastErr := C.GoString(C.routekit_last_error()) return nil, fmt.Errorf("failed to create RouteKit: %s", lastErr) }
return &RouteKit{handle: handle}, nil}
func (k *RouteKit) FindRoutes(departure, destination string, maxRoutes int) ([]RouteResult, error) { cDep := C.CString(departure) cDest := C.CString(destination) defer C.free(unsafe.Pointer(cDep)) defer C.free(unsafe.Pointer(cDest))
result := C.routekit_find_routes(k.handle, cDep, cDest, C.size_t(maxRoutes)) if result == nil { lastErr := C.GoString(C.routekit_last_error()) return nil, fmt.Errorf("route search failed: %s", lastErr) } defer C.routekit_free_string(result)
jsonStr := C.GoString(result)
var routes []RouteResult if err := json.Unmarshal([]byte(jsonStr), &routes); err != nil { return nil, err }
return routes, nil}
func (k *RouteKit) Close() { if k.handle != nil { C.routekit_free(k.handle) k.handle = nil }}这里有个关键点:内存所有权。Rust分配的字符串,必须由Rust自己释放,不能交给Go的GC。所以有了 routekit_free_string这个函数。
性能实测与调优
最终系统的性能数据(基于真实航空数据,约5万航点,20万条边):
| 场景 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| ZGGG-ZSPD(国内正常航路) | 210ms | 45ms | 4.7倍 |
| KJFK-EGLL(跨洋长途) | 850ms | 210ms | 4.0倍 |
| 图构建时间 | 3.2s | 3.2s | - |
| 内存占用 | 180MB | 180MB | - |
优化主要在算法层面,内存占用基本不变。
代码之外的思考
写这个项目的过程中,我有些体会:
- Rust的所有权模型在这类算法中其实是个优势。比如图结构,明确谁拥有节点、谁可以修改,避免了很多race condition。
- 航空数据的特殊性:它不是标准的图论问题,有很多业务规则(方向限制、高度层、程序衔接)。纯粹的最短路径算法不够,需要结合领域知识。
- 性能和安全可以兼得。Rust给了C/C++级别的性能,同时通过所有权系统保证了内存安全。在Go里用unsafe调用C代码总是让人心惊胆战,但Rust这边只要FFI层写对了,核心算法就是安全的。
- 测试的重要性:航路搜索是个组合爆炸的问题,不可能穷举测试。我写了几十个单元测试覆盖典型场景,再加上模糊测试(fuzz testing)随机生成输入,确保系统不会崩溃。
未来可以做的
项目还有很多可以改进的地方:
- 并行搜索:尝试多个起点-终点组合时,可以用rayon并行化
- 实时交通避开:如果有交通数据,可以动态调整边权
- 风场优化:考虑高空风的影响,寻找时间最短而非距离最短的路径
- 缓存热点路径:对于常见机场对,缓存结果
不过目前的功能已经能满足生产需求。项目完全开源,欢迎感兴趣的朋友一起玩:Airway_RouteKit
写这篇博客的时候,我又想到一个可以优化的点…这就是程序员的通病吧,永远觉得代码还能再改一版。但有时候,适可而止也是一种智慧。