Add one change & crime

This commit is contained in:
Andras Schmelczer 2026-06-14 14:51:29 +01:00
parent 8e4c56bb0d
commit 54fbcb1ea6
17 changed files with 306 additions and 154 deletions

View file

@ -66,6 +66,9 @@ export interface TravelTimeEntry {
useBest: boolean;
/** Restrict transit to walk-transit-walk (0 changes). Optional; defaults to false. */
noChange?: boolean;
/** Restrict transit to at most one change. Optional; defaults to false.
* Mutually exclusive with noChange (the UI never sets both). */
oneChange?: boolean;
/** Drop buses from the allowed transit modes. Optional; defaults to false. */
noBuses?: boolean;
}
@ -77,42 +80,44 @@ export interface TravelTimeEntry {
*
* For non-transit modes the entry.mode passes through unchanged.
*
* Note: the transit-one-change* variants exist server-side but are not reachable
* from the UI toggles (only no-change + no-buses are exposed). They're available
* via direct API access for callers that want them.
* noChange and oneChange are mutually exclusive (the UI never sets both); if
* they ever were, noChange wins here.
*/
export function resolveTransitVariant(entry: TravelTimeEntry): string {
if (entry.mode !== 'transit') return entry.mode;
const nc = entry.noChange ?? false;
const oc = entry.oneChange ?? false;
const nb = entry.noBuses ?? false;
if (nc && nb) return 'transit-no-change-no-bus';
if (nc) return 'transit-no-change';
if (nb) return 'transit-no-bus';
return 'transit';
if (nc) return nb ? 'transit-no-change-no-bus' : 'transit-no-change';
if (oc) return nb ? 'transit-one-change-no-bus' : 'transit-one-change';
return nb ? 'transit-no-bus' : 'transit';
}
/**
* Parse a server-side mode string (incl. transit variants) back into a base
* TransportMode + UI toggle booleans. Returns null for mode strings the UI
* cannot represent (currently: transit-one-change, transit-one-change-no-bus).
* Callers should skip entries that parse to null rather than silently
* normalising to a different variant.
* cannot represent. Callers should skip entries that parse to null rather than
* silently normalising to a different variant.
*/
export function parseServerMode(
modeStr: string
): { mode: TransportMode; noChange: boolean; noBuses: boolean } | null {
): { mode: TransportMode; noChange: boolean; oneChange: boolean; noBuses: boolean } | null {
if (modeStr === 'car' || modeStr === 'bicycle' || modeStr === 'walking') {
return { mode: modeStr, noChange: false, noBuses: false };
return { mode: modeStr, noChange: false, oneChange: false, noBuses: false };
}
switch (modeStr) {
case 'transit':
return { mode: 'transit', noChange: false, noBuses: false };
return { mode: 'transit', noChange: false, oneChange: false, noBuses: false };
case 'transit-no-bus':
return { mode: 'transit', noChange: false, noBuses: true };
return { mode: 'transit', noChange: false, oneChange: false, noBuses: true };
case 'transit-no-change':
return { mode: 'transit', noChange: true, noBuses: false };
return { mode: 'transit', noChange: true, oneChange: false, noBuses: false };
case 'transit-no-change-no-bus':
return { mode: 'transit', noChange: true, noBuses: true };
return { mode: 'transit', noChange: true, oneChange: false, noBuses: true };
case 'transit-one-change':
return { mode: 'transit', noChange: false, oneChange: true, noBuses: false };
case 'transit-one-change-no-bus':
return { mode: 'transit', noChange: false, oneChange: true, noBuses: true };
default:
return null;
}
@ -145,6 +150,7 @@ export function useTravelTime(initial?: TravelTimeInitial) {
timeRange: null,
useBest: false,
noChange: false,
oneChange: false,
noBuses: false,
},
]);
@ -182,11 +188,27 @@ export function useTravelTime(initial?: TravelTimeInitial) {
);
}, []);
// noChange and oneChange are mutually exclusive: enabling one clears the
// other so the resolved server variant is never ambiguous.
const handleToggleNoChange = useCallback((index: number) => {
setEntries((prev) =>
dedupeTravelTimeEntries(
prev.map((entry, i) =>
i === index ? { ...entry, noChange: !(entry.noChange ?? false) } : entry
i === index
? { ...entry, noChange: !(entry.noChange ?? false), oneChange: false }
: entry
)
)
);
}, []);
const handleToggleOneChange = useCallback((index: number) => {
setEntries((prev) =>
dedupeTravelTimeEntries(
prev.map((entry, i) =>
i === index
? { ...entry, oneChange: !(entry.oneChange ?? false), noChange: false }
: entry
)
)
);
@ -219,6 +241,7 @@ export function useTravelTime(initial?: TravelTimeInitial) {
handleTimeRangeChange,
handleToggleBest,
handleToggleNoChange,
handleToggleOneChange,
handleToggleNoBuses,
};
}