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:
@@ -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 }
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user