diff --git a/packages/ui/src/app.tsx b/packages/ui/src/app.tsx
index ab099d0..c26b418 100644
--- a/packages/ui/src/app.tsx
+++ b/packages/ui/src/app.tsx
@@ -3,6 +3,7 @@ import { RouterProvider } from 'react-router-dom'
import { GlobalsProvider } from './globals.js'
import { VehiclesProvider } from './contexts/vehicles.js'
+import { SettingsProvider } from './contexts/settings/index.js'
import { router } from './router.js'
import type { FC } from 'react'
@@ -19,9 +20,11 @@ const BusMap: FC = () => {
return (
-
-
-
+
+
+
+
+
)
diff --git a/packages/ui/src/components/busSelector.tsx b/packages/ui/src/components/busSelector.tsx
index c513427..3393f6c 100644
--- a/packages/ui/src/components/busSelector.tsx
+++ b/packages/ui/src/components/busSelector.tsx
@@ -34,7 +34,7 @@ const BusSelector = memo(function BusSelector({ agencies }: BusSelectorProps) {
const navigate = useNavigate()
const bookmark = useBusSelectorBookmark()
const [routeName, setRouteName] = useState()
- const { dispatch, markPredictedVehicles, agency, route, direction, stop } = useGlobals()
+ const { dispatch, agency, route, direction, stop } = useGlobals()
const vehiclesDispatch = useVehiclesDispatch()
const stops = useMemo(() => {
if (direction && route) {
@@ -177,9 +177,6 @@ const BusSelector = memo(function BusSelector({ agencies }: BusSelectorProps) {
)
}
}, [dispatch, vehiclesDispatch, navigate, agency, route, direction])
- const onTogglePredictedVehicles = useCallback(() => {
- dispatch({ type: 'markPredictedVehicles', value: !markPredictedVehicles })
- }, [dispatch, markPredictedVehicles])
const error = getFirstDataError([routesError, routeError])
const isLoading = isRoutesLoading || isRouteLoading
@@ -232,8 +229,6 @@ const BusSelector = memo(function BusSelector({ agencies }: BusSelectorProps) {
selected={stop}
onClear={onClearStop}
onSelect={onSelectStop}
- markPredictedVehicles={markPredictedVehicles}
- onTogglePredictedVehicles={onTogglePredictedVehicles}
isDisabled={isLoading || !agency || !route || !direction}
/>
diff --git a/packages/ui/src/components/predictions.tsx b/packages/ui/src/components/predictions.tsx
index 28f004c..0c53c3c 100644
--- a/packages/ui/src/components/predictions.tsx
+++ b/packages/ui/src/components/predictions.tsx
@@ -2,6 +2,7 @@ import styled, { keyframes } from 'styled-components'
import { PB50T, PB80T } from '@busmap/components/colors'
import { PredictedVehiclesColors } from '../utils.js'
+import { useVehicleSettings } from '../contexts/settings/vehicle.js'
import type { FC } from 'react'
import type { Prediction, Stop } from '../types.js'
@@ -12,7 +13,6 @@ interface PredictionsProps {
isFetching: boolean
timestamp: number
messages: Prediction['messages']
- markPredictedVehicles: boolean
}
const blink = keyframes`
@@ -115,9 +115,11 @@ const List = styled.ul<{ markPredictedVehicles: boolean }>`
}
`
const AffectedByLayover = styled.details`
+ display: inline-block;
margin: 0 0 12px 0;
summary:first-of-type {
font-size: 12px;
+ cursor: pointer;
}
p {
line-height: 1.25;
@@ -130,10 +132,12 @@ const AffectedByLayover = styled.details`
}
`
const Messages = styled.details`
+ display: inline-block;
margin: 12px 0 0 0;
summary:first-of-type {
font-size: 12px;
color: blue;
+ cursor: pointer;
}
ul {
@@ -158,13 +162,9 @@ const Messages = styled.details`
}
}
`
-const Predictions: FC = ({
- preds,
- stop,
- messages,
- timestamp,
- markPredictedVehicles = true
-}) => {
+const Predictions: FC = ({ preds, stop, messages, timestamp }) => {
+ const { markPredictedVehicles } = useVehicleSettings()
+
if (Array.isArray(preds) && stop) {
if (preds.length) {
const values = preds[0].values.slice(0, 3)
diff --git a/packages/ui/src/components/selectors/stops.tsx b/packages/ui/src/components/selectors/stops.tsx
index 01fe876..fa3c111 100644
--- a/packages/ui/src/components/selectors/stops.tsx
+++ b/packages/ui/src/components/selectors/stops.tsx
@@ -2,57 +2,38 @@ import { AutoSuggest } from '@busmap/components/autoSuggest'
import { FormItem } from '../formItem.js'
-import type { FC, ChangeEvent } from 'react'
+import type { FC } from 'react'
import type { Stop } from '../../types.js'
interface Props {
stops: Stop[]
selected?: Stop
isDisabled?: boolean
- markPredictedVehicles: boolean
onClear?: (clearItem: () => void) => void
onSelect: (selected: Stop) => void
- onTogglePredictedVehicles: (evt: ChangeEvent) => void
}
const Stops: FC = ({
stops,
selected,
onSelect,
onClear,
- onTogglePredictedVehicles,
- markPredictedVehicles = true,
isDisabled = Boolean(stops)
}) => {
return (
- <>
-
-
-
-
-
-
- >
+
+
+
)
}
diff --git a/packages/ui/src/components/settings.tsx b/packages/ui/src/components/settings.tsx
new file mode 100644
index 0000000..6ad4ca8
--- /dev/null
+++ b/packages/ui/src/components/settings.tsx
@@ -0,0 +1,91 @@
+import styled from 'styled-components'
+import { useCallback } from 'react'
+
+import { FormItem } from './formItem.js'
+
+import { useSettings } from '../contexts/settings/index.js'
+
+import type { FC, ChangeEvent } from 'react'
+import type { SpeedUnit } from '../contexts/settings/index.js'
+
+const Form = styled.form`
+ fieldset {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ padding: 10px;
+ }
+
+ fieldset.row {
+ flex-direction: row;
+ gap: 10px;
+ }
+ legend {
+ font-size: 14px;
+ line-height: 1;
+ }
+`
+const Settings: FC = () => {
+ const settings = useSettings()
+ const onTogglePredictedVehicles = useCallback(() => {
+ settings.vehicle.dispatch({
+ type: 'markPredictedVehicles',
+ value: !settings.vehicle.markPredictedVehicles
+ })
+ }, [settings.vehicle])
+ const onChangeSpeedUnit = useCallback(
+ (evt: ChangeEvent) => {
+ settings.vehicle.dispatch({
+ type: 'speedUnit',
+ value: evt.currentTarget.value as SpeedUnit
+ })
+ },
+ [settings.vehicle]
+ )
+
+ return (
+
+ )
+}
+
+export { Settings }
diff --git a/packages/ui/src/contexts/settings/index.tsx b/packages/ui/src/contexts/settings/index.tsx
new file mode 100644
index 0000000..e408a07
--- /dev/null
+++ b/packages/ui/src/contexts/settings/index.tsx
@@ -0,0 +1,14 @@
+import { VehicleSettingsProvider, useVehicleSettings } from './vehicle.js'
+
+import type { FC, ReactNode } from 'react'
+
+const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => {
+ return {children}
+}
+const useSettings = () => {
+ return {
+ vehicle: useVehicleSettings()
+ }
+}
+export { SettingsProvider, useSettings }
+export type { SpeedUnit } from './vehicle.js'
diff --git a/packages/ui/src/contexts/settings/vehicle.tsx b/packages/ui/src/contexts/settings/vehicle.tsx
new file mode 100644
index 0000000..cf690c6
--- /dev/null
+++ b/packages/ui/src/contexts/settings/vehicle.tsx
@@ -0,0 +1,50 @@
+import { createContext, useContext, useReducer, useMemo } from 'react'
+
+import type { FC, ReactNode, Dispatch } from 'react'
+
+type SpeedUnit = 'kph' | 'mph'
+interface VehicleSettingsState {
+ markPredictedVehicles: boolean
+ speedUnit: SpeedUnit
+ dispatch: Dispatch
+}
+interface MarkPredictedVehicles {
+ type: 'markPredictedVehicles'
+ value: boolean
+}
+interface SpeedUnitChanged {
+ type: 'speedUnit'
+ value: SpeedUnit
+}
+type VehicleSettingsAction = MarkPredictedVehicles | SpeedUnitChanged
+const defaultState: VehicleSettingsState = {
+ dispatch: () => {},
+ speedUnit: 'kph',
+ markPredictedVehicles: true
+}
+const VehicleSettings = createContext(defaultState)
+const reducer = (state: VehicleSettingsState, action: VehicleSettingsAction) => {
+ switch (action.type) {
+ case 'speedUnit':
+ return { ...state, speedUnit: action.value }
+ case 'markPredictedVehicles':
+ return { ...state, markPredictedVehicles: action.value }
+ default:
+ return { ...defaultState, ...state }
+ }
+}
+const VehicleSettingsProvider: FC<{ children: ReactNode }> = ({ children }) => {
+ const [vehicleSettings, dispatch] = useReducer(reducer, defaultState)
+ const context = useMemo(
+ () => ({ ...vehicleSettings, dispatch }),
+ [vehicleSettings, dispatch]
+ )
+
+ return {children}
+}
+const useVehicleSettings = () => {
+ return useContext(VehicleSettings)
+}
+
+export { VehicleSettingsProvider, useVehicleSettings }
+export type { SpeedUnit }
diff --git a/packages/ui/src/globals.tsx b/packages/ui/src/globals.tsx
index 902910a..7916af9 100644
--- a/packages/ui/src/globals.tsx
+++ b/packages/ui/src/globals.tsx
@@ -8,7 +8,6 @@ type BusmapState = Omit
const defaultGlobals = {
dispatch: () => {},
locationSettled: false,
- markPredictedVehicles: true,
center: { lat: 37.7775, lon: -122.416389 },
bounds: {
sw: {
@@ -55,8 +54,6 @@ const reducer = (state: BusmapState, action: BusmapAction): BusmapState => {
return { ...state, stop: action.value, predictions: undefined }
case 'locationSettled':
return { ...state, locationSettled: action.value }
- case 'markPredictedVehicles':
- return { ...state, markPredictedVehicles: action.value }
case 'predictions':
return { ...state, predictions: action.value }
case 'selected':
diff --git a/packages/ui/src/home.tsx b/packages/ui/src/home.tsx
index 1209cd8..38986b8 100644
--- a/packages/ui/src/home.tsx
+++ b/packages/ui/src/home.tsx
@@ -8,6 +8,7 @@ import { useGlobals } from './globals.js'
import { useVehiclesDispatch } from './contexts/vehicles.js'
import { BusSelector } from './components/busSelector.js'
import { Loading } from './components/loading.js'
+import { Settings } from './components/settings.js'
import { Predictions } from './components/predictions.js'
import { Anchor } from './components/anchor.js'
import { getAll as getAllAgencies } from './api/rb/agency.js'
@@ -55,14 +56,7 @@ interface HomeProps {
}
const Home: FC = () => {
const vehiclesDispatch = useVehiclesDispatch()
- const {
- dispatch: update,
- markPredictedVehicles,
- locationSettled,
- agency,
- route,
- stop
- } = useGlobals()
+ const { dispatch: update, locationSettled, agency, route, stop } = useGlobals()
const [state, dispatch] = useReducer(reducer, initialState)
const { data: agencies, error: agenciesError } = useQuery('agencies', getAllAgencies)
const { data: preds, isFetching: isPredsFetching } = useQuery(
@@ -128,7 +122,7 @@ const Home: FC = () => {
borderRadius="5px 5px 0 0">
-
+
@@ -136,11 +130,11 @@ const Home: FC = () => {
-
- Other
+
+ Info
- Settings
+
Profile
@@ -156,7 +150,6 @@ const Home: FC = () => {
stop={stop}
preds={preds}
messages={messages}
- markPredictedVehicles={markPredictedVehicles}
timestamp={state.timestamp}
/>
diff --git a/packages/ui/src/hooks/common.ts b/packages/ui/src/hooks/common.ts
index 5935d5c..a2206fd 100644
--- a/packages/ui/src/hooks/common.ts
+++ b/packages/ui/src/hooks/common.ts
@@ -2,14 +2,22 @@ import { Marker } from 'leaflet'
import type { MarkerOptions, LatLng } from 'leaflet'
import type { Vehicle } from '../types.js'
+import type { SpeedUnit } from '../contexts/settings/vehicle.js'
class VehicleMarker extends Marker {
#vehicle: Vehicle
+ #speedUnit: SpeedUnit
- constructor(latlng: LatLng, vehicle: Vehicle, options?: MarkerOptions) {
+ constructor(
+ latlng: LatLng,
+ vehicle: Vehicle,
+ speedUnit: SpeedUnit,
+ options?: MarkerOptions
+ ) {
super(latlng, options)
this.#vehicle = vehicle
+ this.#speedUnit = speedUnit
}
get vehicle() {
@@ -19,6 +27,14 @@ class VehicleMarker extends Marker {
set vehicle(value: Vehicle) {
this.#vehicle = value
}
+
+ get speedUnit() {
+ return this.#speedUnit
+ }
+
+ set speedUnit(value: SpeedUnit) {
+ this.#speedUnit = value
+ }
}
export { VehicleMarker }
diff --git a/packages/ui/src/hooks/useVehiclesLayer.ts b/packages/ui/src/hooks/useVehiclesLayer.ts
index 23aa58d..72bff81 100644
--- a/packages/ui/src/hooks/useVehiclesLayer.ts
+++ b/packages/ui/src/hooks/useVehiclesLayer.ts
@@ -5,6 +5,7 @@ import { VehicleMarker } from './common.js'
import { useGlobals } from '../globals.js'
import { useVehicles } from '../contexts/vehicles.js'
+import { useVehicleSettings } from '../contexts/settings/vehicle.js'
import { PredictedVehiclesColors } from '../utils.js'
import type { LayerGroup } from 'leaflet'
@@ -143,8 +144,10 @@ const assignDynamicStyles = ({
* The outcome is a 'N/A' in the vehicle poupup, despite the selector form
* showing the correct direction. Basically a config error from the API source.
*/
-const getVehiclePopupContent = (vehicle: Vehicle, route: Route) => {
+const getVehiclePopupContent = (marker: VehicleMarker, route: Route) => {
+ const { vehicle, speedUnit } = marker
const direction = route.directions.find(dir => dir.id === vehicle.directionId)
+ const speed = speedUnit === 'mph' ? Math.round(vehicle.kph / 1.609) : vehicle.kph
return `
@@ -155,7 +158,7 @@ const getVehiclePopupContent = (vehicle: Vehicle, route: Route) => {
- ID
- ${vehicle.id}
- Speed
- - ${vehicle.kph} (kph)
+ - ${speed} (${speedUnit})
- Heading
- ${vehicle.heading} (deg)
- Predictable
@@ -167,7 +170,9 @@ const getVehiclePopupContent = (vehicle: Vehicle, route: Route) => {
}
const useVehiclesLayer = ({ vehiclesLayer }: UseVehiclesLayer) => {
const vehicles = useVehicles()
- const { route, predictions, markPredictedVehicles } = useGlobals()
+ const { route, predictions } = useGlobals()
+ const { markPredictedVehicles, speedUnit } = useVehicleSettings()
+
const iconDimensions = useRef(null)
const preds = useRef(predictions?.length ? predictions[0].values.slice(0, 3) : [])
@@ -198,7 +203,8 @@ const useVehiclesLayer = ({ vehiclesLayer }: UseVehiclesLayer) => {
dimensions: iconDimensions.current
})
marker.vehicle = vehicle
- marker.getPopup()?.setContent(getVehiclePopupContent(vehicle, route))
+ marker.speedUnit = speedUnit
+ marker.getPopup()?.setContent(getVehiclePopupContent(marker, route))
marker.setLatLng(L.latLng(vehicle.lat, vehicle.lon))
} else {
const div = document.createElement('div')
@@ -209,12 +215,17 @@ const useVehiclesLayer = ({ vehiclesLayer }: UseVehiclesLayer) => {
className: 'busmap-vehicle',
html: div
})
- const marker = new VehicleMarker(L.latLng(vehicle.lat, vehicle.lon), vehicle, {
- icon
- })
+ const marker = new VehicleMarker(
+ L.latLng(vehicle.lat, vehicle.lon),
+ vehicle,
+ speedUnit,
+ {
+ icon
+ }
+ )
const popup = L.popup({
className: 'busmap-vehicle-popup',
- content: getVehiclePopupContent(vehicle, route)
+ content: getVehiclePopupContent(marker, route)
})
marker.bindPopup(popup)
@@ -223,7 +234,7 @@ const useVehiclesLayer = ({ vehiclesLayer }: UseVehiclesLayer) => {
})
marker.on('click', () => {
popup.getElement()?.classList.add('selected')
- popup.setContent(getVehiclePopupContent(marker.vehicle, route))
+ popup.setContent(getVehiclePopupContent(marker, route))
})
title.appendChild(document.createTextNode(route.title))
@@ -260,7 +271,7 @@ const useVehiclesLayer = ({ vehiclesLayer }: UseVehiclesLayer) => {
} else {
vehiclesLayer.clearLayers()
}
- }, [vehicles, vehiclesLayer, route, markPredictedVehicles])
+ }, [vehicles, vehiclesLayer, route, markPredictedVehicles, speedUnit])
}
export { useVehiclesLayer }
diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts
index b269198..e8000fe 100644
--- a/packages/ui/src/types.ts
+++ b/packages/ui/src/types.ts
@@ -130,10 +130,6 @@ interface LocationSettled {
type: 'locationSettled'
value: boolean
}
-interface MarkPredictedVehiclesChanged {
- type: 'markPredictedVehicles'
- value: boolean
-}
type BusmapAction =
| BoundsChanged
| CenterChanged
@@ -144,7 +140,6 @@ type BusmapAction =
| PredictionsChanged
| SelectedChanged
| LocationSettled
- | MarkPredictedVehiclesChanged
interface BusmapGlobals {
dispatch: Dispatch
center: Point
@@ -156,7 +151,6 @@ interface BusmapGlobals {
predictions?: Prediction[]
selected?: Selection
locationSettled: boolean
- markPredictedVehicles: boolean
}
export type {