diff --git a/lib/components/narrative/line-itin/connected-itinerary-body.js b/lib/components/narrative/line-itin/connected-itinerary-body.js index 5c6ba0148..ce38a6f73 100644 --- a/lib/components/narrative/line-itin/connected-itinerary-body.js +++ b/lib/components/narrative/line-itin/connected-itinerary-body.js @@ -12,6 +12,7 @@ import styled from 'styled-components' import { showLegDiagram } from '../../../actions/map' import { setViewedTrip } from '../../../actions/ui' import TransitLegSubheader from './connected-transit-leg-subheader' +import RealtimeTimeColumn from './realtime-time-column' import TripDetails from '../connected-trip-details' import TripTools from '../trip-tools' @@ -67,6 +68,7 @@ class ConnectedItineraryBody extends Component { toRouteAbbreviation={noop} TransitLegSubheader={TransitLegSubheader} TransitLegSummary={TransitLegSummary} + TimeColumnContent={RealtimeTimeColumn} /> diff --git a/lib/components/narrative/line-itin/realtime-time-column.js b/lib/components/narrative/line-itin/realtime-time-column.js new file mode 100644 index 000000000..3cf031fee --- /dev/null +++ b/lib/components/narrative/line-itin/realtime-time-column.js @@ -0,0 +1,139 @@ +import { isTransit } from '@opentripplanner/core-utils/lib/itinerary' +import { + legType, + timeOptionsType +} from '@opentripplanner/core-utils/lib/types' +import { formatTime } from '@opentripplanner/core-utils/lib/time' +import PropTypes from 'prop-types' +import React from 'react' +import styled from 'styled-components' + +const TimeText = styled.div`` + +const TimeStruck = styled.div` + text-decoration: line-through; +` + +const TimeBlock = styled.div` + line-height: 1em; + margin-bottom: 4px; +` + +const TimeColumnBase = styled.div`` + +const StatusText = styled.div` + color: #bbb; + font-size: 80%; + line-height: 1em; +` + +const DelayText = styled.span` + display: block; + white-space: nowrap; +` + +// Reusing stop viewer colors. +const TimeColumnOnTime = styled(TimeColumnBase)` + ${TimeText}, ${StatusText} { + color: #5cb85c; + } +` +const TimeColumnEarly = styled(TimeColumnBase)` + ${TimeText}, ${StatusText} { + color: #337ab7; + } +` +const TimeColumnLate = styled(TimeColumnBase)` + ${TimeText}, ${StatusText} { + color: #d9534f; + } +` + +/** + * This component displays the scheduled departure/arrival time for a leg, + * and, for transit legs, displays any delays or earliness where applicable. + */ +export default function RealtimeTimeColumn ({ + isDestination, + leg, + timeOptions +}) { + const time = isDestination ? leg.endTime : leg.startTime + const formattedTime = time && formatTime(time, timeOptions) + const isTransitLeg = isTransit(leg.mode) + + // For non-real-time legs, show only the scheduled time, + // except for transit legs where we add the "scheduled" text underneath. + if (!leg.realTime) { + return ( + <> + {formattedTime} + {isTransitLeg && scheduled} + + ) + } + + // Delay in seconds. + const delay = isDestination ? leg.arrivalDelay : leg.departureDelay + // Time is in milliseconds. + const originalTime = time - delay * 1000 + const originalFormattedTime = + originalTime && formatTime(originalTime, timeOptions) + + // TODO: refine on-time thresholds. + // const isOnTime = delay >= -60 && delay <= 120; + const isOnTime = delay === 0 + + let statusText + let TimeColumn = TimeColumnBase + if (isOnTime) { + statusText = 'on time' + TimeColumn = TimeColumnOnTime + } else if (delay < 0) { + statusText = 'early' + TimeColumn = TimeColumnEarly + } else if (delay > 0) { + statusText = 'late' + TimeColumn = TimeColumnLate + } + + // Absolute delay in rounded minutes, for display purposes. + const delayInMinutes = Math.abs( + Math.round((isDestination ? leg.arrivalDelay : leg.departureDelay) / 60) + ) + + let renderedTime + if (!isOnTime) { + // If the transit vehicle is not on time, strike the original scheduled time + // and display the updated time underneath. + renderedTime = ( + + {!isOnTime && {originalFormattedTime}} + {formattedTime} + + ) + } else { + renderedTime = {formattedTime} + } + + return ( + + {renderedTime} + + {/* Keep the '5 min' string on the same line. */} + {!isOnTime && {delayInMinutes} min} + {statusText} + + + ) +} + +RealtimeTimeColumn.propTypes = { + isDestination: PropTypes.bool.isRequired, + leg: legType.isRequired, + timeOptions: timeOptionsType +} + +RealtimeTimeColumn.defaultProps = { + timeOptions: null +} diff --git a/package.json b/package.json index cf7d1992a..8e1aa9ac6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@opentripplanner/geocoder": "^1.0.2", "@opentripplanner/humanize-distance": "^0.0.22", "@opentripplanner/icons": "^1.0.1", - "@opentripplanner/itinerary-body": "^1.0.2", + "@opentripplanner/itinerary-body": "^1.1.0", "@opentripplanner/location-field": "^1.0.2", "@opentripplanner/location-icon": "^1.0.0", "@opentripplanner/park-and-ride-overlay": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index 517f5470a..e49301f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1331,12 +1331,12 @@ "@opentripplanner/core-utils" "^1.2.0" prop-types "^15.7.2" -"@opentripplanner/itinerary-body@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@opentripplanner/itinerary-body/-/itinerary-body-1.0.2.tgz#f280932a13723f49bac92c79feb83a088d14329b" - integrity sha512-3x8UvtkL3WmUeNTeWsWTJJtR2FqcWU42xw4833+6pMMzPoS2U4bYzSbw7ucQT64vqcoRQ0ykUjE7as32W+VgGg== +"@opentripplanner/itinerary-body@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/itinerary-body/-/itinerary-body-1.1.0.tgz#6fabc389ad25f6f14db5a5861603a127530e0d9e" + integrity sha512-svu2A0z+CnL5vrkDXuaMjJaDoSC4d52utzdGfgkXdfMaK9prO1D/6SHWiVILXmaDJExdBAoF3mxSuA7xPItTTA== dependencies: - "@opentripplanner/core-utils" "^1.2.0" + "@opentripplanner/core-utils" "^2.1.0" "@opentripplanner/humanize-distance" "^0.0.22" "@opentripplanner/icons" "^1.0.0" "@opentripplanner/location-icon" "^1.0.0"