2cb633e99d
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>
97 lines
3.8 KiB
JavaScript
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));
|