Files
Alexandre 2cb633e99d 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>
2026-08-21 11:10:35 +02:00

97 lines
3.8 KiB
JavaScript

// 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));