Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 21 additions & 97 deletions lib/components/narrative/line-itin/realtime-time-column.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,17 @@ import PropTypes from 'prop-types'
import React from 'react'
import styled from 'styled-components'

const TimeText = styled.div``
import RealtimeStatusLabel, { DelayText, MainContent } from '../../viewers/realtime-status-label'

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 StyledStatusLabel = styled(RealtimeStatusLabel)`
${MainContent} {
font-size: 80%;
line-height: 1em;
}
`
const TimeColumnEarly = styled(TimeColumnBase)`
${TimeText}, ${StatusText} {
color: #337ab7;
}
`
const TimeColumnLate = styled(TimeColumnBase)`
${TimeText}, ${StatusText} {
color: #d9534f;
${DelayText} {
display: block;
}
`

/**
* This component displays the scheduled departure/arrival time for a leg,
* and, for transit legs, displays any delays or earliness where applicable.
Expand All @@ -61,75 +31,27 @@ function RealtimeTimeColumn ({
const time = isDestination ? leg.endTime : leg.startTime
const formattedTime = time && formatTime(time, timeOptions)
const isTransitLeg = isTransit(leg.mode)
const isRealtimeTransitLeg = isTransitLeg && leg.realTime

// 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 (
<>
<TimeText>{formattedTime}</TimeText>
{isTransitLeg && <StatusText>scheduled</StatusText>}
</>
)
// For non-transit legs show only the scheduled time.
if (!isTransitLeg) {
return <div>{formattedTime}</div>
}

// Delay in seconds.
const delay = isDestination ? leg.arrivalDelay : leg.departureDelay
// Time is in milliseconds.
const originalTime = time - delay * 1000
const delaySeconds = isDestination ? leg.arrivalDelay : leg.departureDelay
const originalTimeMillis = time - delaySeconds * 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 = (
<TimeBlock>
{!isOnTime && <TimeStruck>{originalFormattedTime}</TimeStruck>}
<TimeText>{formattedTime}</TimeText>
</TimeBlock>
)
} else {
renderedTime = <TimeText>{formattedTime}</TimeText>
}
originalTimeMillis && formatTime(originalTimeMillis, timeOptions)

return (
<TimeColumn>
{renderedTime}
<StatusText>
{/* Keep the '5 min' string on the same line. */}
{!isOnTime && <DelayText>{delayInMinutes} min</DelayText>}
{statusText}
</StatusText>
</TimeColumn>
<StyledStatusLabel
delay={delaySeconds}
isRealtime={isRealtimeTransitLeg}
originalTime={originalFormattedTime}
time={formattedTime} />
)
}

export default RealtimeTimeColumn

