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,96 @@
|
||||
// Validate a flight plan against the public Eurocontrol IFPUV (guest access).
|
||||
//
|
||||
// Usage:
|
||||
// node validate.mjs '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO","actype":"A320","level":"F360","speed":"N0450"}'
|
||||
// node validate.mjs --raw "(FPL-...-...)"
|
||||
//
|
||||
// Prints one JSON line: { accepted, errors:[{code,msg}], warnings:[...], raw }.
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const URL = "https://www.public.nm.eurocontrol.int/PUBPORTAL/gateway/spec/";
|
||||
const ROUTE_SEL =
|
||||
'[id="FREE_TEXT_EDITOR.FLIGHT_DATA_AREA.GENERAL_DATA_ENTRY.INTRODUCE_FLIGHT_PLAN_FIELD"]';
|
||||
const VALIDATE_SEL = '[id="FREE_TEXT_EDITOR.FLIGHT_DATA_AREA.VALIDATE_ACTION_LABEL"]';
|
||||
|
||||
// --- build the ICAO FPL free-text -----------------------------------------
|
||||
function tomorrowDOF() {
|
||||
const d = new Date(Date.now() + 24 * 3600 * 1000);
|
||||
const p = (n) => String(n).padStart(2, "0");
|
||||
return `${String(d.getUTCFullYear()).slice(2)}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}`;
|
||||
}
|
||||
|
||||
function buildFpl(o) {
|
||||
const cs = o.callsign || o.reg?.replace("-", "") || "TEST01";
|
||||
const type = o.actype || "A320";
|
||||
const wtc = o.wtc || "M";
|
||||
const equip = o.equip || "SDE1E2E3FGHIRWY/LB1";
|
||||
const reg = o.reg || "FGKXA";
|
||||
const pbn = o.pbn || "A1B1C1D1O1S2";
|
||||
const eet = o.eet || "0040";
|
||||
const altn = o.altn ? ` ${o.altn}` : "";
|
||||
const item15 = `${o.speed || "N0450"}${o.level || "F360"} ${o.route.trim()}`;
|
||||
return (
|
||||
`(FPL-${cs}-IS\n` +
|
||||
`-${type}/${wtc}-${equip}\n` +
|
||||
`-${o.adep}${o.eobt || "1200"}\n` +
|
||||
`-${item15}\n` +
|
||||
`-${o.ades}${eet}${altn}\n` +
|
||||
`-PBN/${pbn} REG/${reg} DOF/${o.dof || tomorrowDOF()})`
|
||||
);
|
||||
}
|
||||
|
||||
// --- parse the "Validation Results" text ----------------------------------
|
||||
function parseResults(body) {
|
||||
const start = body.indexOf("Validation Results");
|
||||
const region = start >= 0 ? body.slice(start) : body;
|
||||
const codeRe = /\b([A-Z]{2,6}\d{1,4}):\s*([^\n\t]+)/g;
|
||||
const all = [];
|
||||
let m;
|
||||
while ((m = codeRe.exec(region))) all.push({ code: m[1], msg: m[2].trim() });
|
||||
// Filing-form artifacts (REG/EOBD/EET fields we'd fill correctly in a real
|
||||
// filing), not route faults. PROF191 = our fixed EET differs from the calc.
|
||||
const housekeeping = /^(EFPM(51|234|168|166)|PROF191)$/;
|
||||
const errors = all.filter((e) => !housekeeping.test(e.code));
|
||||
return { errors, all };
|
||||
}
|
||||
|
||||
// --- run -------------------------------------------------------------------
|
||||
const args = process.argv.slice(2);
|
||||
let fpl;
|
||||
if (args[0] === "--raw") {
|
||||
fpl = args.slice(1).join(" ");
|
||||
} else {
|
||||
const payload = JSON.parse(args[0] || '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO"}');
|
||||
fpl = buildFpl(payload);
|
||||
}
|
||||
|
||||
const b = await chromium.launch({ headless: true });
|
||||
const ctx = await b.newContext({ viewport: { width: 1400, height: 950 } });
|
||||
const page = await ctx.newPage();
|
||||
const out = { accepted: null, errors: [], raw: "", fpl, error: null };
|
||||
try {
|
||||
await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(8000);
|
||||
|
||||
const popupP = ctx.waitForEvent("page", { timeout: 15000 }).catch(() => null);
|
||||
await page.getByText("Free Text Editor", { exact: false }).first().click({ timeout: 15000 });
|
||||
const ed = (await popupP) || page;
|
||||
await ed.waitForSelector(ROUTE_SEL, { timeout: 20000 });
|
||||
|
||||
await ed.locator(ROUTE_SEL).fill(fpl);
|
||||
await ed.locator(VALIDATE_SEL).click({ timeout: 15000 });
|
||||
await ed.waitForTimeout(7000);
|
||||
|
||||
const body = await ed.locator("body").innerText();
|
||||
const { errors, all } = parseResults(body);
|
||||
out.errors = errors;
|
||||
out.accepted = errors.length === 0;
|
||||
const s = body.indexOf("Validated ICAO FPL");
|
||||
out.raw = (s >= 0 ? body.slice(s) : body).replace(/\n{3,}/g, "\n").trim().slice(0, 2500);
|
||||
await ed.screenshot({ path: "result.png", fullPage: true });
|
||||
} catch (e) {
|
||||
out.error = e.message;
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
console.log(JSON.stringify(out));
|
||||
Reference in New Issue
Block a user