Skip to content

Commit 34a966b

Browse files
authored
feat: Merge the single-line map with the vehicle map and add a selector for filtering by route or vehicle id. (#1151)
1 parent f58887a commit 34a966b

File tree

15 files changed

+353
-427
lines changed

15 files changed

+353
-427
lines changed

src/hooks/useSingleLineData.ts

Lines changed: 181 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,227 @@
1-
import { useContext, useEffect, useMemo, useState } from 'react'
2-
import { getStopsForRouteAsync } from 'src/api/gtfsService'
1+
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
2+
import { getRoutesAsync, getRoutesByLineRef, getStopsForRouteAsync } from 'src/api/gtfsService'
33
import useVehicleLocations from 'src/api/useVehicleLocations'
44
import { BusStop } from 'src/model/busStop'
5+
import { BusRoute } from 'src/model/busRoute'
56
import { SearchContext } from 'src/model/pageState'
67
import { Point } from 'src/pages/timeBasedMap'
78
import dayjs from 'src/dayjs'
8-
9-
const formatTime = (time: dayjs.Dayjs) => time.format('HH:mm:ss')
10-
11-
export const useSingleLineData = (lineRef?: number, routeIds?: number[]) => {
12-
const {
13-
search: { timestamp },
14-
} = useContext(SearchContext)
15-
9+
import { vehicleIDFormat, routeStartEnd } from 'src/pages/components/utils/rotueUtils'
10+
11+
const formatTime = (time: dayjs.Dayjs) => time.format('HH:mm')
12+
13+
export const useSingleLineData = (
14+
operatorId?: string,
15+
lineNumber?: string,
16+
vehicleNumber?: number,
17+
) => {
18+
const { search, setSearch } = useContext(SearchContext)
19+
const [routes, setRoutes] = useState<BusRoute[] | undefined>(search.routes)
20+
const [routeKey, _setRouteKey] = useState<string | undefined>(search.routeKey)
1621
const [filteredPositions, setFilteredPositions] = useState<Point[]>([])
1722
const [plannedRouteStops, setPlannedRouteStops] = useState<BusStop[]>([])
23+
const [options, setOptions] = useState<{ value: string; label: string }[]>([])
1824
const [startTime, setStartTime] = useState<string>()
25+
const [error, setError] = useState<string>()
26+
27+
const setRouteKey = useCallback(
28+
(routeKey?: string) => {
29+
_setRouteKey(routeKey)
30+
setSearch((prev) => ({ ...prev, routeKey }))
31+
},
32+
[setSearch],
33+
)
34+
35+
useEffect(() => {
36+
if (!operatorId || !lineNumber) {
37+
setRoutes(undefined)
38+
setRouteKey(undefined)
39+
setStartTime(undefined)
40+
setError(undefined)
41+
setSearch((prev) => ({ ...prev, routes: undefined, routeKey: undefined }))
42+
return
43+
}
44+
45+
const controller = new AbortController()
46+
const time = dayjs(search.timestamp)
47+
48+
getRoutesAsync(time, time, operatorId, lineNumber, controller.signal)
49+
.then((routes) => {
50+
setRoutes(routes)
51+
setSearch((prev) => ({ ...prev, routes }))
52+
setError(undefined)
53+
setStartTime(undefined)
54+
})
55+
.catch((err) => {
56+
if (err?.cause?.name !== 'AbortError') {
57+
setRoutes(undefined)
58+
setSearch((prev) => ({ ...prev, routes: undefined }))
59+
setRouteKey(undefined)
60+
setError(err instanceof Error ? err.message : 'Failed to fetch routes')
61+
}
62+
})
63+
64+
return () => {
65+
controller.abort()
66+
}
67+
}, [operatorId, lineNumber, search.timestamp, setSearch, setRouteKey])
68+
69+
const selectedRoute = useMemo(() => {
70+
return routes?.find((route) => route.key === routeKey)
71+
}, [routes, routeKey])
1972

2073
const [today, tomorrow] = useMemo(() => {
21-
const today = dayjs(timestamp).startOf('day')
74+
const today = dayjs(search.timestamp).startOf('day')
2275
return [today, today.add(1, 'day')]
23-
}, [timestamp])
76+
}, [search.timestamp])
77+
78+
const validVehicleNumber = useMemo(() => {
79+
return vehicleNumber && /^\d{8,9}$/.test(vehicleNumber.toString())
80+
? Number(vehicleNumber)
81+
: undefined
82+
}, [vehicleNumber])
2483

2584
const { locations, isLoading: locationsAreLoading } = useVehicleLocations({
2685
from: today.valueOf(),
2786
to: tomorrow.valueOf(),
28-
lineRef,
87+
operatorRef: operatorId ? Number(operatorId) : undefined,
88+
lineRef: selectedRoute?.lineRef ? Number(selectedRoute.lineRef) : undefined,
89+
vehicleRef: validVehicleNumber,
2990
splitMinutes: 360,
30-
pause: !lineRef,
91+
pause: !operatorId || (!selectedRoute?.lineRef && !validVehicleNumber),
3192
})
3293

3394
const positions = useMemo(() => {
34-
return locations.map<Point>((location) => ({
35-
loc: [location.lat, location.lon],
36-
color: location.velocity,
37-
operator: location.siri_route__operator_ref,
38-
bearing: location.bearing,
39-
recorded_at_time: new Date(location.recorded_at_time).getTime(),
40-
point: location,
41-
}))
42-
}, [locations])
43-
44-
const options = useMemo(() => {
45-
const uniqueTimes = new Set<string>()
46-
47-
for (const position of positions) {
48-
const startTime = position.point?.siri_ride__scheduled_start_time
49-
if (startTime) {
95+
return locations
96+
.filter((l) =>
97+
validVehicleNumber ? Number(l.siri_ride__vehicle_ref) === validVehicleNumber : true,
98+
)
99+
.map<Point>((location) => ({
100+
loc: [location.lat, location.lon],
101+
color: location.velocity,
102+
operator: location.siri_route__operator_ref,
103+
bearing: location.bearing,
104+
recorded_at_time: new Date(location.recorded_at_time).getTime(),
105+
point: location,
106+
}))
107+
}, [locations, validVehicleNumber])
108+
109+
useEffect(() => {
110+
const fetchOptions = async () => {
111+
const uniqueTimes = new Map<string, { scheduledTime: string; position: Point }>()
112+
for (const position of positions) {
113+
const startTime = position.point?.siri_ride__scheduled_start_time
114+
if (!startTime) continue
50115
const dayjsTime = dayjs(startTime)
51116
if (dayjsTime.isAfter(today) && dayjsTime.isBefore(tomorrow)) {
52-
uniqueTimes.add(formatTime(dayjsTime))
117+
const formattedTime = formatTime(dayjsTime)
118+
const key = `${formattedTime}|${position.point?.siri_ride__vehicle_ref}`
119+
if (!uniqueTimes.has(key)) {
120+
uniqueTimes.set(key, { scheduledTime: formattedTime, position })
121+
}
53122
}
54123
}
124+
125+
const optionsArray = Array.from(uniqueTimes.values()).sort((a, b) =>
126+
a.scheduledTime.localeCompare(b.scheduledTime),
127+
)
128+
129+
if (vehicleNumber) {
130+
const optionsArray2 = await Promise.all(
131+
optionsArray.map(async (option) => {
132+
const routes = await getRoutesByLineRef(
133+
option.position.point!.siri_route__operator_ref.toString(),
134+
option.position.point!.siri_route__line_ref.toString(),
135+
new Date(option.position.point!.recorded_at_time),
136+
)
137+
const [start, end] = routeStartEnd(routes[0]?.routeLongName)
138+
return {
139+
value: `${option.scheduledTime}|${option.position.point!.siri_ride__vehicle_ref}|${option.position.point!.siri_route__line_ref}`,
140+
label: routes[0]?.routeLongName
141+
? `${option.scheduledTime} (${routes[0]?.routeShortName} - ${start}${end})`
142+
: `${option.scheduledTime} (${vehicleIDFormat(option.position.point!.siri_ride__vehicle_ref)})`,
143+
}
144+
}),
145+
)
146+
setOptions(optionsArray2)
147+
} else {
148+
setOptions(
149+
optionsArray.map((option) => {
150+
return {
151+
value: `${option.scheduledTime}|${option.position.point!.siri_ride__vehicle_ref}`,
152+
label: `${option.scheduledTime} (${vehicleIDFormat(option.position.point!.siri_ride__vehicle_ref)})`,
153+
}
154+
}),
155+
)
156+
}
55157
}
56158

57-
return Array.from(uniqueTimes)
58-
.sort()
59-
.map((time) => ({ value: time, label: time }))
60-
}, [positions, today, tomorrow])
159+
fetchOptions()
160+
}, [positions, today, tomorrow, vehicleNumber])
61161

62-
// Set Bus Postions
63162
useEffect(() => {
64163
if (!startTime) {
65164
setFilteredPositions([])
66165
return
67166
}
68-
69-
const filtered = positions.filter((position) => {
70-
const scheduledTime = position.point?.siri_ride__scheduled_start_time
71-
return scheduledTime && formatTime(dayjs(scheduledTime)) === startTime
72-
})
73-
74-
setFilteredPositions(filtered)
167+
const [scheduledTime, scheduledVehicle, scheduledLine] = startTime.split('|')
168+
169+
setFilteredPositions(
170+
positions.filter((position) => {
171+
const scheduledStart = position.point?.siri_ride__scheduled_start_time
172+
const vehicleRef = position.point?.siri_ride__vehicle_ref.toString()
173+
if (!scheduledStart || !vehicleRef || !scheduledTime || !scheduledVehicle) return false
174+
return (
175+
formatTime(dayjs(scheduledStart)) === scheduledTime &&
176+
scheduledVehicle === vehicleRef &&
177+
(scheduledLine ? scheduledLine === position.point?.siri_route__line_ref.toString() : true)
178+
)
179+
}),
180+
)
75181
}, [startTime, positions])
76182

77-
// Set Planned Route Stops
78183
useEffect(() => {
79-
if (!routeIds?.length) {
80-
setPlannedRouteStops([])
81-
return
82-
}
83-
84184
const fetchStops = async () => {
85-
const [hour, minute] = startTime ? startTime.split(':').map(Number) : [0, 0]
86-
const startTimeTimestamp = today
87-
.set('hour', hour)
88-
.set('minute', minute)
89-
.set('second', 0)
90-
.set('millisecond', 0)
91-
const stops = await getStopsForRouteAsync(routeIds, startTimeTimestamp)
92-
setPlannedRouteStops(stops)
185+
try {
186+
const [scheduledTime, , scheduledLine] = startTime?.split('|') || [
187+
undefined,
188+
undefined,
189+
undefined,
190+
]
191+
const [hour, minute] = scheduledTime ? scheduledTime.split(':').map(Number) : [0, 0]
192+
const startTimeTimestamp = today.hour(hour).minute(minute).second(0).millisecond(0)
193+
let routeIds: number[] | undefined
194+
if (selectedRoute?.routeIds && selectedRoute.routeIds.length > 0) {
195+
routeIds = selectedRoute.routeIds
196+
} else if (scheduledLine && operatorId) {
197+
routeIds = (
198+
await getRoutesByLineRef(operatorId, scheduledLine, startTimeTimestamp.toDate())
199+
).map((route) => route.id)
200+
}
201+
if (!routeIds || routeIds.length === 0) {
202+
setPlannedRouteStops([])
203+
return
204+
}
205+
const stops = await getStopsForRouteAsync(routeIds, startTimeTimestamp)
206+
setPlannedRouteStops(stops)
207+
} catch (err) {
208+
console.error(err)
209+
setPlannedRouteStops([])
210+
}
93211
}
94-
95212
fetchStops()
96-
}, [routeIds, startTime, today])
213+
}, [selectedRoute?.routeIds, operatorId, startTime, today])
97214

98215
return {
99216
positions: filteredPositions,
100217
plannedRouteStops,
101218
options,
102219
startTime,
103-
setStartTime,
104220
locationsAreLoading,
221+
routes,
222+
routeKey,
223+
error,
224+
setStartTime,
225+
setRouteKey,
105226
}
106227
}

0 commit comments

Comments
 (0)