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
19 changes: 16 additions & 3 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ itinerary:
# defaultRouteTextColor: '000000'
# longNameSplitter: ' - '
# order: 2
# Use this config to overwrite the accessibility score gradation
# map that ships with otp-ui
#accessibilityScore:
# gradationMap:
# 0.0:
# color: "#ffb5b9"
# # The text can be overridden in the language section
# text: 'Not Accessible'
# icon: thumbs-down

### Use this config for the standard mode selector
# modeGroups:
Expand All @@ -319,9 +328,13 @@ itinerary:
# common:
# accessModes:
# bikeshare: Blue Bike
# . config:
# menuItems:
# demo-item: Demo Item
# config:
# acessibilityScore:
# gradationMap:
# 0.0: 'Not Accessible'
# 0.9: 'Mostly Accessible'
# menuItems:
# demo-item: Demo Item

### Localization section to provide language/locale settings
#localization:
Expand Down
111 changes: 78 additions & 33 deletions lib/components/narrative/default/default-itinerary.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import coreUtils from '@opentripplanner/core-utils'
import React from 'react'
import { FormattedMessage, FormattedNumber, FormattedTime } from 'react-intl'
import { FormattedMessage, FormattedNumber, FormattedTime, injectIntl } from 'react-intl'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { AccessibilityRating } from '@opentripplanner/itinerary-body'

import FieldTripGroupSize from '../../admin/field-trip-itinerary-group-size'
import NarrativeItinerary from '../narrative-itinerary'
import ItineraryBody from '../line-itin/connected-itinerary-body'
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'
import FormattedDuration from '../../util/formatted-duration'
import { getTotalFare } from '../../../util/state'
import {
getAccessibilityScoreForItinerary,
itineraryHasAccessibilityScores
} from '../../util/accessibility-routing'
import Icon from '../../util/icon'

import ItinerarySummary from './itinerary-summary'

