diff --git a/lib/actions/api.js b/lib/actions/api.js index 9a535c164..0977ee228 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -705,6 +705,6 @@ export const receivedVehiclePositionsError = createAction( 'REALTIME_VEHICLE_POSITIONS_ERROR' ) -export function getVehiclePositionsForRoute(routeId) { - return executeOTPAction('getVehiclePositionsForRoute', routeId) +export function getVehiclePositions(routeId) { + return executeOTPAction('getVehiclePositions', routeId) } diff --git a/lib/actions/apiV1.js b/lib/actions/apiV1.js index ed884a489..3d58631de 100644 --- a/lib/actions/apiV1.js +++ b/lib/actions/apiV1.js @@ -59,7 +59,7 @@ export function findStopTimesForStop(params) { } } -const getVehiclePositionsForRoute = (routeId) => +const getVehiclePositions = (routeId) => createQueryAction( `index/routes/${routeId}/vehicles`, receivedVehiclePositions, @@ -166,7 +166,7 @@ export default { findRoutes, findStopTimesForStop, findTrip, - getVehiclePositionsForRoute, + getVehiclePositions, routingQuery, vehicleRentalQuery } diff --git a/lib/actions/apiV2.js b/lib/actions/apiV2.js index b7854c456..a389ecfa7 100644 --- a/lib/actions/apiV2.js +++ b/lib/actions/apiV2.js @@ -606,12 +606,12 @@ export const findStopTimesForStop = (params) => ) } -const getVehiclePositionsForRoute = (routeId) => +const getVehiclePositions = (routeId) => function (dispatch, getState) { return dispatch( createGraphQLQueryAction( `{ - route(id: "${routeId}") { + route${routeId ? `(id: "${routeId}")` : 's'} { patterns { vehiclePositions { vehicleId @@ -629,6 +629,16 @@ const getVehiclePositionsForRoute = (routeId) => heading lastUpdated trip { + ${ + !routeId && + `route { + shortName + longName + mode + color + textColor + }` + } pattern { id } @@ -643,6 +653,15 @@ const getVehiclePositionsForRoute = (routeId) => { noThrottle: true, rewritePayload: (payload) => { + if (payload.data?.routes) { + const vehicles = payload.data.routes.reduce((prev, cur) => { + return prev.concat( + cur.patterns.map((p) => p.vehiclePositions).flat() + ) + }, []) + + return { vehicles } + } const vehicles = payload.data?.route?.patterns .reduce((prev, cur) => { return prev.concat( @@ -664,6 +683,7 @@ const getVehiclePositionsForRoute = (routeId) => ) }, []) .filter((vehicle) => !!vehicle) + return { routeId, vehicles } } } @@ -1210,7 +1230,7 @@ export default { findRoutes, findStopTimesForStop, findTrip, - getVehiclePositionsForRoute, + getVehiclePositions, retrieveServiceTimeRangeIfNeeded, routingQuery, vehicleRentalQuery diff --git a/lib/components/map/connected-full-transit-vehicle-overlay.tsx b/lib/components/map/connected-full-transit-vehicle-overlay.tsx new file mode 100644 index 000000000..9e0e7d61f --- /dev/null +++ b/lib/components/map/connected-full-transit-vehicle-overlay.tsx @@ -0,0 +1,54 @@ +import { connect } from 'react-redux' +import { injectIntl } from 'react-intl' +import React from 'react' +import TransitVehicleOverlay, { + Circle, + DefaultIconContainer, + RotatingCircle, + RouteNumberIcon, + withRouteColorBackground +} from '@opentripplanner/transit-vehicle-overlay' + +import { DEFAULT_ROUTE_COLOR } from '../util/colors' + +import { VehicleTooltip } from './connected-transit-vehicle-overlay' + +const IconContainer = withRouteColorBackground(Circle) +// connect to the redux store +const mapStateToProps = (state: Record) => { + const { viewedRoute } = state.otp.ui + // TODO: clever behavior for when route viewer is active + + const vehicles = state.otp.transitIndex?.allVehicles + // TODO: CLEAN THIS UP + ?.filter( + (obj, index) => + state.otp.transitIndex?.allVehicles.findIndex( + (item) => item.vehicleId === obj.vehicleId + ) === index + ) + .map((v) => { + const { route } = v?.trip + v.routeType = route?.mode + v.routeColor = + route.color && !route.color.includes('#') + ? '#' + route.color + : route?.color || DEFAULT_ROUTE_COLOR + // Try to populate this attribute, which is required for the vehicle popup to appear. + v.routeShortName = route?.shortName + v.routeLongName = v.routeLongName || route?.longName + v.textColor = route?.textColor + return v + }) + + // TODO: something funky is happening with the mode icon. Ferries are buses! + return { + IconContainer, + TooltipSlot: injectIntl(VehicleTooltip), + VehicleIcon: RouteNumberIcon, + vehicles + } +} + +// @ts-expect-error state.js being typescripted will fix this error +export default injectIntl(connect(mapStateToProps)(TransitVehicleOverlay)) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 0a52f1c1e..5abe4e3c0 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -12,7 +12,9 @@ * 5) This overlay has a custom popup on vehicle hover */ +import { connect } from 'react-redux' import { FormattedMessage, FormattedNumber, injectIntl } from 'react-intl' +import React from 'react' import TransitVehicleOverlay, { Circle, withCaret, @@ -20,14 +22,11 @@ import TransitVehicleOverlay, { } from '@opentripplanner/transit-vehicle-overlay' import { capitalizeFirst } from '../../util/ui' -import { connect } from 'react-redux' import { DEFAULT_ROUTE_COLOR } from '../util/colors' import { formatDuration } from '../util/formatted-duration' import FormattedTransitVehicleStatus from '../util/formatted-transit-vehicle-status' -import React from 'react' - -function VehicleTooltip(props) { +export function VehicleTooltip(props) { const { intl, vehicle } = props let vehicleLabel = vehicle?.label || vehicle?.vehicleId @@ -65,7 +64,9 @@ function VehicleTooltip(props) { { id: 'common.time.durationAgo' }, { duration: formatDuration( - Math.floor(Date.now() / 1000 - vehicle?.seconds), + Math.floor( + Date.now() / 1000 - (vehicle?.seconds || vehicle?.lastUpdated) + ), intl, true ) diff --git a/lib/components/map/default-map.tsx b/lib/components/map/default-map.tsx index 38d7570e4..114757fe5 100644 --- a/lib/components/map/default-map.tsx +++ b/lib/components/map/default-map.tsx @@ -22,9 +22,11 @@ import { MainPanelContent } from '../../actions/ui-constants' import { setLocation, setMapPopupLocationAndGeocode } from '../../actions/map' import { setViewedStop } from '../../actions/ui' import { updateOverlayVisibility } from '../../actions/config' +import VehiclePositionRetriever from '../viewers/vehicle-position-retriever' import ElevationPointMarker from './elevation-point-marker' import EndpointsOverlay from './connected-endpoints-overlay' +import FullTransitVehicleOverlay from './connected-full-transit-vehicle-overlay' import GeoJsonLayer from './connected-geojson-layer' import ItinSummaryOverlay from './itinerary-summary-overlay' import NearbyViewDotOverlay from './nearby-view-dot-overlay' @@ -409,6 +411,8 @@ class DefaultMap extends Component { viewedRouteStops, config.companies ) + case 'realtime-vehicles': + return default: return null } @@ -419,6 +423,9 @@ class DefaultMap extends Component { + {overlays.map((o) => o.type).includes('realtime-vehicles') && ( + + )} ) diff --git a/lib/components/viewers/vehicle-position-retriever.ts b/lib/components/viewers/vehicle-position-retriever.ts index 1a3cad9e6..70ff0387f 100644 --- a/lib/components/viewers/vehicle-position-retriever.ts +++ b/lib/components/viewers/vehicle-position-retriever.ts @@ -4,7 +4,8 @@ import { useCallback, useEffect, useState } from 'react' import * as apiActions from '../../actions/api' interface Props { - getVehiclePositionsForRoute: (id: string) => void + fetchAll?: boolean + getVehiclePositions: (id?: string) => void refreshSeconds: number routeId?: string } @@ -13,21 +14,22 @@ interface Props { * Non-visual component that retrieves vehicle positions for the given route. */ const VehiclePositionRetriever = ({ - getVehiclePositionsForRoute, + fetchAll, + getVehiclePositions, refreshSeconds, routeId }: Props) => { const [refreshTimer, setRefreshTimer] = useState(null) const refreshVehiclePositions = useCallback(() => { - if (routeId) { - getVehiclePositionsForRoute(routeId) + if (routeId || fetchAll) { + getVehiclePositions(routeId) } - }, [routeId, getVehiclePositionsForRoute]) + }, [routeId, getVehiclePositions, fetchAll]) useEffect(() => { // Fetch vehicle positions when initially mounting component and a route id is available. - if (routeId) { + if (routeId || fetchAll) { refreshVehiclePositions() if (!refreshTimer) { @@ -45,7 +47,7 @@ const VehiclePositionRetriever = ({ setRefreshTimer(null) } } - }, [routeId, refreshVehiclePositions, refreshTimer, refreshSeconds]) + }, [routeId, refreshVehiclePositions, refreshTimer, refreshSeconds, fetchAll]) // Component renders nothing. return null @@ -55,13 +57,13 @@ const VehiclePositionRetriever = ({ const mapStateToProps = (state: any) => { return { refreshSeconds: - state.otp.config.routeViewer?.vehiclePositionRefreshSeconds || 30, + state.otp.config.routeViewer?.vehiclePositionRefreshSeconds || 10, routeId: state.otp.ui.viewedRoute?.routeId } } const mapDispatchToProps = { - getVehiclePositionsForRoute: apiActions.getVehiclePositionsForRoute + getVehiclePositions: apiActions.getVehiclePositions } export default connect( diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 0bc899936..69777bd3c 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -690,15 +690,21 @@ function createOtpReducer(config) { case 'REALTIME_VEHICLE_POSITIONS_RESPONSE': if (!action.payload?.vehicles) return state - return update(state, { - transitIndex: { - routes: { - [action.payload.routeId]: { - vehicles: { $set: action.payload.vehicles } + if (action.payload?.routeId) { + return update(state, { + transitIndex: { + routes: { + [action.payload.routeId]: { + vehicles: { $set: action.payload.vehicles } + } } } - } - }) + }) + } else { + return update(state, { + transitIndex: { allVehicles: { $set: action.payload.vehicles } } + }) + } case 'CLEAR_STOPS_OVERLAY': return update(state, { overlay: {