Skip to content

Commit 17598ad

Browse files
Merge pull request #1276 from opentripplanner/pass-operator-logos-to-map-popup
Pass transit operator logos to map-popup
2 parents 4848925 + a0d87c5 commit 17598ad

File tree

9 files changed

+4032
-197
lines changed

9 files changed

+4032
-197
lines changed

__tests__/components/viewers/__snapshots__/nearby-view.js.snap

Lines changed: 3829 additions & 134 deletions
Large diffs are not rendered by default.

__tests__/components/viewers/__snapshots__/stop-schedule-viewer.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ exports[`components > viewers > stop viewer should render with initial stop id a
267267
</div>
268268
<styled.div>
269269
<div
270-
className="sc-liccgK jihWTk"
270+
className="sc-cuWdqJ jtAZHv"
271271
>
272272
<styled.div>
273273
<div

lib/actions/apiV2.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -499,15 +499,13 @@ export const fetchNearby = (position, radius) => {
499499
export const findStopTimesForStop = (params) =>
500500
function (dispatch, getState) {
501501
dispatch(fetchingStopTimesForStop(params))
502-
const { date, stopId } = params
502+
const { date, onlyRequestForOperators, stopId } = params
503503
const timeZone = getState().otp.config.homeTimezone
504504

505505
// Create a service date timestamp from 3:30am local.
506506
const serviceDay = getServiceStart(date, timeZone).getTime() / 1000
507507

508-
return dispatch(
509-
createGraphQLQueryAction(
510-
`query StopTimes(
508+
const fullStopTimesQuery = `query StopTimes(
511509
$serviceDay: Long!
512510
$stopId: String!
513511
) {
@@ -567,7 +565,48 @@ export const findStopTimesForStop = (params) =>
567565
}
568566
}
569567
}
570-
}`,
568+
}`
569+
570+
const shorterStopTimesQueryForOperators = `query StopTimes(
571+
$stopId: String!
572+
) {
573+
stop(id: $stopId) {
574+
gtfsId
575+
code
576+
routes {
577+
id: gtfsId
578+
agency {
579+
gtfsId
580+
name
581+
}
582+
patterns {
583+
id
584+
headsign
585+
}
586+
}
587+
stoptimesForPatterns(numberOfDepartures: 100, omitNonPickups: true, omitCanceled: false) {
588+
pattern {
589+
desc: name
590+
headsign
591+
id: code
592+
route {
593+
agency {
594+
gtfsId
595+
}
596+
gtfsId
597+
}
598+
}
599+
}
600+
}
601+
}`
602+
603+
const query = onlyRequestForOperators
604+
? shorterStopTimesQueryForOperators
605+
: fullStopTimesQuery
606+
607+
return dispatch(
608+
createGraphQLQueryAction(
609+
query,
571610
{
572611
serviceDay,
573612
stopId

lib/components/map/default-map.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// @ts-nocheck
44
import { connect } from 'react-redux'
55
import { GeolocateControl, NavigationControl } from 'react-map-gl'
6+
import { getCurrentDate } from '@opentripplanner/core-utils/lib/time'
67
import { injectIntl } from 'react-intl'
78
import BaseMap from '@opentripplanner/base-map'
89
import generateOTP2TileLayers from '@opentripplanner/otp2-tile-overlay'
@@ -13,6 +14,7 @@ import {
1314
assembleBasePath,
1415
bikeRentalQuery,
1516
carRentalQuery,
17+
findStopTimesForStop,
1618
vehicleRentalQuery
1719
} from '../../actions/api'
1820
import { ComponentContext } from '../../util/contexts'
@@ -22,6 +24,7 @@ import { MainPanelContent } from '../../actions/ui-constants'
2224
import { setLocation, setMapPopupLocationAndGeocode } from '../../actions/map'
2325
import { setViewedStop } from '../../actions/ui'
2426
import { updateOverlayVisibility } from '../../actions/config'
27+
import TransitOperatorIcons from '../util/connected-transit-operator-icons'
2528

2629
import ElevationPointMarker from './elevation-point-marker'
2730
import EndpointsOverlay from './connected-endpoints-overlay'
@@ -153,6 +156,17 @@ class DefaultMap extends Component {
153156
}
154157
}
155158

159+
// Generate operator logos to pass through OTP tile layer to map-popup
160+
getEntityPrefix = (entity) => {
161+
const stopId = entity.gtfsId
162+
this.props.findStopTimesForStop({
163+
date: getCurrentDate(),
164+
onlyRequestForOperators: true,
165+
stopId
166+
})
167+
return <TransitOperatorIcons stopId={stopId} />
168+
}
169+
156170
/**
157171
* Checks whether the modes have changed between old and new queries and
158172
* whether to update the map overlays accordingly (e.g., to show rental vehicle
@@ -407,7 +421,8 @@ class DefaultMap extends Component {
407421
setLocation,
408422
setViewedStop,
409423
viewedRouteStops,
410-
config.companies
424+
config.companies,
425+
this.getEntityPrefix
411426
)
412427
default:
413428
return null
@@ -468,6 +483,7 @@ const mapStateToProps = (state) => {
468483
const mapDispatchToProps = {
469484
bikeRentalQuery,
470485
carRentalQuery,
486+
findStopTimesForStop,
471487
getCurrentPosition,
472488
setLocation,
473489
setMapPopupLocationAndGeocode,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { connect } from 'react-redux'
2+
import { TransitOperator } from '@opentripplanner/types'
3+
import React from 'react'
4+
5+
import { AppReduxState } from '../../util/state-types'
6+
import { FETCH_STATUS } from '../../util/constants'
7+
8+
import { StopData } from './types'
9+
import TransitOperatorLogos from './transit-operator-icons'
10+
11+
interface Props {
12+
stopData?: StopData
13+
transitOperators: TransitOperator[]
14+
}
15+
16+
function TransitOperatorIcons({ stopData, transitOperators }: Props) {
17+
const loading = stopData?.fetchStatus === FETCH_STATUS.FETCHING
18+
return (
19+
<TransitOperatorLogos
20+
loading={loading}
21+
stopData={stopData}
22+
transitOperators={transitOperators}
23+
/>
24+
)
25+
}
26+
27+
const mapStateToProps = (
28+
state: AppReduxState,
29+
ownProps: Props & { stopId: string }
30+
) => {
31+
const stops = state.otp.transitIndex.stops
32+
return {
33+
stopData: stops?.[ownProps.stopId],
34+
transitOperators: state.otp.config.transitOperators || []
35+
}
36+
}
37+
38+
export default connect(mapStateToProps)(TransitOperatorIcons)

lib/components/util/operator-logo.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,32 @@ import { TransitOperator } from '@opentripplanner/types'
22
import React from 'react'
33
import styled from 'styled-components'
44

5-
const OperatorImg = styled.img`
5+
const OperatorImg = styled.img<{ marginRight?: number; maxHeight?: number }>`
66
&:not(:last-of-type) {
77
margin-right: 0.5ch;
88
}
99
width: 25px;
1010
`
1111

12+
const StyledOperatorImg = styled(OperatorImg)`
13+
margin-right: 0.5ch;
14+
max-height: 1em;
15+
// Make sure icons stay square
16+
max-width: 1em;
17+
`
18+
1219
type Props = {
1320
alt?: string
1421
operator?: TransitOperator
22+
styled?: boolean
1523
}
1624

17-
const OperatorLogo = ({ alt, operator }: Props): JSX.Element | null => {
25+
const OperatorLogo = ({ alt, operator, styled }: Props): JSX.Element | null => {
1826
if (!operator?.logo) return null
27+
if (styled) {
28+
return <StyledOperatorImg alt={alt || operator.name} src={operator.logo} />
29+
}
30+
1931
return <OperatorImg alt={alt || operator.name} src={operator.logo} />
2032
}
2133

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { MapPin } from '@styled-icons/fa-solid'
2+
import { useIntl } from 'react-intl'
3+
import React from 'react'
4+
import Skeleton from 'react-loading-skeleton'
5+
import type { TransitOperator } from '@opentripplanner/types'
6+
7+
import InvisibleA11yLabel from './invisible-a11y-label'
8+
import OperatorLogo from './operator-logo'
9+
import type { StopData } from './types'
10+
11+
const Operator = ({ operator }: { operator?: TransitOperator }) => {
12+
const intl = useIntl()
13+
14+
if (!operator) {
15+
return null
16+
} else {
17+
const operatorLogoAriaLabel = intl.formatMessage(
18+
{
19+
id: 'components.StopViewer.operatorLogoAriaLabel'
20+
},
21+
{
22+
operatorName: operator.name
23+
}
24+
)
25+
return operator.logo ? (
26+
// Span with agency classname allows optional contrast/customization in user
27+
// config for logos with poor contrast. Class name is hyphenated agency name
28+
// e.g. "sound-transit"
29+
<span
30+
className={
31+
operator.name ? operator.name.replace(/\s+/g, '-').toLowerCase() : ''
32+
}
33+
>
34+
<OperatorLogo alt={operatorLogoAriaLabel} operator={operator} styled />
35+
</span>
36+
) : (
37+
// If operator exists but logo is missing,
38+
// we still need to announce the operator name to screen readers.
39+
<>
40+
<MapPin />
41+
<InvisibleA11yLabel>{operatorLogoAriaLabel}</InvisibleA11yLabel>
42+
</>
43+
)
44+
}
45+
}
46+
47+
const TransitOperatorLogos = ({
48+
loading = false,
49+
stopData,
50+
transitOperators
51+
}: {
52+
loading?: boolean
53+
stopData?: StopData
54+
transitOperators?: TransitOperator[]
55+
}): JSX.Element => {
56+
const agencies =
57+
(stopData &&
58+
stopData.stoptimesForPatterns?.reduce<Set<string>>((prev, cur) => {
59+
// @ts-expect-error The agency type is not yet compatible with OTP2
60+
const agencyGtfsId = cur.pattern.route.agency?.gtfsId
61+
return agencyGtfsId ? prev.add(agencyGtfsId) : prev
62+
}, new Set())) ||
63+
new Set()
64+
65+
return (
66+
<>
67+
{loading ? (
68+
<Skeleton height={20} style={{ marginRight: '0.5ch' }} width={20} />
69+
) : (
70+
transitOperators
71+
?.filter((to) => Array.from(agencies).includes(to.agencyId))
72+
// Second pass to remove duplicates based on name
73+
.filter(
74+
(to, index, arr) =>
75+
index === arr.findIndex((t) => t?.name === to?.name)
76+
)
77+
.map((to) => <Operator key={to.agencyId} operator={to} />)
78+
)}
79+
</>
80+
)
81+
}
82+
83+
export default TransitOperatorLogos

lib/components/viewers/nearby/stop-card-header.tsx

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { connect } from 'react-redux'
22
import { FormattedMessage, useIntl } from 'react-intl'
3-
import { MapPin } from '@styled-icons/fa-solid'
43
import { Search } from '@styled-icons/fa-solid/Search'
54
import { TransitOperator } from '@opentripplanner/types'
65
import React, { ComponentType } from 'react'
@@ -10,8 +9,8 @@ import { Icon, IconWithText } from '../../util/styledIcon'
109
import { StopData } from '../../util/types'
1110
import InvisibleA11yLabel from '../../util/invisible-a11y-label'
1211
import Link from '../../util/link'
13-
import OperatorLogo from '../../util/operator-logo'
1412
import Strong from '../../util/strong-text'
13+
import TransitOperatorLogos from '../../util/transit-operator-icons'
1514

1615
import { CardBody, CardHeader, CardTitle } from './styled'
1716
import DistanceDisplay from './distance-display'
@@ -28,41 +27,6 @@ type Props = {
2827
transitOperators?: TransitOperator[]
2928
}
3029

31-
const Operator = ({ operator }: { operator?: TransitOperator }) => {
32-
const intl = useIntl()
33-
if (!operator) {
34-
return null
35-
} else {
36-
const operatorLogoAriaLabel = intl.formatMessage(
37-
{
38-
id: 'components.StopViewer.operatorLogoAriaLabel'
39-
},
40-
{
41-
operatorName: operator.name
42-
}
43-
)
44-
return operator.logo ? (
45-
// Span with agency classname allows optional contrast/customization in user
46-
// config for logos with poor contrast. Class name is hyphenated agency name
47-
// e.g. "sound-transit"
48-
<span
49-
className={
50-
operator.name ? operator.name.replace(/\s+/g, '-').toLowerCase() : ''
51-
}
52-
>
53-
<OperatorLogo alt={operatorLogoAriaLabel} operator={operator} />
54-
</span>
55-
) : (
56-
// If operator exists but logo is missing,
57-
// we still need to announce the operator name to screen readers.
58-
<>
59-
<MapPin />
60-
<InvisibleA11yLabel>{operatorLogoAriaLabel}</InvisibleA11yLabel>
61-
</>
62-
)
63-
}
64-
}
65-
6630
const StopCardHeader = ({
6731
actionIcon,
6832
actionParams,
@@ -75,12 +39,7 @@ const StopCardHeader = ({
7539
transitOperators
7640
}: Props): JSX.Element => {
7741
const intl = useIntl()
78-
const agencies =
79-
stopData.stoptimesForPatterns?.reduce<Set<string>>((prev, cur) => {
80-
// @ts-expect-error The agency type is not yet compatible with OTP2
81-
const agencyGtfsId = cur.pattern.route.agency?.gtfsId
82-
return agencyGtfsId ? prev.add(agencyGtfsId) : prev
83-
}, new Set()) || new Set()
42+
8443
const zoomButtonText = onZoomClick
8544
? intl.formatMessage({
8645
id: 'components.StopViewer.zoomToStop'
@@ -92,16 +51,10 @@ const StopCardHeader = ({
9251
<CardHeader>
9352
{/* @ts-expect-error The 'as' prop in styled-components is not listed for TypeScript. */}
9453
<CardTitle as={titleAs}>
95-
{transitOperators
96-
?.filter((to) => Array.from(agencies).includes(to.agencyId))
97-
// Second pass to remove duplicates based on name
98-
.filter(
99-
(to, index, arr) =>
100-
index === arr.findIndex((t) => t?.name === to?.name)
101-
)
102-
.map((to) => (
103-
<Operator key={to.agencyId} operator={to} />
104-
))}
54+
<TransitOperatorLogos
55+
stopData={stopData}
56+
transitOperators={transitOperators}
57+
/>
10558
<span>{stopData.name}</span>
10659
</CardTitle>
10760
<DistanceDisplay distance={stopData.distance} />

lib/components/viewers/nearby/styled.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export const CardTitle = styled.p`
4848
display: flex;
4949
font-size: 22px;
5050
font-weight: 600;
51-
gap: 0.5ch;
5251
grid-column: 1;
5352
margin: 0;
5453
/* Prevent svg and images to be taller than the text. */

0 commit comments

Comments
 (0)