Expand Down Expand Up @@ -164,6 +170,7 @@ class DefaultItinerary extends NarrativeItinerary {

render () {
const {
accessibilityScoreGradationMap,
active,
configCosts,
currency,
Expand All @@ -181,7 +188,9 @@ class DefaultItinerary extends NarrativeItinerary {

return (
<div
className={`option default-itin${active ? ' active' : ''}${expanded ? ' expanded' : ''}`}
className={`option default-itin${active ? ' active' : ''}${
expanded ? ' expanded' : ''
}`}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
role='presentation'
Expand All @@ -195,53 +204,89 @@ class DefaultItinerary extends NarrativeItinerary {
<ItineraryDescription itinerary={itinerary} />
</div>
<ul className='list-unstyled itinerary-attributes'>
{ITINERARY_ATTRIBUTES
.sort((a, b) => {
const aSelected = this._isSortingOnAttribute(a)
const bSelected = this._isSortingOnAttribute(b)
if (aSelected) return -1
if (bSelected) return 1
else return a.order - b.order
})
.map(attribute => {
const isSelected = this._isSortingOnAttribute(attribute)
const options = attribute.id === 'arrivalTime' ? timeOptions : {}
if (isSelected) {
options.isSelected = true
options.selection = this.props.sort.type
}
options.LegIcon = LegIcon
options.configCosts = configCosts
options.currency = currency
return (
<li className={`${attribute.id}${isSelected ? ' main' : ''}`} key={attribute.id}>
{attribute.render(itinerary, options)}
</li>
)
})
}
{ITINERARY_ATTRIBUTES.sort((a, b) => {
const aSelected = this._isSortingOnAttribute(a)
const bSelected = this._isSortingOnAttribute(b)
if (aSelected) return -1
if (bSelected) return 1

return a.order - b.order
}).map((attribute) => {
const isSelected = this._isSortingOnAttribute(attribute)
const options = attribute.id === 'arrivalTime' ? timeOptions : {}
if (isSelected) {
options.isSelected = true
options.selection = this.props.sort.type
}
options.LegIcon = LegIcon
options.configCosts = configCosts
options.currency = currency
return (
<li
className={`${attribute.id}${isSelected ? ' main' : ''}`}
key={attribute.id}
>
{attribute.render(itinerary, options)}
</li>
)
})}
</ul>
<ItinerarySummary itinerary={itinerary} LegIcon={LegIcon} />
{itineraryHasAccessibilityScores(itinerary) && (
<AccessibilityRating
gradationMap={accessibilityScoreGradationMap}
large
score={getAccessibilityScoreForItinerary(itinerary)}
/>
)}
<FieldTripGroupSize itinerary={itinerary} />
{(active && !expanded) &&
{active && !expanded && (
<DetailsHint>
<FormattedMessage id='components.DefaultItinerary.clickDetails' />
</DetailsHint>
}
)}
</button>
{(active && expanded) &&
{active && expanded && (
<>
{showRealtimeAnnotation && <SimpleRealtimeAnnotation />}
<ItineraryBody itinerary={itinerary} LegIcon={LegIcon} setActiveLeg={setActiveLeg} timeOptions={timeOptions} />
<ItineraryBody
accessibilityScoreGradationMap={accessibilityScoreGradationMap}
itinerary={itinerary}
LegIcon={LegIcon}
setActiveLeg={setActiveLeg}
timeOptions={timeOptions}
/>
</>
}
)}
</div>
)
}
}

const mapStateToProps = (state, ownProps) => {
const {intl} = ownProps
const gradationMap = state.otp.config.accessibilityScore?.gradationMap

// Generate icons based on fa icon keys in config
// Override text fields if translation set
gradationMap && Object.keys(gradationMap).forEach(key => {
const {icon} = gradationMap[key]
if (icon && typeof icon === 'string') {
gradationMap[key].icon = <Icon type={icon} />
}

// As these localization keys are in the config, rather than
// standard language files, the message ids must be dynamically generated
const localizationId = `config.acessibilityScore.gradationMap.${key}`
const localizedText = intl.formatMessage({id: localizationId})
// Override the config label if a localized label exists
if (localizationId !== localizedText) {
gradationMap[key].text = localizedText
}
})

return {
accessibilityScoreGradationMap: gradationMap,
configCosts: state.otp.config.itinerary?.costs,
// The configured (ambient) currency is needed for rendering the cost
// of itineraries whether they include a fare or not, in which case
Expand All @@ -250,4 +295,4 @@ const mapStateToProps = (state, ownProps) => {
}
}

export default connect(mapStateToProps)(DefaultItinerary)
export default injectIntl(connect(mapStateToProps)(DefaultItinerary))
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class ConnectedItineraryBody extends Component {

render () {
const {
accessibilityScoreGradationMap,
config,
diagramVisible,
itinerary,
Expand All @@ -56,6 +57,7 @@ class ConnectedItineraryBody extends Component {
return (
<ItineraryBodyContainer>
<StyledItineraryBody
accessibilityScoreGradationMap={accessibilityScoreGradationMap}
config={config}
diagramVisible={diagramVisible}
itinerary={itinerary}
Expand Down
18 changes: 18 additions & 0 deletions lib/components/util/accessibility-routing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Determine if an itinerary has accessibility scores
*/
export const itineraryHasAccessibilityScores = (itinerary) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

For the i18n work, I was organising util functions into lib/util, and utility components like FormattedDuration into lib/components/util. Should these be moved to lib/util?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good point, if the function is used from multiple files then it should go to lib/util, if not, it should become private to this file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry I was merging too many things this morning and accidentally forgot about this! Very sorry. The fix is in #487

return !!itinerary.legs.find(leg => !!leg.accessibilityScore)
}

/**
* Calculates the total itinerary score based on leg score by weighting
* each leg equally
*/
export const getAccessibilityScoreForItinerary = (itinerary) => {
const scores = itinerary.legs
.map((leg) => leg.accessibilityScore || null)
.filter((score) => score !== null)

return scores.reduce((prev, cur) => prev + (cur * (1 / scores.length)), 0)
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@
"@opentripplanner/geocoder": "^1.1.1",
"@opentripplanner/humanize-distance": "^1.1.0",
"@opentripplanner/icons": "^1.2.0",
"@opentripplanner/itinerary-body": "^2.4.0",
"@opentripplanner/location-field": "^1.7.0",
"@opentripplanner/itinerary-body": "^2.5.0",
"@opentripplanner/location-field": "^1.7.1",
"@opentripplanner/location-icon": "^1.4.0",
"@opentripplanner/park-and-ride-overlay": "^1.2.2",
"@opentripplanner/printable-itinerary": "^1.3.0",
"@opentripplanner/printable-itinerary": "^1.3.1",
"@opentripplanner/route-viewer-overlay": "^1.1.1",
"@opentripplanner/stop-viewer-overlay": "^1.1.1",
"@opentripplanner/stops-overlay": "^3.3.1",
"@opentripplanner/transit-vehicle-overlay": "^2.3.1",
"@opentripplanner/transitive-overlay": "^1.1.2",
"@opentripplanner/trip-details": "^1.4.0",
"@opentripplanner/trip-form": "^1.4.0",
"@opentripplanner/trip-form": "^1.6.0",
"@opentripplanner/trip-viewer-overlay": "^1.1.1",
"@opentripplanner/vehicle-rental-overlay": "^1.2.1",
"blob-stream": "^0.1.3",
Expand Down
Loading