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
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "flightplanner-server"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "flightplanner-server"
path = "src/main.rs"
[dependencies]
flightplanner-core = { workspace = true }
flightplanner-rad = { path = "../rad" }
axum = "0.7"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
tower-http = { version = "0.5", features = ["cors"] }
serde = { workspace = true }
serde_json = { workspace = true }
+134
View File
@@ -0,0 +1,134 @@
//! Custom HTTP backend for Flight Planner — the "server" mode of the app.
//!
//! Exposes the same `core::api::plan` engine over HTTP so several clients can
//! share one navdata DB / route database. The GUI can point at this instead of
//! computing locally (chosen by the end user in Settings).
//!
//! Config via env: `FP_DB` (navdata SQLite), `FP_AIRCRAFT_DIR`, `FP_BIND`.
use std::net::SocketAddr;
use std::sync::Arc;
use axum::{
extract::{Query, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::Deserialize;
use tower_http::cors::CorsLayer;
use flightplanner_core::api::{self, CachedRouteDto, PlanRequest, PlanResult};
use flightplanner_core::rad::RadData;
/// Server-side data locations (the client never dictates server file paths).
#[derive(Clone)]
struct Cfg {
db: String,
aircraft_dir: String,
cifp: String,
rad: Option<Arc<RadData>>,
}
async fn health() -> &'static str {
"ok"
}
#[derive(Deserialize)]
struct RoutesQuery {
dep: String,
dest: String,
limit: Option<usize>,
}
/// Our own database of stored flight plans for a city pair.
async fn routes(
State(cfg): State<Cfg>,
Query(q): Query<RoutesQuery>,
) -> Result<Json<Vec<CachedRouteDto>>, (StatusCode, String)> {
let db = cfg.db.clone();
let res = tokio::task::spawn_blocking(move || {
api::recent_routes(&db, &q.dep, &q.dest, q.limit.unwrap_or(10))
})
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
res.map(Json)
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
}
async fn stats(State(cfg): State<Cfg>) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
let db = cfg.db.clone();
let n = tokio::task::spawn_blocking(move || api::route_db_count(&db))
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
Ok(Json(serde_json::json!({ "routes": n })))
}
async fn plan(
State(cfg): State<Cfg>,
Json(mut req): Json<PlanRequest>,
) -> Result<Json<PlanResult>, (StatusCode, String)> {
// Force the server's own data paths, ignoring whatever the client sent.
req.db_path = cfg.db.clone();
req.aircraft_dir = cfg.aircraft_dir.clone();
req.cifp_dir = Some(cfg.cifp.clone());
let rad = cfg.rad.clone();
// `api::plan` is blocking (SQLite + compute) — keep it off the async pool.
let res = tokio::task::spawn_blocking(move || api::plan(&req, rad.as_deref()))
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
res.map(Json)
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
}
/// Load the RAD workbook once (env `FP_RAD`, default `rad/RAD_current.xlsx`).
fn load_rad() -> Option<RadData> {
let path = std::env::var("FP_RAD").unwrap_or_else(|_| "rad/RAD_current.xlsx".into());
if !std::path::Path::new(&path).exists() {
eprintln!("RAD: file not found ({path}) — RAD checks disabled");
return None;
}
match flightplanner_rad::parse(&path) {
Ok(rad) => {
let (f, o, c) = rad.dct_counts();
println!(
"RAD loaded: {} areas, {} DCT ({f} forbidden / {o} conditional / {c} compulsory)",
rad.areas.len(),
rad.dct.len()
);
Some(rad)
}
Err(e) => {
eprintln!("RAD: parse failed ({e}) — RAD checks disabled");
None
}
}
}
#[tokio::main]
async fn main() {
let rad = load_rad();
let cfg = Cfg {
db: std::env::var("FP_DB").unwrap_or_else(|_| "real.db".into()),
aircraft_dir: std::env::var("FP_AIRCRAFT_DIR").unwrap_or_else(|_| "data/aircraft".into()),
cifp: std::env::var("FP_CIFP").unwrap_or_else(|_| "navdata/CIFP".into()),
rad: rad.map(Arc::new),
};
let bind = std::env::var("FP_BIND").unwrap_or_else(|_| "0.0.0.0:8787".into());
let app = Router::new()
.route("/health", get(health))
.route("/plan", post(plan))
.route("/routes", get(routes))
.route("/stats", get(stats))
.layer(CorsLayer::permissive())
.with_state(cfg.clone());
let addr: SocketAddr = bind.parse().expect("invalid FP_BIND");
println!("Flight Planner server → http://{addr}");
println!(" navdata DB : {}", cfg.db);
println!(" aircraft dir : {}", cfg.aircraft_dir);
let listener = tokio::net::TcpListener::bind(addr).await.expect("bind failed");
axum::serve(listener, app).await.expect("server crashed");
}