import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { NumericText } from './NumericText'; import { FittedDestination } from './FittedDestination'; import { durationParts } from './lib/presentation.mjs'; import { readability } from './lib/departures'; import { transliterateStopName } from './lib/transliteration'; import type { OnwardJourney } from './lib/journey'; import type { Departure, Settings } from './types'; import type { CSSProperties } from 'react'; // Fixed-size cells move on the compositor. Both text sizes are fitted once, // so moving to the next stop never resizes a cell or remounts its heading. const JourneyRail = memo(function JourneyRail({ journey, focus, translit }: { journey: OnwardJourney; focus: number; translit: boolean }) { const rail = useRef(null); const [geometry, setGeometry] = useState({ slot: 0, focused: 0, width: 0 }); useLayoutEffect(() => { const element = rail.current; if (!element) return; const measure = () => { const cell = element.querySelector('.journey-station'); const heading = cell?.querySelector('.journey-name-focus'); const next = { slot: cell?.getBoundingClientRect().width || 0, focused: heading ? parseFloat(getComputedStyle(heading).width) : 0, width: element.getBoundingClientRect().width }; setGeometry(previous => previous.slot === next.slot && previous.focused === next.focused && previous.width === next.width ? previous : next); }; const observer = new ResizeObserver(measure); observer.observe(element); measure(); return () => observer.disconnect(); }, [journey.stops.length]); // With no continuation the line ends at the last real stop, including a // single-stop journey. Otherwise it meets the moving dashed continuation. const remaining = journey.stops.length - 1 - focus; const lineEnd = !journey.more && !remaining ? 0 : (remaining + (journey.more ? 1 : 0)) * geometry.slot + geometry.focused - geometry.slot; const scale = geometry.width > 24 && geometry.slot > 0 ? Math.max(0, lineEnd / (geometry.width - 24)) : 1; return
0 ? geometry.slot / geometry.focused : .4 } as CSSProperties}>
; }, (previous, next) => previous.focus === next.focus && previous.translit === next.translit && previous.journey.more === next.journey.more && previous.journey.stops.length === next.journey.stops.length && previous.journey.stops.every((stop, index) => { const other = next.journey.stops[index]; return stop.id === other.id && stop.name === other.name && stop.minutes === other.minutes; })); export function NextStops({ departure, journey, loading, paused, settings, active, onPages }: { departure?: Departure; journey: OnwardJourney | null; loading: boolean; paused: boolean; settings: Settings; active: boolean; onPages: (pages: number) => void; }) { const [page, setPage] = useState(0); const reading = readability(settings); const count = journey?.stops.length || 0; const pages = Math.max(1, count); const focus = page % pages; useEffect(() => { setPage(0); }, [departure?.id]); useEffect(() => { if (active) onPages(pages); }, [active, pages, onPages]); useEffect(() => { if (paused || !active || pages < 2) return; const timer = setInterval(() => setPage(value => value + 1), reading.calm ? 6000 : 4000); return () => clearInterval(timer); }, [paused, active, pages, reading.calm]); return <>

Дальше по ближайшему рейсу

{!departure ?

{loading ? 'Получаем расписание' : 'Нет доступного рейса'}

: <> {!journey ?

{loading ? 'Загружаем остановки' : 'Схема маршрута пока недоступна'}

: !count ?

Здесь конечная этого рейса

: <>
0 && focus === 0}>
{settings.demo ? 'Демонстрация поездки' : 'После посадки'}{focus + 1}-я остановка
Время пути по расписаниюПосле посадки · по расписанию{pages > 1 && {focus + 1} / {count}}
} } ; }