Skip to content

Commit

Permalink
Merge pull request #435 from opentripplanner/route-viewer-filter-and-…
Browse files Browse the repository at this point in the history
…search

Route Viewer Search and Filter
  • Loading branch information
miles-grant-ibigroup authored Sep 20, 2021
2 parents b1d6b92 + 3483791 commit dab6175
Show file tree
Hide file tree
Showing 33 changed files with 1,551 additions and 268 deletions.
8 changes: 8 additions & 0 deletions __tests__/reducers/__snapshots__/create-otp-reducer.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ Object {
"rideEstimates": Object {},
},
"transitIndex": Object {
"routes": Object {},
"stops": Object {},
"trips": Object {},
},
Expand All @@ -102,6 +103,13 @@ Object {
"localizedMessages": Object {},
"mobileScreen": 1,
"printView": false,
"routeViewer": Object {
"filter": Object {
"agency": null,
"mode": null,
"search": "",
},
},
},
"useRealtime": true,
"user": Object {
Expand Down
2 changes: 1 addition & 1 deletion example.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

.sidebar {
height: 100%;
padding: 10px;
padding: 0;
box-shadow: 3px 0px 12px #00000052;
z-index: 1000;
}
Expand Down
37 changes: 36 additions & 1 deletion i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,47 @@ _name: English
# - In contrast, some strings are common to multiple components,
# so it makes sense to group them by theme (e.g. accessModes) under the 'common' category.


# Component-specific messages (e.g. button captions)
# are defined for each component under the 'components' category.
components:
BatchRoutingPanel:
shortTitle: Plan Trip
DefaultItinerary:
clickDetails: Click to view details
# Use ordered placeholders when multiple modes are involved
# (this will accommodate right-to-left languages by swapping the order/separator in this string).
multiModeSummary: "{accessMode} to {transitMode}"
# If trip is less than one hour only display the minutes.
tripDurationFormatZeroHours: "{minutes, number} min"
# TODO: Distinguish between one hour (singular) and 2 hours or more?
tripDurationFormat: "{hours, number} hr {minutes, number} min"
RouteDetails:
operatedBy: "Operated by {agencyName}"
moreDetails: "More Details"
stopsTo: "Towards"
selectADirection: "Select a direction..."
RouteViewer:
allAgencies: All Agencies
allModes: All Modes # Note to translator: This text is width-constrained.
findARoute: Find A Route
noFilteredRoutesFound: No routes match your filter!
noRouteUrl: No route URL provided.
title: Route Viewer
shortTitle: View Routes
agencyFilter: Agency Filter
modeFilter: Mode Filter
details: " " # If the string is left blank, React-Intl renders the id
RouteRow:
operatorLogoAltText: '{operatorName} logo'
TransitVehicleOverlay:
# keys designed to match API output
incoming_at: "approaching {stop}"
stopped_at: "doors open at {stop}"
in_transit_to: "next stop {stop}"

vehicleName: "Vehicle {vehicleNumber}: "
realtimeVehicleInfo: "<b>{vehicleNameOrBlank}</b>{relativeTime}"
travelingAt: "traveling at {milesPerHour}"
ItinerarySummary:
fareCost: "{useMaxFare, select,
true {{minTotalFare} - {maxTotalFare}}
Expand Down Expand Up @@ -114,6 +146,9 @@ components:
# Common messages that appear in multiple components and modules
# are grouped below by topic.
common:
# Standard navigation
navigation:
back: Back
# OTP access modes
accessModes:
bike: Bike
Expand Down
36 changes: 36 additions & 0 deletions i18n/fr-FR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,42 @@ _id: fr-FR
_name: Unofficial French Translations!

components:
BatchRoutingPanel:
shortTitle: Planifier un trajet
DefaultItinerary:
clickDetails: Cliquez pour afficher les détails
multiModeSummary: "{accessMode} + {transitMode}"
# If trip is less than one hour only display the minutes.
tripDurationFormatZeroHours: "{minutes, number} mn"
# TODO: Distinguish between one hour (singular) and 2 hours or more?
tripDurationFormat: "{hours, number} h {minutes, number} mn"
RouteDetails:
operatedBy: "Exploité par {agencyName}"
moreDetails: "Plus d'infos"
stopsTo: "Direction"
selectADirection: "Choisissez une direction..."
RouteViewer:
allAgencies: Tous exploitants
allModes: Tous modes # Note to translator: This text is width-constrained.
findARoute: Chercher une ligne
noFilteredRoutesFound: Aucune ligne ne correspond à vos critères
noRouteUrl: Aucun lien fourni pour cette ligne.
title: Index des lignes
shortTitle: Index des lignes
agencyFilter: Filtre pour les exploitants
modeFilter: Filtre pour les modes
details: " " # If the string is left blank, React-Intl renders the id
RouteRow:
operatorLogoAltText: "Logo de {operatorName}"
TransitVehicleOverlay:
# keys designed to match API output
incoming_at: "Approchant {stop}"
stopped_at: "À quai à {stop}"
in_transit_to: "Prochain arrêt : {stop}"

vehicleName: "Véhicule {vehicleNumber}: "
realtimeVehicleInfo: "<b>{vehicleNameOrBlank}</b>{relativeTime}"
travelingAt: "Vitesse : {milesPerHour}"
ItinerarySummary:
fareCost: "{useMaxFare, select,
true {{minTotalFare} - {maxTotalFare}}
Expand Down Expand Up @@ -85,6 +118,9 @@ components:
startOver: Recommencer

common:
# Standard navigation
navigation:
back: Retour
accessModes:
bike: Vélo
bikeshare: Vélo en libre-service
Expand Down
61 changes: 56 additions & 5 deletions lib/actions/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,19 +507,25 @@ export function findRoute (params) {

export function findPatternsForRoute (params) {
return createQueryAction(
`index/routes/${params.routeId}/patterns`,
`index/routes/${params.routeId}/patterns?includeGeometry=true`,
findPatternsForRouteResponse,
findPatternsForRouteError,
{
noThrottle: true,
postprocess: (payload, dispatch) => {
// load geometry for each pattern
payload.forEach(ptn => {
dispatch(findGeometryForPattern({
patternId: ptn.id,
routeId: params.routeId
}))
// Some OTP instances don't support includeGeometry.
// We need to manually fetch geometry in these cases.
if (!ptn.geometry) {
dispatch(findGeometryForPattern({
patternId: ptn.id,
routeId: params.routeId
}))
}
})
},

rewritePayload: (payload) => {
// convert pattern array to ID-mapped object
const patterns = {}
Expand Down Expand Up @@ -556,6 +562,29 @@ export function findGeometryForPattern (params) {
)
}

// Stops for pattern query

export const findStopsForPatternResponse = createAction('FIND_STOPS_FOR_PATTERN_RESPONSE')
export const findStopsForPatternError = createAction('FIND_STOPS_FOR_PATTERN_ERROR')

export function findStopsForPattern (params) {
return createQueryAction(
`index/patterns/${params.patternId}/stops`,
findStopsForPatternResponse,
findStopsForPatternError,
{
noThrottle: true,
rewritePayload: (payload) => {
return {
patternId: params.patternId,
routeId: params.routeId,
stops: payload
}
}
}
)
}

// TNC ETA estimate lookup query

export const transportationNetworkCompanyEtaResponse = createAction('TNC_ETA_RESPONSE')
Expand Down Expand Up @@ -682,6 +711,27 @@ export function findStopsWithinBBox (params) {

export const clearStops = createAction('CLEAR_STOPS_OVERLAY')

// Realtime Vehicle positions query

const receivedVehiclePositions = createAction('REALTIME_VEHICLE_POSITIONS_RESPONSE')
const receivedVehiclePositionsError = createAction('REALTIME_VEHICLE_POSITIONS_ERROR')

export function getVehiclePositionsForRoute (routeId) {
return createQueryAction(
`index/routes/${routeId}/vehicles`,
receivedVehiclePositions,
receivedVehiclePositionsError,
{
rewritePayload: (payload) => {
return {
routeId: routeId,
vehicles: payload
}
}
}
)
}

const throttledUrls = {}

function now () {
Expand Down Expand Up @@ -720,6 +770,7 @@ window.setInterval(() => {
*/

function createQueryAction (endpoint, responseAction, errorAction, options = {}) {
/* eslint-disable-next-line complexity */
return async function (dispatch, getState) {
const state = getState()
const { config } = state.otp
Expand Down
59 changes: 49 additions & 10 deletions lib/actions/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import coreUtils from '@opentripplanner/core-utils'
import { createAction } from 'redux-actions'
import { matchPath } from 'react-router'

import { getUiUrlParams } from '../util/state'
import { getUiUrlParams, getModesForActiveAgencyFilter } from '../util/state'
import { getDefaultLocale, loadLocaleData } from '../util/i18n'
import { getPathFromParts } from '../util/ui'

import { findRoute, setUrlSearch } from './api'
import { setMapCenter, setMapZoom, setRouterId } from './config'
Expand Down Expand Up @@ -47,22 +48,31 @@ export function routeTo (url, replaceSearch, routingMethod = push) {
* route or stop).
*/
export function matchContentToUrl (location) {
// eslint-disable-next-line complexity
return function (dispatch, getState) {
// This is a bit of a hack to make up for the fact that react-router does
// not always provide the match params as expected.
// https://github.com/ReactTraining/react-router/issues/5870#issuecomment-394194338
const root = location.pathname.split('/')[1]
const match = matchPath(location.pathname, {
exact: true,
exact: false,
path: `/${root}/:id`,
strict: false
})
const id = match && match.params && match.params.id
const id = match?.params?.id
switch (root) {
case 'route':
if (id) {
dispatch(findRoute({ routeId: id }))
dispatch(setViewedRoute({ routeId: id }))
// Check for pattern "submatch"
const subMatch = matchPath(location.pathname, {
exact: true,
path: `/${root}/:id/pattern/:patternId`,
strict: false
})
const patternId = subMatch?.params?.patternId
// patternId may be undefined, which is OK as the route will still be routed
dispatch(setViewedRoute({ patternId, routeId: id }))
} else {
dispatch(setViewedRoute(null))
dispatch(setMainPanelContent(MainPanelContent.ROUTE_VIEWER))
Expand Down Expand Up @@ -196,23 +206,29 @@ export const clearPanel = createAction('CLEAR_MAIN_PANEL')
export function setViewedStop (payload) {
return function (dispatch, getState) {
dispatch(viewStop(payload))
const path = payload && payload.stopId
? `/stop/${payload.stopId}`
: '/stop'
// payload.stopId may be undefined, which is ok as will be ignored by getPathFromParts
const path = getPathFromParts('stop', payload?.stopId)
dispatch(routeTo(path))
}
}

const viewStop = createAction('SET_VIEWED_STOP')

export const setHoveredStop = createAction('SET_HOVERED_STOP')

export const setViewedTrip = createAction('SET_VIEWED_TRIP')

export function setViewedRoute (payload) {
return function (dispatch, getState) {
dispatch(viewRoute(payload))
const path = payload && payload.routeId
? `/route/${payload.routeId}`
: '/route'

const path = getPathFromParts(
'route',
payload?.routeId,
// If a pattern is supplied, include pattern in path
payload?.patternId && 'pattern',
payload?.patternId
)
dispatch(routeTo(path))
}
}
Expand Down Expand Up @@ -337,3 +353,26 @@ export function setLocale (locale) {
dispatch(updateLocale({ locale: effectiveLocale, messages }))
}
}

const updateRouteViewerFilter = createAction('UPDATE_ROUTE_VIEWER_FILTER')
/**
* Updates the route viewer filter
* @param {*} filter Object which includes either agency, mode, and/or search
*/
export function setRouteViewerFilter (filter) {
return async function (dispatch, getState) {
dispatch(updateRouteViewerFilter(filter))

// If we're changing agency, and have a mode selected,
// ensure that the mode filter doesn't select non-existent modes!
const activeModeFilter = getState().otp.ui.routeViewer.filter.mode
if (
filter.agency &&
activeModeFilter &&
!getModesForActiveAgencyFilter(getState()).includes(activeModeFilter.toUpperCase())
) {
// If invalid mode is selected, reset mode
dispatch(updateRouteViewerFilter({ mode: null }))
}
}
}
9 changes: 4 additions & 5 deletions lib/components/app/app-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class AppMenu extends Component {
const {
callTakerEnabled,
fieldTripEnabled,
languageConfig,
mailablesEnabled,
resetAndToggleCallHistory,
resetAndToggleFieldTrips,
Expand All @@ -60,8 +59,10 @@ class AppMenu extends Component {
id='app-menu'
noCaret
title={(<Icon type='bars' />)}>
<MenuItem onClick={this._showRouteViewer}>
<Icon type='bus' /> {languageConfig.routeViewer || 'Route Viewer'}
{/* This item is duplicated by the view-switcher, but only shown on mobile
when the view switcher isn't shown (using css) */}
<MenuItem className='app-menu-route-viewer-link' onClick={this._showRouteViewer}>
<Icon type='bus' /> Route Viewer
</MenuItem>
{callTakerEnabled &&
<MenuItem onClick={resetAndToggleCallHistory}>
Expand Down Expand Up @@ -90,11 +91,9 @@ class AppMenu extends Component {
// connect to the redux store

const mapStateToProps = (state, ownProps) => {
const {language} = state.otp.config
return {
callTakerEnabled: isModuleEnabled(state, Modules.CALL_TAKER),
fieldTripEnabled: isModuleEnabled(state, Modules.FIELD_TRIP),
languageConfig: language,
mailablesEnabled: isModuleEnabled(state, Modules.MAILABLES)
}
}
Expand Down
Loading

0 comments on commit dab6175

Please sign in to comment.