Implement journey ranking and improve UI presentation
Add `rankJourneys` utility to score connections based on arrival fit, directness, and duration. Update `JourneyList` to display the best option first with a "Top" badge and allow expanding to see more. Fix HAFAS parser to include train direction in journey details. Limit API result count to 5 and add TypeScript declaration for PNG assets.
This commit is contained in:
@@ -37,7 +37,11 @@ export function parseHafasJourneys(json: RawJson, hafasDate: string, queryDate:
|
||||
const delay = Math.max(0, Math.round((rD.getTime() - sD.getTime()) / 60000));
|
||||
const trains: string[] = (con.secL ?? [])
|
||||
.filter((s: RawJson) => s.jny)
|
||||
.map((s: RawJson) => s.jny?.stopL?.[0]?.name ?? "")
|
||||
.map((s: RawJson) => {
|
||||
const name = s.jny?.stopL?.[0]?.name ?? "";
|
||||
const direction = s.jny?.dirTxt ?? s.jny?.dir ?? "";
|
||||
return name && direction ? `${name} -> ${direction}` : name;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './status-utils';
|
||||
export * from './hafas-time';
|
||||
export * from './hafas-parser';
|
||||
export * from './defaults';
|
||||
export * from './journey-scoring';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Journey } from "./types";
|
||||
|
||||
export interface RankedJourney {
|
||||
journey: Journey;
|
||||
score: number;
|
||||
}
|
||||
|
||||
function durationMinutes(journey: Journey): number {
|
||||
return Math.max(0, (journey.rA.getTime() - journey.rD.getTime()) / 60_000);
|
||||
}
|
||||
|
||||
function arrivalDeltaMinutes(journey: Journey, targetArrivalTime: Date, finalLegDurationMs: number): number {
|
||||
const finalArrival = journey.rA.getTime() + finalLegDurationMs;
|
||||
return Math.abs(finalArrival - targetArrivalTime.getTime()) / 60_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank journeys by arrival fit, transfer count, and duration.
|
||||
*
|
||||
* Arrival fit is weighted highest because users asked for connections close to
|
||||
* the planned arrival. Direct connections are favored next, then shorter trips.
|
||||
* Late arrivals are heavily penalized so an on-time option usually wins.
|
||||
*/
|
||||
export function rankJourneys(
|
||||
journeys: Journey[],
|
||||
targetArrivalTime: Date,
|
||||
finalLegDurationMs = 0,
|
||||
): RankedJourney[] {
|
||||
const durations = journeys.map(durationMinutes);
|
||||
const minDuration = Math.min(...durations);
|
||||
const maxDuration = Math.max(...durations);
|
||||
const durationRange = Math.max(1, maxDuration - minDuration);
|
||||
|
||||
return journeys
|
||||
.map((journey): RankedJourney => {
|
||||
const finalArrivalMs = journey.rA.getTime() + finalLegDurationMs;
|
||||
const targetArrivalMs = targetArrivalTime.getTime();
|
||||
const deltaMinutes = Math.abs(finalArrivalMs - targetArrivalMs) / 60_000;
|
||||
const lateMinutes = Math.max(0, (finalArrivalMs - targetArrivalMs) / 60_000);
|
||||
const duration = durationMinutes(journey);
|
||||
|
||||
const arrivalScore = Math.max(0, 100 - deltaMinutes * 4) - lateMinutes * 10;
|
||||
const changesScore = Math.max(0, 100 - journey.changes * 35);
|
||||
const durationScore = 100 - ((duration - minDuration) / durationRange) * 100;
|
||||
const cancelledPenalty = journey.cancelled ? 1_000 : 0;
|
||||
|
||||
const score = arrivalScore * 0.55 + changesScore * 0.3 + durationScore * 0.15 - cancelledPenalty;
|
||||
return { journey, score };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
|
||||
const arrivalTie = arrivalDeltaMinutes(a.journey, targetArrivalTime, finalLegDurationMs)
|
||||
- arrivalDeltaMinutes(b.journey, targetArrivalTime, finalLegDurationMs);
|
||||
if (arrivalTie !== 0) return arrivalTie;
|
||||
|
||||
if (a.journey.changes !== b.journey.changes) return a.journey.changes - b.journey.changes;
|
||||
|
||||
return durationMinutes(a.journey) - durationMinutes(b.journey);
|
||||
});
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export interface Journey {
|
||||
delay: number; // delay in minutes
|
||||
platform: string;
|
||||
changes: number;
|
||||
/** Train legs, optionally including direction text when supplied by HAFAS. */
|
||||
trains: string[];
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user