fix: filter out problematic stops on error
This commit is contained in:
parent
4380c63643
commit
793a100598
1 changed files with 100 additions and 0 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue