Initial commit: offline flight planner (Rust, PFPX-class)

Clean-room reproduction of PFPX: route generation + IFPS validation + OFP.
Independent design — official ICAO/EUROCONTROL data only (RAD, FRA points,
IFPUV oracle); no community FPL sources.

Workspace crates: core (routing/discover/rad/navdata/perf/export),
cli, server, rad (Annex parser), gui (Tauri v2 + React + MapLibre).
Route discovery: oracle-in-the-loop repair against Eurocontrol IFPUV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 11:10:35 +02:00
commit 2cb633e99d
108 changed files with 19460 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
//! Step-5 test: export a fixture route to `.pln`, `.fms` and OFP text.
use std::path::PathBuf;
use flightplanner_core::{db, export, routing};
use rusqlite::Connection;
fn fixtures_db() -> Connection {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let mut conn = Connection::open_in_memory().unwrap();
db::import_navdata(&mut conn, &dir).unwrap();
conn
}
#[test]
fn exports_pln_fms_and_ofp() {
let conn = fixtures_db();
let route = routing::plan_route(&conn, "LFPG", "EGLL", None).unwrap();
let pln = export::pln::to_pln(&conn, &route, 34000).unwrap();
assert!(pln.contains("<SimBase.Document"));
assert!(pln.contains("<DepartureID>LFPG</DepartureID>"));
assert!(pln.contains("<DestinationID>EGLL</DestinationID>"));
assert!(pln.contains("<ATCWaypointType>Airport</ATCWaypointType>"));
let fms = export::fms::to_fms(&conn, &route, 34000).unwrap();
assert!(fms.starts_with("I\n1100 Version"));
assert!(fms.contains("ADEP LFPG"));
assert!(fms.contains("ADES EGLL"));
assert!(fms.contains("NUMENR "));
let ofp = export::ofp::to_ofp(&route, None, Some("A320"), Some(340));
assert!(ofp.contains("OPERATIONAL FLIGHT PLAN"));
assert!(ofp.contains("LFPG -> EGLL"));
assert!(ofp.contains("FL340"));
}
+2
View File
@@ -0,0 +1,2 @@
RWY:RW09L, , ,00079, , ,3, ;N51285200,W000273600,0000;
RWY:RW27R, , ,00078, , ,3, ;N51284900,W000260900,0000;
+2
View File
@@ -0,0 +1,2 @@
RWY:RW08L, , ,00338, ,GLE ,3, ;N48594447,E002330988,0000;
RWY:RW26R, , ,00318, ,GAU ,3, ;N48595395,E002360724,1725;
+8
View File
@@ -0,0 +1,8 @@
I
1100 Version - test fixture, not real navdata
ABEAM LF 11 BEACN LF 11 N 1 35 245 T100
BEACN LF 11 CROSS LF 11 N 1 35 245 T100
CROSS LF 11 DOVER EG 11 N 2 245 460 U200
DOVER EG 11 ENTRY EG 11 N 2 245 460 U200
99
+9
View File
@@ -0,0 +1,9 @@
I
1101 Version - test fixture, not real navdata
49.000000000 2.500000000 ABEAM ENRT LF 1000001
49.500000000 2.000000000 BEACN ENRT LF 1000002
50.000000000 1.000000000 CROSS ENRT LF 1000003
50.500000000 0.000000000 DOVER ENRT EG 1000004
51.000000000 -0.500000000 ENTRY ENRT EG 1000005
99
+7
View File
@@ -0,0 +1,7 @@
I
1150 Version - test fixture, not real navdata
3 49.010000000 2.600000000 300 11400 130 1.000 PGV ENRT LF PARIS VOR
2 50.900000000 -0.400000000 100 3700 50 0.000 EGN ENRT EG ENTRY NDB
4 49.000000000 2.550000000 50 11000 25 0.000 ILP ENRT LF PARIS ILS
99
+57
View File
@@ -0,0 +1,57 @@
//! End-to-end step-2 test: parse the hand-made fixtures and import them into an
//! in-memory SQLite database, then assert row counts and a couple of lookups.
use std::path::PathBuf;
use flightplanner_core::db;
use rusqlite::Connection;
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn count(conn: &Connection, table: &str) -> i64 {
conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0))
.unwrap()
}
#[test]
fn imports_fixture_navdata() {
let mut conn = Connection::open_in_memory().unwrap();
let stats = db::import_navdata(&mut conn, &fixtures_dir()).unwrap();
// Fixtures: 5 fixes, 1 VOR + 1 NDB (ILS skipped), 4 airway segments, 2 airports.
assert_eq!(stats.waypoints, 5);
assert_eq!(stats.navaids, 2);
assert_eq!(stats.airway_segments, 4);
assert_eq!(stats.airports, 2);
assert_eq!(count(&conn, "waypoints"), 5);
assert_eq!(count(&conn, "navaids"), 2);
assert_eq!(count(&conn, "airway_segments"), 4);
assert_eq!(count(&conn, "airports"), 2);
// A specific fix carries the right region.
let region: String = conn
.query_row(
"SELECT region FROM waypoints WHERE ident = 'DOVER'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(region, "EG");
// LFPG reference point is the centroid of its two runway thresholds.
let (lat, lon): (f64, f64) = conn
.query_row(
"SELECT lat, lon FROM airports WHERE icao = 'LFPG'",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert!((lat - 48.99).abs() < 0.05, "lat = {lat}");
assert!((lon - 2.56).abs() < 0.05, "lon = {lon}");
// Distinct airway names were recorded.
assert_eq!(count(&conn, "airways"), 2);
}
+89
View File
@@ -0,0 +1,89 @@
//! End-to-end step-3 test: import the fixtures, then route LFPG→EGLL through the
//! hand-made airway network and check the result is sensible.
use std::path::PathBuf;
use flightplanner_core::routing::graph::RouteGraph;
use flightplanner_core::{db, routing};
use rusqlite::Connection;
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn fixtures_db() -> Connection {
let mut conn = Connection::open_in_memory().unwrap();
db::import_navdata(&mut conn, &fixtures_dir()).unwrap();
conn
}
#[test]
fn routes_lfpg_to_egll_via_airways() {
let conn = fixtures_db();
let route = routing::plan_route(&conn, "LFPG", "EGLL", None).unwrap();
assert!(route.via_airways, "expected an airway route, got {route:?}");
assert_eq!(route.legs.first().unwrap().from, "LFPG");
assert_eq!(route.legs.last().unwrap().to, "EGLL");
// The fixture network forces travel over the T100/U200 airways.
let airways: Vec<&str> = route.legs.iter().map(|l| l.airway.as_str()).collect();
assert!(
airways.iter().any(|a| *a != "DCT"),
"route used no airway: {airways:?}"
);
// Sanity on total distance (direct LFPG-EGLL ≈ 188 nm; via fixtures a bit more).
assert!(
route.total_nm > 150.0 && route.total_nm < 400.0,
"total = {} nm",
route.total_nm
);
}
#[test]
fn unknown_airport_is_reported() {
let conn = fixtures_db();
let err = routing::plan_route(&conn, "LFPG", "ZZZZ", None).unwrap_err();
assert!(err.to_string().contains("ZZZZ"), "{err}");
}
#[test]
fn graph_filters_airways_by_flight_level() {
let conn = fixtures_db();
// Fixtures: T100 (low, FL35..245) links ABEAM/BEACN/CROSS,
// U200 (high, FL245..460) links CROSS/DOVER/ENTRY.
assert_eq!(RouteGraph::build(&conn, None).unwrap().node_count(), 5);
assert_eq!(RouteGraph::build(&conn, Some(200)).unwrap().node_count(), 3); // T100 only
assert_eq!(RouteGraph::build(&conn, Some(300)).unwrap().node_count(), 3); // U200 only
assert_eq!(RouteGraph::build(&conn, Some(500)).unwrap().node_count(), 0); // above both
}
#[test]
fn fra_produces_a_dct_chain() {
let conn = fixtures_db();
let route = routing::fra::plan_fra(&conn, "LFPG", "EGLL", &[], &[], 360, None).unwrap();
assert!(!route.via_airways, "FRA is DCT-based: {route:?}");
assert!(route.legs.iter().all(|l| l.airway == "DCT"), "{route:?}");
assert_eq!(route.legs.first().unwrap().from, "LFPG");
assert_eq!(route.legs.last().unwrap().to, "EGLL");
assert!(
route.total_nm > 150.0 && route.total_nm < 400.0,
"total = {} nm",
route.total_nm
);
}
#[test]
fn connects_departure_via_given_fix() {
let conn = fixtures_db();
// Force LFPG to join the network at CROSS (a SID exit fix, say) rather than
// the nearest airway point.
let route =
routing::plan_route_conn(&conn, "LFPG", "EGLL", None, &["CROSS".to_string()], &[]).unwrap();
assert!(route.via_airways, "{route:?}");
let first = route.legs.first().unwrap();
assert_eq!(first.from, "LFPG");
assert_eq!(first.to, "CROSS", "should connect via the given fix: {route:?}");
}