RealtimeTimeColumn.propTypes = {
isDestination: PropTypes.bool.isRequired,
leg: legType.isRequired,
Expand All @@ -139,3 +61,5 @@ RealtimeTimeColumn.propTypes = {
RealtimeTimeColumn.defaultProps = {
timeOptions: null
}

export default RealtimeTimeColumn
18 changes: 9 additions & 9 deletions lib/components/viewers/pattern-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, { Component } from 'react'
import { VelocityTransitionGroup } from 'velocity-react'

import Icon from '../narrative/icon'
import { getFormattedStopTime, getStatusLabel } from '../../util/viewer'
import { getFormattedStopTime } from '../../util/viewer'
import RealtimeStatusLabel from './realtime-status-label'

/**
* Represents a single pattern row for displaying arrival times in the stop
Expand Down Expand Up @@ -90,6 +91,7 @@ export default class PatternRow extends Component {
{/* list of upcoming trips */}
{hasStopTimes && (
sortedStopTimes.map((stopTime, i) => {
const { departureDelay: delay, realtimeState } = stopTime
return (
<div
className='trip-row'
Expand All @@ -102,14 +104,12 @@ export default class PatternRow extends Component {
{getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat)}
</div>
<div className='cell status-column'>
{stopTime.realtimeState === 'UPDATED'
? getStatusLabel(stopTime.departureDelay)
: <div
className='status-label'
style={{ backgroundColor: '#bbb' }}>
Scheduled
</div>
}
<RealtimeStatusLabel
className='status-label'
delay={delay}
isRealtime={realtimeState === 'UPDATED'}
withBackground
/>
</div>
</div>
)
Expand Down
97 changes: 97 additions & 0 deletions lib/components/viewers/realtime-status-label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { formatDuration } from '@opentripplanner/core-utils/lib/time'
import React from 'react'
import styled from 'styled-components'

import { getTripStatus, REALTIME_STATUS } from '../../util/viewer'

// If shown, keep the '5 min' portion of the status string on the same line.
export const DelayText = styled.span`
white-space: nowrap;
`

export const MainContent = styled.div``

const Container = styled.div`
${props => props.withBackground
? `background-color: ${props.color};`
: `color: ${props.color};`
}
`

const TimeStruck = styled.div`
text-decoration: line-through;
`

const TimeBlock = styled.div`
line-height: 1em;
margin-bottom: 4px;
`

const STATUS = {
EARLY: {
color: '#337ab7',
label: 'early'
},
LATE: {
color: '#d9534f',
label: 'late'
},
ON_TIME: {
color: '#5cb85c',
label: 'on time'
},
SCHEDULED: {
label: 'scheduled'
}
}

/**
* This component renders a string such as '5 min late' or 'on time'
* while keeping the '5 min' portion on the same line.
*
* If the formatted time/original time values (e.g. 5:11 pm) are provided, they
* will be rendered above the status. Also, this can optionally be rendered with
* a background color for a label-like presentation.
*/
const RealtimeStatusLabel = ({ withBackground, className, delay, isRealtime, originalTime, time }) => {
const status = getTripStatus(isRealtime, delay)
const isEarlyOrLate = status === REALTIME_STATUS.EARLY || status === REALTIME_STATUS.LATE
// Use a default background color if the status object doesn't set a color
// (e.g. for 'Scheduled' status), but only in withBackground mode.
const color = STATUS[status].color || (withBackground && '#bbb')
// Render time if provided.
let renderedTime
if (time) {
// If transit vehicle is not on time, strike the original scheduled time
// and display the updated time underneath.
renderedTime = isEarlyOrLate
? (
<TimeBlock>
<TimeStruck>{originalTime}</TimeStruck>
<div>{time}</div>
</TimeBlock>
)
: <div>{time}</div>
}
return (
<Container
withBackground={withBackground}
className={className}
color={color}
>
{renderedTime}
<MainContent>
{isEarlyOrLate &&
<DelayText>
{formatDuration(Math.abs(delay))}
{/* A spacer is needed between '5 min' and 'early/late'. */}
{' '}
</DelayText>
}
{STATUS[status].label}
</MainContent>
</Container>
)
}

export default RealtimeStatusLabel
11 changes: 6 additions & 5 deletions lib/components/viewers/viewers.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,15 @@
}

.otp .stop-viewer .trip-table .status-label {
width: 100%;
text-align: center;
text-transform: uppercase;
font-weight: 500;
border-radius: 2px;
font-size: 11px;
color: white;
display: block;
font-size: 11px;
font-weight: 500;
padding: 2px 0px 0px 0px;
text-align: center;
text-transform: uppercase;
width: 100%;
}

/* trip viewer styles */
Expand Down
60 changes: 32 additions & 28 deletions lib/util/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,34 +100,6 @@ export function getRouteIdForPattern (pattern) {
return routeId
}

// helper method to generate status label
export function getStatusLabel (delay) {
// late departure
if (delay > 60) {
return (
<div className='status-label' style={{ backgroundColor: '#d9534f' }}>
{formatDuration(delay)} Late
</div>
)
}

// early departure
if (delay < -60) {
return (
<div className='status-label' style={{ backgroundColor: '#337ab7' }}>
{formatDuration(Math.abs(delay))} Early
</div>
)
}

// on-time departure
return (
<div className='status-label' style={{ backgroundColor: '#5cb85c' }}>
On Time
</div>
)
}

export function getStopTimesByPattern (stopData) {
const stopTimesByPattern = {}
if (stopData && stopData.routes && stopData.stopTimes) {
Expand Down Expand Up @@ -185,3 +157,35 @@ export function getModeFromRoute (route) {
}
return route.mode || modeLookup[route.type]
}

/**
* Enum to represent transit realtime status for trips/stop times.
*/
export const REALTIME_STATUS = {
EARLY: 'EARLY',
LATE: 'LATE',
ON_TIME: 'ON_TIME',
SCHEDULED: 'SCHEDULED'
}

/**
* Get one of the realtime states (on-time, late...) if a leg/stoptime is
* registering realtime info and given a delay value in seconds.
*/
export function getTripStatus (isRealtime, delaySeconds) {
if (isRealtime) {
if (delaySeconds > 60) {
// late departure
return REALTIME_STATUS.LATE
} else if (delaySeconds < -60) {
// early departure
return REALTIME_STATUS.EARLY
} else {
// on-time departure
return REALTIME_STATUS.ON_TIME
}
Comment on lines +177 to +186
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider basing this PR off of #305 and then refactor this function so that it uses the configurable on-time threshold.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since #305 was introduced after the latest commit here, I think it might be unfair to request this. Therefore, I'm going to merge this and make #305 dependent on this.

} else {
// Schedule only
return REALTIME_STATUS.SCHEDULED
}
}