diff --git a/lib/components/util/types.ts b/lib/components/util/types.ts index d75469556..febaa585d 100644 --- a/lib/components/util/types.ts +++ b/lib/components/util/types.ts @@ -1,3 +1,5 @@ +import { Route } from '@opentripplanner/types' + // TYPESCRIPT TODO: move this to a larger shared types file, preferably within otp-ui export interface StopData { bikeRental: BikeRental @@ -22,15 +24,6 @@ export interface BikeRental { stations: any[] } -export interface Route { - agencyId: string - agencyName: string - id: string - longName: string - mode: string - sortOrder: number -} - // FIXME: incomplete export interface StopTime { departureDelay: number @@ -45,6 +38,11 @@ export interface Pattern { desc: string headsign: string id: string + patternGeometry?: { + length: number + points: string + } + stops?: StopData[] } export interface Time { @@ -80,14 +78,18 @@ export interface ViewedRouteState { routeId: string } +export interface RouteVehicle { + patternId: string +} + // Routes have many properties beside id, but none of these are guaranteed. -export interface ViewedRouteObject { - id: string - longName?: string +export interface ViewedRouteObject extends Route { patterns?: Record pending?: boolean - shortName?: string - textColor?: string + url?: string + vehicles?: RouteVehicle[] } export type SetViewedRouteHandler = (route?: ViewedRouteState) => void + +export type SetViewedStopHandler = (payload: { stopId: string }) => void diff --git a/lib/components/viewers/pattern-viewer.tsx b/lib/components/viewers/pattern-viewer.tsx index 62102089e..1f6204633 100644 --- a/lib/components/viewers/pattern-viewer.tsx +++ b/lib/components/viewers/pattern-viewer.tsx @@ -131,11 +131,7 @@ const PatternViewer = ({ - + ) } diff --git a/lib/components/viewers/route-details.js b/lib/components/viewers/route-details.tsx similarity index 55% rename from lib/components/viewers/route-details.js rename to lib/components/viewers/route-details.tsx index b3d7099f3..579e376cc 100644 --- a/lib/components/viewers/route-details.js +++ b/lib/components/viewers/route-details.tsx @@ -1,20 +1,24 @@ -// FIXME: typescript -/* eslint-disable react/prop-types */ import { connect } from 'react-redux' -import { FormattedMessage, injectIntl } from 'react-intl' +import { FormattedMessage, injectIntl, IntlShape } from 'react-intl' import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route' - -import { LinkOpensNewWindow } from '../util/externalLink' -import PropTypes from 'prop-types' +import { TransitOperator } from '@opentripplanner/types' import React, { Component } from 'react' +import styled from 'styled-components' +import * as uiActions from '../../actions/ui' import { extractHeadsignFromPattern, getRouteColorBasedOnSettings } from '../../util/viewer' -import { findStopsForPattern } from '../../actions/api' import { getOperatorName } from '../../util/state' -import { setHoveredStop, setViewedRoute, setViewedStop } from '../../actions/ui' +import { LinkOpensNewWindow } from '../util/externalLink' +import { + SetViewedRouteHandler, + SetViewedStopHandler, + ViewedRouteObject +} from '../util/types' +import { SortResultsDropdown } from '../util/dropdown' +import { UnstyledButton } from '../util/unstyled-button' import { Container, @@ -26,41 +30,37 @@ import { StopContainer, StopLink } from './styled' -import { SortResultsDropdown } from '../util/dropdown' -import { UnstyledButton } from '../util/unstyled-button' - -import styled from 'styled-components' -class RouteDetails extends Component { - static propTypes = { - findStopsForPattern: findStopsForPattern.type, - 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 +const PatternSelectButton = styled(UnstyledButton)` + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } +` - /** - * Requests stop list for current pattern - */ - getStops = () => { - const { findStopsForPattern, pattern, route } = this.props - if (pattern && route) { - findStopsForPattern({ patternId: pattern.id, routeId: route.id }) - } - } +interface PatternSummary { + geometryLength: number + headsign: string + id: string +} + +interface Props { + intl: IntlShape + operator: TransitOperator + patternId: string + route: ViewedRouteObject + setHoveredStop: (id: string | null) => void + setViewedRoute: SetViewedRouteHandler + setViewedStop: SetViewedStopHandler +} +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 = (id: string) => { const { route, setViewedRoute } = this.props setViewedRoute({ patternId: id, routeId: route.id }) } @@ -68,80 +68,70 @@ class RouteDetails extends Component { /** * If a stop link is clicked, redirect to stop viewer */ - _stopLinkClicked = (stopId) => { + _stopLinkClicked = (stopId: string) => { const { setViewedStop } = this.props setViewedStop({ stopId }) } render() { - const { intl, operator, pattern, route, setHoveredStop, viewedRoute } = - this.props - const { agency, patterns, shortName, url } = route + const { intl, operator, patternId, route, setHoveredStop } = this.props + const { agency, patterns = {}, shortName, url } = route + const pattern = patterns[patternId] const routeColor = getRouteColorBasedOnSettings(operator, route) - const PatternSelectButton = styled(UnstyledButton)` - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - ` - - const headsigns = - patterns && - Object.entries(patterns) - .map((pattern) => { - return { - geometryLength: pattern[1].geometry?.length, - headsign: extractHeadsignFromPattern(pattern[1], shortName), - id: pattern[0] - } + const headsigns = Object.entries(patterns) + .map( + ([id, pat]): PatternSummary => ({ + geometryLength: pat.patternGeometry?.length || 0, + headsign: extractHeadsignFromPattern(pat, shortName), + id }) - // 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 - 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) + ) + // 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: PatternSummary[], cur) => { + const amended = prev + 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 } - 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 - - // if both have the same count, sort by pattern geometry length - if (aVehicleCount === bVehicleCount) { - return b.geometryLength - a.geometryLength - } - return bVehicleCount - aVehicleCount - }) + } else { + amended.push(cur) + } + return amended + }, []) + .sort((a, b) => { + // sort by number of vehicles on that pattern + const aVehicleCount = + route.vehicles?.filter((vehicle) => vehicle.patternId === a.id) + .length || 0 + const bVehicleCount = + route.vehicles?.filter((vehicle) => vehicle.patternId === b.id) + .length || 0 + + // if both have the same count, sort by pattern geometry length + if (aVehicleCount === bVehicleCount) { + return b.geometryLength - a.geometryLength + } + return bVehicleCount - aVehicleCount + }) const patternSelectLabel = intl.formatMessage({ id: 'components.RouteDetails.selectADirection' }) + const patternSelectName = - headsigns?.find((h) => h.id === viewedRoute?.patternId)?.headsign || + headsigns.find((h) => h.id === pattern?.id)?.headsign || patternSelectLabel - // if no pattern is set, we are in the routeRow return ( - {headsigns.map((h) => ( + {headsigns.map((h: PatternSummary) => (
  • this._headSignButtonClicked(h.id)} @@ -204,7 +194,7 @@ class RouteDetails extends Component {

    @@ -215,11 +205,11 @@ class RouteDetails extends Component { onMouseLeave={() => setHoveredStop(null)} textColor={getMostReadableTextColor(routeColor, route?.textColor)} > - {pattern?.stops?.map((stop) => ( + {pattern?.stops?.map((stop, index) => ( this._stopLinkClicked(stop.id)} - onFocus={() => setHoveredStop(stop.id)} onMouseOver={() => setHoveredStop(stop.id)} routeColor={ routeColor.includes('ffffff') ? '#333' : routeColor @@ -232,6 +222,7 @@ class RouteDetails extends Component { this._stopLinkClicked(stop.id)} + onFocus={() => setHoveredStop(stop.id)} textColor={getMostReadableTextColor( routeColor, route?.textColor @@ -250,20 +241,10 @@ class RouteDetails extends Component { } // connect to redux store -const mapStateToProps = (state) => { - return { - viewedRoute: state.otp.ui.viewedRoute - } -} - const mapDispatchToProps = { - findStopsForPattern, - setHoveredStop, - setViewedRoute, - setViewedStop + setHoveredStop: uiActions.setHoveredStop, + setViewedRoute: uiActions.setViewedRoute, + setViewedStop: uiActions.setViewedStop } -export default connect( - mapStateToProps, - mapDispatchToProps -)(injectIntl(RouteDetails)) +export default connect(null, mapDispatchToProps)(injectIntl(RouteDetails)) diff --git a/lib/components/viewers/styled.js b/lib/components/viewers/styled.ts similarity index 89% rename from lib/components/viewers/styled.js rename to lib/components/viewers/styled.ts index e1a4bd984..16bfbe173 100644 --- a/lib/components/viewers/styled.js +++ b/lib/components/viewers/styled.ts @@ -1,7 +1,14 @@ import styled from 'styled-components' +interface RenderProps { + backgroundColor?: string + full?: boolean + routeColor?: string + textColor?: string +} + /** Route Details */ -export const Container = styled.div` +export const Container = styled.div` background-color: ${(props) => props.full ? props.backgroundColor || '#ddd' : 'inherit'}; color: ${(props) => (props.full ? props.textColor : 'inherit')}; @@ -60,7 +67,7 @@ export const PatternContainer = styled.div` } ` -export const StopContainer = styled.ol` +export const StopContainer = styled.ol` color: ${(props) => props?.textColor || '#333'}; background-color: ${(props) => props?.backgroundColor || '#fff'}; overflow-y: scroll; @@ -69,7 +76,7 @@ export const StopContainer = styled.ol` are shown when browsers don't calculate 100% sensibly */ padding: 15px 0 100px; ` -export const StopLink = styled.button` +export const StopLink = styled.button` color: ${(props) => props?.textColor + 'da' || '#333'}; background-color: transparent; border: none; @@ -82,7 +89,8 @@ export const StopLink = styled.button` text-decoration: underline; } ` -export const Stop = styled.li` + +export const Stop = styled.li` cursor: pointer; display: block; white-space: nowrap;