Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add trip search by license plate number #1022

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
19 changes: 15 additions & 4 deletions src/api/useVehicleLocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const config = {
fromField: 'recorded_at_time_from',
toField: 'recorded_at_time_to',
lineRefField: 'siri_routes__line_ref',
vehicleRefField: 'siri_ride__vehicle_ref',
operatorRefField: 'siri_routes__operator_ref',
} as const

Expand Down Expand Up @@ -44,14 +45,16 @@ class LocationObservable {
from,
to,
lineRef,
vehicleRef,
operatorRef,
}: {
from: Dateable
to: Dateable
lineRef?: number
vehicleRef?: number
operatorRef?: number
}) {
this.#loadData({ from, to, lineRef, operatorRef })
this.#loadData({ from, to, lineRef, vehicleRef, operatorRef })
}

data: VehicleLocation[] = []
Expand All @@ -61,11 +64,13 @@ class LocationObservable {
from,
to,
lineRef,
vehicleRef,
operatorRef,
}: {
from: Dateable
to: Dateable
lineRef?: number
vehicleRef?: number
operatorRef?: number
}) {
let offset = 0
Expand All @@ -76,6 +81,7 @@ class LocationObservable {
}&offset=${offset}`
if (operatorRef) url += `&${config.operatorRefField}=${operatorRef}`
if (lineRef) url += `&${config.lineRefField}=${lineRef}`
if (vehicleRef) url += `&${config.vehicleRefField}=${vehicleRef}`

const response = await fetchWithQueue(url)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
Expand Down Expand Up @@ -137,18 +143,20 @@ function getLocations({
from,
to,
lineRef,
vehicleRef,
onUpdate,
operatorRef,
}: {
from: Dateable
to: Dateable
lineRef?: number
vehicleRef?: number
operatorRef?: number
onUpdate: (locations: VehicleLocation[] | { finished: true }) => void // the observer will be called every time with all the locations that were loaded
}) {
const key = `${formatTime(from)}-${formatTime(to)}-${operatorRef}-${lineRef}`
const key = `${formatTime(from)}-${formatTime(to)}-${operatorRef}-${lineRef}-${vehicleRef}`
if (!loadedLocations.has(key)) {
loadedLocations.set(key, new LocationObservable({ from, to, lineRef, operatorRef }))
loadedLocations.set(key, new LocationObservable({ from, to, lineRef, vehicleRef, operatorRef }))
}
const observable = loadedLocations.get(key)!
return observable.observe(onUpdate)
Expand All @@ -170,13 +178,15 @@ export default function useVehicleLocations({
from,
to,
lineRef,
vehicleRef,
operatorRef,
splitMinutes: split = 1,
pause = false,
}: {
from: Dateable
to: Dateable
lineRef?: number
vehicleRef?: number
operatorRef?: number
splitMinutes?: false | number
pause?: boolean
Expand All @@ -192,6 +202,7 @@ export default function useVehicleLocations({
from,
to,
lineRef,
vehicleRef,
operatorRef,
onUpdate: (data) => {
if ('finished' in data) {
Expand All @@ -216,7 +227,7 @@ export default function useVehicleLocations({
unmounts.forEach((unmount) => unmount())
setIsLoading([])
}
}, [from, to, lineRef, split])
}, [from, to, lineRef, vehicleRef, split])
return {
locations,
isLoading: isLoading.some((loading) => loading),
Expand Down
146 changes: 146 additions & 0 deletions src/hooks/useSingleVehicleData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import moment from 'moment'
import { useContext, useEffect, useMemo, useState } from 'react'
import { getStopsForRouteAsync } from 'src/api/gtfsService'
import useVehicleLocations from 'src/api/useVehicleLocations'
import { BusStop } from 'src/model/busStop'
import { SearchContext } from 'src/model/pageState'
import { Point } from 'src/pages/timeBasedMap'

export const useSingleVehicleData = (vehicleRef?: number, routeIds?: number[]) => {
const {
search: { timestamp },
} = useContext(SearchContext)
const [filteredPositions, setFilteredPositions] = useState<Point[]>([])
const [startTime, setStartTime] = useState<string>('00:00:00')
const [plannedRouteStops, setPlannedRouteStops] = useState<BusStop[]>([])

const today = new Date(timestamp)
const tomorrow = new Date(today)
tomorrow.setDate(tomorrow.getDate() + 1)

const { locations, isLoading: locationsAreLoading } = useVehicleLocations({
from: +today.setHours(0, 0, 0, 0),
to: +tomorrow.setHours(0, 0, 0, 0),
vehicleRef,
splitMinutes: 360,
pause: !vehicleRef,
})
// console.log('locations:', locations) // הוסף הודעת לוג כאן

const positions = useMemo(() => {
const pos = locations
.filter((location) => location.siri_ride__vehicle_ref == vehicleRef)
.map<Point>((location) => ({
loc: [location.lat, location.lon],
color: location.velocity,
operator: location.siri_route__operator_ref,
bearing: location.bearing,
recorded_at_time: new Date(location.recorded_at_time).getTime(),
point: location,
}))
return pos
}, [locations])

function convertTo24HourAndToNumber(time: string): number {
const match = time.match(/(\d+):(\d+):(\d+)\s(AM|PM)/)
if (!match) return 0

const [, hour, minute, , modifier] = match
let newHour = parseInt(hour, 10)
if (modifier === 'AM' && newHour === 12) newHour = 0
if (modifier === 'PM' && newHour !== 12) newHour += 12

return newHour * 60 + parseInt(minute, 10)
}

const options = useMemo(() => {
const filteredPositions = positions.filter((position) => {
const startTime = position.point?.siri_ride__scheduled_start_time
return !!startTime && +new Date(startTime) > +today.setHours(0, 0, 0, 0)
})

if (filteredPositions.length === 0) return []

const uniqueTimes = Array.from(
new Set(
filteredPositions
.map((position) => position.point?.siri_ride__scheduled_start_time)
.filter((time): time is string => !!time)
.map((time) => time.trim()),
),
)
.map((time) => new Date(time).toLocaleTimeString()) // Convert to 24-hour time string
.map((time) => ({
value: time,
label: time,
}))

const sortedOptions = uniqueTimes.sort(
(a, b) => convertTo24HourAndToNumber(a.value) - convertTo24HourAndToNumber(b.value),
)

return sortedOptions
}, [positions])
//עדכון ברירת מחדל ל-startTime כאשר יש options
useEffect(() => {
if (options.length > 0 && startTime === '00:00:00') {
setStartTime(options[0].value)
// console.log('Updated startTime to:', options[0].value)
}
}, [options])

// חיפוש לפי startTime
useEffect(() => {
if (positions.length === 0) {
console.warn('No positions available to filter.')
return
}
// console.log('Start time:', startTime)

if (startTime !== '00:00:00') {
const newFilteredPositions = positions.filter((position) => {
const scheduledStartTime = moment(position.point?.siri_ride__scheduled_start_time)
.utc()
.format('HH:mm:ss') // המרת הזמן לפורמט HH:mm:ss

const formattedStartTime = moment(startTime, 'HH:mm:ss').utc().format('HH:mm:ss')

// console.log('Scheduled start time (formatted):', scheduledStartTime)
// console.log('Start time (formatted):', formattedStartTime)
// console.log('Comparison result:', scheduledStartTime === formattedStartTime);
return scheduledStartTime === formattedStartTime
})
setFilteredPositions(newFilteredPositions)
// console.log('New filtered positions:', newFilteredPositions)
}

if (positions.length > 0 && startTime !== '00:00:00') {
const [hours, minutes] = startTime.split(':')
const startTimeTimestamp = +new Date(
positions[0].point?.siri_ride__scheduled_start_time ?? 0,
).setHours(+hours, +minutes, 0, 0)
handlePlannedRouteStops(routeIds ?? [], startTimeTimestamp)
}
}, [startTime, positions])

// הפונקציה להוצאת תחנות למזהה רכבת (או רכב)
const handlePlannedRouteStops = async (routeIds: number[], startTimeTs: number) => {
try {
const stops = await getStopsForRouteAsync(routeIds, moment(startTimeTs))
// console.log('Retrieved stops:', stops)
setPlannedRouteStops(stops)
} catch (error) {
console.error('Error retrieving stops:', error)
}
}

return {
locationsAreLoading,
positions,
options,
filteredPositions,
plannedRouteStops,
startTime,
setStartTime,
}
}
4 changes: 4 additions & 0 deletions src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@
"gaps_patterns_page_title": "patterns",
"gaps_patterns_page_description": "A graphic display of routes performed according to schedule, grouped by time of day\\severity, by user-given bus operator, route number, route & date",
"singleline_map_page_title": "Map by line",
"singlevehicle_map_page_title": "Map by vehicle",
"singleline_map_page_description": "Display of bus route on map by user-given bus operator, route number, route, date & time",
"singlevehicle_map_page_description": "Display of bus route on map by user-given bus operator, vehicle number, route, date & time",
"open_menu_description": "Displays different options and parameters which are accessible in the application",
"choose_datetime": "Date and time",
"choose_date": "Date",
"choose_time": "Time",
"choose_operator": "Operating Company",
"operator_placeholder": "For example: Dan",
"choose_line": "line number",
"choose_vehicle": "Vehicle number",
"line_placeholder": "For example: 17a",
"vehicle_placeholder": "For example: 12345",
"choose_route": "Choosing a travel route (XXX options)",
"choose_stop": "Select a station (XXX options)",
"direction_arrow": "⟵",
Expand Down
4 changes: 4 additions & 0 deletions src/locale/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@
"gaps_patterns_page_title": "דפוסי נסיעות שלא בוצעו",
"gaps_patterns_page_description": "תצוגה גרפית של אחוז הנסיעות שבוצעו בהתאם ללוח הזמנים בחלוקה לשעות\\רמת חומרה, לפי חברת אוטובוסים, מספר קו, מסלול ותאריך",
"singleline_map_page_title": "מפה לפי קו",
"singlevehicle_map_page_title": "מפה לפי רכב",
"singleline_map_page_description": "תצוגה של מסלול קו על מפה ע\"פ נתוני חברת אוטובוסים, קו, מסלול, תאריך ושעה",
"singlevehicle_map_page_description": "תצוגה של מיקום רכב על מפה ע\"פ נתוני חברת אוטובוסים, רכב, תאריך ושעה",
"open_menu_description": "תצוגה של שלל אפשרויות ופרמטרים שונים הניתנים לביצוע באפליקצייה",
"choose_datetime": "תאריך ושעה",
"choose_date": "תאריך",
"choose_time": "שעה",
"choose_operator": "חברה מפעילה",
"operator_placeholder": "לדוגמה: דן",
"choose_line": "מספר קו",
"choose_vehicle": "מספר רכב",
"line_placeholder": "לדוגמה: 17א",
"vehicle_placeholder": "לדוגמה: 12345",
"choose_route": "בחירת מסלול נסיעה (XXX אפשרויות)",
"choose_stop": "בחירת תחנה (XXX אפשרויות)",
"direction_arrow": "⟵",
Expand Down
1 change: 1 addition & 0 deletions src/model/pageState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type PageSearchState = {
timestamp: number
operatorId?: string
lineNumber?: string
vehicleNumber?: number
routeKey?: string
routes?: BusRoute[]
}
Expand Down
56 changes: 56 additions & 0 deletions src/pages/components/VehicleSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useCallback, useLayoutEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import debounce from 'lodash.debounce'
import { TextField } from '@mui/material'
import classNames from 'classnames'
import ClearButton from './ClearButton'
import './Selector.scss'

type VehicleSelectorProps = {
vehicleNumber: number | undefined
setVehicleNumber: (vehicleNumber: number) => void
}

const VehicleSelector = ({ vehicleNumber, setVehicleNumber }: VehicleSelectorProps) => {
const [value, setValue] = useState<VehicleSelectorProps['vehicleNumber']>(vehicleNumber)
const debouncedSetVehicleNumber = useCallback(debounce(setVehicleNumber, 200), [setVehicleNumber])
const { t } = useTranslation()

useLayoutEffect(() => {
setValue(vehicleNumber)
}, [])

const handleClearInput = () => {
setValue(0)
setVehicleNumber(0)
}

const textFieldClass = classNames({
'selector-vehicle-text-field': true,
'selector-vehicle-text-field_visible': value,
'selector-vehicle-text-field_hidden': !value,
})
return (
<TextField
className={textFieldClass}
label={t('choose_vehicle')}
type="text"
value={value && +value < 0 ? 0 : value}
onChange={(e) => {
const inputValue = e.target.value
const numericValue = inputValue === '' ? undefined : parseInt(inputValue, 10) || 0
setValue(numericValue)
debouncedSetVehicleNumber(numericValue || 0)
}}
InputLabelProps={{
shrink: true,
}}
InputProps={{
placeholder: t('vehicle_placeholder'),
endAdornment: <ClearButton onClearInput={handleClearInput} />,
}}
/>
)
}

export default VehicleSelector
Loading
Loading