Skip to content

Commit

Permalink
Enabled trip search by license plate number
Browse files Browse the repository at this point in the history
Created the ability to search for trips by license plate number
  • Loading branch information
RGvirer committed Jan 24, 2025
1 parent 08a8e66 commit 283e6e7
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 4 deletions.
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])

Check failure on line 230 in src/api/useVehicleLocations.ts

View workflow job for this annotation

GitHub Actions / local-tests

Insert `·`
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

Check failure on line 84 in src/hooks/useSingleVehicleData.ts

View workflow job for this annotation

GitHub Actions / local-tests

Insert `··`
useEffect(() => {
if (options.length > 0 && startTime === '00:00:00') {
setStartTime(options[0].value)
// console.log('Updated startTime to:', options[0].value)
}
}, [options])

Check failure on line 91 in src/hooks/useSingleVehicleData.ts

View workflow job for this annotation

GitHub Actions / local-tests

Delete `··`
// חיפוש לפי 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(

Check failure on line 102 in src/hooks/useSingleVehicleData.ts

View workflow job for this annotation

GitHub Actions / local-tests

Replace `⏎··········position.point?.siri_ride__scheduled_start_time,` with `position.point?.siri_ride__scheduled_start_time)`
position.point?.siri_ride__scheduled_start_time,
).utc().format('HH:mm:ss') // המרת הזמן לפורמט HH:mm:ss

Check failure on line 104 in src/hooks/useSingleVehicleData.ts

View workflow job for this annotation

GitHub Actions / local-tests

Replace `).utc()` with `··.utc()⏎··········`

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

0 comments on commit 283e6e7

Please sign in to comment.