Builds out the rest of the transit UX from data Transitous actually provides. - Time picker: Leave now / Depart at / Arrive by (MOTIS time + arriveBy). - Mode filter: Bus/Tram/Metro/Train/Ferry groups -> MOTIS transitModes. - Stop departure boards: tap a stop name -> MOTIS stoptimes, with live and cancelled badges and a feed-provided platform hint. - Fares: MOTIS carries NO prices (verified live: debugOutput.fares=0, agencyFareUrl empty), so a 'tickets' link is shown ONLY when the feed supplies an operator fare URL - never an invented price. New src/lib/transitUi.ts holds the mode vocabulary (emoji/colour/groups) so the itinerary and the board cannot drift; StopDepartures.tsx is its own dialog. Docs: AGENTS.md transit note.
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
/**
|
|
* Transit presentation helpers
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* Shared by RouteDialog (the itinerary) and StopDepartures (the departure
|
|
* board), so the mode → icon/colour vocabulary lives in exactly one place.
|
|
* Modes are MOTIS's own `mode` values.
|
|
*/
|
|
|
|
import type { TransitItinerary, TransitLeg } from './routing';
|
|
|
|
/** Line colour for a transit leg, keyed by the MOTIS mode. */
|
|
export function transitLegColor(mode: string): string {
|
|
switch (mode) {
|
|
case 'BUS':
|
|
case 'COACH': return '#22c55e';
|
|
case 'TRAM': return '#eab308';
|
|
case 'SUBWAY':
|
|
case 'METRO': return '#3b82f6';
|
|
case 'RAIL':
|
|
case 'REGIONAL_RAIL':
|
|
case 'REGIONAL_FAST_RAIL':
|
|
case 'LONG_DISTANCE':
|
|
case 'HIGHSPEED_RAIL':
|
|
case 'NIGHT_RAIL':
|
|
case 'SUBURBAN': return '#ef4444';
|
|
case 'FERRY': return '#06b6d4';
|
|
default: return '#ccff00';
|
|
}
|
|
}
|
|
|
|
/** Emoji for a transit leg's mode. */
|
|
export function transitModeEmoji(mode: string): string {
|
|
switch (mode) {
|
|
case 'BUS':
|
|
case 'COACH': return '🚌';
|
|
case 'TRAM': return '🚊';
|
|
case 'SUBWAY':
|
|
case 'METRO': return '🚇';
|
|
case 'RAIL':
|
|
case 'REGIONAL_RAIL':
|
|
case 'REGIONAL_FAST_RAIL':
|
|
case 'LONG_DISTANCE':
|
|
case 'HIGHSPEED_RAIL':
|
|
case 'NIGHT_RAIL':
|
|
case 'SUBURBAN': return '🚆';
|
|
case 'FERRY': return '⛴️';
|
|
case 'WALK': return '🚶';
|
|
case 'BIKE': return '🚲';
|
|
default: return '🚇';
|
|
}
|
|
}
|
|
|
|
/** Leading emoji for a whole itinerary — its first vehicle leg, else walking. */
|
|
export function transitPrimaryEmoji(it: TransitItinerary): string {
|
|
const vehicle = it.legs.find((l) => l.mode !== 'WALK' && l.mode !== 'BIKE');
|
|
return transitModeEmoji(vehicle?.mode ?? 'WALK');
|
|
}
|
|
|
|
/** Seconds between a leg's start and end (0 when either time is missing). */
|
|
export function legDurationS(leg: TransitLeg): number {
|
|
if (!leg.startISO || !leg.endISO) return 0;
|
|
const ms = Date.parse(leg.endISO) - Date.parse(leg.startISO);
|
|
return Number.isFinite(ms) && ms > 0 ? Math.round(ms / 1000) : 0;
|
|
}
|
|
|
|
/**
|
|
* The transit mode picker's groups, each mapping to MOTIS `transitModes`
|
|
* values. ⚠️ MOTIS classifies a German S-Bahn as `METRO` (verified live), so it
|
|
* lands under "Metro", not "Train" — that is the feed's own classification.
|
|
*/
|
|
export const TRANSIT_MODE_GROUPS: ReadonlyArray<{
|
|
key: string;
|
|
label: string;
|
|
emoji: string;
|
|
modes: readonly string[];
|
|
}> = [
|
|
{ key: 'bus', label: 'Bus', emoji: '🚌', modes: ['BUS', 'COACH'] },
|
|
{ key: 'tram', label: 'Tram', emoji: '🚊', modes: ['TRAM'] },
|
|
{ key: 'metro', label: 'Metro', emoji: '🚇', modes: ['SUBWAY', 'METRO'] },
|
|
{
|
|
key: 'train',
|
|
label: 'Train',
|
|
emoji: '🚆',
|
|
modes: [
|
|
'RAIL',
|
|
'REGIONAL_RAIL',
|
|
'REGIONAL_FAST_RAIL',
|
|
'HIGHSPEED_RAIL',
|
|
'LONG_DISTANCE',
|
|
'NIGHT_RAIL',
|
|
'SUBURBAN',
|
|
],
|
|
},
|
|
{ key: 'ferry', label: 'Ferry', emoji: '⛴️', modes: ['FERRY'] },
|
|
];
|