Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass transit operator logos to map-popup #1276

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
49 changes: 44 additions & 5 deletions lib/actions/apiV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,15 +498,13 @@ export const fetchNearby = (position, radius) => {
export const findStopTimesForStop = (params) =>
function (dispatch, getState) {
dispatch(fetchingStopTimesForStop(params))
const { date, stopId } = params
const { date, onlyRequestForOperators, stopId } = params
const timeZone = getState().otp.config.homeTimezone

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

return dispatch(
createGraphQLQueryAction(
`query StopTimes(
const fullStopTimesQuery = `query StopTimes(
$serviceDay: Long!
$stopId: String!
) {
Expand Down Expand Up @@ -566,7 +564,48 @@ export const findStopTimesForStop = (params) =>
}
}
}
}`,
}`

const shorterStopTimesQueryForOperators = `query StopTimes(
$stopId: String!
) {
stop(id: $stopId) {
gtfsId
code
routes {
id: gtfsId
agency {
gtfsId
name
}
patterns {
id
headsign
}
}
stoptimesForPatterns(numberOfDepartures: 100, omitNonPickups: true, omitCanceled: false) {
pattern {
desc: name
headsign
id: code
route {
agency {
gtfsId
}
gtfsId
}
}
}
}
}`

const query = onlyRequestForOperators
? shorterStopTimesQueryForOperators
: fullStopTimesQuery

return dispatch(
createGraphQLQueryAction(
query,
{
serviceDay,
stopId
Expand Down
18 changes: 17 additions & 1 deletion lib/components/map/default-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// @ts-nocheck
import { connect } from 'react-redux'
import { GeolocateControl, NavigationControl } from 'react-map-gl'
import { getCurrentDate } from '@opentripplanner/core-utils/lib/time'
import { injectIntl } from 'react-intl'
import BaseMap from '@opentripplanner/base-map'
import generateOTP2TileLayers from '@opentripplanner/otp2-tile-overlay'
Expand All @@ -13,6 +14,7 @@ import {
assembleBasePath,
bikeRentalQuery,
carRentalQuery,
findStopTimesForStop,
vehicleRentalQuery
} from '../../actions/api'
import { ComponentContext } from '../../util/contexts'
Expand All @@ -34,6 +36,7 @@ import RoutePreviewOverlay from './route-preview-overlay'
import RouteViewerOverlay from './connected-route-viewer-overlay'
import StopsOverlay from './connected-stops-overlay'
import TransitiveOverlay from './connected-transitive-overlay'
import TransitOperatorLogos from '../util/connected-transit-operator-icons'
import TransitVehicleOverlay from './connected-transit-vehicle-overlay'
import TripViewerOverlay from './connected-trip-viewer-overlay'
import VehicleRentalOverlay from './connected-vehicle-rental-overlay'
Expand Down Expand Up @@ -153,6 +156,17 @@ class DefaultMap extends Component {
}
}

// Generate operator logos to pass through OTP tile layer to map-popup
getEntityPrefix = (entity) => {
const stopId = entity.gtfsId
this.props.findStopTimesForStop({
date: getCurrentDate(),
onlyRequestForOperators: true,
stopId
})
return <TransitOperatorLogos stopId={stopId} />
}

/**
* Checks whether the modes have changed between old and new queries and
* whether to update the map overlays accordingly (e.g., to show rental vehicle
Expand Down Expand Up @@ -405,7 +419,8 @@ class DefaultMap extends Component {
vectorTilesEndpoint,
setLocation,
setViewedStop,
config.companies
config.companies,
this.getEntityPrefix
)
default:
return null
Expand Down Expand Up @@ -445,6 +460,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = {
bikeRentalQuery,
carRentalQuery,
findStopTimesForStop,
getCurrentPosition,
setLocation,
setMapPopupLocationAndGeocode,
Expand Down
46 changes: 46 additions & 0 deletions lib/components/util/connected-transit-operator-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { connect } from 'react-redux'
import { TransitOperator } from '@opentripplanner/types'
import React, { Component } from 'react'

import { AppReduxState } from '../../util/state-types'
import { FETCH_STATUS } from '../../util/constants'

import { StopData } from './types'
import TransitOperatorLogos from './transit-operator-icons'

interface Props {
stopData?: StopData
transitOperators: TransitOperator[]
}

class ConnectedTransitOperatorLogos extends Component<Props> {
render() {
const loading = this.props.stopData?.fetchStatus === FETCH_STATUS.FETCHING
return (
<TransitOperatorLogos
loading={loading}
stopData={this.props.stopData}
transitOperators={this.props.transitOperators}
/>
)
}
}

const mapDispatchToProps = {}

const mapStateToProps = (
state: AppReduxState,
ownProps: Props & { stopId: string }
) => {
const stops = state.otp.transitIndex.stops
// clean this up
return {
stopData: stops?.[ownProps.stopId],
transitOperators: state.otp.config.transitOperators || []
}
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(ConnectedTransitOperatorLogos)
24 changes: 21 additions & 3 deletions lib/components/util/operator-logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,39 @@ import { TransitOperator } from '@opentripplanner/types'
import React from 'react'
import styled from 'styled-components'

const OperatorImg = styled.img`
const OperatorImg = styled.img<{ marginRight?: number; maxHeight?: number }>`
&:not(:last-of-type) {
margin-right: 0.5ch;
}
margin-right: ${(props) => props.marginRight && props.marginRight}ch;
max-height: ${(props) => props.maxHeight && props.maxHeight}em;
// Make sure icons stay square
max-width: ${(props) => props.maxHeight && props.maxHeight}em;
width: 25px;
`

type Props = {
alt?: string
marginRight?: number
maxHeight?: number
operator?: TransitOperator
}

const OperatorLogo = ({ alt, operator }: Props): JSX.Element | null => {
const OperatorLogo = ({
alt,
marginRight,
maxHeight,
operator
}: Props): JSX.Element | null => {
if (!operator?.logo) return null
return <OperatorImg alt={alt || operator.name} src={operator.logo} />
return (
<OperatorImg
alt={alt || operator.name}
marginRight={marginRight}
maxHeight={maxHeight}
src={operator.logo}
/>
)
}

export default OperatorLogo
88 changes: 88 additions & 0 deletions lib/components/util/transit-operator-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { MapPin } from '@styled-icons/fa-solid'
import { useIntl } from 'react-intl'
import React from 'react'
import Skeleton from 'react-loading-skeleton'
import type { TransitOperator } from '@opentripplanner/types'

import InvisibleA11yLabel from './invisible-a11y-label'
import OperatorLogo from './operator-logo'
import type { StopData } from './types'

const Operator = ({ operator }: { operator?: TransitOperator }) => {
const intl = useIntl()

if (!operator) {
return null
} else {
const operatorLogoAriaLabel = intl.formatMessage(
{
id: 'components.StopViewer.operatorLogoAriaLabel'
},
{
operatorName: operator.name
}
)
return operator.logo ? (
// Span with agency classname allows optional contrast/customization in user
// config for logos with poor contrast. Class name is hyphenated agency name
// e.g. "sound-transit"
<span
className={
operator.name ? operator.name.replace(/\s+/g, '-').toLowerCase() : ''
}
>
<OperatorLogo
alt={operatorLogoAriaLabel}
marginRight={0.5}
maxHeight={1}
operator={operator}
/>
</span>
) : (
// If operator exists but logo is missing,
// we still need to announce the operator name to screen readers.
<>
<MapPin />
<InvisibleA11yLabel>{operatorLogoAriaLabel}</InvisibleA11yLabel>
</>
)
}
}

const TransitOperatorLogos = ({
loading = false,
stopData,
transitOperators
}: {
loading?: boolean
stopData: StopData | undefined
transitOperators?: TransitOperator[]
}): JSX.Element => {
const agencies =
(stopData &&
stopData.stoptimesForPatterns?.reduce<Set<string>>((prev, cur) => {
// @ts-expect-error The agency type is not yet compatible with OTP2
const agencyGtfsId = cur.pattern.route.agency?.gtfsId
return agencyGtfsId ? prev.add(agencyGtfsId) : prev
}, new Set())) ||
new Set()

return (
<>
{loading ? (
<Skeleton height={20} style={{ marginRight: '0.5ch' }} width={20} />
) : (
transitOperators
?.filter((to) => Array.from(agencies).includes(to.agencyId))
// Second pass to remove duplicates based on name
.filter(
(to, index, arr) =>
index === arr.findIndex((t) => t?.name === to?.name)
)
.map((to) => <Operator key={to.agencyId} operator={to} />)
)}
</>
)
}

export default TransitOperatorLogos
Loading
Loading