fix: filter out problematic stops on error

This commit is contained in:
Jan-Henrik 2026-03-11 17:16:59 +01:00
parent 4380c63643
commit 793a100598

View file

@ -95,6 +95,106 @@ function runProcess(cmd: string, args: string[]): Promise<void> {
});
}
class ProcessError extends Error {
constructor(message: string, public readonly lines: string[], public readonly signal: string | null) {
super(message);
}
}
/** Like runProcess but captures stdout+stderr (still printed) for error analysis. */
function runProcessCapture(cmd: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
console.log(`[build-valhalla] Running: ${cmd} ${args.join(" ")}`);
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
const lines: string[] = [];
const onData = (d: Buffer) => {
process.stdout.write(d);
lines.push(...d.toString().split(/\r?\n/).filter((l) => l.length > 0));
};
child.stdout!.on("data", onData);
child.stderr!.on("data", onData);
child.on("error", reject);
child.on("exit", (code, signal) => {
if (code === 0) resolve();
else reject(new ProcessError(
`${cmd} exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`,
lines, signal,
));
});
});
}
// ─── Transit stop quarantine ───────────────────────────────────────────────────
/** Path to the quarantine file (outside feed/ so it survives re-filters). */
function quarantineFilePath(citySlug: string): string {
return `${GTFS_DATA_DIR}/${citySlug}/.quarantine`;
}
function readQuarantine(citySlug: string): [number, number][] {
const p = quarantineFilePath(citySlug);
if (!existsSync(p)) return [];
try { return JSON.parse(readFileSync(p, "utf8")) as [number, number][]; } catch { return []; }
}
/** Append new bad coordinates to the quarantine file. */
function appendQuarantine(citySlug: string, badCoords: [number, number][]): void {
const merged = [...readQuarantine(citySlug), ...badCoords];
writeFileSync(quarantineFilePath(citySlug), JSON.stringify(merged));
console.log(`[build-valhalla] Quarantine updated for ${citySlug}: ${merged.length} stop(s) total`);
}
/**
* Remove quarantined stops from the city's filtered GTFS feed (stops.txt + stop_times.txt).
* Called before valhalla_ingest_transit so the bad stops never enter the transit graph.
*/
function applyQuarantine(citySlug: string): void {
const badCoords = readQuarantine(citySlug);
if (badCoords.length === 0) return;
const feedDir = cityGtfsFeedDir(citySlug);
const stopsPath = `${feedDir}/stops.txt`;
if (!existsSync(stopsPath)) return;
const stopLines = readFileSync(stopsPath, "utf8").split(/\r?\n/).filter((l) => l.trim());
if (stopLines.length < 2) return;
const headers = stopLines[0].split(",").map((h) => h.trim().replace(/^\uFEFF/, ""));
const stopIdCol = headers.indexOf("stop_id");
const latCol = headers.indexOf("stop_lat");
const lonCol = headers.indexOf("stop_lon");
if (stopIdCol < 0 || latCol < 0 || lonCol < 0) return;
const removedIds = new Set<string>();
const keptLines = [stopLines[0]];
for (let i = 1; i < stopLines.length; i++) {
const fields = stopLines[i].split(",");
const lat = parseFloat(fields[latCol] ?? "");
const lon = parseFloat(fields[lonCol] ?? "");
if (badCoords.some(([bLat, bLon]) => Math.abs(lat - bLat) < 1e-5 && Math.abs(lon - bLon) < 1e-5)) {
removedIds.add(fields[stopIdCol] ?? "");
} else {
keptLines.push(stopLines[i]);
}
}
if (removedIds.size === 0) return;
console.log(`[build-valhalla] Quarantine: removing stop(s) ${[...removedIds].join(", ")} from feed`);
writeFileSync(stopsPath, keptLines.join("\n") + "\n");
const stPath = `${feedDir}/stop_times.txt`;
if (!existsSync(stPath)) return;
const stLines = readFileSync(stPath, "utf8").split(/\r?\n/).filter((l) => l.trim());
if (stLines.length < 2) return;
const stHeaders = stLines[0].split(",").map((h) => h.trim());
const stStopCol = stHeaders.indexOf("stop_id");
if (stStopCol < 0) return;
const stOut = [stLines[0]];
for (let i = 1; i < stLines.length; i++) {
if (!removedIds.has(stLines[i].split(",")[stStopCol] ?? "")) stOut.push(stLines[i]);
}
writeFileSync(stPath, stOut.join("\n") + "\n");
}
function buildTimezoneDb(): Promise<void> {
return new Promise((resolve, reject) => {
console.log("[build-valhalla] Running: valhalla_build_timezones → " + TIMEZONE_SQLITE);