From bc7e27e4a6b7cb59060dc69f3e3169a3cbeece06 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 19 Aug 2021 16:46:40 +0200 Subject: [PATCH 01/36] 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 1c0655b5d871a036173fd7d626e859a0a135d84c Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 24 Aug 2021 12:32:50 +0100 Subject: [PATCH 02/36] 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 03/36] 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 09427705e6902c3ffc73a5da7e7c49be9d85a33b Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 25 Aug 2021 18:27:09 +0100 Subject: [PATCH 04/36] 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 05/36] 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 06/36] 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 07/36] 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 08/36] 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 09/36] 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 10/36] 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 11/36] 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 12/36] 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 13/36] 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 14/36] 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 15/36] 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 16/36] 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 17/36] 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 18/36] 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 19/36] 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 237eeac12b159af14161f9db9a5a940397451013 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 30 Aug 2021 11:42:58 +0100 Subject: [PATCH 20/36] 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 21/36] 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 22/36] 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 23/36] 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) => (
-
+
+ {route && ModeIcon && ( + + )} {route.shortName}
@@ -257,6 +268,7 @@ class RouteViewer extends Component { findRoute={findRoute} getVehiclePositionsForRoute={getVehiclePositionsForRoute} initialRender={initialRender} + intl={intl} isActive={viewedRoute && viewedRoute.routeId === route.id} key={route.id} operator={operator} @@ -384,32 +396,52 @@ class RouteRow extends PureComponent { } render () { - const { isActive, operator, route } = this.props + const { intl, isActive, operator, route } = this.props const { ModeIcon } = this.context - const { backgroundColor, color, longName } = getColorAndNameFromRoute( + const { backgroundColor, color, longName, potentiallyUnreadableBackgroundColor } = getColorAndNameFromRoute( operator, route ) return ( - + {operator && operator.logo && ( - + )} - + @@ -420,9 +452,7 @@ class RouteRow extends PureComponent { enter={{ animation: 'slideDown' }} leave={{ animation: 'slideUp' }} > - {isActive && ( - - )} + {isActive && } ) diff --git a/lib/components/viewers/styled.js b/lib/components/viewers/styled.js index b885b32aa..0a47d0b03 100644 --- a/lib/components/viewers/styled.js +++ b/lib/components/viewers/styled.js @@ -70,7 +70,7 @@ export const StopContainer = styled.div` background-color: #fff; overflow-y: scroll; height: 100%; - /* 150px bottom padding is needed to ensure all stops + /* 100px bottom padding is needed to ensure all stops are shown when browsers don't calculate 100% sensibly */ padding: 15px 0 100px; ` diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 430cb87c9..f466fd75d 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -63,6 +63,11 @@ font-weight: 700; font-size: 24px; } +.otp .route-viewer .header-text.route-expanded { + display: flex; + align-items: center; + gap: 10px; +} /* stop viewer styles */ diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 14f864d54..fe76ec6fa 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -288,14 +288,35 @@ function getCleanRouteLongName ({ longNameSplitter, route }) { */ export function getColorAndNameFromRoute (operator, route) { const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {} - const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` + const potentiallyUnreadableBackgroundColor = `#${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 contrastColor = getContrastYIQ(potentiallyUnreadableBackgroundColor) 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 } + + // 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 + } } From d5080c92bb7a2e113eb24b2f2c96c4e1dd35f405 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 15 Sep 2021 16:16:52 +0100 Subject: [PATCH 30/36] refactor(connected-transit-vehicle-overlay): adjust for latest OTP changes --- lib/components/map/connected-transit-vehicle-overlay.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/components/map/connected-transit-vehicle-overlay.js b/lib/components/map/connected-transit-vehicle-overlay.js index 32a4def69..f9de38a68 100644 --- a/lib/components/map/connected-transit-vehicle-overlay.js +++ b/lib/components/map/connected-transit-vehicle-overlay.js @@ -45,6 +45,8 @@ function VehicleTooltip (props) { name = `${mode}${name && ' '}${name}` } + 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 @@ -83,7 +85,7 @@ function VehicleTooltip (props) { {vehicle?.nextStopName && (
Date: Thu, 16 Sep 2021 11:30:56 +0100 Subject: [PATCH 31/36] revert(RouteDetails): remove color; remove headsign buttons --- i18n/en-US.yml | 3 +- lib/components/viewers/route-details.js | 48 ++++++++++++------- lib/components/viewers/route-viewer.js | 62 +++++++++---------------- lib/components/viewers/styled.js | 41 ++++++---------- lib/util/viewer.js | 20 ++------ 5 files changed, 72 insertions(+), 102 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 9452d7f84..841a28c53 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -39,7 +39,8 @@ components: RouteDetails: operatedBy: "Operated by {agencyName}" moreDetails: "More Details" - stopsTo: "Stops To" + stopsTo: "Towards" + selectADirection: "Select a direction..." RouteViewer: allAgencies: All Agencies allModes: All Modes # Note to translator: This text is width-constrained. diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.js index 101982e6e..0dbe4ba69 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.js @@ -1,8 +1,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 { FormattedMessage, injectIntl } from 'react-intl' import Icon from '../util/icon' import { extractHeadsignFromPattern, getColorAndNameFromRoute } from '../../util/viewer' @@ -63,7 +62,9 @@ class RouteDetails extends Component { * If a headsign link is clicked, set that pattern in redux state so that the * view can adjust */ - _headSignButtonClicked = (id) => { + _headSignButtonClicked = (e) => { + const { target } = e + const { value: id } = target const { route, setViewedRoute } = this.props setViewedRoute({ patternId: id, routeId: route.id }) }; @@ -77,12 +78,11 @@ class RouteDetails extends Component { }; render () { - const { className, operator, pattern, route, setHoveredStop } = this.props + const { intl, operator, pattern, route, setHoveredStop, viewedRoute } = this.props const { agency, patterns, url } = route const { - backgroundColor: routeColor, - color: textColor + backgroundColor: routeColor } = getColorAndNameFromRoute(operator, route) const headsigns = @@ -131,9 +131,10 @@ class RouteDetails extends Component { return bVehicleCount - aVehicleCount }) + // if no pattern is set, we are in the routeRow return ( - - + + {agency && } {url && ( - + )} - -

+ +

+ +

{headsigns && - headsigns.map((h) => ( - + ))} + }
{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 32/36] 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 33/36] 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 34/36] 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 */}