From b378eb24ca35cad76401126a7f7f59ddaec1d13c Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 18 Aug 2021 21:44:34 +0200 Subject: [PATCH 01/63] feat(route-viewer): support filtering routes by agency or mode --- lib/components/viewers/route-viewer.js | 149 ++++++++++++++++++------- lib/components/viewers/viewers.css | 16 +++ lib/reducers/create-otp-reducer.js | 3 +- 3 files changed, 127 insertions(+), 41 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 66c74a7d8..25f8876ba 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -33,14 +33,32 @@ class RouteViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, routes: PropTypes.object - } + }; + + state = { + agency: null, + mode: null + }; - _backClicked = () => this.props.setMainPanelContent(null) + _backClicked = () => this.props.setMainPanelContent(null); componentDidMount () { this.props.findRoutes() } + /** + * Handle filter dropdown change. Id of the filter is equivalent to the key in the + * route object + */ + onFilterChange = (event) => { + const { target } = event + const { id, value } = target + // id will be either 'agency' or 'mode' based on the dropdown used + this.setState({ + [id]: value + }) + }; + render () { const { findRoute, @@ -51,11 +69,36 @@ class RouteViewer extends Component { transitOperators, viewedRoute } = this.props - const sortedRoutes = routes - ? Object.values(routes).sort( - coreUtils.route.makeRouteComparator(transitOperators) + const { agency, mode } = this.state + const routesArray = routes ? Object.values(routes) : [] + + const filteredAndSortedRoutes = routesArray + .filter( + (route) => + // If the filter isn't defined, don't check. + (!agency || agency === route.agencyName) && + (!mode || mode === route.mode) + ) + .sort(coreUtils.route.makeRouteComparator(transitOperators)) + + // get agency and mode lists from route list + const agencies = Array.from( + new Set(routesArray.map((route) => route.agencyName || route.agency.name)) + ) + .filter((agency) => agency !== undefined) + .sort() + + const modes = Array.from( + new Set( + routesArray + .filter((route) => route.mode !== undefined) + .map((route) => route.mode.toLowerCase()) ) - : [] + ) + // Correct capitalization + .map((mode) => mode[0].toUpperCase() + mode.substr(1).toLowerCase()) + .sort() + return (
{/* Header Block */} @@ -74,32 +117,51 @@ class RouteViewer extends Component {
{languageConfig.routeViewer || 'Route Viewer'}
-
- {languageConfig.routeViewerDetails} +
{languageConfig.routeViewerDetails}
+
+ + +
- {sortedRoutes - .map(route => { - // Find operator based on agency_id (extracted from OTP route ID). - const operator = coreUtils.route.getTransitOperatorFromOtpRoute( + {filteredAndSortedRoutes.map((route) => { + // Find operator based on agency_id (extracted from OTP route ID). + const operator = + coreUtils.route.getTransitOperatorFromOtpRoute( route, transitOperators ) || {} - return ( - - ) - }) - } + return ( + + ) + })} + {(agency || mode) && filteredAndSortedRoutes.length === 0 && ( + + No routes match your filter! + + )}
) @@ -177,9 +239,10 @@ class RouteRow extends PureComponent { // Attempt to split route name if splitter is defined for operator (to // remove short name value from start of long name value). const nameParts = route.longName.split(longNameSplitter) - longName = (longNameSplitter && nameParts.length > 1) - ? nameParts[1] - : route.longName + longName = + longNameSplitter && nameParts.length > 1 + ? nameParts[1] + : route.longName // If long name and short name are identical, set long name to be an empty // string. if (longName === route.shortName) longName = '' @@ -191,14 +254,17 @@ class RouteRow extends PureComponent { const { isActive, operator, route } = this.props const { ModeIcon } = this.context - const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {} + const { defaultRouteColor, defaultRouteTextColor, longNameSplitter } = + operator || {} const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` // NOTE: text color is not a part of short response route object, so there // is no way to determine from OTP what the text color should be if the // background color is, say, black. Instead, determine the appropriate // contrast color and use that if no text color is available. const contrastColor = getContrastYIQ(backgroundColor) - const color = `#${defaultRouteTextColor || route.textColor || contrastColor}` + const color = `#${defaultRouteTextColor || + route.textColor || + contrastColor}` // Default long name is empty string (long name is an optional GTFS value). const longName = this.getCleanRouteLongName({ longNameSplitter, route }) return ( @@ -208,12 +274,9 @@ class RouteRow extends PureComponent { onClick={this._onClick} > - {operator && operator.logo && - - } + {operator && operator.logo && ( + + )} @@ -226,13 +289,19 @@ class RouteRow extends PureComponent { {route.shortName} {longName} - + {isActive && ( - {route.url - ? Route Details - : 'No route URL provided.' - } + {route.url ? ( + + Route Details + + ) : ( + 'No route URL provided.' + )} )} diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index b4097dccd..041a0271e 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -268,3 +268,19 @@ float: right; width: 50px; } + +/* Route Viewer Updates */ +.search-and-filter { + margin-top: 5px; +} +.search-and-filter select { + max-width: 125px; + margin: 0 5px; +} + +.route-viewer-body .noRoutesFoundMessage { + display: flex; + align-items: center; + justify-content: center; + padding-top: 10px; +} diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 488f99ce5..bc55af11a 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -252,6 +252,7 @@ function createOtpReducer (config) { // validate the initial state validateInitialState(initialState) + // eslint-disable-next-line complexity return (state = initialState, action) => { const searchId = action.payload && action.payload.searchId const requestId = action.payload && action.payload.requestId @@ -899,7 +900,7 @@ function createOtpReducer (config) { // Otherwise, overwrite only this route return update(state, { transitIndex: { - routes: { [action.payload.id]: { $set: action.payload } } + routes: { [action.payload.id]: { $merge: action.payload } } } }) case 'FIND_PATTERNS_FOR_ROUTE_RESPONSE': From 0196e5924bc812fc735434041fd6bd0b902fd7e1 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 19 Aug 2021 12:19:48 +0200 Subject: [PATCH 02/63] improvement(route-viewer): allow filtering routes via search --- lib/components/viewers/route-viewer.js | 84 ++++++++++++++++++-------- lib/components/viewers/viewers.css | 32 +++++++++- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 25f8876ba..b14984b1f 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -37,7 +37,8 @@ class RouteViewer extends Component { state = { agency: null, - mode: null + mode: null, + search: '' }; _backClicked = () => this.props.setMainPanelContent(null); @@ -59,6 +60,15 @@ class RouteViewer extends Component { }) }; + /** + * Update search state when user updates search field + */ + onSearchChange = (event) => { + const { target } = event + const { value } = target + this.setState({ search: value }) + }; + render () { const { findRoute, @@ -69,7 +79,7 @@ class RouteViewer extends Component { transitOperators, viewedRoute } = this.props - const { agency, mode } = this.state + const { agency, mode, search } = this.state const routesArray = routes ? Object.values(routes) : [] const filteredAndSortedRoutes = routesArray @@ -77,7 +87,11 @@ class RouteViewer extends Component { (route) => // If the filter isn't defined, don't check. (!agency || agency === route.agencyName) && - (!mode || mode === route.mode) + (!mode || mode === route.mode) && + // If user search is active, filter here + (!search || + ((route.longName && route.longName.includes(search)) || + (route.shortName && route.shortName.includes(search)))) ) .sort(coreUtils.route.makeRouteComparator(transitOperators)) @@ -106,10 +120,10 @@ class RouteViewer extends Component { {/* Back button */} {!hideBackButton && (
- +
)} @@ -118,24 +132,42 @@ class RouteViewer extends Component { {languageConfig.routeViewer || 'Route Viewer'}
{languageConfig.routeViewerDetails}
-
- - - -
-
+
+ + + + + + + + + +
@@ -157,7 +189,7 @@ class RouteViewer extends Component { /> ) })} - {(agency || mode) && filteredAndSortedRoutes.length === 0 && ( + {(agency || mode || search) && filteredAndSortedRoutes.length === 0 && ( No routes match your filter! diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 041a0271e..3ca8d82cf 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -271,13 +271,43 @@ /* Route Viewer Updates */ .search-and-filter { - margin-top: 5px; + margin-top: 10px; + display: flex; + flex-wrap: wrap; + gap: 10px; } .search-and-filter select { max-width: 125px; margin: 0 5px; } +.search-and-filter .routeFilter { + display: flex; + align-items: center; + justify-content: center; +} +.search-and-filter .routeSearch { + display: flex; + align-items: center; + justify-content: center; +} +.search-and-filter .routeSearch input { + border: none; + padding: 0.125em 0.5em; + border-radius: 5px; + margin-left: 5px; + width: 145px; +} +.routeSearch input::-webkit-search-cancel-button { + /* show clear button on webkit browsers */ + -webkit-appearance: searchfield-cancel-button; +} + +.mobile .routeSearch, +.mobile .routeSearch input { + width: 100%; +} + .route-viewer-body .noRoutesFoundMessage { display: flex; align-items: center; From e4e246b834c886f646d48eee3cb4592ad7dedca0 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 19 Aug 2021 13:46:28 +0200 Subject: [PATCH 03/63] improvement(app-menu): move route viewer link to main navbar --- lib/components/app/app-menu.js | 6 -- lib/components/app/app.css | 22 +++++ lib/components/app/desktop-nav.js | 2 + lib/components/app/view-switcher.js | 85 ++++++++++++++++++++ lib/components/mobile/batch-search-screen.js | 5 +- lib/components/mobile/navigation-bar.js | 4 +- lib/components/mobile/route-viewer.js | 1 - lib/components/mobile/search-screen.js | 2 +- 8 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 lib/components/app/view-switcher.js diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index ccbf42fa1..ac6cc0df9 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -45,7 +45,6 @@ class AppMenu extends Component { const { callTakerEnabled, fieldTripEnabled, - languageConfig, mailablesEnabled, resetAndToggleCallHistory, resetAndToggleFieldTrips, @@ -60,9 +59,6 @@ class AppMenu extends Component { id='app-menu' noCaret title={()}> - - {languageConfig.routeViewer || 'Route Viewer'} - {callTakerEnabled && Call History @@ -90,11 +86,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) } } diff --git a/lib/components/app/app.css b/lib/components/app/app.css index 1547bc4f6..de792c84c 100644 --- a/lib/components/app/app.css +++ b/lib/components/app/app.css @@ -51,3 +51,25 @@ margin-bottom: 30px; box-sizing: border-box; } + +/* View Switcher Styling */ +.view-switcher { + align-items: center; + display: flex; + justify-content: center; +} +.view-switcher button.btn-link { + color: rgba(255, 255, 255, 0.85); + border-radius: 15px; +} +.view-switcher button.btn-link.active { + background: rgba(255, 255, 255, 0.15); +} +.view-switcher button.btn-link:hover, +.view-switcher button.btn-link:focus { + text-decoration: none; +} + +.view-switcher button.btn-link:hover { + color: #fff; +} diff --git a/lib/components/app/desktop-nav.js b/lib/components/app/desktop-nav.js index 1914690b0..b92154d29 100644 --- a/lib/components/app/desktop-nav.js +++ b/lib/components/app/desktop-nav.js @@ -7,6 +7,7 @@ import { accountLinks, getAuth0Config } from '../../util/auth' import { DEFAULT_APP_TITLE } from '../../util/constants' import AppMenu from './app-menu' +import ViewSwitcher from './view-switcher' /** * The desktop navigation bar, featuring a `branding` logo or a `title` text @@ -53,6 +54,7 @@ const DesktopNav = ({ otpConfig }) => { + {showLogin && ( diff --git a/lib/components/app/view-switcher.js b/lib/components/app/view-switcher.js new file mode 100644 index 000000000..4dc2d067d --- /dev/null +++ b/lib/components/app/view-switcher.js @@ -0,0 +1,85 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {Button} from 'react-bootstrap' +import { withRouter } from 'react-router' +import { connect } from 'react-redux' + +import { MainPanelContent, setMainPanelContent } from '../../actions/ui' + +class ViewSwitcher extends Component { + static propTypes = { + activePanel: PropTypes.string, + setMainPanelContent: PropTypes.func, + sticky: PropTypes.bool + } + + _showRouteViewer = () => { + this.props.setMainPanelContent(MainPanelContent.ROUTE_VIEWER) + } + _showTripPlanner = () => { + this.props.setMainPanelContent(null) + } + + render () { + const { activePanel, language, sticky } = this.props + + return ( +
+ + + + +
+ ) + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const {language} = state.otp.config + const {mainPanelContent} = state.otp.ui + + // Reverse the ID to string mapping + let activePanel = Object.entries(MainPanelContent).find( + (keyValuePair) => keyValuePair[1] === mainPanelContent + ) + // activePanel is array of form [string, ID] + // The trip planner has id null + activePanel = (activePanel && activePanel[1]) || null + + return { + activePanel, + language + } +} + +const mapDispatchToProps = { + setMainPanelContent +} + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ViewSwitcher)) diff --git a/lib/components/mobile/batch-search-screen.js b/lib/components/mobile/batch-search-screen.js index e6582fa96..bc87a8fad 100644 --- a/lib/components/mobile/batch-search-screen.js +++ b/lib/components/mobile/batch-search-screen.js @@ -6,12 +6,11 @@ import BatchSettings from '../form/batch-settings' import DefaultMap from '../map/default-map' import LocationField from '../form/connected-location-field' import SwitchButton from '../form/switch-button' +import { MobileScreens, setMobileScreen } from '../../actions/ui' import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' -import { MobileScreens, setMobileScreen } from '../../actions/ui' - const { SET_DATETIME, SET_FROM_LOCATION, @@ -33,7 +32,7 @@ class BatchSearchScreen extends Component { render () { return ( - +
{headerText ?
{headerText}
- :
{defaultMobileTitle}
+ : }
diff --git a/lib/components/mobile/route-viewer.js b/lib/components/mobile/route-viewer.js index 97b25fe5b..13ab929b3 100644 --- a/lib/components/mobile/route-viewer.js +++ b/lib/components/mobile/route-viewer.js @@ -28,7 +28,6 @@ class MobileRouteViewer extends Component { return ( diff --git a/lib/components/mobile/search-screen.js b/lib/components/mobile/search-screen.js index 01ce77a55..898962987 100644 --- a/lib/components/mobile/search-screen.js +++ b/lib/components/mobile/search-screen.js @@ -43,7 +43,7 @@ class MobileSearchScreen extends Component { render () { return ( - +
Date: Thu, 19 Aug 2021 13:56:44 +0200 Subject: [PATCH 04/63] refactor: adjust component behavior to match new route viewer link --- lib/components/viewers/stop-viewer.js | 2 +- lib/components/viewers/viewer-container.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index fa540f208..772415115 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -52,7 +52,7 @@ class StopViewer extends Component { viewedStop: PropTypes.object } - _backClicked = () => this.props.setMainPanelContent(null) + _backClicked = () => window.history.back() _setLocationFromStop = (locationType) => { const { setLocation, stopData } = this.props diff --git a/lib/components/viewers/viewer-container.js b/lib/components/viewers/viewer-container.js index 97a292f00..0f7239aae 100644 --- a/lib/components/viewers/viewer-container.js +++ b/lib/components/viewers/viewer-container.js @@ -2,10 +2,11 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' +import { MainPanelContent } from '../../actions/ui' + import StopViewer from './stop-viewer' import TripViewer from './trip-viewer' import RouteViewer from './route-viewer' -import { MainPanelContent } from '../../actions/ui' class ViewerContainer extends Component { static propTypes = { @@ -18,7 +19,7 @@ class ViewerContainer extends Component { // check for main panel content if (uiState.mainPanelContent === MainPanelContent.ROUTE_VIEWER) { - return + return } // check for stop viewer From 1843b83262e6539d32ae9fbe95ed7d6d529f2466 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 19 Aug 2021 14:10:57 +0200 Subject: [PATCH 05/63] refactor(desktop-nav): allow hamburger menu to be clicked --- lib/components/app/desktop-nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/app/desktop-nav.js b/lib/components/app/desktop-nav.js index b92154d29..09ee99afb 100644 --- a/lib/components/app/desktop-nav.js +++ b/lib/components/app/desktop-nav.js @@ -43,7 +43,7 @@ const DesktopNav = ({ otpConfig }) => { return ( - + {/* TODO: Reconcile CSS class and inline style. */}
From bc7e27e4a6b7cb59060dc69f3e3169a3cbeece06 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 19 Aug 2021 16:46:40 +0200 Subject: [PATCH 06/63] feat(route-viewer): route details viewer prototype --- example.css | 2 +- lib/components/app/app.css | 5 ++ lib/components/viewers/route-details.js | 99 +++++++++++++++++++++++++ lib/components/viewers/route-viewer.js | 13 +--- 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 lib/components/viewers/route-details.js diff --git a/example.css b/example.css index 9c3d962d7..afd7900f5 100644 --- a/example.css +++ b/example.css @@ -38,7 +38,7 @@ .sidebar { height: 100%; - padding: 10px; + padding: 0; box-shadow: 3px 0px 12px #00000052; z-index: 1000; } diff --git a/lib/components/app/app.css b/lib/components/app/app.css index 1547bc4f6..4adad4e99 100644 --- a/lib/components/app/app.css +++ b/lib/components/app/app.css @@ -51,3 +51,8 @@ margin-bottom: 30px; box-sizing: border-box; } + +/* Batch routing panel requires top padding missing from sidebar */ +.batch-routing-panel { + padding-top: 10px; +} diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js new file mode 100644 index 000000000..5d46168ad --- /dev/null +++ b/lib/components/viewers/route-details.js @@ -0,0 +1,99 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { Button } from 'react-bootstrap' + +import Icon from '../narrative/icon' + +/** Converts text color (either black or white) to rgb */ +const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') + +const Wrapper = styled.div` + padding: 8px; +` +const LogoLinkContainer = styled.div` + display: flex; + padding-top: 10px; + justify-content: space-between; +` +const MoreDetailsLink = styled.a` + color: ${(props) => props.color}; + background-color: rgba(${(props) => textHexToRgb(props.color)},0.1); + padding: 5px; + border-radius: 5px; + transition: 0.1s all ease-in-out; + + &:hover { + background-color: rgba(255,255,255,0.8); + color: #333; + } +} +` +const PatternContainer = styled.div` + background-color: ${(props) => props.color}; + color: ${(props) => props.textColor}; + display: flex; + justify-content: flex-start; + align-items: baseline; + gap: 16px; + padding: 0 8px; + margin: 0; + filter: saturate(200%); + + h4 { + margin-bottom: 0px; + } + + button { + color: inherit; + border-bottom: 3px solid ${(props) => props.textColor}; + } + + button:hover { + color: ${(props) => props.color}; + background-color: ${(props) => props.textColor}; + text-decoration: none; + } +} +` + +class RouteDetails extends Component { + static propTypes = { + color: PropTypes.string, + contrastColor: PropTypes.string, + route: PropTypes.shape({ id: PropTypes.string }) + }; + render () { + const { className, route, routeColor, textColor } = this.props + const { agency, url } = route + return ( +
+ + This route runs every ?? minutes, ?? days of the week. + + {agency && Run by {agency.name}} + {url && ( + + + + More details + + + )} + + + +

Stops To

+ +
+
+ ) + } +} + +const StyledRouteDetails = styled(RouteDetails)` + background-color: ${(props) => props.routeColor}; + color: ${(props) => props.textColor}; +` + +export default StyledRouteDetails diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 66c74a7d8..092c9bda5 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -12,6 +12,8 @@ import { findRoutes, findRoute } from '../../actions/api' import { ComponentContext } from '../../util/contexts' import { getModeFromRoute } from '../../util/viewer' +import RouteDetails from './route-details' + /** * Determine the appropriate contrast color for text (white or black) based on * the input hex string (e.g., '#ff00ff') value. @@ -152,10 +154,6 @@ const RouteNameElement = styled(Label)` text-overflow: ellipsis; ` -const RouteDetails = styled.div` - padding: 8px; -` - class RouteRow extends PureComponent { static contextType = ComponentContext @@ -228,12 +226,7 @@ class RouteRow extends PureComponent { {isActive && ( - - {route.url - ? Route Details - : 'No route URL provided.' - } - + )} From 9258e4c05fab4c7fbadf18bca5cdd54b5f6d8e27 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 10:46:48 +0200 Subject: [PATCH 07/63] refactor: resolve react/redux errors --- lib/components/app/view-switcher.js | 2 +- lib/reducers/create-otp-reducer.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/components/app/view-switcher.js b/lib/components/app/view-switcher.js index 4dc2d067d..79adc4ff0 100644 --- a/lib/components/app/view-switcher.js +++ b/lib/components/app/view-switcher.js @@ -8,7 +8,7 @@ import { MainPanelContent, setMainPanelContent } from '../../actions/ui' class ViewSwitcher extends Component { static propTypes = { - activePanel: PropTypes.string, + activePanel: PropTypes.number, setMainPanelContent: PropTypes.func, sticky: PropTypes.bool } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index bc55af11a..024541ee7 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -898,6 +898,14 @@ function createOtpReducer (config) { }) } // Otherwise, overwrite only this route + if (!state.transitIndex.routes[action.payload.id]) { + return update(state, { + transitIndex: { + // If it is a new route, set rather than merge with an empty object + routes: { [action.payload.id]: { $set: action.payload } } + } + }) + } return update(state, { transitIndex: { routes: { [action.payload.id]: { $merge: action.payload } } From e886cfc75dde3dbec1844daa3cd88c51a0750e23 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 12:58:00 +0200 Subject: [PATCH 08/63] refactor(route-viewer): move filtering logic to redux --- lib/actions/ui.js | 25 ++++++++- lib/components/viewers/route-viewer.js | 72 +++++++++++--------------- lib/reducers/create-otp-reducer.js | 19 ++++++- lib/util/state.js | 53 +++++++++++++++++++ 4 files changed, 124 insertions(+), 45 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index dee3c40c2..b16cb6cc9 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -3,7 +3,7 @@ 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 { findRoute, setUrlSearch } from './api' @@ -337,3 +337,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 })) + } + } +} diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index b14984b1f..6d1fa2db1 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -7,8 +7,9 @@ import { connect } from 'react-redux' import styled from 'styled-components' import Icon from '../narrative/icon' -import { setMainPanelContent, setViewedRoute } from '../../actions/ui' +import { setMainPanelContent, setViewedRoute, setRouteViewerFilter } from '../../actions/ui' import { findRoutes, findRoute } from '../../actions/api' +import { getFilteredRoutes, getModesForActiveAgencyFilter, getAgenciesFromRoutes } from '../../util/state' import { ComponentContext } from '../../util/contexts' import { getModeFromRoute } from '../../util/viewer' @@ -31,13 +32,13 @@ function getContrastYIQ (hexcolor) { class RouteViewer extends Component { static propTypes = { + agencies: PropTypes.array, hideBackButton: PropTypes.bool, - routes: PropTypes.object + modes: PropTypes.array, + routes: PropTypes.array }; state = { - agency: null, - mode: null, search: '' }; @@ -52,12 +53,15 @@ class RouteViewer extends Component { * route object */ onFilterChange = (event) => { - const { target } = event + const { eventPhase, target } = event + // If the dropdown changes without user interaction, don't update! + // see https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase + if (eventPhase !== Event.BUBBLING_PHASE) { + return + } const { id, value } = target // id will be either 'agency' or 'mode' based on the dropdown used - this.setState({ - [id]: value - }) + this.props.setRouteViewerFilter({[id]: value}) }; /** @@ -66,53 +70,29 @@ class RouteViewer extends Component { onSearchChange = (event) => { const { target } = event const { value } = target + // Search field state is duplicated to ensure there is no + // input lag while the redux store updates this.setState({ search: value }) + this.props.setRouteViewerFilter({ search: value }) }; render () { const { + agencies, findRoute, hideBackButton, languageConfig, + modes, routes, setViewedRoute, transitOperators, viewedRoute } = this.props - const { agency, mode, search } = this.state - const routesArray = routes ? Object.values(routes) : [] + const { search } = this.state - const filteredAndSortedRoutes = routesArray - .filter( - (route) => - // If the filter isn't defined, don't check. - (!agency || agency === route.agencyName) && - (!mode || mode === route.mode) && - // If user search is active, filter here - (!search || - ((route.longName && route.longName.includes(search)) || - (route.shortName && route.shortName.includes(search)))) - ) + const sortedRoutes = routes .sort(coreUtils.route.makeRouteComparator(transitOperators)) - // get agency and mode lists from route list - const agencies = Array.from( - new Set(routesArray.map((route) => route.agencyName || route.agency.name)) - ) - .filter((agency) => agency !== undefined) - .sort() - - const modes = Array.from( - new Set( - routesArray - .filter((route) => route.mode !== undefined) - .map((route) => route.mode.toLowerCase()) - ) - ) - // Correct capitalization - .map((mode) => mode[0].toUpperCase() + mode.substr(1).toLowerCase()) - .sort() - return (
{/* Header Block */} @@ -145,6 +125,8 @@ class RouteViewer extends Component { ))} + {/* Do not show a mode selector that can't change anything */} + {modes.length > 2 && + } @@ -171,7 +153,7 @@ class RouteViewer extends Component {
- {filteredAndSortedRoutes.map((route) => { + {sortedRoutes.map((route) => { // Find operator based on agency_id (extracted from OTP route ID). const operator = coreUtils.route.getTransitOperatorFromOtpRoute( @@ -189,7 +171,8 @@ class RouteViewer extends Component { /> ) })} - {(agency || mode || search) && filteredAndSortedRoutes.length === 0 && ( + { /* check modes length to differentiate between loading and over-filtered */ } + {modes.length > 0 && sortedRoutes.length === 0 && ( No routes match your filter! @@ -345,8 +328,10 @@ class RouteRow extends PureComponent { const mapStateToProps = (state, ownProps) => { return { + agencies: getAgenciesFromRoutes(state), languageConfig: state.otp.config.language, - routes: state.otp.transitIndex.routes, + modes: getModesForActiveAgencyFilter(state), + routes: getFilteredRoutes(state), transitOperators: state.otp.config.transitOperators, viewedRoute: state.otp.ui.viewedRoute } @@ -356,6 +341,7 @@ const mapDispatchToProps = { findRoute, findRoutes, setMainPanelContent, + setRouteViewerFilter, setViewedRoute } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 024541ee7..5014ed979 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -222,6 +222,7 @@ export function getInitialState (userDefinedConfig) { rideEstimates: {} }, transitIndex: { + routes: {}, stops: {}, trips: {} }, @@ -230,7 +231,14 @@ export function getInitialState (userDefinedConfig) { locale: null, localizedMessages: {}, mobileScreen: MobileScreens.WELCOME_SCREEN, - printView: window.location.href.indexOf('/print/') !== -1 + printView: window.location.href.indexOf('/print/') !== -1, + routeViewer: { + filter: { + agency: null, + mode: null, + search: '' + } + } }, user: { autoRefreshStopTimes, @@ -1030,6 +1038,15 @@ function createOtpReducer (config) { locale: { $set: action.payload.locale }, localizedMessages: { $set: action.payload.messages } }}) + + case 'UPDATE_ROUTE_VIEWER_FILTER': + return update(state, { + ui: { + routeViewer: { + filter: { $merge: action.payload } + } + } + }) default: return state } diff --git a/lib/util/state.js b/lib/util/state.js index 0e1f1fa3b..e2ed5e434 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -462,6 +462,59 @@ function getItineraryToRender (state) { return itins[visibleItineraryIndex] || activeItinerary } +const routeSelector = state => Object.values(state.otp.transitIndex.routes) +const routeViewerFilterSelector = state => state.otp.ui.routeViewer.filter + +/** + * Returns all routes that match the route viewer filters + */ +export const getFilteredRoutes = createSelector( + routeSelector, + routeViewerFilterSelector, + (routes, filter) => + routes.filter( + (route) => + // If the filter isn't defined, don't check. + (!filter.agency || filter.agency === route.agencyName) && + (!filter.mode || filter.mode === route.mode) && + // If user search is active, filter by either the long or short name + (!filter.search || + ((route.longName && route.longName.toLowerCase().includes(filter.search.toLowerCase())) || + (route.shortName && route.shortName.toLowerCase().includes(filter.search.toLowerCase())))) + ) +) + +/** + * Get the modes available for the current agency filter. First filters only by agency, + * then extracts and correctly capitalizes modes + */ +export const getModesForActiveAgencyFilter = createSelector( + routeSelector, + routeViewerFilterSelector, + (routes, filter) => Array.from( + new Set( + routes + .filter(route => (route.mode && (!filter.agency || filter.agency === route.agencyName))) + // Capitalize the mode name + .map((route) => route.mode[0].toUpperCase() + route.mode.substr(1).toLowerCase()) + ) + ) + .sort() + +) + +/** + * Returns list of agencies present within all routes + */ +export const getAgenciesFromRoutes = createSelector( + routeSelector, + (routes) => Array.from( + new Set(routes.map((route) => route.agencyName || route.agency.name)) + ) + .filter((agency) => agency !== undefined) + .sort() +) + /** * Converts an OTP itinerary to the transitive.js format, * using a selector to prevent unnecessary re-renderings of the transitive overlay. From ba5c1d60545c65b3be40171f2c7f13dd8f90cbd1 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 13:08:58 +0200 Subject: [PATCH 09/63] test: update snapshots --- .../reducers/__snapshots__/create-otp-reducer.js.snap | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 9cad075ba..9df6d1faf 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -93,6 +93,7 @@ Object { "rideEstimates": Object {}, }, "transitIndex": Object { + "routes": Object {}, "stops": Object {}, "trips": Object {}, }, @@ -102,6 +103,13 @@ Object { "localizedMessages": Object {}, "mobileScreen": 1, "printView": false, + "routeViewer": Object { + "filter": Object { + "agency": null, + "mode": null, + "search": "", + }, + }, }, "useRealtime": true, "user": Object { From f638c7f23bedaa87c1472472090145f129563cca Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 14:08:33 +0200 Subject: [PATCH 10/63] refactor(route-viewer): css and a11y adjustments --- lib/components/viewers/route-viewer.js | 8 +++++++- lib/components/viewers/viewers.css | 24 +++++++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 6d1fa2db1..c2aa77563 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -79,6 +79,7 @@ class RouteViewer extends Component { render () { const { agencies, + filter, findRoute, hideBackButton, languageConfig, @@ -116,9 +117,11 @@ class RouteViewer extends Component { {modes.map((mode) => ( @@ -294,7 +299,7 @@ class RouteRow extends PureComponent { )} - + { return { agencies: getAgenciesFromRoutes(state), + filter: state.otp.ui.routeViewer.filter, languageConfig: state.otp.config.language, modes: getModesForActiveAgencyFilter(state), routes: getFilteredRoutes(state), diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 3ca8d82cf..5d1b1701b 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -277,14 +277,25 @@ gap: 10px; } .search-and-filter select { - max-width: 125px; margin: 0 5px; + text-overflow: ellipsis; + min-width: 105px; + + border: none; + background: #eee; + border-radius: 5px; + padding: 5px; +} +.search-and-filter select option { + /* This allows the dropdowns to shrink and stretch */ + max-width: 0; } .search-and-filter .routeFilter { - display: flex; + display: grid; align-items: center; - justify-content: center; + grid-template-columns: 0fr 2fr 1fr; + width: 100%; } .search-and-filter .routeSearch { display: flex; @@ -296,21 +307,20 @@ padding: 0.125em 0.5em; border-radius: 5px; margin-left: 5px; - width: 145px; } .routeSearch input::-webkit-search-cancel-button { /* show clear button on webkit browsers */ -webkit-appearance: searchfield-cancel-button; } -.mobile .routeSearch, -.mobile .routeSearch input { +.routeSearch, +.routeSearch input { width: 100%; } .route-viewer-body .noRoutesFoundMessage { display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; padding-top: 10px; } From b0b505e5c798b552775785e640c35b01516374b2 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 14:09:56 +0200 Subject: [PATCH 11/63] chore: make css cross-browser compatible --- lib/components/viewers/viewers.css | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 5d1b1701b..e3ad9e1c0 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -281,6 +281,7 @@ text-overflow: ellipsis; min-width: 105px; + -webkit-appearance: none; border: none; background: #eee; border-radius: 5px; From 95b844cd352ec38ad85b5ef28bc345a72bfa6018 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 14:19:34 +0200 Subject: [PATCH 12/63] revert: adjust webkit specific rules to favor chrome over safari --- lib/components/viewers/viewers.css | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index e3ad9e1c0..5d1b1701b 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -281,7 +281,6 @@ text-overflow: ellipsis; min-width: 105px; - -webkit-appearance: none; border: none; background: #eee; border-radius: 5px; From ae4bf350860169e3e74de8c5596047fdb3add159 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 15:07:10 +0200 Subject: [PATCH 13/63] refactor(route-viewer): internationalize route-viewer component --- i18n/en-US.yml | 11 +++- lib/components/viewers/route-viewer.js | 82 ++++++++++++++++---------- lib/components/viewers/viewers.css | 2 +- lib/util/state.js | 3 +- 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index f8a220fad..8cddf64fa 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -38,10 +38,19 @@ components: tripDurationFormatZeroHours: "{minutes, number} min" # TODO: Distinguish between one hour (singular) and 2 hours or more? tripDurationFormat: "{hours, number} hr {minutes, number} min" + RouteViewer: + allAgencies: All Agencies + allModes: All Modes + findARoute: Find A Route + noFilteredRoutesFound: No routes match your filter! + noRouteUrl: No route URL provided. # 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 @@ -49,7 +58,7 @@ common: drive: Drive micromobility: E-Scooter micromobilityRent: Rental E-Scooter - walk: Walk + walk: Walk # OTP transit modes # Note that identifiers are OTP modes converted to lowercase. diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index c2aa77563..93d5366b4 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -5,6 +5,7 @@ import { Label, Button } from 'react-bootstrap' import { VelocityTransitionGroup } from 'velocity-react' import { connect } from 'react-redux' import styled from 'styled-components' +import { FormattedMessage, injectIntl } from 'react-intl' import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute, setRouteViewerFilter } from '../../actions/ui' @@ -82,6 +83,7 @@ class RouteViewer extends Component { filter, findRoute, hideBackButton, + intl, languageConfig, modes, routes, @@ -91,8 +93,9 @@ class RouteViewer extends Component { } = this.props const { search } = this.state - const sortedRoutes = routes - .sort(coreUtils.route.makeRouteComparator(transitOperators)) + const sortedRoutes = routes.sort( + coreUtils.route.makeRouteComparator(transitOperators) + ) return (
@@ -103,7 +106,7 @@ class RouteViewer extends Component {
)} @@ -121,35 +124,50 @@ class RouteViewer extends Component { id='agency' onBlur={this.onFilterChange} onChange={this.onFilterChange} - value={filter.agency} + value={filter.agency || ''} > - + {agencies.map((agency) => ( - + ))} {/* Do not show a mode selector that can't change anything */} - {modes.length > 2 && - + - ))} - } + {modes.map((mode) => ( + + ))} + + )} @@ -161,14 +179,16 @@ class RouteViewer extends Component { {sortedRoutes.map((route) => { // Find operator based on agency_id (extracted from OTP route ID). const operator = - coreUtils.route.getTransitOperatorFromOtpRoute( - route, - transitOperators - ) || {} + coreUtils.route.getTransitOperatorFromOtpRoute( + route, + transitOperators + ) || {} return ( ) })} - { /* check modes length to differentiate between loading and over-filtered */ } + {/* check modes length to differentiate between loading and over-filtered */} {modes.length > 0 && sortedRoutes.length === 0 && ( - No routes match your filter! + )}
@@ -320,7 +342,7 @@ class RouteRow extends PureComponent { Route Details ) : ( - 'No route URL provided.' + )} )} @@ -351,4 +373,4 @@ const mapDispatchToProps = { setViewedRoute } -export default connect(mapStateToProps, mapDispatchToProps)(RouteViewer) +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(RouteViewer)) diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 5d1b1701b..034293a75 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -321,6 +321,6 @@ .route-viewer-body .noRoutesFoundMessage { display: flex; align-items: center; - justify-content: flex-start; + justify-content: center; padding-top: 10px; } diff --git a/lib/util/state.js b/lib/util/state.js index e2ed5e434..22cccbdde 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -496,7 +496,8 @@ export const getModesForActiveAgencyFilter = createSelector( routes .filter(route => (route.mode && (!filter.agency || filter.agency === route.agencyName))) // Capitalize the mode name - .map((route) => route.mode[0].toUpperCase() + route.mode.substr(1).toLowerCase()) + .map((route) => route.mode) + .filter((mode) => mode !== undefined) ) ) .sort() From b046f097e7325e2a6e5d363bef7c696c4d69fca4 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 20 Aug 2021 15:19:21 +0200 Subject: [PATCH 14/63] refactor: fully remove languageConfig among new code --- i18n/en-US.yml | 6 +++++- lib/components/app/view-switcher.js | 11 +++++------ lib/components/viewers/route-viewer.js | 6 ++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 8cddf64fa..3f99e95f8 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -22,10 +22,11 @@ _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 for the departure-arrival string @@ -44,6 +45,9 @@ components: findARoute: Find A Route noFilteredRoutesFound: No routes match your filter! noRouteUrl: No route URL provided. + title: Route Viewer + shortTitle: Routes + details: " " # Nothing is default # Common messages that appear in multiple components and modules # are grouped below by topic. diff --git a/lib/components/app/view-switcher.js b/lib/components/app/view-switcher.js index 79adc4ff0..780f398fd 100644 --- a/lib/components/app/view-switcher.js +++ b/lib/components/app/view-switcher.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import {Button} from 'react-bootstrap' import { withRouter } from 'react-router' import { connect } from 'react-redux' +import { FormattedMessage } from 'react-intl' import { MainPanelContent, setMainPanelContent } from '../../actions/ui' @@ -21,7 +22,7 @@ class ViewSwitcher extends Component { } render () { - const { activePanel, language, sticky } = this.props + const { activePanel, sticky } = this.props return (
- Plan Trip +
@@ -61,7 +62,6 @@ class ViewSwitcher extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - const {language} = state.otp.config const {mainPanelContent} = state.otp.ui // Reverse the ID to string mapping @@ -73,8 +73,7 @@ const mapStateToProps = (state, ownProps) => { activePanel = (activePanel && activePanel[1]) || null return { - activePanel, - language + activePanel } } diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 93d5366b4..c5136264b 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -84,7 +84,6 @@ class RouteViewer extends Component { findRoute, hideBackButton, intl, - languageConfig, modes, routes, setViewedRoute, @@ -113,9 +112,9 @@ class RouteViewer extends Component { {/* Header Text */}
- {languageConfig.routeViewer || 'Route Viewer'} +
-
{languageConfig.routeViewerDetails}
+
@@ -357,7 +356,6 @@ const mapStateToProps = (state, ownProps) => { return { agencies: getAgenciesFromRoutes(state), filter: state.otp.ui.routeViewer.filter, - languageConfig: state.otp.config.language, modes: getModesForActiveAgencyFilter(state), routes: getFilteredRoutes(state), transitOperators: state.otp.config.transitOperators, From 428ff8bd42e0757e6967d56f15b19efe0d8f63b3 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 23 Aug 2021 18:13:53 +0100 Subject: [PATCH 15/63] refactor(route-viewer): replace superflous react state with redux state --- lib/components/viewers/route-viewer.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index c5136264b..a63394b34 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -39,10 +39,6 @@ class RouteViewer extends Component { routes: PropTypes.array }; - state = { - search: '' - }; - _backClicked = () => this.props.setMainPanelContent(null); componentDidMount () { @@ -71,9 +67,6 @@ class RouteViewer extends Component { onSearchChange = (event) => { const { target } = event const { value } = target - // Search field state is duplicated to ensure there is no - // input lag while the redux store updates - this.setState({ search: value }) this.props.setRouteViewerFilter({ search: value }) }; @@ -90,7 +83,7 @@ class RouteViewer extends Component { transitOperators, viewedRoute } = this.props - const { search } = this.state + const { search } = filter const sortedRoutes = routes.sort( coreUtils.route.makeRouteComparator(transitOperators) From 67f840b677862a27ef75f939f02aaa45b729967f Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 23 Aug 2021 19:44:18 +0100 Subject: [PATCH 16/63] revert(route-viewer): never hide mode selector --- lib/components/viewers/route-viewer.js | 37 ++++++++++++-------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index d263880a2..ae1a07076 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -129,29 +129,26 @@ class RouteViewer extends Component { ))} - {/* Do not show a mode selector that can't change anything */} - {modes.length > 2 && ( - + + {modes.map((mode) => ( + - {modes.map((mode) => ( - - ))} - - )} + ))} + From 65183557b4eafa05df2bd5e9b81067aab918ba24 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 24 Aug 2021 10:54:30 +0100 Subject: [PATCH 17/63] refactor(route-viewer): fix localization id --- lib/components/viewers/route-viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index ae1a07076..f891a444f 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -331,7 +331,7 @@ class RouteRow extends PureComponent { Route Details ) : ( - + )} )} From ec7e88565eba513e9799ecc01296f122ac9a937f Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 24 Aug 2021 10:54:49 +0100 Subject: [PATCH 18/63] refactor(route-viewer): move route sorting to redux --- lib/components/viewers/route-viewer.js | 10 +++------- lib/util/state.js | 11 +++++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index f891a444f..7581501cd 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -10,7 +10,7 @@ import { FormattedMessage, injectIntl } from 'react-intl' import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute, setRouteViewerFilter } from '../../actions/ui' import { findRoutes, findRoute } from '../../actions/api' -import { getFilteredRoutes, getModesForActiveAgencyFilter, getAgenciesFromRoutes } from '../../util/state' +import { getSortedFilteredRoutes, getModesForActiveAgencyFilter, getAgenciesFromRoutes } from '../../util/state' import { ComponentContext } from '../../util/contexts' import { getModeFromRoute } from '../../util/viewer' @@ -78,17 +78,13 @@ class RouteViewer extends Component { hideBackButton, intl, modes, - routes, + routes: sortedRoutes, setViewedRoute, transitOperators, viewedRoute } = this.props const { search } = filter - const sortedRoutes = routes.sort( - coreUtils.route.makeRouteComparator(transitOperators) - ) - return (
{/* Header Block */} @@ -347,7 +343,7 @@ const mapStateToProps = (state, ownProps) => { agencies: getAgenciesFromRoutes(state), filter: state.otp.ui.routeViewer.filter, modes: getModesForActiveAgencyFilter(state), - routes: getFilteredRoutes(state), + routes: getSortedFilteredRoutes(state), transitOperators: state.otp.config.transitOperators, viewedRoute: state.otp.ui.viewedRoute } diff --git a/lib/util/state.js b/lib/util/state.js index 22cccbdde..c0bb475f3 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -484,6 +484,17 @@ export const getFilteredRoutes = createSelector( ) ) +/** + * Sorts routes filtered by the selector which filters routes + */ +export const getSortedFilteredRoutes = createSelector( + getFilteredRoutes, + state => state.otp.config.transitOperators, + (routes, transitOperators) => routes.sort( + coreUtils.route.makeRouteComparator(transitOperators) + ) +) + /** * Get the modes available for the current agency filter. First filters only by agency, * then extracts and correctly capitalizes modes From 3dc9b2724e11449944af5f491b97eb461110c728 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 24 Aug 2021 11:24:31 +0100 Subject: [PATCH 19/63] improvement: have both nav bar buttons be verbs --- i18n/en-US.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 3f99e95f8..efd84a31a 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -46,7 +46,7 @@ components: noFilteredRoutesFound: No routes match your filter! noRouteUrl: No route URL provided. title: Route Viewer - shortTitle: Routes + shortTitle: View Routes details: " " # Nothing is default # Common messages that appear in multiple components and modules From 1c0655b5d871a036173fd7d626e859a0a135d84c Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 24 Aug 2021 12:32:50 +0100 Subject: [PATCH 20/63] improvement(route-viewer): improve color styling using tinycolor --- lib/components/viewers/route-details.js | 34 +++++++++++++++++------ lib/components/viewers/route-viewer.js | 37 ++++++++++++++++++------- package.json | 1 + yarn.lock | 5 ++++ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 5d46168ad..b77dfb567 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { Button } from 'react-bootstrap' +import tinycolor from 'tinycolor2' import Icon from '../narrative/icon' @@ -10,16 +11,18 @@ const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') const Wrapper = styled.div` padding: 8px; + color: ${props => props.textColor} ` const LogoLinkContainer = styled.div` display: flex; - padding-top: 10px; + align-items: center; justify-content: space-between; ` const MoreDetailsLink = styled.a` color: ${(props) => props.color}; background-color: rgba(${(props) => textHexToRgb(props.color)},0.1); padding: 5px; + display: block; border-radius: 5px; transition: 0.1s all ease-in-out; @@ -30,7 +33,10 @@ const MoreDetailsLink = styled.a` } ` const PatternContainer = styled.div` - background-color: ${(props) => props.color}; + background-color: ${(props) => + tinycolor(props.color).isDark() + ? tinycolor(props.color).lighten(5).toHexString() + : tinycolor(props.color).darken(5).toHexString()}; color: ${(props) => props.textColor}; display: flex; justify-content: flex-start; @@ -38,7 +44,6 @@ const PatternContainer = styled.div` gap: 16px; padding: 0 8px; margin: 0; - filter: saturate(200%); h4 { margin-bottom: 0px; @@ -49,10 +54,12 @@ const PatternContainer = styled.div` border-bottom: 3px solid ${(props) => props.textColor}; } - button:hover { + button:hover, button:focus, button:visited { + text-decoration: none; + } + button:hover, button:focus { color: ${(props) => props.color}; background-color: ${(props) => props.textColor}; - text-decoration: none; } } ` @@ -64,12 +71,23 @@ class RouteDetails extends Component { route: PropTypes.shape({ id: PropTypes.string }) }; render () { - const { className, route, routeColor, textColor } = this.props + const { className, route, routeColor } = this.props const { agency, url } = route + + const textColorOptions = [ + tinycolor(routeColor).lighten(80).toHexString(), + tinycolor(routeColor).darken(80).toHexString() + ] + + const textColor = tinycolor + .mostReadable(routeColor, textColorOptions, { + includeFallbackColors: true, + level: 'AAA' + }) + .toHexString() return (
- - This route runs every ?? minutes, ?? days of the week. + {agency && Run by {agency.name}} {url && ( diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 092c9bda5..d4ffaa226 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -5,6 +5,7 @@ import { Label, Button } from 'react-bootstrap' import { VelocityTransitionGroup } from 'velocity-react' import { connect } from 'react-redux' import styled from 'styled-components' +import tinycolor from 'tinycolor2' import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute } from '../../actions/ui' @@ -23,12 +24,11 @@ import RouteDetails from './route-details' * TODO: Move to @opentripplanner/core-utils once otp-rr uses otp-ui library. */ function getContrastYIQ (hexcolor) { - hexcolor = hexcolor.replace('#', '') - const r = parseInt(hexcolor.substr(0, 2), 16) - const g = parseInt(hexcolor.substr(2, 2), 16) - const b = parseInt(hexcolor.substr(4, 2), 16) - const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000 - return (yiq >= 128) ? '000000' : 'ffffff' + return tinycolor + .mostReadable(hexcolor, ['#fff', '#000'], { level: 'AAA' }) + .toHexString() + // Have to do this to remain compatible with former version of this function + .split('#')[1] } class RouteViewer extends Component { @@ -109,7 +109,7 @@ class RouteViewer extends Component { } const StyledRouteRow = styled.div` - background-color: ${props => props.isActive ? '#f6f8fa' : 'white'}; + background-color: ${props => props.isActive ? props.routeColor : 'white'}; border-bottom: 1px solid gray; ` @@ -118,6 +118,21 @@ const RouteRowButton = styled(Button)` display: flex; padding: 6px; width: 100%; + transition: all ease-in-out 0.1s; + + &:hover { + background-color: ${(props) => + !props.isActive && tinycolor(props.routeColor) + .brighten(90) + .toHexString()}; + border-radius: 0; + } + + &:active:focus, + &:active:hover { + background-color: ${(props) => props.routeColor}; + border-radius: 0; + } ` const RouteRowElement = styled.span` @@ -200,10 +215,12 @@ class RouteRow extends PureComponent { // Default long name is empty string (long name is an optional GTFS value). const longName = this.getCleanRouteLongName({ longNameSplitter, route }) return ( - + {operator && operator.logo && @@ -214,7 +231,7 @@ class RouteRow extends PureComponent { } - + {isActive && ( - + )} diff --git a/package.json b/package.json index 87479045c..f87b53e12 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "reselect": "^4.0.0", "seamless-immutable": "^7.1.3", "styled-components": "^5.0.0", + "tinycolor2": "^1.4.2", "transitive-js": "^0.13.7", "velocity-react": "^1.3.3", "yup": "^0.29.3" diff --git a/yarn.lock b/yarn.lock index ba47abdfe..011bfc52c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17195,6 +17195,11 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinycolor2@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" + integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" From 30117515dee26eef7f1519ae1fb58e7bbf2fa8ac Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 24 Aug 2021 18:52:11 +0100 Subject: [PATCH 21/63] improvement(route-viewer): route details functionality + fill full sidebar --- lib/components/viewers/route-details.js | 91 +++++++++++++++++++----- lib/components/viewers/route-viewer.js | 91 ++++++++++++------------ lib/util/viewer.js | 94 +++++++++++++++++++++++-- 3 files changed, 208 insertions(+), 68 deletions(-) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index b77dfb567..b5c0fb1d3 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -3,8 +3,11 @@ import PropTypes from 'prop-types' import styled from 'styled-components' import { Button } from 'react-bootstrap' import tinycolor from 'tinycolor2' +import { connect } from 'react-redux' import Icon from '../narrative/icon' +import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' +import { setViewedRoute } from '../../actions/ui' /** Converts text color (either black or white) to rgb */ const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') @@ -45,8 +48,11 @@ const PatternContainer = styled.div` padding: 0 8px; margin: 0; + overflow-x: scroll; + h4 { margin-bottom: 0px; + white-space: nowrap; } button { @@ -57,26 +63,62 @@ const PatternContainer = styled.div` button:hover, button:focus, button:visited { text-decoration: none; } - button:hover, button:focus { + button:hover, button:focus, button.active { color: ${(props) => props.color}; background-color: ${(props) => props.textColor}; } } ` +const StopContainer = styled.div` + color: pink; + background-color: #fff; +` + class RouteDetails extends Component { static propTypes = { - color: PropTypes.string, - contrastColor: PropTypes.string, - route: PropTypes.shape({ id: PropTypes.string }) + // TODO: proptypes + pattern: PropTypes.shape({id: PropTypes.string}), + route: PropTypes.shape({ id: PropTypes.string }), + setViewedRoute: setViewedRoute.type }; + + _headSignButtonClicked = (id) => { + const { route, setViewedRoute } = this.props + setViewedRoute({ patternId: id, routeId: route.id }) + } + render () { - const { className, route, routeColor } = this.props - const { agency, url } = route + const { className, operator, pattern, route } = this.props + const { agency, patterns, url } = route + + const { backgroundColor: routeColor } = getColorAndNameFromRoute( + operator, + route + ) + + const headsigns = + patterns && + Object.entries(patterns) + .map((pattern) => { + return { + headsign: extractHeadsignFromPattern(pattern[1]), + id: pattern[0] + } + }) + // Remove duplicate headsigns. Using a reducer means that the first pattern + // with a specific headsign is the accepted one. TODO: is this good behavior? + .reduce((prev, cur) => { + const amended = prev + if (!prev.find(h => h.headsign === cur.headsign)) { + amended.push(cur) + } + return amended + }, []) const textColorOptions = [ - tinycolor(routeColor).lighten(80).toHexString(), - tinycolor(routeColor).darken(80).toHexString() + tinycolor(routeColor).darken(80).toHexString(), + tinycolor(routeColor).lighten(80).toHexString() ] const textColor = tinycolor @@ -86,9 +128,9 @@ class RouteDetails extends Component { }) .toHexString() return ( -
+
- + {agency && Run by {agency.name}} {url && ( @@ -102,16 +144,33 @@ class RouteDetails extends Component {

Stops To

- + {headsigns && + headsigns.map((h) => ( + + ))}
+ {pattern && stops}
) } } -const StyledRouteDetails = styled(RouteDetails)` - background-color: ${(props) => props.routeColor}; - color: ${(props) => props.textColor}; -` +// connect to redux store +const mapStateToProps = (state, ownProps) => { + return { + viewedRoute: state.otp.ui.viewedRoute + } +} + +const mapDispatchToProps = { + setViewedRoute +} -export default StyledRouteDetails +export default connect(mapStateToProps, mapDispatchToProps)(RouteDetails) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index d4ffaa226..c487dc259 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -11,26 +11,10 @@ import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute } from '../../actions/ui' import { findRoutes, findRoute } from '../../actions/api' import { ComponentContext } from '../../util/contexts' -import { getModeFromRoute } from '../../util/viewer' +import { getColorAndNameFromRoute, getModeFromRoute } from '../../util/viewer' import RouteDetails from './route-details' -/** - * Determine the appropriate contrast color for text (white or black) based on - * the input hex string (e.g., '#ff00ff') value. - * - * From https://stackoverflow.com/a/11868398/915811 - * - * TODO: Move to @opentripplanner/core-utils once otp-rr uses otp-ui library. - */ -function getContrastYIQ (hexcolor) { - return tinycolor - .mostReadable(hexcolor, ['#fff', '#000'], { level: 'AAA' }) - .toHexString() - // Have to do this to remain compatible with former version of this function - .split('#')[1] -} - class RouteViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, @@ -58,6 +42,42 @@ class RouteViewer extends Component { coreUtils.route.makeRouteComparator(transitOperators) ) : [] + + // If patternId is present, we're looking at a specific pattern's stops + if (viewedRoute?.patternId) { + const {patternId, routeId} = viewedRoute + const route = routes[routeId] + // Find operator based on agency_id (extracted from OTP route ID). + const operator = coreUtils.route.getTransitOperatorFromOtpRoute( + route, + transitOperators + ) || {} + const { backgroundColor, color } = getColorAndNameFromRoute( + operator, + route + ) + + return ( +
+ {/* Header Block */} +
+ {/* Always show back button, as we don't write a route anymore */} +
+ +
+ +
+ {route.shortName} +
+
+ +
+ ) + } + return (
{/* Header Block */} @@ -123,7 +143,7 @@ const RouteRowButton = styled(Button)` &:hover { background-color: ${(props) => !props.isActive && tinycolor(props.routeColor) - .brighten(90) + .lighten(50) .toHexString()}; border-radius: 0; } @@ -176,7 +196,7 @@ class RouteRow extends PureComponent { const { findRoute, isActive, route, setViewedRoute } = this.props if (isActive) { // Deselect current route if active. - setViewedRoute({ routeId: null }) + setViewedRoute({ patternId: null, routeId: null }) } else { // Otherwise, set active and fetch route patterns. findRoute({ routeId: route.id }) @@ -184,36 +204,15 @@ class RouteRow extends PureComponent { } } - getCleanRouteLongName ({ longNameSplitter, route }) { - let longName = '' - if (route.longName) { - // Attempt to split route name if splitter is defined for operator (to - // remove short name value from start of long name value). - const nameParts = route.longName.split(longNameSplitter) - longName = (longNameSplitter && nameParts.length > 1) - ? nameParts[1] - : route.longName - // If long name and short name are identical, set long name to be an empty - // string. - if (longName === route.shortName) longName = '' - } - return longName - } - render () { const { isActive, operator, route } = this.props const { ModeIcon } = this.context - const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {} - const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` - // NOTE: text color is not a part of short response route object, so there - // is no way to determine from OTP what the text color should be if the - // background color is, say, black. Instead, determine the appropriate - // contrast color and use that if no text color is available. - const contrastColor = getContrastYIQ(backgroundColor) - const color = `#${defaultRouteTextColor || route.textColor || contrastColor}` - // Default long name is empty string (long name is an optional GTFS value). - const longName = this.getCleanRouteLongName({ longNameSplitter, route }) + const { backgroundColor, color, longName } = getColorAndNameFromRoute( + operator, + route + ) + return ( {isActive && ( - + )} diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 1f52ab41d..8e5432eb8 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -1,3 +1,5 @@ +import tinycolor from 'tinycolor2' + import { isBlank } from './ui' /** @@ -47,6 +49,38 @@ export function routeIsValid (route, routeId) { return true } +/** + * Run heuristic on pattern description to extract headsign from pattern description + * @param {*} pattern pattern to extract headsign out of + * @returns headsign of pattern + */ +export function extractHeadsignFromPattern (pattern) { + let headsign = pattern.headsign + // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute + // (format: '49 to ()[ from ( ()[ from ( 1) + ? nameParts[1] + : route.longName + // If long name and short name are identical, set long name to be an empty + // string. + if (longName === route.shortName) longName = '' + } + return longName +} +/** + * Using an operator and route, apply heuristics to determine color and contrast color + * as well as a full route name + */ +export function getColorAndNameFromRoute (operator, route) { + const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {} + const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` + // NOTE: text color is not a part of short response route object, so there + // is no way to determine from OTP what the text color should be if the + // background color is, say, black. Instead, determine the appropriate + // contrast color and use that if no text color is available. + const contrastColor = getContrastYIQ(backgroundColor) + const color = `#${defaultRouteTextColor || route.textColor || contrastColor}` + // Default long name is empty string (long name is an optional GTFS value). + const longName = getCleanRouteLongName({ longNameSplitter, route }) + return { backgroundColor, color, longName } +} From 2b85bbc484d641323dcc885541d14ac5b7e03285 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 25 Aug 2021 10:33:57 +0100 Subject: [PATCH 22/63] revert(app-menu): show route viewer in app menu on mobile --- lib/components/app/app-menu.js | 3 +++ lib/components/app/app.css | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index ac6cc0df9..da473c88f 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -59,6 +59,9 @@ class AppMenu extends Component { id='app-menu' noCaret title={()}> + + Route Viewer + {callTakerEnabled && Call History diff --git a/lib/components/app/app.css b/lib/components/app/app.css index de792c84c..5791153aa 100644 --- a/lib/components/app/app.css +++ b/lib/components/app/app.css @@ -31,6 +31,14 @@ color: #ddd; } +/* Don't show route viewer link in the app menu on desktop as it is in the navbar */ +.app-menu-route-viewer-link { + display: none; +} +.otp.mobile .app-menu-route-viewer-link { + display: block; +} + /* PrintLayout styles */ .otp.print-layout { From 09427705e6902c3ffc73a5da7e7c49be9d85a33b Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 25 Aug 2021 18:27:09 +0100 Subject: [PATCH 23/63] improvement(route-details): show stops of pattern --- lib/actions/api.js | 23 +++++ lib/components/viewers/route-details.js | 132 +++++++++++++++++++----- lib/reducers/create-otp-reducer.js | 15 +++ lib/util/viewer.js | 17 ++- 4 files changed, 155 insertions(+), 32 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 4be9c5bf9..88d5037da 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -556,6 +556,28 @@ 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, + { + rewritePayload: (payload) => { + return { + patternId: params.patternId, + routeId: params.routeId, + stops: payload + } + } + } + ) +} + // TNC ETA estimate lookup query export const transportationNetworkCompanyEtaResponse = createAction('TNC_ETA_RESPONSE') @@ -720,6 +742,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 diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index b5c0fb1d3..d58900f19 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -2,19 +2,25 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { Button } from 'react-bootstrap' -import tinycolor from 'tinycolor2' import { connect } from 'react-redux' import Icon from '../narrative/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' -import { setViewedRoute } from '../../actions/ui' +import { findStopsForPattern } from '../../actions/api' +import { setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' /** Converts text color (either black or white) to rgb */ const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') -const Wrapper = styled.div` +const Container = styled.div` + background-color: ${(props) => props.backgroundColor}; + color: ${(props) => props.textColor}; + overflow-y: hidden; + height: 100%; +` +const RouteNameContainer = styled.div` padding: 8px; - color: ${props => props.textColor} + color: ${props => props.textColor}; ` const LogoLinkContainer = styled.div` display: flex; @@ -36,10 +42,7 @@ const MoreDetailsLink = styled.a` } ` const PatternContainer = styled.div` - background-color: ${(props) => - tinycolor(props.color).isDark() - ? tinycolor(props.color).lighten(5).toHexString() - : tinycolor(props.color).darken(5).toHexString()}; + background-color: ${(props) => props.routeColor}; color: ${(props) => props.textColor}; display: flex; justify-content: flex-start; @@ -71,28 +74,100 @@ const PatternContainer = styled.div` ` const StopContainer = styled.div` - color: pink; + color: #333; background-color: #fff; + overflow-y: scroll; + height: 100%; + /* 150px bottom padding is needed to ensure all stops + are shown when browsers don't calculate 100% sensibly */ + padding: 15px 0 100px; +` +const Stop = styled.a` + cursor: pointer; + color: #333; + display: block; + margin-left: 45px; + /* negative margin accounts for the height of the stop blob */ + margin-top: -25px; + + &:hover { + color: #23527c; + } + + /* this is the station blob */ + &::before { + content: ''; + display: block; + height: 20px; + width: 20px; + border: 5px solid ${props => props.routeColor}; + background: #fff; + position: relative; + top: 20px; + left: -35px; + border-radius: 20px; + } + + /* this is the line between the blobs */ + &::after { + content: ''; + display: block; + height: 20px; + width: 10px; + background: ${props => props.routeColor}; + position: relative; + left: -30px; + /* this is 2px into the blob (to make it look attached) + 30px so that each + stop's bar connects the previous bar with the current one */ + top: -37px; + } ` class RouteDetails extends Component { static propTypes = { // TODO: proptypes + findStopsForPattern: findStopsForPattern.type, pattern: PropTypes.shape({id: PropTypes.string}), route: PropTypes.shape({ id: PropTypes.string }), setViewedRoute: setViewedRoute.type }; + componentDidMount = () => { + this.getStops() + } + + componentDidUpdate = (prevProps) => { + if (prevProps.pattern?.id !== this.props.pattern?.id) { + this.getStops() + } + } + + /** + * Requests stop list for current pattern + */ + getStops = () => { + const { findStopsForPattern, pattern, route } = this.props + if (pattern && route) { + findStopsForPattern({ patternId: pattern.id, routeId: route.id }) + } + } + _headSignButtonClicked = (id) => { const { route, setViewedRoute } = this.props setViewedRoute({ patternId: id, routeId: route.id }) } + _stopLinkClicked = (stopId) => { + const { setMainPanelContent, setViewedStop } = this.props + setMainPanelContent(null) + setViewedStop({ stopId }) + } + render () { const { className, operator, pattern, route } = this.props const { agency, patterns, url } = route - const { backgroundColor: routeColor } = getColorAndNameFromRoute( + const { backgroundColor: routeColor, color: textColor } = getColorAndNameFromRoute( operator, route ) @@ -116,20 +191,12 @@ class RouteDetails extends Component { return amended }, []) - const textColorOptions = [ - tinycolor(routeColor).darken(80).toHexString(), - tinycolor(routeColor).lighten(80).toHexString() - ] - - const textColor = tinycolor - .mostReadable(routeColor, textColorOptions, { - includeFallbackColors: true, - level: 'AAA' - }) - .toHexString() return ( -
- + + {agency && Run by {agency.name}} {url && ( @@ -141,7 +208,7 @@ class RouteDetails extends Component { )} - +

Stops To

{headsigns && @@ -156,8 +223,16 @@ class RouteDetails extends Component { ))}
- {pattern && stops} -
+ {pattern && ( + + {pattern?.stops?.map((stop) => ( + this._stopLinkClicked(stop.id)} routeColor={routeColor}> + {stop.name} + + ))} + + )} + ) } } @@ -170,7 +245,10 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - setViewedRoute + findStopsForPattern, + setMainPanelContent, + setViewedRoute, + setViewedStop } export default connect(mapStateToProps, mapDispatchToProps)(RouteDetails) diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 488f99ce5..1983601f3 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -252,6 +252,7 @@ function createOtpReducer (config) { // validate the initial state validateInitialState(initialState) + /* eslint-disable-next-line complexity */ return (state = initialState, action) => { const searchId = action.payload && action.payload.searchId const requestId = action.payload && action.payload.requestId @@ -828,6 +829,20 @@ function createOtpReducer (config) { } } }) + case 'FIND_STOPS_FOR_PATTERN_RESPONSE': + return update(state, { + transitIndex: { + routes: { + [action.payload.routeId]: { + patterns: { + [action.payload.patternId]: { + stops: { $set: action.payload.stops } + } + } + } + } + } + }) case 'FIND_STOP_TIMES_FOR_TRIP_RESPONSE': return update(state, { transitIndex: { diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 8e5432eb8..14f864d54 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -243,15 +243,22 @@ export function getTripStatus ( /** * Determine the appropriate contrast color for text (white or black) based on - * the input hex string (e.g., '#ff00ff') value. - * - * From https://stackoverflow.com/a/11868398/915811 + * the input hex string (e.g., '#ff00ff') value. Uses a tinted white/black if + * it is acceptably readable. * * TODO: Move to @opentripplanner/core-utils once otp-rr uses otp-ui library. */ -function getContrastYIQ (hexcolor) { +function getContrastYIQ (routeColor) { + const textColorOptions = [ + tinycolor(routeColor).darken(80).toHexString(), + tinycolor(routeColor).lighten(80).toHexString() + ] + return tinycolor - .mostReadable(hexcolor, ['#fff', '#000'], { level: 'AAA' }) + .mostReadable(routeColor, textColorOptions, { + includeFallbackColors: true, + level: 'AAA' + }) .toHexString() // Have to do this to remain compatible with former version of this function .split('#')[1] From 6e61767b6881abd6da90eec1d3923499cad4e027 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 10:31:33 +0100 Subject: [PATCH 24/63] improvement(route-viewer): resolve route details stop list edge cases --- lib/components/viewers/route-details.js | 12 +++++++----- lib/components/viewers/route-viewer.js | 7 +++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index d58900f19..4cf0dd820 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -86,6 +86,7 @@ const Stop = styled.a` cursor: pointer; color: #333; display: block; + white-space: nowrap; margin-left: 45px; /* negative margin accounts for the height of the stop blob */ margin-top: -25px; @@ -192,10 +193,7 @@ class RouteDetails extends Component { }, []) return ( - + {agency && Run by {agency.name}} @@ -226,7 +224,11 @@ class RouteDetails extends Component { {pattern && ( {pattern?.stops?.map((stop) => ( - this._stopLinkClicked(stop.id)} routeColor={routeColor}> + this._stopLinkClicked(stop.id)} + routeColor={routeColor.includes('ffffff') ? '#333' : routeColor} + > {stop.name} ))} diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index c487dc259..89e2399b0 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -19,9 +19,12 @@ class RouteViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, routes: PropTypes.object - } + }; - _backClicked = () => this.props.setMainPanelContent(null) + _backClicked = () => + this.props.viewedRoute === null + ? this.props.setMainPanelContent(null) + : this.props.setViewedRoute(null); componentDidMount () { this.props.findRoutes() From ced75d891300e3b025705ef947cbabf057905e38 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 12:09:27 +0100 Subject: [PATCH 25/63] feat(route-viewer): show realtime vehicle positions relies on https://github.com/ibi-group/OpenTripPlanner/pull/63 closes #440 --- lib/actions/api.js | 21 ++++++++++++++++ .../map/connected-transit-vehicle-overlay.js | 24 +++++++++++++++++++ lib/components/map/default-map.js | 2 ++ lib/components/viewers/route-viewer.js | 14 +++++++++-- lib/reducers/create-otp-reducer.js | 12 +++++++++- 5 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 lib/components/map/connected-transit-vehicle-overlay.js diff --git a/lib/actions/api.js b/lib/actions/api.js index 88d5037da..c404d76b7 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -704,6 +704,27 @@ export function findStopsWithinBBox (params) { export const clearStops = createAction('CLEAR_STOPS_OVERLAY') +// Realtime Vehcile 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 () { diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js new file mode 100644 index 000000000..7e97b632e --- /dev/null +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -0,0 +1,24 @@ +import TransitVehicleOverlay from '@opentripplanner/transit-vehicle-overlay' +import { connect } from 'react-redux' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const viewedRoute = state.otp.ui.viewedRoute + + let vehicleList = [] + + if (viewedRoute?.routeId) { + vehicleList = state.otp.transitIndex?.routes?.[viewedRoute.routeId].vehicles + if (viewedRoute.patternId) { + vehicleList = vehicleList.filter( + (vehicle) => vehicle.patternId === viewedRoute.patternId + ) + } + } + return { vehicleList } +} + +const mapDispatchToProps = {} + +export default connect(mapStateToProps, mapDispatchToProps)(TransitVehicleOverlay) diff --git a/lib/components/map/default-map.js b/lib/components/map/default-map.js index 2e28626cb..e99af0bec 100644 --- a/lib/components/map/default-map.js +++ b/lib/components/map/default-map.js @@ -20,6 +20,7 @@ import BoundsUpdatingOverlay from './bounds-updating-overlay' import EndpointsOverlay from './connected-endpoints-overlay' import ParkAndRideOverlay from './connected-park-and-ride-overlay' import RouteViewerOverlay from './connected-route-viewer-overlay' +import TransitVehicleOverlay from './connected-transit-vehicle-overlay' import StopViewerOverlay from './connected-stop-viewer-overlay' import StopsOverlay from './connected-stops-overlay' import TransitiveOverlay from './connected-transitive-overlay' @@ -159,6 +160,7 @@ class DefaultMap extends Component { + diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 89e2399b0..b10e168a3 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -9,7 +9,7 @@ import tinycolor from 'tinycolor2' import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute } from '../../actions/ui' -import { findRoutes, findRoute } from '../../actions/api' +import { getVehiclePositionsForRoute, findRoutes, findRoute } from '../../actions/api' import { ComponentContext } from '../../util/contexts' import { getColorAndNameFromRoute, getModeFromRoute } from '../../util/viewer' @@ -33,6 +33,7 @@ class RouteViewer extends Component { render () { const { findRoute, + getVehiclePositionsForRoute, hideBackButton, languageConfig, routes, @@ -116,6 +117,7 @@ class RouteViewer extends Component { return ( { + const {getVehiclePositionsForRoute, isActive, route} = this.props + if (isActive && route?.id) { + getVehiclePositionsForRoute(route.id) + } + } _onClick = () => { - const { findRoute, isActive, route, setViewedRoute } = this.props + const { findRoute, getVehiclePositionsForRoute, isActive, route, setViewedRoute } = this.props if (isActive) { // Deselect current route if active. setViewedRoute({ patternId: null, routeId: null }) @@ -204,6 +212,7 @@ class RouteRow extends PureComponent { // Otherwise, set active and fetch route patterns. findRoute({ routeId: route.id }) setViewedRoute({ routeId: route.id }) + getVehiclePositionsForRoute(route.id) } } @@ -266,6 +275,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findRoute, findRoutes, + getVehiclePositionsForRoute, setMainPanelContent, setViewedRoute } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 1983601f3..2656e1eeb 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -752,6 +752,16 @@ function createOtpReducer (config) { } } }) + case 'REALTIME_VEHICLE_POSITIONS_RESPONSE': + return update(state, { + transitIndex: { + routes: { + [action.payload.routeId]: { + vehicles: { $set: action.payload.vehicles } + } + } + } + }) case 'CLEAR_STOPS_OVERLAY': return update(state, { overlay: { @@ -914,7 +924,7 @@ function createOtpReducer (config) { // Otherwise, overwrite only this route return update(state, { transitIndex: { - routes: { [action.payload.id]: { $set: action.payload } } + routes: { [action.payload.id]: { $merge: action.payload } } } }) case 'FIND_PATTERNS_FOR_ROUTE_RESPONSE': From 583982f5233a5b659dd325a3b1955a2136dbf547 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 12:28:56 +0100 Subject: [PATCH 26/63] refactor: avoid crash on missing route data --- lib/components/map/connected-transit-vehicle-overlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 7e97b632e..4cea746a9 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -9,7 +9,7 @@ const mapStateToProps = (state, ownProps) => { let vehicleList = [] if (viewedRoute?.routeId) { - vehicleList = state.otp.transitIndex?.routes?.[viewedRoute.routeId].vehicles + vehicleList = state.otp.transitIndex?.routes?.[viewedRoute.routeId]?.vehicles if (viewedRoute.patternId) { vehicleList = vehicleList.filter( (vehicle) => vehicle.patternId === viewedRoute.patternId From 516d21a9537858fbd4e614b66653bec60f24af2a Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 13:13:40 +0100 Subject: [PATCH 27/63] improvement(map): when viewing pattern, show only pattern and its stops --- .../map/connected-route-viewer-overlay.js | 14 +++++++++++--- lib/components/map/connected-stops-overlay.js | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js index 1aa83fce2..30a1fd8fb 100644 --- a/lib/components/map/connected-route-viewer-overlay.js +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -5,10 +5,18 @@ import { connect } from 'react-redux' const mapStateToProps = (state, ownProps) => { const viewedRoute = state.otp.ui.viewedRoute + + const routeData = viewedRoute && state.otp.transitIndex.routes + ? state.otp.transitIndex.routes[viewedRoute.routeId] : null + let filteredPatterns = routeData?.patterns + + // If a pattern is selected, hide all other patterns + if (viewedRoute?.patternId) { + filteredPatterns = {[viewedRoute.patternId]: routeData.patterns[viewedRoute.patternId]} + } + return { - routeData: viewedRoute && state.otp.transitIndex.routes - ? state.otp.transitIndex.routes[viewedRoute.routeId] - : null + routeData: { ...routeData, patterns: filteredPatterns } } } diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index d52f845f1..ee835a83c 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -1,17 +1,29 @@ import StopsOverlay from '@opentripplanner/stops-overlay' -import StopMarker from './connected-stop-marker' import { connect } from 'react-redux' import { findStopsWithinBBox } from '../../actions/api' +import StopMarker from './connected-stop-marker' + // connect to the redux store const mapStateToProps = (state, ownProps) => { + const viewedRoute = state.otp.ui.viewedRoute + + let { stops } = state.otp.overlay.transit + let minZoom = 15 + + // If a pattern is being shown, show only the pattern's stops and show them large + if (viewedRoute?.patternId) { + stops = state.otp.transitIndex.routes[viewedRoute.routeId].patterns[viewedRoute.patternId].stops + minZoom = 2 + } + return { - stops: state.otp.overlay.transit.stops, + stops, symbols: [ { - minZoom: 15, + minZoom, symbol: StopMarker } ] From c0ccde2e4fc0c5fbcd1d77a7034fe24903a199b5 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 14:24:47 +0100 Subject: [PATCH 28/63] improvement(route-details-viewer): sort headsigns by number of vehicles on the route --- lib/components/viewers/route-details.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 4cf0dd820..67d955b9f 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -191,6 +191,12 @@ class RouteDetails extends Component { } return amended }, []) + .sort((a, b) => { + // sort by number of vehicles on that pattern + const aVehicleCount = route.vehicles?.filter(vehicle => vehicle.patternId === a.id).length + const bVehicleCount = route.vehicles?.filter(vehicle => vehicle.patternId === b.id).length + return bVehicleCount - aVehicleCount + }) return ( From 747e65110c4575f236567344edef5c1eeb77e230 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 14:25:02 +0100 Subject: [PATCH 29/63] style(route-viewer): cleanup --- lib/components/viewers/route-viewer.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index b10e168a3..a6fe482e1 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -19,7 +19,7 @@ class RouteViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, routes: PropTypes.object - }; + } _backClicked = () => this.props.viewedRoute === null @@ -27,7 +27,8 @@ class RouteViewer extends Component { : this.props.setViewedRoute(null); componentDidMount () { - this.props.findRoutes() + const { findRoutes } = this.props + findRoutes() } render () { @@ -198,11 +199,13 @@ class RouteRow extends PureComponent { static contextType = ComponentContext componentDidMount = () => { - const {getVehiclePositionsForRoute, isActive, route} = this.props + const { getVehiclePositionsForRoute, isActive, route } = this.props if (isActive && route?.id) { + // Update data to populate map getVehiclePositionsForRoute(route.id) } } + _onClick = () => { const { findRoute, getVehiclePositionsForRoute, isActive, route, setViewedRoute } = this.props if (isActive) { @@ -211,8 +214,8 @@ class RouteRow extends PureComponent { } else { // Otherwise, set active and fetch route patterns. findRoute({ routeId: route.id }) - setViewedRoute({ routeId: route.id }) getVehiclePositionsForRoute(route.id) + setViewedRoute({ routeId: route.id }) } } From 99ce62c4e29d2228f59333eb9f324e2aa9640fb8 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 15:19:38 +0100 Subject: [PATCH 30/63] improvement(map): realtime vehicles show icon based on their mode --- .../map/connected-transit-vehicle-overlay.js | 31 ++++++++++++++++--- lib/reducers/create-otp-reducer.js | 8 ++--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 4cea746a9..f502bd055 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -1,22 +1,43 @@ import TransitVehicleOverlay from '@opentripplanner/transit-vehicle-overlay' +import { Circle, CircledVehicle } from '@opentripplanner/transit-vehicle-overlay/lib/components/markers/ModeCircles' import { connect } from 'react-redux' +const vehicleSymbols = [ + { + minZoom: 0, + symbol: Circle + }, + { + minZoom: 10, + symbol: CircledVehicle + } +] // connect to the redux store const mapStateToProps = (state, ownProps) => { const viewedRoute = state.otp.ui.viewedRoute + const route = state.otp.transitIndex?.routes?.[viewedRoute?.routeId] let vehicleList = [] + // Add missing fields to vehicle list if (viewedRoute?.routeId) { - vehicleList = state.otp.transitIndex?.routes?.[viewedRoute.routeId]?.vehicles + vehicleList = route?.vehicles?.map(vehicle => { + vehicle.routeType = route?.mode + vehicle.routeColor = route?.color + vehicle.textColor = route?.routeTextColor + return vehicle + }) + + // Remove all vehicles not on pattern being currently viewed if (viewedRoute.patternId) { - vehicleList = vehicleList.filter( - (vehicle) => vehicle.patternId === viewedRoute.patternId - ) + vehicleList = vehicleList + .filter( + (vehicle) => vehicle.patternId === viewedRoute.patternId + ) } } - return { vehicleList } + return { symbols: vehicleSymbols, vehicleList } } const mapDispatchToProps = {} diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 2656e1eeb..0a9755377 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -906,13 +906,9 @@ function createOtpReducer (config) { transitIndex: { routes: { $set: action.payload } } }) } - // Otherwise, merge in only the routes not already defined - const currentRouteIds = Object.keys(state.transitIndex.routes) - const newRoutes = Object.keys(action.payload) - .filter(key => !currentRouteIds.includes(key)) - .reduce((res, key) => Object.assign(res, { [key]: action.payload[key] }), {}) + // otherwise, merge new data into what's already defined return update(state, { - transitIndex: { routes: { $merge: newRoutes } } + transitIndex: { routes: { $merge: action.payload } } }) case 'FIND_ROUTE_RESPONSE': // If routes is undefined, initialize it w/ this route only From 9df6f5695152bc33b463cd8ae9551349334c21f2 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 26 Aug 2021 15:52:50 +0100 Subject: [PATCH 31/63] improvement(route-details): support routing directly to pattern viewer --- lib/actions/ui.js | 24 +++++++++++++++---- .../map/connected-route-viewer-overlay.js | 2 +- lib/components/map/connected-stops-overlay.js | 2 +- .../map/connected-transit-vehicle-overlay.js | 2 +- lib/components/viewers/route-details.js | 7 +++++- lib/components/viewers/route-viewer.js | 4 ++-- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index dee3c40c2..1b92779fb 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -47,22 +47,34 @@ 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 + if (patternId) { + dispatch(setViewedRoute({ patternId, routeId: id })) + } else { + dispatch(setViewedRoute({ routeId: id })) + } } else { dispatch(setViewedRoute(null)) dispatch(setMainPanelContent(MainPanelContent.ROUTE_VIEWER)) @@ -210,8 +222,10 @@ 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}` + const patternSuffix = payload?.patternId ? `/pattern/${payload.patternId}` : '' + + const path = payload?.routeId + ? `/route/${payload.routeId}${patternSuffix}` : '/route' dispatch(routeTo(path)) } diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js index 30a1fd8fb..5932f6d70 100644 --- a/lib/components/map/connected-route-viewer-overlay.js +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -11,7 +11,7 @@ const mapStateToProps = (state, ownProps) => { let filteredPatterns = routeData?.patterns // If a pattern is selected, hide all other patterns - if (viewedRoute?.patternId) { + if (viewedRoute?.patternId && routeData?.patterns) { filteredPatterns = {[viewedRoute.patternId]: routeData.patterns[viewedRoute.patternId]} } diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index ee835a83c..6a3921131 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -14,7 +14,7 @@ const mapStateToProps = (state, ownProps) => { let minZoom = 15 // If a pattern is being shown, show only the pattern's stops and show them large - if (viewedRoute?.patternId) { + if (viewedRoute?.patternId && state.otp.transitIndex.routes?.patterns) { stops = state.otp.transitIndex.routes[viewedRoute.routeId].patterns[viewedRoute.patternId].stops minZoom = 2 } diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index f502bd055..8404d40c5 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -30,7 +30,7 @@ const mapStateToProps = (state, ownProps) => { }) // Remove all vehicles not on pattern being currently viewed - if (viewedRoute.patternId) { + if (viewedRoute.patternId && vehicleList) { vehicleList = vehicleList .filter( (vehicle) => vehicle.patternId === viewedRoute.patternId diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 67d955b9f..e3f8edc7a 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux' import Icon from '../narrative/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' -import { findStopsForPattern } from '../../actions/api' +import { getVehiclePositionsForRoute, findStopsForPattern } from '../../actions/api' import { setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' /** Converts text color (either black or white) to rgb */ @@ -134,6 +134,10 @@ class RouteDetails extends Component { }; componentDidMount = () => { + const { getVehiclePositionsForRoute, route } = this.props + if (!route.vehicles) { + getVehiclePositionsForRoute(route.id) + } this.getStops() } @@ -254,6 +258,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findStopsForPattern, + getVehiclePositionsForRoute, setMainPanelContent, setViewedRoute, setViewedStop diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index a6fe482e1..8ceabfaf5 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -49,7 +49,7 @@ class RouteViewer extends Component { : [] // If patternId is present, we're looking at a specific pattern's stops - if (viewedRoute?.patternId) { + if (viewedRoute?.patternId && routes) { const {patternId, routeId} = viewedRoute const route = routes[routeId] // Find operator based on agency_id (extracted from OTP route ID). @@ -78,7 +78,7 @@ class RouteViewer extends Component { {route.shortName}
- +
) } From fef76d15c2c95d546e00a0c3fab14710e7e8d81e Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 27 Aug 2021 11:43:03 +0100 Subject: [PATCH 32/63] improvement(route-details): resolve duplicate headsigns based on geometry length --- lib/components/viewers/route-details.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index e3f8edc7a..4162aac2d 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -182,6 +182,7 @@ class RouteDetails extends Component { Object.entries(patterns) .map((pattern) => { return { + geometryLength: pattern[1].geometry?.length, headsign: extractHeadsignFromPattern(pattern[1]), id: pattern[0] } @@ -190,7 +191,14 @@ class RouteDetails extends Component { // with a specific headsign is the accepted one. TODO: is this good behavior? .reduce((prev, cur) => { const amended = prev - if (!prev.find(h => h.headsign === cur.headsign)) { + const alreadyExistingIndex = prev.findIndex(h => h.headsign === cur.headsign) + // If the item we're replacing has less geometry, replace it! + if (alreadyExistingIndex >= 0) { + // Only replace if new pattern has greater geometry + if (amended[alreadyExistingIndex].geometryLength < cur.geometryLength) { + amended[alreadyExistingIndex] = cur + } + } else { amended.push(cur) } return amended From f99cd2cca73f4b998041af73ac2a794c87f1d3f4 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 27 Aug 2021 12:08:55 +0100 Subject: [PATCH 33/63] improvement(api): reduce queries to OTP by receiving all pattern geometries in one request --- lib/actions/api.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index c404d76b7..d2b1db321 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -507,19 +507,10 @@ export function findRoute (params) { export function findPatternsForRoute (params) { return createQueryAction( - `index/routes/${params.routeId}/patterns`, + `index/routes/${params.routeId}/patterns?includeGeometry=true`, findPatternsForRouteResponse, findPatternsForRouteError, { - postprocess: (payload, dispatch) => { - // load geometry for each pattern - payload.forEach(ptn => { - dispatch(findGeometryForPattern({ - patternId: ptn.id, - routeId: params.routeId - })) - }) - }, rewritePayload: (payload) => { // convert pattern array to ID-mapped object const patterns = {} From 9baddcaeeb23fe467bea9d1e2a1af66f41c02068 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 27 Aug 2021 12:13:08 +0100 Subject: [PATCH 34/63] refactor(connected-stops-overlay): fix regression and show pattern stops --- lib/components/map/connected-stops-overlay.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index 6a3921131..09662fe4d 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -8,14 +8,14 @@ import StopMarker from './connected-stop-marker' // connect to the redux store const mapStateToProps = (state, ownProps) => { - const viewedRoute = state.otp.ui.viewedRoute + const { viewedRoute } = state.otp.ui let { stops } = state.otp.overlay.transit let minZoom = 15 // If a pattern is being shown, show only the pattern's stops and show them large - if (viewedRoute?.patternId && state.otp.transitIndex.routes?.patterns) { - stops = state.otp.transitIndex.routes[viewedRoute.routeId].patterns[viewedRoute.patternId].stops + if (viewedRoute?.patternId && state.otp.transitIndex.routes) { + stops = state.otp.transitIndex.routes[viewedRoute.routeId]?.patterns?.[viewedRoute.patternId].stops minZoom = 2 } From f1d6f5c17cacf90a8d34f4d58d2824357b560f86 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 27 Aug 2021 12:49:30 +0100 Subject: [PATCH 35/63] improvement(route-details): highlight hovered stops on map --- lib/actions/api.js | 1 + lib/actions/ui.js | 8 ++++++++ lib/components/map/connected-stop-marker.js | 10 ++++++++++ lib/components/viewers/route-details.js | 7 +++++-- lib/reducers/create-otp-reducer.js | 3 +++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index d2b1db321..82e24e8be 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -558,6 +558,7 @@ export function findStopsForPattern (params) { findStopsForPatternResponse, findStopsForPatternError, { + noThrottle: true, rewritePayload: (payload) => { return { patternId: params.patternId, diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 1b92779fb..57dedd98d 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -217,6 +217,14 @@ export function setViewedStop (payload) { const viewStop = createAction('SET_VIEWED_STOP') +export function setHighlightedStop (payload) { + return function (dispatch, getState) { + dispatch(setHoveredStop(payload)) + } +} + +const setHoveredStop = createAction('SET_HOVERED_STOP') + export const setViewedTrip = createAction('SET_VIEWED_TRIP') export function setViewedRoute (payload) { diff --git a/lib/components/map/connected-stop-marker.js b/lib/components/map/connected-stop-marker.js index e46ebcaea..c57a1dede 100644 --- a/lib/components/map/connected-stop-marker.js +++ b/lib/components/map/connected-stop-marker.js @@ -7,8 +7,18 @@ import { setViewedStop } from '../../actions/ui' // connect to the redux store const mapStateToProps = (state, ownProps) => { + const { highlightedStop, viewedRoute } = state.otp.ui + const routeData = viewedRoute && state.otp.transitIndex.routes?.[viewedRoute.routeId] + const hoverColor = routeData?.routeColor || '#333' + return { languageConfig: state.otp.config.language, + leafletPath: { + color: '#000', + fillColor: highlightedStop === ownProps.entity.id ? hoverColor : '#FFF', + fillOpacity: 1, + weight: 1 + }, stop: ownProps.entity } } diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 4162aac2d..676a340bc 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux' import Icon from '../narrative/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' import { getVehiclePositionsForRoute, findStopsForPattern } from '../../actions/api' -import { setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' +import { setHighlightedStop, setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' /** Converts text color (either black or white) to rgb */ const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') @@ -169,7 +169,7 @@ class RouteDetails extends Component { } render () { - const { className, operator, pattern, route } = this.props + const { className, operator, pattern, route, setHighlightedStop } = this.props const { agency, patterns, url } = route const { backgroundColor: routeColor, color: textColor } = getColorAndNameFromRoute( @@ -245,6 +245,8 @@ class RouteDetails extends Component { this._stopLinkClicked(stop.id)} + onFocus={() => setHighlightedStop(stop.id)} + onMouseOver={() => setHighlightedStop(stop.id)} routeColor={routeColor.includes('ffffff') ? '#333' : routeColor} > {stop.name} @@ -267,6 +269,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findStopsForPattern, getVehiclePositionsForRoute, + setHighlightedStop, setMainPanelContent, setViewedRoute, setViewedStop diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 0a9755377..c0bfe3aee 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -808,6 +808,9 @@ function createOtpReducer (config) { case 'CLEAR_VIEWED_TRIP': return update(state, { ui: { viewedTrip: { $set: null } } }) + case 'SET_HOVERED_STOP': + return update(state, { ui: { highlightedStop: { $set: action.payload } } }) + case 'SET_VIEWED_ROUTE': if (action.payload) { // If setting to a route (not null), also set main panel. From 0bef23b11eb948cae3626373d46242c42b9d8376 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 27 Aug 2021 13:56:28 +0100 Subject: [PATCH 36/63] improvement(route-details): show realtime vehicle position info on hover --- .../map/connected-transit-vehicle-overlay.js | 48 ++++++++++++++++++- lib/components/viewers/route-details.js | 5 +- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 8404d40c5..8d8b7fc85 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -1,6 +1,11 @@ import TransitVehicleOverlay from '@opentripplanner/transit-vehicle-overlay' +import coreUtils from '@opentripplanner/core-utils' import { Circle, CircledVehicle } from '@opentripplanner/transit-vehicle-overlay/lib/components/markers/ModeCircles' +import { Tooltip } from 'react-leaflet' import { connect } from 'react-redux' +import { injectIntl } from 'react-intl' + +const { formatDurationWithSeconds } = coreUtils.time const vehicleSymbols = [ { @@ -12,6 +17,47 @@ const vehicleSymbols = [ symbol: CircledVehicle } ] + +function VehicleTooltip (props) { + const { direction, intl, permanent, vehicle } = props + + let name = vehicle?.label + if (name !== null && name?.length <= 5) { + const mode = vehicle.routeType ? intl.formatMessage({ + id: `common.otpTransitModes.${vehicle.routeType.toLowerCase()}` + }) : 'Line' + name = `${mode} ${name}` + } + + // FIXME: move to coreutils + let stopStatusString + switch (vehicle?.stopStatus) { + case 'INCOMING_AT': + stopStatusString = 'arriving at' + break + case 'STOPPED_AT': + stopStatusString = 'currently at' + break + case 'IN_TRANSIT_TO': + default: + stopStatusString = 'next stop' + } + + // FIXME: This may not be timezone adjusted as reported seconds may be in the wrong timezone. + // All needed info to fix this is available via route.agency.timezone + // However, the needed coreUtils methods are not + return ( + + + {name}: + {formatDurationWithSeconds(vehicle.seconds)} ago + + {/* TODO: localize MPH? */} + {vehicle.speed > 0 &&
travelling at {vehicle.speed} Mph
} + {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop}
} +
+ ) +} // connect to the redux store const mapStateToProps = (state, ownProps) => { @@ -37,7 +83,7 @@ const mapStateToProps = (state, ownProps) => { ) } } - return { symbols: vehicleSymbols, vehicleList } + return { symbols: vehicleSymbols, TooltipSlot: injectIntl(VehicleTooltip), vehicleList } } const mapDispatchToProps = {} diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 676a340bc..f152342db 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -240,7 +240,10 @@ class RouteDetails extends Component { ))} {pattern && ( - + setHighlightedStop(null)} + routeColor={routeColor} + > {pattern?.stops?.map((stop) => ( Date: Fri, 27 Aug 2021 14:04:46 +0100 Subject: [PATCH 37/63] refactor(connected-transit-vehicle-overlay): adjust tooltip to server changes --- lib/components/map/connected-transit-vehicle-overlay.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 8d8b7fc85..b379cdbb5 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -33,10 +33,10 @@ function VehicleTooltip (props) { let stopStatusString switch (vehicle?.stopStatus) { case 'INCOMING_AT': - stopStatusString = 'arriving at' + stopStatusString = 'approaching' break case 'STOPPED_AT': - stopStatusString = 'currently at' + stopStatusString = 'doors open at' break case 'IN_TRANSIT_TO': default: @@ -53,8 +53,8 @@ function VehicleTooltip (props) { {formatDurationWithSeconds(vehicle.seconds)} ago {/* TODO: localize MPH? */} - {vehicle.speed > 0 &&
travelling at {vehicle.speed} Mph
} - {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop}
} + {vehicle?.speed > 0 &&
travelling at {vehicle.speed} Mph
} + {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop.name}
} ) } From 2f07d6b7aadbfe8a4c8a5a0cbc13ea60865766ba Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 27 Aug 2021 14:20:07 +0100 Subject: [PATCH 38/63] revert: match OTP vehicle position schema --- lib/components/map/connected-transit-vehicle-overlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index b379cdbb5..f5ea60407 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -54,7 +54,7 @@ function VehicleTooltip (props) { {/* TODO: localize MPH? */} {vehicle?.speed > 0 &&
travelling at {vehicle.speed} Mph
} - {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop.name}
} + {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop}
} ) } From 85bcb56a190f9eef583ab29039f7941c6b7f0b9b Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 30 Aug 2021 10:40:21 +0100 Subject: [PATCH 39/63] revert: don't show view switcher on mobile --- lib/components/app/app-menu.js | 2 ++ lib/components/mobile/batch-search-screen.js | 2 +- lib/components/mobile/route-viewer.js | 1 + lib/components/mobile/search-screen.js | 2 +- lib/components/mobile/welcome-screen.js | 8 ++++---- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index da473c88f..e82bfe40b 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -59,6 +59,8 @@ class AppMenu extends Component { id='app-menu' noCaret title={()}> + {/* This item is duplicated by the view-switcher, but only shown on mobile + when the view switcher isn't shown */} Route Viewer diff --git a/lib/components/mobile/batch-search-screen.js b/lib/components/mobile/batch-search-screen.js index bc87a8fad..c110feb14 100644 --- a/lib/components/mobile/batch-search-screen.js +++ b/lib/components/mobile/batch-search-screen.js @@ -32,7 +32,7 @@ class BatchSearchScreen extends Component { render () { return ( - +
diff --git a/lib/components/mobile/search-screen.js b/lib/components/mobile/search-screen.js index 898962987..01ce77a55 100644 --- a/lib/components/mobile/search-screen.js +++ b/lib/components/mobile/search-screen.js @@ -43,7 +43,7 @@ class MobileSearchScreen extends Component { render () { return ( - +
- +
Date: Mon, 30 Aug 2021 10:51:26 +0100 Subject: [PATCH 40/63] refactor(route-viewer): complete internationalization --- i18n/en-US.yml | 2 ++ lib/components/viewers/route-viewer.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index efd84a31a..bc4388743 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -47,6 +47,8 @@ components: noRouteUrl: No route URL provided. title: Route Viewer shortTitle: View Routes + agencyFilter: Agency Filter + modeFilter: Mode Filter details: " " # Nothing is default # Common messages that appear in multiple components and modules diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 7581501cd..43420573c 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -108,7 +108,7 @@ class RouteViewer extends Component { Date: Mon, 30 Aug 2021 10:52:52 +0100 Subject: [PATCH 41/63] refactor(route-viewer): add back button on desktop --- lib/components/viewers/viewer-container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/viewers/viewer-container.js b/lib/components/viewers/viewer-container.js index 0f7239aae..78f3feaa4 100644 --- a/lib/components/viewers/viewer-container.js +++ b/lib/components/viewers/viewer-container.js @@ -19,7 +19,7 @@ class ViewerContainer extends Component { // check for main panel content if (uiState.mainPanelContent === MainPanelContent.ROUTE_VIEWER) { - return + return } // check for stop viewer From 237eeac12b159af14161f9db9a5a940397451013 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 30 Aug 2021 11:42:58 +0100 Subject: [PATCH 42/63] refactor: address pr comments --- lib/actions/api.js | 2 +- lib/actions/ui.js | 14 +- lib/components/app/app.css | 4 +- .../map/connected-route-viewer-overlay.js | 6 +- lib/components/viewers/route-details.js | 138 ++---------------- lib/components/viewers/styled.js | 117 +++++++++++++++ 6 files changed, 142 insertions(+), 139 deletions(-) create mode 100644 lib/components/viewers/styled.js diff --git a/lib/actions/api.js b/lib/actions/api.js index 82e24e8be..f350b7c14 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -696,7 +696,7 @@ export function findStopsWithinBBox (params) { export const clearStops = createAction('CLEAR_STOPS_OVERLAY') -// Realtime Vehcile positions query +// Realtime Vehicle positions query const receivedVehiclePositions = createAction('REALTIME_VEHICLE_POSITIONS_RESPONSE') const receivedVehiclePositionsError = createAction('REALTIME_VEHICLE_POSITIONS_ERROR') diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 57dedd98d..3beebdf6d 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -70,11 +70,7 @@ export function matchContentToUrl (location) { strict: false }) const patternId = subMatch?.params?.patternId - if (patternId) { - dispatch(setViewedRoute({ patternId, routeId: id })) - } else { - dispatch(setViewedRoute({ routeId: id })) - } + dispatch(setViewedRoute({ patternId, routeId: id })) } else { dispatch(setViewedRoute(null)) dispatch(setMainPanelContent(MainPanelContent.ROUTE_VIEWER)) @@ -217,13 +213,7 @@ export function setViewedStop (payload) { const viewStop = createAction('SET_VIEWED_STOP') -export function setHighlightedStop (payload) { - return function (dispatch, getState) { - dispatch(setHoveredStop(payload)) - } -} - -const setHoveredStop = createAction('SET_HOVERED_STOP') +export const setHoveredStop = createAction('SET_HOVERED_STOP') export const setViewedTrip = createAction('SET_VIEWED_TRIP') diff --git a/lib/components/app/app.css b/lib/components/app/app.css index 4adad4e99..26303d20c 100644 --- a/lib/components/app/app.css +++ b/lib/components/app/app.css @@ -52,7 +52,7 @@ box-sizing: border-box; } -/* Batch routing panel requires top padding missing from sidebar */ +/* Batch routing panel requires padding removed from sidebar */ .batch-routing-panel { - padding-top: 10px; + padding: 10px; } diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js index 5932f6d70..e9fab5969 100644 --- a/lib/components/map/connected-route-viewer-overlay.js +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -6,8 +6,10 @@ import { connect } from 'react-redux' const mapStateToProps = (state, ownProps) => { const viewedRoute = state.otp.ui.viewedRoute - const routeData = viewedRoute && state.otp.transitIndex.routes - ? state.otp.transitIndex.routes[viewedRoute.routeId] : null + const routeData = + viewedRoute && state.otp.transitIndex.routes + ? state.otp.transitIndex.routes[viewedRoute.routeId] + : null let filteredPatterns = routeData?.patterns // If a pattern is selected, hide all other patterns diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index f152342db..7665526ff 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -1,128 +1,22 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import styled from 'styled-components' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import Icon from '../narrative/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' import { getVehiclePositionsForRoute, findStopsForPattern } from '../../actions/api' -import { setHighlightedStop, setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' - -/** Converts text color (either black or white) to rgb */ -const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') - -const Container = styled.div` - background-color: ${(props) => props.backgroundColor}; - color: ${(props) => props.textColor}; - overflow-y: hidden; - height: 100%; -` -const RouteNameContainer = styled.div` - padding: 8px; - color: ${props => props.textColor}; -` -const LogoLinkContainer = styled.div` - display: flex; - align-items: center; - justify-content: space-between; -` -const MoreDetailsLink = styled.a` - color: ${(props) => props.color}; - background-color: rgba(${(props) => textHexToRgb(props.color)},0.1); - padding: 5px; - display: block; - border-radius: 5px; - transition: 0.1s all ease-in-out; - - &:hover { - background-color: rgba(255,255,255,0.8); - color: #333; - } -} -` -const PatternContainer = styled.div` - background-color: ${(props) => props.routeColor}; - color: ${(props) => props.textColor}; - display: flex; - justify-content: flex-start; - align-items: baseline; - gap: 16px; - padding: 0 8px; - margin: 0; - - overflow-x: scroll; - - h4 { - margin-bottom: 0px; - white-space: nowrap; - } - - button { - color: inherit; - border-bottom: 3px solid ${(props) => props.textColor}; - } - - button:hover, button:focus, button:visited { - text-decoration: none; - } - button:hover, button:focus, button.active { - color: ${(props) => props.color}; - background-color: ${(props) => props.textColor}; - } -} -` - -const StopContainer = styled.div` - color: #333; - background-color: #fff; - overflow-y: scroll; - height: 100%; - /* 150px bottom padding is needed to ensure all stops - are shown when browsers don't calculate 100% sensibly */ - padding: 15px 0 100px; -` -const Stop = styled.a` - cursor: pointer; - color: #333; - display: block; - white-space: nowrap; - margin-left: 45px; - /* negative margin accounts for the height of the stop blob */ - margin-top: -25px; - - &:hover { - color: #23527c; - } - - /* this is the station blob */ - &::before { - content: ''; - display: block; - height: 20px; - width: 20px; - border: 5px solid ${props => props.routeColor}; - background: #fff; - position: relative; - top: 20px; - left: -35px; - border-radius: 20px; - } - - /* this is the line between the blobs */ - &::after { - content: ''; - display: block; - height: 20px; - width: 10px; - background: ${props => props.routeColor}; - position: relative; - left: -30px; - /* this is 2px into the blob (to make it look attached) + 30px so that each - stop's bar connects the previous bar with the current one */ - top: -37px; - } -` +import { setHoveredStop, setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' + +import { + Container, + RouteNameContainer, + LogoLinkContainer, + MoreDetailsLink, + PatternContainer, + StopContainer, + Stop +} from './styled' class RouteDetails extends Component { static propTypes = { @@ -169,7 +63,7 @@ class RouteDetails extends Component { } render () { - const { className, operator, pattern, route, setHighlightedStop } = this.props + const { className, operator, pattern, route, setHoveredStop } = this.props const { agency, patterns, url } = route const { backgroundColor: routeColor, color: textColor } = getColorAndNameFromRoute( @@ -241,15 +135,15 @@ class RouteDetails extends Component { {pattern && ( setHighlightedStop(null)} + onMouseLeave={() => setHoveredStop(null)} routeColor={routeColor} > {pattern?.stops?.map((stop) => ( this._stopLinkClicked(stop.id)} - onFocus={() => setHighlightedStop(stop.id)} - onMouseOver={() => setHighlightedStop(stop.id)} + onFocus={() => setHoveredStop(stop.id)} + onMouseOver={() => setHoveredStop(stop.id)} routeColor={routeColor.includes('ffffff') ? '#333' : routeColor} > {stop.name} @@ -272,7 +166,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findStopsForPattern, getVehiclePositionsForRoute, - setHighlightedStop, + setHoveredStop, setMainPanelContent, setViewedRoute, setViewedStop diff --git a/lib/components/viewers/styled.js b/lib/components/viewers/styled.js new file mode 100644 index 000000000..b885b32aa --- /dev/null +++ b/lib/components/viewers/styled.js @@ -0,0 +1,117 @@ +import styled from 'styled-components' + +/** Converts text color (either black or white) to rgb */ +const textHexToRgb = (color) => (color === '#ffffff' ? '255,255,255' : '0,0,0') + +/** Route Details */ +export const Container = styled.div` + background-color: ${(props) => props.backgroundColor}; + color: ${(props) => props.textColor}; + overflow-y: hidden; + height: 100%; +` +export const RouteNameContainer = styled.div` + padding: 8px; + color: ${props => props.textColor}; +` +export const LogoLinkContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +` +export const MoreDetailsLink = styled.a` + color: ${(props) => props.color}; + background-color: rgba(${(props) => textHexToRgb(props.color)},0.1); + padding: 5px; + display: block; + border-radius: 5px; + transition: 0.1s all ease-in-out; + + &:hover { + background-color: rgba(255,255,255,0.8); + color: #333; + } +} +` +export const PatternContainer = styled.div` + background-color: ${(props) => props.routeColor}; + color: ${(props) => props.textColor}; + display: flex; + justify-content: flex-start; + align-items: baseline; + gap: 16px; + padding: 0 8px; + margin: 0; + + overflow-x: scroll; + + h4 { + margin-bottom: 0px; + white-space: nowrap; + } + + button { + color: inherit; + border-bottom: 3px solid ${(props) => props.textColor}; + } + + button:hover, button:focus, button:visited { + text-decoration: none; + } + button:hover, button:focus, button.active { + color: ${(props) => props.color}; + background-color: ${(props) => props.textColor}; + } +} +` + +export const StopContainer = styled.div` + color: #333; + background-color: #fff; + overflow-y: scroll; + height: 100%; + /* 150px bottom padding is needed to ensure all stops + are shown when browsers don't calculate 100% sensibly */ + padding: 15px 0 100px; +` +export const Stop = styled.a` + cursor: pointer; + color: #333; + display: block; + white-space: nowrap; + margin-left: 45px; + /* negative margin accounts for the height of the stop blob */ + margin-top: -25px; + + &:hover { + color: #23527c; + } + + /* this is the station blob */ + &::before { + content: ''; + display: block; + height: 20px; + width: 20px; + border: 5px solid ${props => props.routeColor}; + background: #fff; + position: relative; + top: 20px; + left: -35px; + border-radius: 20px; + } + + /* this is the line between the blobs */ + &::after { + content: ''; + display: block; + height: 20px; + width: 10px; + background: ${props => props.routeColor}; + position: relative; + left: -30px; + /* this is 2px into the blob (to make it look attached) + 30px so that each + stop's bar connects the previous bar with the current one */ + top: -37px; + } +` From dbf66c766fa47e0e88324a03d367a9c8351b6397 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 30 Aug 2021 11:55:41 +0100 Subject: [PATCH 43/63] refactor: more robust path generation --- lib/actions/ui.js | 18 +++++++++++------- lib/util/ui.js | 13 +++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 3beebdf6d..9d5e6a161 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -5,6 +5,7 @@ import { matchPath } from 'react-router' import { getUiUrlParams } 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' @@ -70,6 +71,7 @@ export function matchContentToUrl (location) { 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)) @@ -204,9 +206,8 @@ 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)) } } @@ -220,11 +221,14 @@ export const setViewedTrip = createAction('SET_VIEWED_TRIP') export function setViewedRoute (payload) { return function (dispatch, getState) { dispatch(viewRoute(payload)) - const patternSuffix = payload?.patternId ? `/pattern/${payload.patternId}` : '' - const path = payload?.routeId - ? `/route/${payload.routeId}${patternSuffix}` - : '/route' + const path = getPathFromParts( + 'route', + payload?.routeId, + // If a pattern is supplied, include pattern in path + payload.patternId && 'pattern', + payload.patternId + ) dispatch(routeTo(path)) } } diff --git a/lib/util/ui.js b/lib/util/ui.js index f947128f7..9be25250d 100644 --- a/lib/util/ui.js +++ b/lib/util/ui.js @@ -44,3 +44,16 @@ export function getErrorStates (props) { * Browser navigate back. */ export const navigateBack = () => window.history.back() + +/** + * Assembles a path from a variable list of parts + * @param {...any} parts List of string components to assemble into path + * @returns A path made of the components passed in + */ +export function getPathFromParts (...parts) { + let path = '' + parts.forEach(p => { + if (p) path += `/${p}` + }) + return path +} From 78686a5ec6a5e41aef288ae5d69b6c6de03f4890 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 30 Aug 2021 13:09:59 +0100 Subject: [PATCH 44/63] refactor: polish and cleanup --- lib/actions/ui.js | 2 +- lib/components/map/connected-stops-overlay.js | 2 +- .../map/connected-transit-vehicle-overlay.js | 2 +- lib/components/viewers/route-details.js | 56 ++++++++++----- lib/components/viewers/route-viewer.js | 71 ++++++++++++++----- lib/components/viewers/stop-viewer.js | 3 +- 6 files changed, 94 insertions(+), 42 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 0ae8d1452..ac123df10 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -207,7 +207,7 @@ export function setViewedStop (payload) { return function (dispatch, getState) { dispatch(viewStop(payload)) // payload.stopId may be undefined, which is ok as will be ignored by getPathFromParts - const path = getPathFromParts('stop', payload.stopId) + const path = getPathFromParts('stop', payload?.stopId) dispatch(routeTo(path)) } } diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index 09662fe4d..643f08c7c 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -20,7 +20,7 @@ const mapStateToProps = (state, ownProps) => { } return { - stops, + stops: stops || [], symbols: [ { minZoom, diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index f5ea60407..75782dcda 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -53,7 +53,7 @@ function VehicleTooltip (props) { {formatDurationWithSeconds(vehicle.seconds)} ago {/* TODO: localize MPH? */} - {vehicle?.speed > 0 &&
travelling at {vehicle.speed} Mph
} + {vehicle?.speed > 0 &&
travelling at {Math.round(vehicle.speed)} Mph
} {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop}
} ) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 7665526ff..c2546ca8b 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux' import Icon from '../narrative/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' import { getVehiclePositionsForRoute, findStopsForPattern } from '../../actions/api' -import { setHoveredStop, setMainPanelContent, setViewedStop, setViewedRoute } from '../../actions/ui' +import { setHoveredStop, setViewedStop, setViewedRoute } from '../../actions/ui' import { Container, @@ -20,10 +20,17 @@ import { class RouteDetails extends Component { static propTypes = { - // TODO: proptypes + className: PropTypes.string, findStopsForPattern: findStopsForPattern.type, - pattern: PropTypes.shape({id: PropTypes.string}), + operator: PropTypes.shape({ + defaultRouteColor: PropTypes.string, + defaultRouteTextColor: PropTypes.string, + longNameSplitter: PropTypes.string + }), + // There are more items in pattern and route, but none mandatory + pattern: PropTypes.shape({ id: PropTypes.string }), route: PropTypes.shape({ id: PropTypes.string }), + setHoveredStop: setHoveredStop.type, setViewedRoute: setViewedRoute.type }; @@ -33,13 +40,13 @@ class RouteDetails extends Component { getVehiclePositionsForRoute(route.id) } this.getStops() - } + }; componentDidUpdate = (prevProps) => { if (prevProps.pattern?.id !== this.props.pattern?.id) { this.getStops() } - } + }; /** * Requests stop list for current pattern @@ -49,27 +56,26 @@ class RouteDetails extends Component { if (pattern && route) { findStopsForPattern({ patternId: pattern.id, routeId: route.id }) } - } + }; _headSignButtonClicked = (id) => { const { route, setViewedRoute } = this.props setViewedRoute({ patternId: id, routeId: route.id }) - } + }; _stopLinkClicked = (stopId) => { - const { setMainPanelContent, setViewedStop } = this.props - setMainPanelContent(null) + const { setViewedStop } = this.props setViewedStop({ stopId }) - } + }; render () { const { className, operator, pattern, route, setHoveredStop } = this.props const { agency, patterns, url } = route - const { backgroundColor: routeColor, color: textColor } = getColorAndNameFromRoute( - operator, - route - ) + const { + backgroundColor: routeColor, + color: textColor + } = getColorAndNameFromRoute(operator, route) const headsigns = patterns && @@ -85,11 +91,15 @@ class RouteDetails extends Component { // with a specific headsign is the accepted one. TODO: is this good behavior? .reduce((prev, cur) => { const amended = prev - const alreadyExistingIndex = prev.findIndex(h => h.headsign === cur.headsign) + const alreadyExistingIndex = prev.findIndex( + (h) => h.headsign === cur.headsign + ) // If the item we're replacing has less geometry, replace it! if (alreadyExistingIndex >= 0) { // Only replace if new pattern has greater geometry - if (amended[alreadyExistingIndex].geometryLength < cur.geometryLength) { + if ( + amended[alreadyExistingIndex].geometryLength < cur.geometryLength + ) { amended[alreadyExistingIndex] = cur } } else { @@ -99,8 +109,17 @@ class RouteDetails extends Component { }, []) .sort((a, b) => { // sort by number of vehicles on that pattern - const aVehicleCount = route.vehicles?.filter(vehicle => vehicle.patternId === a.id).length - const bVehicleCount = route.vehicles?.filter(vehicle => vehicle.patternId === b.id).length + const aVehicleCount = route.vehicles?.filter( + (vehicle) => vehicle.patternId === a.id + ).length + const bVehicleCount = route.vehicles?.filter( + (vehicle) => vehicle.patternId === b.id + ).length + + // if both have the same count, sort by pattern geometry length + if (aVehicleCount === bVehicleCount) { + return b.geometryLength - a.geometryLength + } return bVehicleCount - aVehicleCount }) @@ -167,7 +186,6 @@ const mapDispatchToProps = { findStopsForPattern, getVehiclePositionsForRoute, setHoveredStop, - setMainPanelContent, setViewedRoute, setViewedStop } diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 8fd94d517..30e69b1bd 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -20,11 +20,30 @@ import RouteDetails from './route-details' class RouteViewer extends Component { static propTypes = { agencies: PropTypes.array, + filter: PropTypes.shape({ + agency: PropTypes.string, + mode: PropTypes.string, + search: PropTypes.string + }), + findRoute: findRoute.type, + getVehiclePositionsForRoute: getVehiclePositionsForRoute.type, hideBackButton: PropTypes.bool, modes: PropTypes.array, - routes: PropTypes.array + routes: PropTypes.array, + setViewedRoute: setViewedRoute.type, + transitOperators: PropTypes.array, + viewedRoute: PropTypes.shape({ + patternId: PropTypes.string, + routeId: PropTypes.string + }), + // Routes have many more properties, but none are guaranteed + viewedRouteObject: PropTypes.shape({ id: PropTypes.string }) }; + /** + * If we're viewing a pattern's stops, route to + * main route viewer, otherwise go back to main view + */ _backClicked = () => this.props.viewedRoute === null ? this.props.setMainPanelContent(null) @@ -48,7 +67,7 @@ class RouteViewer extends Component { } const { id, value } = target // id will be either 'agency' or 'mode' based on the dropdown used - this.props.setRouteViewerFilter({[id]: value}) + this.props.setRouteViewerFilter({ [id]: value }) }; /** @@ -81,10 +100,11 @@ class RouteViewer extends Component { const { patternId } = viewedRoute const route = viewedRouteObject // Find operator based on agency_id (extracted from OTP route ID). - const operator = coreUtils.route.getTransitOperatorFromOtpRoute( - route, - transitOperators - ) || {} + const operator = + coreUtils.route.getTransitOperatorFromOtpRoute( + route, + transitOperators + ) || {} const { backgroundColor, color } = getColorAndNameFromRoute( operator, route @@ -93,20 +113,27 @@ class RouteViewer extends Component { return (
{/* Header Block */} -
+
{/* Always show back button, as we don't write a route anymore */}
- +
{route.shortName}
- +
) } @@ -130,12 +157,16 @@ class RouteViewer extends Component {
-
+
+ +
{ // Find operator based on agency_id (extracted from OTP route ID). const operator = - coreUtils.route.getTransitOperatorFromOtpRoute( - route, - transitOperators - ) || {} + coreUtils.route.getTransitOperatorFromOtpRoute( + route, + transitOperators + ) || {} return ( window.history.back() + _backClicked = () => navigateBack() _setLocationFromStop = (locationType) => { const { setLocation, stopData } = this.props From 62843e87e6fb0eed6f3e8f03a1797620146209ba Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 30 Aug 2021 13:38:09 +0100 Subject: [PATCH 45/63] improvement(route-details): add internationalization --- i18n/en-US.yml | 11 +++++++ .../map/connected-transit-vehicle-overlay.js | 31 +++++++++---------- lib/components/viewers/route-details.js | 19 ++++++++++-- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index bc4388743..a4c51e44f 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -39,6 +39,10 @@ components: tripDurationFormatZeroHours: "{minutes, number} min" # TODO: Distinguish between one hour (singular) and 2 hours or more? tripDurationFormat: "{hours, number} hr {minutes, number} min" + RouteDetails: + runBy: "Run by {agencyName}" + moreDetails: "More Details" + stopsTo: "Stops To" RouteViewer: allAgencies: All Agencies allModes: All Modes @@ -50,6 +54,13 @@ components: agencyFilter: Agency Filter modeFilter: Mode Filter details: " " # Nothing is default + TransitVehicleOverlay: + # keys designed to match API output + incoming_at: "approaching" + stopped_at: "doors open at" + in_transit_to: "next stop" + travellingAt: "travelling at" + relativeTime: "{seconds} ago" # Common messages that appear in multiple components and modules # are grouped below by topic. diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 75782dcda..17097278c 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -3,7 +3,7 @@ import coreUtils from '@opentripplanner/core-utils' import { Circle, CircledVehicle } from '@opentripplanner/transit-vehicle-overlay/lib/components/markers/ModeCircles' import { Tooltip } from 'react-leaflet' import { connect } from 'react-redux' -import { injectIntl } from 'react-intl' +import { FormattedMessage, injectIntl } from 'react-intl' const { formatDurationWithSeconds } = coreUtils.time @@ -26,22 +26,16 @@ function VehicleTooltip (props) { const mode = vehicle.routeType ? intl.formatMessage({ id: `common.otpTransitModes.${vehicle.routeType.toLowerCase()}` }) : 'Line' - name = `${mode} ${name}` + // Only render space if name is present + name = `${mode}${name && ' '}${name}` } // FIXME: move to coreutils - let stopStatusString - switch (vehicle?.stopStatus) { - case 'INCOMING_AT': - stopStatusString = 'approaching' - break - case 'STOPPED_AT': - stopStatusString = 'doors open at' - break - case 'IN_TRANSIT_TO': - default: - stopStatusString = 'next stop' - } + const stopStatusString = vehicle.stopStatus + ? intl.formatMessage({ + id: `components.TransitVehicleOverlay.${vehicle.stopStatus.toLowerCase()}` + }) + : '' // FIXME: This may not be timezone adjusted as reported seconds may be in the wrong timezone. // All needed info to fix this is available via route.agency.timezone @@ -50,10 +44,15 @@ function VehicleTooltip (props) { {name}: - {formatDurationWithSeconds(vehicle.seconds)} ago + {/* TODO: localize MPH? */} - {vehicle?.speed > 0 &&
travelling at {Math.round(vehicle.speed)} Mph
} + {vehicle?.speed > 0 &&
{Math.round(vehicle.speed)} Mph
} {vehicle?.nextStop &&
{stopStatusString} {vehicle.nextStop}
}
) diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index c2546ca8b..4554c450b 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' +import { FormattedMessage } from 'react-intl' import Icon from '../narrative/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' @@ -58,11 +59,18 @@ class RouteDetails extends Component { } }; + /** + * If a headsign link is clicked, set that pattern in redux state so that the + * view can adjust + */ _headSignButtonClicked = (id) => { const { route, setViewedRoute } = this.props setViewedRoute({ patternId: id, routeId: route.id }) }; + /** + * If a stop link is clicked, redirect to stop viewer + */ _stopLinkClicked = (stopId) => { const { setViewedStop } = this.props setViewedStop({ stopId }) @@ -127,19 +135,24 @@ class RouteDetails extends Component { - {agency && Run by {agency.name}} + {agency && } {url && ( - More details + )} -

Stops To

+

{headsigns && headsigns.map((h) => ( - -
-
+ + + ) } } diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index a89dc87fb..b5bb976a2 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -5,7 +5,6 @@ import { connect } from 'react-redux' import { setMobileScreen } from '../../actions/ui' import AppMenu from '../app/app-menu' -import ViewSwitcher from '../app/view-switcher' import NavLoginButtonAuth0 from '../../components/user/nav-login-button-auth0' import Icon from '../narrative/icon' import { accountLinks, getAuth0Config } from '../../util/auth' @@ -31,6 +30,7 @@ class MobileNavigationBar extends Component { render () { const { auth0Config, + defaultMobileTitle, headerAction, headerText, showBackButton @@ -59,7 +59,7 @@ class MobileNavigationBar extends Component {
{headerText ?
{headerText}
- : + :
{defaultMobileTitle}
}
diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 43420573c..2ba2e7a38 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -103,7 +103,7 @@ class RouteViewer extends Component {
-
+
@@ -119,11 +119,12 @@ class RouteViewer extends Component { id: 'components.RouteViewer.allAgencies' })} - {agencies.map((agency) => ( - - ))} + {agencies + .map((agency) => ( + + ))} - {agencies - .map((agency) => ( - - ))} + {agencies.map((agency) => ( + + ))} } {pattern && ( {/* Header Block */}
{/* Always show back button, as we don't write a route anymore */}
@@ -166,11 +160,10 @@ class RouteViewer extends Component { )} - {route.shortName} +
props.isActive ? props.routeColor : 'white'}; + background-color: white; border-bottom: 1px solid gray; ` @@ -311,20 +304,6 @@ const RouteRowButton = styled(Button)` padding: 6px; width: 100%; transition: all ease-in-out 0.1s; - - &:hover { - background-color: ${(props) => - !props.isActive && tinycolor(props.routeColor) - .lighten(50) - .toHexString()}; - border-radius: 0; - } - - &:active:focus, - &:active:hover { - background-color: ${(props) => props.routeColor}; - border-radius: 0; - } ` const RouteRowElement = styled.span`` @@ -358,6 +337,22 @@ const RouteNameElement = styled(Label)` text-overflow: ellipsis; ` +const RouteName = ({operator, route}) => { + const { backgroundColor, color, longName } = getColorAndNameFromRoute( + operator, + route + ) + return ( + + {route.shortName} {longName} + + ) +} + class RouteRow extends PureComponent { static contextType = ComponentContext @@ -405,22 +400,15 @@ class RouteRow extends PureComponent { const { intl, isActive, operator, route } = this.props const { ModeIcon } = this.context - const { backgroundColor, color, longName, potentiallyUnreadableBackgroundColor } = getColorAndNameFromRoute( - operator, - route - ) - return ( {operator && operator.logo && ( @@ -440,19 +428,13 @@ class RouteRow extends PureComponent { aria-label={getModeFromRoute(route)} height={22} mode={getModeFromRoute(route)} - style={{ fill: isActive && color }} width={22} /> - - {route.shortName} {longName} - + (color === '#ffffff' ? '255,255,255' : '0,0,0') - /** Route Details */ export const Container = styled.div` - background-color: ${(props) => props.backgroundColor}; - color: ${(props) => props.textColor}; overflow-y: hidden; height: 100%; + background-color: ${props => props.full ? '#ddd' : 'inherit'} ` + export const RouteNameContainer = styled.div` padding: 8px; - color: ${props => props.textColor}; + background-color: inherit; ` export const LogoLinkContainer = styled.div` display: flex; @@ -20,27 +17,27 @@ export const LogoLinkContainer = styled.div` justify-content: space-between; ` export const MoreDetailsLink = styled.a` - color: ${(props) => props.color}; - background-color: rgba(${(props) => textHexToRgb(props.color)},0.1); + color: #333; + background-color: rgba(0,0,0,0.1); padding: 5px; display: block; border-radius: 5px; transition: 0.1s all ease-in-out; &:hover { - background-color: rgba(255,255,255,0.8); - color: #333; + background-color: rgba(0,0,0,0.8); + color: #eee; } } ` export const PatternContainer = styled.div` - background-color: ${(props) => props.routeColor}; - color: ${(props) => props.textColor}; + background-color: inherit; + color: inherit; display: flex; justify-content: flex-start; align-items: baseline; gap: 16px; - padding: 0 8px; + padding: 0 8px 8px; margin: 0; overflow-x: scroll; @@ -49,19 +46,6 @@ export const PatternContainer = styled.div` margin-bottom: 0px; white-space: nowrap; } - - button { - color: inherit; - border-bottom: 3px solid ${(props) => props.textColor}; - } - - button:hover, button:focus, button:visited { - text-decoration: none; - } - button:hover, button:focus, button.active { - color: ${(props) => props.color}; - background-color: ${(props) => props.textColor}; - } } ` @@ -114,4 +98,9 @@ export const Stop = styled.a` stop's bar connects the previous bar with the current one */ top: -37px; } + + /* hide the first line between blobs */ + &:first-of-type::after { + background: transparent; + } ` diff --git a/lib/util/viewer.js b/lib/util/viewer.js index fe76ec6fa..6685cd309 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -288,35 +288,21 @@ function getCleanRouteLongName ({ longNameSplitter, route }) { */ export function getColorAndNameFromRoute (operator, route) { const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {} - const potentiallyUnreadableBackgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` + const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` // NOTE: text color is not a part of short response route object, so there // is no way to determine from OTP what the text color should be if the // background color is, say, black. Instead, determine the appropriate // contrast color and use that if no text color is available. - const contrastColor = getContrastYIQ(potentiallyUnreadableBackgroundColor) + const contrastColor = getContrastYIQ(backgroundColor) const color = `#${defaultRouteTextColor || route.textColor || contrastColor}` // Default long name is empty string (long name is an optional GTFS value). const longName = getCleanRouteLongName({ longNameSplitter, route }) // Choose a color that the text color will look good against - let backgroundColor = potentiallyUnreadableBackgroundColor - if ( - !tinycolor.isReadable( - tinycolor(potentiallyUnreadableBackgroundColor), - tinycolor(color), - // Buses are likely to have less care put into their color selection - {level: route.mode === 'BUS' ? 'AAA' : 'AA'} - ) - ) { - backgroundColor = tinycolor(potentiallyUnreadableBackgroundColor) - .desaturate(40) - .toHexString() - } return { backgroundColor, color, - longName, - potentiallyUnreadableBackgroundColor + longName } } From 140c81e8d652d4c488c4fcfaf3af10f2423ad954 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 16 Sep 2021 11:47:17 +0100 Subject: [PATCH 59/63] refactor(RouteViewer): clean up, extract routeRow to new file --- i18n/en-US.yml | 2 +- .../map/connected-transit-vehicle-overlay.js | 12 +- lib/components/viewers/RouteRow.js | 160 +++++++++++++++++ lib/components/viewers/route-viewer.js | 164 +----------------- 4 files changed, 172 insertions(+), 166 deletions(-) create mode 100644 lib/components/viewers/RouteRow.js diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 841a28c53..a5c806a73 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -64,7 +64,7 @@ components: relativeTime: "{seconds} ago" routeNumberAndName: "{mode}{name && ' '}{name}" line: "Line" - realtimeVehicleName: "{name}: " + realtimeVehicleName: "{mode}{headsign}: " # name will include a space if it is set, otherwise be blank details: " " # If the string is left blank, React-Intl renders the id ItinerarySummary: fareCost: "{useMaxFare, select, diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index f9de38a68..8cd988a5c 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -41,8 +41,11 @@ function VehicleTooltip (props) { : intl.formatMessage({ id: `components.TransitVehicleOverlay.line` }) - // Only render space if name is present - name = `${mode}${name && ' '}${name}` + name = intl.formatMessage( + { id: 'components.TransitVehicleOverlay.realtimeVehicleName' }, + // Only render space if name is present + { headsign: `${name && ' '}${name}`, mode } + ) } const stopStatus = vehicle.stopStatus || 'in_transit_to' @@ -54,10 +57,7 @@ function VehicleTooltip (props) { - + {name} { + const { getVehiclePositionsForRoute, isActive, route } = this.props + if (isActive && route?.id) { + // Update data to populate map + getVehiclePositionsForRoute(route.id) + // This is fired when coming back from the route details view + this.activeRef.current.scrollIntoView() + } + }; + + componentDidUpdate () { + /* + If the initial route row list is being rendered and there is an active + route, scroll to it. The initialRender prop prohibits the row being scrolled to + if the user has clicked on a route + */ + if (this.props.isActive && this.props.initialRender) { + this.activeRef.current.scrollIntoView() + } + } + + _onClick = () => { + const { findRoute, getVehiclePositionsForRoute, isActive, route, setViewedRoute } = this.props + if (isActive) { + // Deselect current route if active. + setViewedRoute({ patternId: null, routeId: null }) + } else { + // Otherwise, set active and fetch route patterns. + findRoute({ routeId: route.id }) + getVehiclePositionsForRoute(route.id) + setViewedRoute({ routeId: route.id }) + } + }; + + render () { + const { intl, isActive, operator, route } = this.props + const { ModeIcon } = this.context + + return ( + + + + {operator && operator.logo && ( + + )} + + + + + + + + {isActive && } + + + ) + } +} + +export const StyledRouteRow = styled.div` + background-color: white; + border-bottom: 1px solid gray; +` + +export const RouteRowButton = styled(Button)` + align-items: center; + display: flex; + padding: 6px; + width: 100%; + transition: all ease-in-out 0.1s; +` + +export const RouteRowElement = styled.span`` + +export const OperatorImg = styled.img` + height: 25px; + margin-right: 8px; +` + +export const ModeIconElement = styled.span` + display: inline-block; + vertical-align: bottom; + height: 22px; +` + +const RouteNameElement = styled(Label)` + background-color: ${(props) => + props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' + ? 'rgba(0,0,0,0)' + : props.backgroundColor}; + color: ${(props) => props.color}; + flex: 0 1 auto; + font-size: medium; + font-weight: 400; + margin-left: ${(props) => + props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' + ? 0 + : '8px'}; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; +` + +export const RouteName = ({operator, route}) => { + const { backgroundColor, color, longName } = getColorAndNameFromRoute( + operator, + route + ) + return ( + + {route.shortName} {longName} + + ) +} diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 60b4d19fd..577da4c57 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -1,15 +1,13 @@ import coreUtils from '@opentripplanner/core-utils' -import React, { Component, PureComponent } from 'react' -import PropTypes from 'prop-types' -import { Label, Button } from 'react-bootstrap' -import { VelocityTransitionGroup } from 'velocity-react' +import React, { Component } from 'react' import { connect } from 'react-redux' -import styled from 'styled-components' +import PropTypes from 'prop-types' +import { Button } from 'react-bootstrap' import { FormattedMessage, injectIntl } from 'react-intl' import Icon from '../util/icon' import { getVehiclePositionsForRoute, findRoutes, findRoute } from '../../actions/api' -import { getColorAndNameFromRoute, getModeFromRoute } from '../../util/viewer' +import { getModeFromRoute } from '../../util/viewer' import { setMainPanelContent, setViewedRoute, @@ -23,6 +21,7 @@ import { import { ComponentContext } from '../../util/contexts' import RouteDetails from './route-details' +import { RouteRow, RouteName } from './RouteRow' class RouteViewer extends Component { static propTypes = { @@ -293,159 +292,6 @@ class RouteViewer extends Component { } } -const StyledRouteRow = styled.div` - background-color: white; - border-bottom: 1px solid gray; -` - -const RouteRowButton = styled(Button)` - align-items: center; - display: flex; - padding: 6px; - width: 100%; - transition: all ease-in-out 0.1s; -` - -const RouteRowElement = styled.span`` - -const OperatorImg = styled.img` - height: 25px; - margin-right: 8px; -` - -const ModeIconElement = styled.span` - display: inline-block; - vertical-align: bottom; - height: 22px; -` - -const RouteNameElement = styled(Label)` - background-color: ${(props) => - props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' - ? 'rgba(0,0,0,0)' - : props.backgroundColor}; - color: ${(props) => props.color}; - flex: 0 1 auto; - font-size: medium; - font-weight: 400; - margin-left: ${(props) => - props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' - ? 0 - : '8px'}; - margin-top: 2px; - overflow: hidden; - text-overflow: ellipsis; -` - -const RouteName = ({operator, route}) => { - const { backgroundColor, color, longName } = getColorAndNameFromRoute( - operator, - route - ) - return ( - - {route.shortName} {longName} - - ) -} - -class RouteRow extends PureComponent { - static contextType = ComponentContext - - constructor (props) { - super(props) - // Create a ref used to scroll to - this.activeRef = React.createRef() - } - - componentDidMount = () => { - const { getVehiclePositionsForRoute, isActive, route } = this.props - if (isActive && route?.id) { - // Update data to populate map - getVehiclePositionsForRoute(route.id) - // This is fired when coming back from the route details view - this.activeRef.current.scrollIntoView() - } - } - - componentDidUpdate () { - /* - If the initial route row list is being rendered and there is an active - route, scroll to it. The initialRender prop prohibits the row being scrolled to - if the user has clicked on a route - */ - if (this.props.isActive && this.props.initialRender) { - this.activeRef.current.scrollIntoView() - } - } - - _onClick = () => { - const { findRoute, getVehiclePositionsForRoute, isActive, route, setViewedRoute } = this.props - if (isActive) { - // Deselect current route if active. - setViewedRoute({ patternId: null, routeId: null }) - } else { - // Otherwise, set active and fetch route patterns. - findRoute({ routeId: route.id }) - getVehiclePositionsForRoute(route.id) - setViewedRoute({ routeId: route.id }) - } - } - - render () { - const { intl, isActive, operator, route } = this.props - const { ModeIcon } = this.context - - return ( - - - - {operator && operator.logo && ( - - )} - - - - - - - - {isActive && } - - - ) - } -} // connect to redux store const mapStateToProps = (state, ownProps) => { From 30f4a83923d902b17646402d63834eb22f9045e9 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 17 Sep 2021 12:22:54 +0100 Subject: [PATCH 60/63] improvement: show all route shapes when OTP instances don't support includeGeometry --- lib/actions/api.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/actions/api.js b/lib/actions/api.js index 6b8fb23ba..31d6624cc 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -512,6 +512,20 @@ export function findPatternsForRoute (params) { findPatternsForRouteError, { noThrottle: true, + postprocess: (payload, dispatch) => { + // load geometry for each pattern + payload.forEach(ptn => { + // 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 = {} From 9a7139548d78db23db07dfddf9940bd85078072b Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 20 Sep 2021 12:46:49 +0100 Subject: [PATCH 61/63] refactor: address pr comments --- i18n/en-US.yml | 7 +-- .../map/connected-transit-vehicle-overlay.js | 46 ++++++++----------- lib/components/mobile/route-viewer.js | 10 ++-- lib/components/viewers/RouteRow.js | 6 +-- lib/components/viewers/route-details.js | 11 ++--- lib/components/viewers/route-viewer.js | 14 +++--- lib/components/viewers/styled.js | 14 ------ 7 files changed, 40 insertions(+), 68 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index a5c806a73..8d3b82578 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -60,12 +60,9 @@ components: stopped_at: "doors open at {stop}" in_transit_to: "next stop {stop}" + vehicleName: "Vehicle {vehicleNumber}: " + realtimeVehicleInfo: "{vehicleNameOrBlank}{relativeTime}" travellingAt: "travelling at {milesPerHour}" - relativeTime: "{seconds} ago" - routeNumberAndName: "{mode}{name && ' '}{name}" - line: "Line" - realtimeVehicleName: "{mode}{headsign}: " # name will include a space if it is set, otherwise be blank - details: " " # If the string is left blank, React-Intl renders the id ItinerarySummary: fareCost: "{useMaxFare, select, true {{minTotalFare} - {maxTotalFare}} diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 8cd988a5c..b15ccae41 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -9,14 +9,11 @@ * 4) This overlay does not render route paths * 5) This overlay has a custom popup on vehicle hover */ -import TransitVehicleOverlay from '@opentripplanner/transit-vehicle-overlay' -import coreUtils from '@opentripplanner/core-utils' import { Circle, CircledVehicle } from '@opentripplanner/transit-vehicle-overlay/lib/components/markers/ModeCircles' -import { Tooltip } from 'react-leaflet' import { connect } from 'react-redux' import { FormattedMessage, FormattedNumber, injectIntl } from 'react-intl' - -const { formatDurationWithSeconds } = coreUtils.time +import { Tooltip } from 'react-leaflet' +import TransitVehicleOverlay from '@opentripplanner/transit-vehicle-overlay' const vehicleSymbols = [ { @@ -32,41 +29,36 @@ const vehicleSymbols = [ function VehicleTooltip (props) { const { direction, intl, permanent, vehicle } = props - let name = vehicle?.label - if (name !== null && name?.length <= 5) { - const mode = vehicle.routeType - ? intl.formatMessage({ - id: `common.otpTransitModes.${vehicle.routeType.toLowerCase()}` - }) - : intl.formatMessage({ - id: `components.TransitVehicleOverlay.line` - }) - name = intl.formatMessage( - { id: 'components.TransitVehicleOverlay.realtimeVehicleName' }, - // Only render space if name is present - { headsign: `${name && ' '}${name}`, mode } + let vehicleLabel = vehicle?.label + if (vehicleLabel !== null && vehicleLabel?.length <= 5) { + vehicleLabel = intl.formatMessage( + { id: 'components.TransitVehicleOverlay.vehicleName' }, + { vehicleNumber: vehicleLabel } ) + } else { + vehicleLabel = '' } - const stopStatus = vehicle.stopStatus || 'in_transit_to' + const stopStatus = vehicle?.stopStatus || 'in_transit_to' // FIXME: This may not be timezone adjusted as reported seconds may be in the wrong timezone. // All needed info to fix this is available via route.agency.timezone - // However, the needed coreUtils methods are not + // However, the needed coreUtils methods are not updated to support this return ( - - {name} - - {m}, + relativeTime: intl.formatRelativeTime(Math.floor(vehicle?.seconds - Date.now() / 1000)), + vehicleNameOrBlank: vehicleLabel }} /> - {vehicle?.speed > 0 && ( + {stopStatus !== 'STOPPED_AT' && vehicle?.speed > 0 && (
} {url && ( - + - + )} @@ -162,7 +161,7 @@ class RouteDetails extends Component { name='headsigns' onBlur={this._headSignButtonClicked} onChange={this._headSignButtonClicked} - value={viewedRoute.patternId} + value={viewedRoute?.patternId} > {!viewedRoute.patternId && } {headsigns.map((h) => ( diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 577da4c57..ac089ede9 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -1,13 +1,13 @@ -import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' -import { connect } from 'react-redux' -import PropTypes from 'prop-types' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' import { FormattedMessage, injectIntl } from 'react-intl' +import coreUtils from '@opentripplanner/core-utils' +import PropTypes from 'prop-types' -import Icon from '../util/icon' -import { getVehiclePositionsForRoute, findRoutes, findRoute } from '../../actions/api' import { getModeFromRoute } from '../../util/viewer' +import { getVehiclePositionsForRoute, findRoutes, findRoute } from '../../actions/api' +import Icon from '../util/icon' import { setMainPanelContent, setViewedRoute, @@ -143,9 +143,7 @@ class RouteViewer extends Component { return (
{/* Header Block */} -
+
{/* Always show back button, as we don't write a route anymore */}