stop-times', () => {
+ describe('sortAndGroupStopTimesByDay', () => {
+ it('should sort and group stop times by day', () => {
+ const stopTimesByPatternByDay = groupAndSortStopTimesByPatternByDay(
+ stopData,
+ now,
+ daysAhead,
+ 3
+ )
+
+ function getPatternDays(id: string) {
+ return stopTimesByPatternByDay
+ .filter((p) => p.id === id)
+ .map((p) => p.day)
+ }
+
+ // Stop time data has 4 patterns aggregating to 3 final destinations.
+ // (This method assumes that the routing is roughly the same for patterns
+ // that have the same destination.)
+ const headsigns = new Set(
+ stopTimesByPatternByDay.map((st) => st.pattern.headsign)
+ )
+ expect(headsigns).toEqual(
+ new Set(['Northgate', 'Stadium', 'University of Washington'])
+ )
+
+ // The pattern to UWA should be included twice, once per service day outside of today.
+ expect(getPatternDays('40:100479-University of Washington')).toEqual([
+ 1695279600, 1695366000
+ ])
+
+ // Patterns to Stadium (last trips in the evening) should be for "today"'s service day.
+ expect(getPatternDays('40:100479-Stadium')).toEqual([1695193200])
+
+ // No next-day pattern to Northgate should be returned (same-day depatures exist).
+ expect(getPatternDays('40:100479-Northgate')).toEqual([1695193200])
+
+ // Patterns should be sorted by day, then by departure time.
+ for (let i = 1; i < stopTimesByPatternByDay.length; i++) {
+ const prevPattern = stopTimesByPatternByDay[i - 1]
+ const thisPattern = stopTimesByPatternByDay[i]
+ expect(
+ prevPattern.day < thisPattern.day ||
+ (prevPattern.day === thisPattern.day &&
+ prevPattern.times[0].realtimeDeparture <=
+ thisPattern.times[0].realtimeDeparture)
+ ).toBe(true)
+ }
+ })
+ })
+})
diff --git a/i18n/es.yml b/i18n/es.yml
index 8ddaa7901..63e8913cd 100644
--- a/i18n/es.yml
+++ b/i18n/es.yml
@@ -30,8 +30,7 @@ actions:
No se puede guardar el plan: este plan no se pudo guardar debido a la
falta de capacidad en uno o más vehículos. Por favor, vuelva a planificar
su viaje.
- maxTripRequestsExceeded: Número de solicitudes de viaje superadas sin resultados
- válidos
+ maxTripRequestsExceeded: Número de solicitudes de viaje superadas sin resultados válidos
saveItinerariesError: "No se pudieron guardar los itinerarios: {err}"
setDateError: "Error al establecer la fecha:"
setGroupSizeError: "No se pudo establecer el tamaño del grupo:"
@@ -53,11 +52,9 @@ actions:
authTokenError: Error al obtener un token de autorización.
confirmDeleteMonitoredTrip: ¿Desea eliminar este viaje?
confirmDeletePlace: ¿Quiere eliminar este lugar?
- emailVerificationResent: El mensaje de verificación de correo electrónico ha sido
- reenviado.
+ emailVerificationResent: El mensaje de verificación de correo electrónico ha sido reenviado.
genericError: "Se ha encontrado un error: {err}"
- itineraryExistenceCheckFailed: Comprobación de errores para ver si el viaje seleccionado
- es posible.
+ itineraryExistenceCheckFailed: Comprobación de errores para ver si el viaje seleccionado es posible.
preferencesSaved: Sus preferencias se han guardado.
smsInvalidCode: El código introducido no es válido. Por favor, inténtelo de nuevo.
smsResendThrottled: >-
@@ -241,12 +238,12 @@ components:
modeSelectorLabel: Seleccione un modo de transporte
BatchSettings:
destination: destino
+ invalidModeSelection: >-
+ No se puede planificar un viaje utilizando los modos seleccionados. Prueba
+ a incluir el transporte publico en la selección de modos.
origin: origen
planTripTooltip: Planificar viaje
- validationMessage: "Por favor, defina los siguientes campos para planificar un
- viaje: {issues}"
- invalidModeSelection: No se puede planificar un viaje utilizando los modos seleccionados.
- Prueba a incluir el transporte publico en la selección de modos.
+ validationMessage: "Por favor, defina los siguientes campos para planificar un viaje: {issues}"
BeforeSignInScreen:
mainTitle: Iniciando sesión
message: >
@@ -538,6 +535,8 @@ components:
Otro viaje guardado ya utiliza este nombre. Por favor, elija un nombre
diferente.
tripNameRequired: Por favor, introduzca el nombre del viaje.
+ SequentialPaneDisplay:
+ stepNumber: Paso {step} de {total}
SessionTimeout:
body: >-
Su sesión expirará en un minuto. Pulse 'Continuar sesión' para mantener su
@@ -545,8 +544,7 @@ components:
header: ¡La sesión está a punto de terminar!
keepSession: Continuar sesión
SimpleRealtimeAnnotation:
- usingRealtimeInfo: Este viaje utiliza información de tráfico y retrasos en tiempo
- real
+ usingRealtimeInfo: Este viaje utiliza información de tráfico y retrasos en tiempo real
StackedPaneDisplay:
savePreferences: Guardar preferencias
StopScheduleTable:
@@ -609,19 +607,16 @@ components:
travelingAt: Viajando a {milesPerHour}
vehicleName: Vehículo {vehicleNumber}
TripBasicsPane:
- checkingItineraryExistence: Comprobación de la existencia de itinerarios para
- cada día de la semana…
+ checkingItineraryExistence: Comprobación de la existencia de itinerarios para cada día de la semana…
selectAtLeastOneDay: Por favor, seleccione al menos un día para el seguimiento.
tripDaysPrompt: ¿Qué días hace este viaje?
- tripIsAvailableOnDaysIndicated: Su viaje está disponible en los días de la semana
- indicados anteriormente.
+ tripIsAvailableOnDaysIndicated: Su viaje está disponible en los días de la semana indicados anteriormente.
tripNamePrompt: "Por favor, indique un nombre para este viaje:"
tripNotAvailableOnDay: El viaje no está disponible el {repeatedDay}
unsavedChangesExistingTrip: >-
Todavía no ha guardado su viaje. Si abandona la página, los cambios se
perderán.
- unsavedChangesNewTrip: Todavía no ha guardado su nuevo viaje. Si abandona la página,
- se perderá.
+ unsavedChangesNewTrip: Todavía no ha guardado su nuevo viaje. Si abandona la página, se perderá.
TripNotificationsPane:
advancedSettings: Configuración avanzada
altRouteRecommended: Se recomienda una ruta alternativa o un punto de transferencia
@@ -781,8 +776,6 @@ components:
switcher: Botón de cambio
WelcomeScreen:
prompt: ¿A donde quiere ir?
- SequentialPaneDisplay:
- stepNumber: Paso {step} de {total}
config:
accessModes:
bicycle: Tránsito + Bicicleta Personal
diff --git a/i18n/vi.yml b/i18n/vi.yml
index ba852fde4..e31f08271 100644
--- a/i18n/vi.yml
+++ b/i18n/vi.yml
@@ -5,8 +5,7 @@ actions:
callQuerySaveError: "Lỗi khi lưu trữ các truy vấn cuộc gọi: {err}"
callSaveError: "Không thể lưu cuộc gọi: {err}"
checkSessionError: "Lỗi khi thiết lập phiên ủy quyền: {err}"
- couldNotFindCallError: Không thể tìm thấy cuộc gọi. Đang hủy yêu cầu lưu truy
- vấn.
+ couldNotFindCallError: Không thể tìm thấy cuộc gọi. Đang hủy yêu cầu lưu truy vấn.
fetchCallsError: "Lỗi khi tìm nạp cuộc gọi: {err}"
queryFetchError: "Lỗi khi tìm nạp các truy vấn: {err}"
fieldTrip:
@@ -29,16 +28,14 @@ actions:
Không thể lưu kế hoạch chuyến đi: Không thể lưu kế hoạch chuyến đi này do
thiếu sức chứa trên một hoặc nhiều xe. Vui lòng lên kế hoạch lại chuyến đi
của bạn.
- maxTripRequestsExceeded: Đã vượt quá số lượng yêu cầu chuyến đi mà không có kết
- quả hợp lệ
+ maxTripRequestsExceeded: Đã vượt quá số lượng yêu cầu chuyến đi mà không có kết quả hợp lệ
saveItinerariesError: "Không lưu được hành trình: {err}"
setDateError: "Lỗi khi cài đặt ngày:"
setGroupSizeError: "Lỗi khi cài đặt kích thước nhóm:"
setPaymentError: "Lỗi khi cài đặt thông tin thanh toán:"
setRequestStatusError: "Lỗi khi cài đặt trạng thái yêu cầu:"
location:
- geolocationNotSupportedError: Định vị địa lý không được hỗ trợ bởi trình duyệt
- của bạn
+ geolocationNotSupportedError: Định vị địa lý không được hỗ trợ bởi trình duyệt của bạn
unknownPositionError: Lỗi không xác định khi tìm vị trí
map:
currentLocation: (Vị trí hiện tại)
@@ -49,8 +46,7 @@ actions:
confirmDeletePlace: Bạn có muốn loại bỏ nơi này không?
emailVerificationResent: Thông báo xác minh email đã được gửi lại.
genericError: "Phát sinh lỗi: {err}"
- itineraryExistenceCheckFailed: Lỗi kiểm tra xem chuyến đi được chọn của bạn là
- có thể.
+ itineraryExistenceCheckFailed: Lỗi kiểm tra xem chuyến đi được chọn của bạn là có thể.
preferencesSaved: Những sở thích của bạn đã được lưu lại.
smsInvalidCode: Mã bạn nhập không hợp lệ. Vui lòng thử lại.
smsResendThrottled: >-
@@ -160,14 +156,12 @@ common:
{} other {# giây}}
components:
A11yPrefs:
- accessibilityRoutingByDefault: Thích những chuyến đi có thể truy cập theo mặc
- định
+ accessibilityRoutingByDefault: Thích những chuyến đi có thể truy cập theo mặc định
AccountSetupFinishPane:
message: Bạn đã sẵn sàng để bắt đầu lên kế hoạch cho các chuyến đi của bạn.
AddPlaceButton:
addPlace: Thêm địa điểm
- needOriginDestination: Xác định nguồn gốc hoặc đích đến để thêm các địa điểm trung
- gian
+ needOriginDestination: Xác định nguồn gốc hoặc đích đến để thêm các địa điểm trung gian
tooManyPlaces: Địa điểm trung gian tối đa đạt được
AdvancedOptions:
bannedRoutes: Chọn các tuyến đường bị cấm…
@@ -278,8 +272,7 @@ components:
editPlaceGeneric: Chỉnh sửa vị trí
invalidAddress: Vui lòng cài đặt một vị trí cho nơi này.
invalidName: Vui lòng nhập tên cho nơi này.
- nameAlreadyUsed: Bạn đã sử dụng tên này cho một nơi khác. Vui lòng nhập một tên
- khác.
+ nameAlreadyUsed: Bạn đã sử dụng tên này cho một nơi khác. Vui lòng nhập một tên khác.
placeNotFound: Không tìm thấy địa điểm
placeNotFoundDescription: Xin lỗi, địa điểm được yêu cầu không được tìm thấy.
FormNavigationButtons:
@@ -521,22 +514,17 @@ components:
travelingAt: di chuyển với tốc độ {milesPerHour}
vehicleName: Phương tiện giao thông {vehicleNumber}
TripBasicsPane:
- checkingItineraryExistence: Kiểm tra sự tồn tại của hành trình cho mỗi ngày trong
- tuần…
+ checkingItineraryExistence: Kiểm tra sự tồn tại của hành trình cho mỗi ngày trong tuần…
selectAtLeastOneDay: Vui lòng chọn ít nhất một ngày để theo dõi.
tripDaysPrompt: Bạn thực hiện chuyến đi này vào những ngày nào?
- tripIsAvailableOnDaysIndicated: Chuyến đi của bạn có sẵn vào những ngày trong
- tuần như đã nêu ở trên.
+ tripIsAvailableOnDaysIndicated: Chuyến đi của bạn có sẵn vào những ngày trong tuần như đã nêu ở trên.
tripNamePrompt: "Vui lòng cung cấp tên cho chuyến đi này:"
tripNotAvailableOnDay: Chuyến đi không có sẵn vào {repeatedDay}
- unsavedChangesExistingTrip: Bạn chưa lưu chuyến đi của mình. Nếu bạn rời đi, những
- thay đổi sẽ bị mất.
- unsavedChangesNewTrip: Bạn chưa lưu chuyến đi mới của mình. Nếu bạn rời đi, nó
- sẽ bị mất.
+ unsavedChangesExistingTrip: Bạn chưa lưu chuyến đi của mình. Nếu bạn rời đi, những thay đổi sẽ bị mất.
+ unsavedChangesNewTrip: Bạn chưa lưu chuyến đi mới của mình. Nếu bạn rời đi, nó sẽ bị mất.
TripNotificationsPane:
advancedSettings: Cài đặt nâng cao
- altRouteRecommended: Một tuyến đường hoặc điểm trung chuyển thay thế được khuyến
- nghị
+ altRouteRecommended: Một tuyến đường hoặc điểm trung chuyển thay thế được khuyến nghị
delaysAboveThreshold: Có sự chậm trễ hoặc gián đoạn của hơn
howToReceiveAlerts: >
Để nhận thông báo cho các chuyến đi đã lưu của bạn, bật thông báo trong
@@ -545,8 +533,7 @@ components:
notificationsTurnedOff: Thông báo được tắt cho tài khoản của bạn.
notifyViaChannelWhen: "Thông báo cho tôi qua {channel} khi:"
oneHour: 1 tiếng
- realtimeAlertFlagged: Có một cảnh báo thời gian thực được gắn cờ trên hành trình
- của tôi
+ realtimeAlertFlagged: Có một cảnh báo thời gian thực được gắn cờ trên hành trình của tôi
timeBefore: "{time} trước"
TripStatus:
alerts: "{alerts, plural, one {# cảnh báo!} other {# cảnh báo!}}"
@@ -559,8 +546,7 @@ components:
earlyHeading: >-
Chuyến đi đang diễn ra và sẽ đến sớm hơn {formattedDuration} so với dự
kiến!
- noDataHeading: Chuyến đi đang được tiến hành (không có cập nhật thời gian thực
- có sẵn).
+ noDataHeading: Chuyến đi đang được tiến hành (không có cập nhật thời gian thực có sẵn).
onTimeHeading: Chuyến đi đang được tiến hành và đúng giờ.
base:
lastCheckedDefaultText: Thời gian được kiểm tra lần cuối không xác định
@@ -603,8 +589,7 @@ components:
tripStartIsEarly: >-
Thời gian bắt đầu chuyến đi đang diễn ra sớm hơn ${duration} so với dự
kiến!
- tripStartsSoonNoUpdates: Chuyến đi đang bắt đầu sớm (không có cập nhật về thời
- gian thực).
+ tripStartsSoonNoUpdates: Chuyến đi đang bắt đầu sớm (không có cập nhật về thời gian thực).
tripStartsSoonOnTime: Chuyến đi đang bắt đầu sớm và sắp đúng giờ.
TripSummary:
arriveAt: "Đến nơi "
diff --git a/lib/components/util/types.ts b/lib/components/util/types.ts
index 8988c7126..664008344 100644
--- a/lib/components/util/types.ts
+++ b/lib/components/util/types.ts
@@ -24,13 +24,12 @@ export interface BikeRental {
stations: any[]
}
-// FIXME: incomplete
export interface StopTime {
- departureDelay: number
+ departureDelay?: number
headsign: string
pattern: Pattern
- realtimeDeparture: boolean
- realtimeState: string
+ realtimeDeparture?: boolean
+ realtimeState?: string
times: Time[]
}
@@ -66,6 +65,17 @@ export interface Time {
tripId: string
}
+export interface PatternStopTimes {
+ id: string
+ pattern: Pattern
+ route: Route
+ times: Time[]
+}
+
+export interface PatternDayStopTimes extends PatternStopTimes {
+ day: number
+}
+
export interface VehicleRental {
errorsByNetwork: { [key: string]: { message?: string; severity?: string } }
systemInformationDataByNetwork: {
diff --git a/lib/components/viewers/live-stop-times.tsx b/lib/components/viewers/live-stop-times.tsx
index e8a772024..f07bc7662 100644
--- a/lib/components/viewers/live-stop-times.tsx
+++ b/lib/components/viewers/live-stop-times.tsx
@@ -1,4 +1,3 @@
-import { addDays, isBefore } from 'date-fns'
import { format, utcToZonedTime } from 'date-fns-tz'
import {
FormattedMessage,
@@ -12,14 +11,9 @@ import coreUtils from '@opentripplanner/core-utils'
import isSameDay from 'date-fns/isSameDay'
import React, { Component } from 'react'
-import {
- getRouteIdForPattern,
- getStopTimesByPattern,
- patternComparator,
- routeIsValid,
- stopTimeComparator
-} from '../../util/viewer'
+import { groupAndSortStopTimesByPatternByDay } from '../../util/stop-times'
import { IconWithText } from '../util/styledIcon'
+import { StopData } from '../util/types'
import FormattedDayOfWeek from '../util/formatted-day-of-week'
import SpanWithSpace from '../util/span-with-space'
@@ -39,18 +33,16 @@ type Props = {
findStopTimesForStop: ({ stopId }: { stopId: string }) => void
homeTimezone: string
intl: IntlShape
- nearbyStops: any // TODO: shared types
+ nearbyStops: string[]
setHoveredStop: (stopId: string) => void
showNearbyStops: boolean
showOperatorLogo?: boolean
- // TODO: shared types
- stopData: any
+ stopData: StopData
stopViewerArriving: React.ReactNode
// TODO: shared types
stopViewerConfig: any
toggleAutoRefresh: (enable: boolean) => void
- // TODO: shared types
- transitOperators: any
+ transitOperators: TransitOperator[]
viewedStop: { stopId: string }
}
@@ -164,76 +156,45 @@ class LiveStopTimes extends Component
{
} = this.props
const { spin } = this.state
const userTimezone = getUserTimezone()
- // construct a lookup table mapping pattern (e.g. 'ROUTE_ID-HEADSIGN') to
- // an array of stoptimes
- const stopTimesByPattern = getStopTimesByPattern(stopData)
const now = utcToZonedTime(Date.now(), homeTimezone)
// Time range is set in seconds, so convert to days
- const timeRange = stopViewerConfig.timeRange / 86400 || 2
+ const daysAhead = stopViewerConfig.timeRange / 86400 || 2
const refreshButtonText = intl.formatMessage({
id: 'components.LiveStopTimes.refresh'
})
- const routeTimes = Object.values(stopTimesByPattern)
- .filter(
- ({ pattern, route, times }) =>
- times &&
- times.length !== 0 &&
- routeIsValid(route, getRouteIdForPattern(pattern))
- )
- .sort(patternComparator)
- .map((route) => {
- const sortedTimes = route.times
- .concat()
- ?.sort(stopTimeComparator)
- // filter any times according to time range set in config.
- .filter((time: any, i: number, array: Array) => {
- const departureTime = time.serviceDay + time.realtimeDeparture
- return isBefore(departureTime, addDays(now, timeRange))
- })
- const { serviceDay } = sortedTimes[0]
- return {
- ...route,
- day: serviceDay || null,
- times: sortedTimes
- }
- })
- // if the time range filter removes all times, remove route
- .filter(({ times }) => times.length !== 0)
+ const routeTimes = groupAndSortStopTimesByPatternByDay(
+ stopData,
+ now,
+ daysAhead,
+ stopViewerConfig.numberOfDepartures
+ )
return (
<>
- {routeTimes.map((time: any, index: number) => {
- const { id, pattern, route, times } = time
- return (
-
- {((index > 0 &&
- !isSameDay(
- time.day * 1000,
- routeTimes[index - 1]?.day * 1000
- )) ||
- (index === 0 && !isSameDay(now, time.day * 1000))) &&
- this.renderDay(homeTimezone, time.day, now)}
- o.agencyId === route.agencyId
- )
- }}
- showOperatorLogo={showOperatorLogo}
- stopTimes={times}
- stopViewerArriving={stopViewerArriving}
- stopViewerConfig={stopViewerConfig}
- />
-
- )
- })}
+ {routeTimes.map(({ day, id, pattern, route, times }, index) => (
+
+ {((index > 0 &&
+ !isSameDay(day * 1000, routeTimes[index - 1]?.day * 1000)) ||
+ (index === 0 && !isSameDay(now, day * 1000))) &&
+ this.renderDay(homeTimezone, day, now)}
+ o.agencyId === route.agencyId
+ )
+ }}
+ showOperatorLogo={showOperatorLogo}
+ stopTimes={times}
+ />
+
+ ))}
{/* Auto update controls for realtime arrivals */}
diff --git a/lib/components/viewers/next-arrival-for-pattern.tsx b/lib/components/viewers/next-arrival-for-pattern.tsx
index 2fceb4cf8..bed35582c 100644
--- a/lib/components/viewers/next-arrival-for-pattern.tsx
+++ b/lib/components/viewers/next-arrival-for-pattern.tsx
@@ -21,7 +21,7 @@ type Props = {
pattern: Pattern
// Not the true operator type, but the one that's used here
// It is annoying to shoehorn the operator in here like this, but passing
- // it in indvidually would cause a situation where a list of routes
+ // it in individually would cause a situation where a list of routes
// needs to be matched up with a list of operators
route: Route & { operator?: { colorMode?: string } }
routeColor: string
diff --git a/lib/components/viewers/pattern-row.tsx b/lib/components/viewers/pattern-row.tsx
index 0b4084251..cc1765974 100644
--- a/lib/components/viewers/pattern-row.tsx
+++ b/lib/components/viewers/pattern-row.tsx
@@ -1,6 +1,5 @@
import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route'
-import { injectIntl, IntlShape } from 'react-intl'
-import React, { Component } from 'react'
+import React, { useContext } from 'react'
import type { Route, TransitOperator } from '@opentripplanner/types'
import { ComponentContext } from '../../util/contexts'
@@ -16,107 +15,81 @@ import OperatorLogo from '../util/operator-logo'
import StopTimeCell from './stop-time-cell'
type Props = {
- homeTimezone?: any
- intl: IntlShape
+ homeTimezone?: string
pattern: Pattern
route: Route & { operator?: TransitOperator & { colorMode?: string } }
showOperatorLogo?: boolean
stopTimes: Time[]
- stopViewerArriving: React.ReactNode
- stopViewerConfig: { numberOfDepartures: number }
}
-type State = { expanded: boolean }
+
/**
* Represents a single pattern row for displaying arrival times in the stop
* viewer.
*/
-class PatternRow extends Component {
- constructor(props: Props) {
- super(props)
- this.state = { expanded: false }
- }
-
- static contextType = ComponentContext
+const PatternRow = ({
+ homeTimezone,
+ pattern,
+ route,
+ showOperatorLogo,
+ stopTimes
+}: Props): JSX.Element | null => {
+ // @ts-expect-error FIXME: No type on ComponentContext
+ const { RouteRenderer: CustomRouteRenderer } = useContext(ComponentContext)
+ const RouteRenderer = CustomRouteRenderer || DefaultRouteRenderer
- _toggleExpandedView = () => {
- this.setState({ expanded: !this.state.expanded })
+ const hasStopTimes = stopTimes && stopTimes.length > 0
+ if (!hasStopTimes) {
+ return null
}
- render() {
- const { RouteRenderer: CustomRouteRenderer } = this.context
- const RouteRenderer = CustomRouteRenderer || DefaultRouteRenderer
- const {
- homeTimezone,
- pattern,
- route,
- showOperatorLogo,
- stopTimes,
- stopViewerConfig
- } = this.props
+ const routeName = route.shortName ? route.shortName : route.longName
+ const routeColor = getRouteColorBasedOnSettings(route.operator, route)
- // sort stop times by next departure
- let sortedStopTimes: Time[] = []
- const hasStopTimes = stopTimes && stopTimes.length > 0
- if (hasStopTimes) {
- sortedStopTimes = stopTimes
- // We request only x departures per pattern, but the patterns are merged
- // according to shared headsigns, so we need to slice the stop times
- // here as well to ensure only x times are shown per route/headsign combo.
- .slice(0, stopViewerConfig.numberOfDepartures)
- } else {
- // Do not render pattern row if it has no stop times.
- return null
- }
-
- const routeName = route.shortName ? route.shortName : route.longName
- const routeColor = getRouteColorBasedOnSettings(route.operator, route)
-
- return (
-
- {/* header row */}
-
- {/* route name */}
-
-
- {showOperatorLogo && }
-
-
- {pattern.headsign}
-
- {/* next departure preview */}
- {hasStopTimes && (
-
- {[0, 1, 2].map(
- (index) =>
- sortedStopTimes?.[index] && (
- -
-
-
- )
+ return (
+ -
+ {/* header row */}
+
+ {/* route name */}
+
+
+ {showOperatorLogo && }
+
- )}
+ leg={generateFakeLegForRouteRenderer(route, true)}
+ style={{ fontSize: routeNameFontSize(routeName) }}
+ />
+
+ {pattern.headsign}
-
- )
- }
+ {/* next departure preview (only shows up to 3 entries) */}
+ {hasStopTimes && (
+
+ {[0, 1, 2].map(
+ (index) =>
+ stopTimes?.[index] && (
+ -
+
+
+ )
+ )}
+
+ )}
+
+
+ )
}
-export default injectIntl(PatternRow)
+export default PatternRow
diff --git a/lib/util/arrays.ts b/lib/util/arrays.ts
new file mode 100644
index 000000000..d5429ae6c
--- /dev/null
+++ b/lib/util/arrays.ts
@@ -0,0 +1,12 @@
+/** Retrieves a key from a dictionary or adds it with the desired value if absent. */
+export function getOrPutEntry(
+ entries: Record,
+ key: K,
+ newValue: (newKey: K) => V
+): V {
+ let entry = entries[key]
+ if (!entry) {
+ entries[key] = entry = newValue(key)
+ }
+ return entry
+}
diff --git a/lib/util/stop-times.ts b/lib/util/stop-times.ts
new file mode 100644
index 000000000..f58deae2f
--- /dev/null
+++ b/lib/util/stop-times.ts
@@ -0,0 +1,82 @@
+import { addDays, isBefore } from 'date-fns'
+
+import {
+ PatternDayStopTimes,
+ PatternStopTimes,
+ StopData,
+ Time
+} from '../components/util/types'
+
+import { getOrPutEntry } from './arrays'
+import {
+ getRouteIdForPattern,
+ getStopTimesByPattern,
+ patternComparator,
+ routeIsValid,
+ stopTimeComparator
+} from './viewer'
+
+function hasValidTimesAndRoute({
+ pattern,
+ route,
+ times
+}: PatternStopTimes): boolean {
+ return (
+ times &&
+ times.length !== 0 &&
+ routeIsValid(route, getRouteIdForPattern(pattern))
+ )
+}
+
+/** Filter any times according to time range set in config. */
+function isWithinDaysAhead(time: Time, now: Date, daysAhead: number) {
+ const departureTime = time.serviceDay + time.realtimeDeparture
+ return isBefore(departureTime, addDays(now, daysAhead))
+}
+
+/** Helper to sort and group stop times by pattern by service day */
+export function groupAndSortStopTimesByPatternByDay(
+ stopData: StopData,
+ now: Date,
+ daysAhead: number,
+ numberOfDepartures: number
+): PatternDayStopTimes[] {
+ // construct a lookup table mapping pattern (e.g. 'ROUTE_ID-HEADSIGN') to
+ // an array of stoptimes
+ const stopTimesByPattern = getStopTimesByPattern(stopData) as Record<
+ string,
+ PatternStopTimes
+ >
+
+ return (
+ Object.values(stopTimesByPattern)
+ .filter(hasValidTimesAndRoute)
+ .map((pattern) =>
+ pattern.times
+ .concat([])
+ .sort(stopTimeComparator)
+ .filter((time) => isWithinDaysAhead(time, now, daysAhead))
+ // remove excess departure times
+ .slice(0, numberOfDepartures)
+ // collect times by pattern by day
+ // (every pattern returned has stop times within the days ahead.)
+ .reduce>((days, t) => {
+ const { serviceDay } = t
+ const patternDay = getOrPutEntry(days, serviceDay, (day) => ({
+ ...pattern,
+ day,
+ times: []
+ }))
+ patternDay.times.push(t)
+ return days
+ }, {})
+ )
+ // Concatenate all resulting patterns
+ .reduce(
+ (result, cur) => result.concat(Object.values(cur)),
+ []
+ )
+ // Sort route times by service day then realtime departure
+ .sort(patternComparator)
+ )
+}
diff --git a/lib/util/viewer.js b/lib/util/viewer.js
index bdb27b3cb..9f1f9ef9d 100644
--- a/lib/util/viewer.js
+++ b/lib/util/viewer.js
@@ -40,7 +40,7 @@ function excludeLastStop(stopTime) {
* Checks that the given route object from an OTP pattern is valid.
* If it is not, logs a warning message.
*
- * FIXME: there is currently a bug with the alernative transit index
+ * FIXME: there is currently a bug with the alternative transit index
* where routes are not associated with the stop if the only stoptimes
* for the stop are drop off only. See https://github.com/ibi-group/trimet-mod-otp/issues/217
*