import { useState, type FormEvent } from 'react'; import { hasKey, useI18n, type Key } from '../lib/i18n'; import type { LatLon } from '../lib/geo'; import { resolveLabel, type Labels } from '../lib/labels'; import { findRoute, HARD_FLAGS, type RouteGraph, type RouteResult, type RouteSegment } from '../lib/route'; import type { Profile } from '../lib/types'; import { Alert, Icon } from './ui'; export interface RouteForm { fromText: string; toText: string; /** Set when the start came from device geolocation. */ fromPoint: LatLon | null; } interface Props { graph: RouteGraph; labels: Labels; profile: Profile; bbox: [number, number, number, number]; form: RouteForm; onForm: (f: RouteForm) => void; result: RouteResult | null; onResult: (r: RouteResult | null, from: LatLon | null, to: LatLon | null) => void; presets: { id: string; fromId: string; toId: string }[]; onFocus: (p: LatLon) => void; } export function RouteView({ graph, labels, profile, bbox, form, onForm, result, onResult, presets, onFocus }: Props) { const { t, tm } = useI18n(); const [error, setError] = useState(null); const [locating, setLocating] = useState(false); const run = (f: RouteForm) => { setError(null); const from = f.fromPoint ?? (() => { const p = resolveLabel(labels, f.fromText); return p ? ([p.lat, p.lon] as LatLon) : null; })(); const to = (() => { const p = resolveLabel(labels, f.toText); return p ? ([p.lat, p.lon] as LatLon) : null; })(); if (!from) { setError(t('route.unknownPlace', { text: f.fromText })); onResult(null, null, null); return; } if (!to) { setError(t('route.unknownPlace', { text: f.toText })); onResult(null, null, null); return; } if (from[0] === to[0] && from[1] === to[1]) { setError(t('route.same')); onResult(null, null, null); return; } onResult(findRoute(graph, from, to, profile), from, to); }; const submit = (e: FormEvent) => { e.preventDefault(); run(form); }; const locate = () => { setLocating(true); setError(null); navigator.geolocation.getCurrentPosition( (pos) => { setLocating(false); const { latitude: lat, longitude: lon } = pos.coords; const [s, w, n, e] = bbox; if (lat < s || lat > n || lon < w || lon > e) { setError(t('route.locOut')); return; } onForm({ ...form, fromText: t('route.myLocation'), fromPoint: [lat, lon] }); }, () => { setLocating(false); setError(t('route.locFail')); }, { enableHighAccuracy: true, timeout: 10_000 }, ); }; const swap = () => { if (form.fromPoint) return; onForm({ fromText: form.toText, toText: form.fromText, fromPoint: null }); }; return (

{t('route.title')}

{t('route.intro')}

{labels.list.map((l) => onForm({ ...form, fromText: e.target.value, fromPoint: null })} /> {'geolocation' in navigator && ( )}
onForm({ ...form, toText: e.target.value })} />
{presets.length > 0 && (

{t('route.presets')}

    {presets.map((p) => { const a = labels.byLabel.size ? [...labels.byLabel.entries()].find(([, pl]) => pl.id === p.fromId) : undefined; const b = [...labels.byLabel.entries()].find(([, pl]) => pl.id === p.toId); if (!a || !b) return null; return (
  • ); })}
)}
{error && {error}} {result && }
); } function Result({ result, onFocus, t }: { result: RouteResult; onFocus: (p: LatLon) => void; t: ReturnType['t']; tm: ReturnType['tm']; }) { if (result.status === 'none') { return {t(`route.none.${result.reason ?? 'disconnected'}` as Key)}; } const blocked = result.status === 'blocked'; const km = (result.distanceM / 1000).toFixed(2); const min = Math.max(1, Math.round(result.durationMin)); const notes = result.barriers.filter((b) => b.flag !== 'surface-unknown' && b.flag !== 'ramp-steps'); return ( <>

{blocked ? t('route.blocked') : t('route.ok')}

{blocked ? t('route.blocked.long') : t('route.summary', { km, min })}

{blocked &&

{t('route.summary', { km, min })}

}

{t('route.quality', { pct: result.stats.knownSurfacePct })}

{result.stats.unknownKerbs > 0 &&

{t('route.unknownKerbs', { n: result.stats.unknownKerbs })}

}

{t('route.estimate')}

{t('route.legend')}

{t('route.barriers')}

{notes.length === 0 ? (

{t('route.barriers.none')}

) : (
    {notes.map((b, i) => (
  • {t(`flag.${b.flag}` as Key)} {b.name ? ` · ${b.name}` : ''} {b.detail && b.flag === 'steps' ? ` · ${b.detail}` : ''} {b.lengthM >= 10 ? ` · ${Math.round(b.lengthM / 5) * 5} m` : ''}
  • ))}
)}

{t('route.steps')}

    {result.segments.map((s, i) => (
  1. {segmentText(s, i === 0, t)}
  2. ))}
); } function segmentText(s: RouteSegment, first: boolean, t: ReturnType['t']) { const kindKey = `seg.hw.${s.hw}`; const kindLabel = hasKey(kindKey) ? t(kindKey) : s.hw; const surface = s.surface ? t('seg.surfaceSuffix', { surface: `@surface.${s.surface}` }) : ''; const flags = s.flags.filter((f) => f !== 'surface-unknown' && f !== 'ramp-steps').map((f) => t(`flag.${f}` as Key)); const line = t('seg.line', { turn: t(`turn.${first ? 'start' : s.turn}` as Key), name: s.name ?? t('seg.unnamed'), kind: kindLabel, m: Math.max(5, Math.round(s.distanceM / 5) * 5), surface, }); return flags.length ? `${line}. ${t('status.warn')}: ${flags.join(', ')}.` : line; }