//! Our own database of flight plans — routes we've generated and IFPS-pre-checked, //! cached per city pair in the `routes` table (see `db::schema`). use rusqlite::{params, Connection}; use crate::error::Result; const SEP: &str = " ||| "; /// A stored (generated + pre-checked) flight plan. #[derive(Debug, Clone)] pub struct CachedRoute { pub dep: String, pub dest: String, pub cruise_fl: i32, pub route_string: String, pub dist_nm: f64, pub via_airways: bool, pub ifps_ok: bool, pub ifps_errors: Vec, pub source: String, pub generated_at: String, } /// Insert or refresh a route (unique on dep/dest/FL/route_string). #[allow(clippy::too_many_arguments)] pub fn record( conn: &Connection, dep: &str, dest: &str, cruise_fl: i32, route_string: &str, dist_nm: f64, via_airways: bool, ifps_ok: bool, ifps_errors: &[String], source: &str, ) -> Result<()> { conn.execute( "INSERT INTO routes \ (dep,dest,cruise_fl,route_string,dist_nm,via_airways,ifps_ok,ifps_errors,source,generated_at) \ VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9, datetime('now')) \ ON CONFLICT(dep,dest,cruise_fl,route_string) DO UPDATE SET \ dist_nm=excluded.dist_nm, via_airways=excluded.via_airways, ifps_ok=excluded.ifps_ok, \ ifps_errors=excluded.ifps_errors, source=excluded.source, generated_at=excluded.generated_at", params![ dep.to_uppercase(), dest.to_uppercase(), cruise_fl, route_string, dist_nm, via_airways as i32, ifps_ok as i32, ifps_errors.join(SEP), source, ], )?; Ok(()) } /// Stored routes for a city pair, most recent first. pub fn recent(conn: &Connection, dep: &str, dest: &str, limit: usize) -> Result> { let mut stmt = conn.prepare( "SELECT dep,dest,cruise_fl,route_string,dist_nm,via_airways,ifps_ok,ifps_errors,source,generated_at \ FROM routes WHERE dep=?1 AND dest=?2 ORDER BY generated_at DESC LIMIT ?3", )?; let rows = stmt.query_map( params![dep.to_uppercase(), dest.to_uppercase(), limit as i64], |r| { let errs: String = r.get(7)?; Ok(CachedRoute { dep: r.get(0)?, dest: r.get(1)?, cruise_fl: r.get(2)?, route_string: r.get(3)?, dist_nm: r.get(4)?, via_airways: r.get::<_, i32>(5)? != 0, ifps_ok: r.get::<_, i32>(6)? != 0, ifps_errors: if errs.is_empty() { Vec::new() } else { errs.split(SEP).map(str::to_string).collect() }, source: r.get(8)?, generated_at: r.get(9)?, }) }, )?; let mut out = Vec::new(); for row in rows { out.push(row?); } Ok(out) } /// Total number of stored routes. pub fn count(conn: &Connection) -> Result { Ok(conn.query_row("SELECT COUNT(*) FROM routes", [], |r| r.get(0))?) } /// Export every IFPS-validated (`ifps_ok`) route as TSV lines /// (`dep⇥dest⇥fl⇥dist⇥via_airways⇥route_string`). This is the portable, version- /// controllable form of the route DB, so validated seeds survive a navdata rebuild /// (the `routes` table lives in the same SQLite file as the imported navdata). pub fn export_validated(conn: &Connection) -> Result { let mut stmt = conn.prepare( "SELECT dep,dest,cruise_fl,dist_nm,via_airways,route_string FROM routes \ WHERE ifps_ok=1 GROUP BY dep,dest ORDER BY dep,dest", )?; let rows = stmt.query_map([], |r| { Ok(format!( "{}\t{}\t{}\t{:.0}\t{}\t{}", r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, i32>(2)?, r.get::<_, f64>(3)?, r.get::<_, i32>(4)?, r.get::<_, String>(5)?, )) })?; let mut out = String::new(); for row in rows { out.push_str(&row?); out.push('\n'); } Ok(out) } /// Import validated routes from TSV (as produced by [`export_validated`]), /// recording each as an `ifps_ok` seed (`source="seed"`). Returns the count. pub fn import_seeds(conn: &Connection, tsv: &str) -> Result { let mut n = 0; for line in tsv.lines() { let line = line.trim(); if line.is_empty() { continue; } let f: Vec<&str> = line.splitn(6, '\t').collect(); if f.len() < 6 { continue; } record( conn, f[0], f[1], f[2].parse().unwrap_or(0), f[5], f[3].parse().unwrap_or(0.0), f[4] == "1", true, &[], "seed", )?; n += 1; } Ok(n) }