89270c01a3
The oracle-validated route DB (the realistic path to 100% under prose-only) lived only in the gitignored real.db, so a navdata rebuild would lose it. Now it is portable and version-controlled: - routes::export_validated -> TSV of every ifps_ok route (one per pair); import_seeds loads them back as reusable seeds (source=seed). - CLI `export-routes` / `import-routes`. - discover reuse now matches any ifps_ok route (not just source=ifps), so imported seeds are reused instantly (verified round-trip). - data/validated_routes.tsv: 65 validated European city-pair routes, built by bulk-seeding discover over ~85 common pairs (the DB grew ~11 -> 65). This is the PFPX-style route DB, committed: 65 pairs are now no-error & instant on any machine; `learn` + more bulk-seed grow it further. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
158 lines
4.8 KiB
Rust
158 lines
4.8 KiB
Rust
//! 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<String>,
|
|
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<Vec<CachedRoute>> {
|
|
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<i64> {
|
|
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<String> {
|
|
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<usize> {
|
|
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)
|
|
}
|