Skip to content

Commit 76273a3

Browse files
committed
fix(vehicle-rental): fix place names for eScooter rentals
Previously place names for vehicle (scooter) rentals defaulted to the place name, which often tend to be UUIDs that have no relevance to the ID found on the scooter/vehicle. This changes the place name to be $COMPANY $VEHICLE_TYPE and uses the same shared component for bike, car, and micromobility rental. fix ibi-group/trimet-mod-otp#213
1 parent 642ac13 commit 76273a3

File tree

4 files changed

+124
-36
lines changed

4 files changed

+124
-36
lines changed

lib/components/narrative/default/access-leg.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { distanceString } from '../../../util/distance'
77
import { getStepInstructions } from '../../../util/itinerary'
88
import { formatDuration } from '../../../util/time'
99

10+
/**
11+
* Default access leg component for narrative itinerary.
12+
*/
1013
export default class AccessLeg extends Component {
1114
static propTypes = {
1215
activeStep: PropTypes.number,

lib/components/narrative/line-itin/access-leg-body.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ import currencyFormatter from 'currency-formatter'
66
import LegDiagramPreview from '../leg-diagram-preview'
77

88
import { distanceString } from '../../../util/distance'
9-
import { getLegModeLabel, getLegIcon, getPlaceName, getStepDirection, getStepStreetName } from '../../../util/itinerary'
9+
import {
10+
getLegModeLabel,
11+
getLegIcon,
12+
getPlaceName,
13+
getStepDirection,
14+
getStepStreetName
15+
} from '../../../util/itinerary'
1016
import { formatDuration, formatTime } from '../../../util/time'
1117
import { isMobile } from '../../../util/ui'
1218

1319
import DirectionIcon from '../../icons/direction-icon'
1420

21+
/**
22+
* Component for access (e.g. walk/bike/etc.) leg in narrative itinerary. This
23+
* particular component is used in the line-itin (i.e., trimet-mod-otp) version
24+
* of the narrative itinerary.
25+
*/
1526
export default class AccessLegBody extends Component {
1627
static propTypes = {
1728
leg: PropTypes.object,
@@ -32,15 +43,27 @@ export default class AccessLegBody extends Component {
3243
}
3344

3445
render () {
35-
const { customIcons, followsTransit, leg, timeOptions } = this.props
46+
const { config, customIcons, followsTransit, leg, timeOptions } = this.props
3647

3748
if (leg.mode === 'CAR' && leg.hailedCar) {
38-
return <TNCLeg leg={leg} onSummaryClick={this._onSummaryClick} timeOptions={timeOptions} followsTransit={followsTransit} customIcons={customIcons} />
49+
return (
50+
<TNCLeg
51+
config={config}
52+
leg={leg}
53+
onSummaryClick={this._onSummaryClick}
54+
timeOptions={timeOptions}
55+
followsTransit={followsTransit}
56+
customIcons={customIcons} />
57+
)
3958
}
4059

4160
return (
4261
<div className='leg-body'>
43-
<AccessLegSummary leg={leg} onSummaryClick={this._onSummaryClick} customIcons={customIcons} />
62+
<AccessLegSummary
63+
config={config}
64+
leg={leg}
65+
onSummaryClick={this._onSummaryClick}
66+
customIcons={customIcons} />
4467

4568
<div onClick={this._onStepsHeaderClick} className='steps-header'>
4669
{formatDuration(leg.duration)}
@@ -60,6 +83,7 @@ class TNCLeg extends Component {
6083
render () {
6184
// TODO: ensure that client ID fields are populated
6285
const {
86+
config,
6387
LYFT_CLIENT_ID,
6488
UBER_CLIENT_ID,
6589
customIcons,
@@ -82,7 +106,11 @@ class TNCLeg extends Component {
82106

83107
<div className='leg-body'>
84108
{/* The icon/summary row */}
85-
<AccessLegSummary leg={leg} onSummaryClick={this.props.onSummaryClick} customIcons={customIcons} />
109+
<AccessLegSummary
110+
config={config}
111+
leg={leg}
112+
onSummaryClick={this.props.onSummaryClick}
113+
customIcons={customIcons} />
86114

87115
{/* The "Book Ride" button */}
88116
<div style={{ marginTop: 10, marginBottom: 10, height: 32, position: 'relative' }}>
@@ -125,7 +153,7 @@ class TNCLeg extends Component {
125153

126154
class AccessLegSummary extends Component {
127155
render () {
128-
const { customIcons, leg } = this.props
156+
const { config, customIcons, leg } = this.props
129157
return (
130158
<div className='summary leg-description' onClick={this.props.onSummaryClick}>
131159
{/* Mode-specific icon */}
@@ -136,7 +164,7 @@ class AccessLegSummary extends Component {
136164
{getLegModeLabel(leg)}
137165
{' '}
138166
{leg.distance && <span> {distanceString(leg.distance)}</span>}
139-
{` to ${getPlaceName(leg.to)}`}
167+
{` to ${getPlaceName(leg.to, config.companies)}`}
140168
</div>
141169
</div>
142170
)

lib/components/narrative/line-itin/place-row.js

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import React, { Component, PureComponent } from 'react'
2+
import { connect } from 'react-redux'
23

34
import LocationIcon from '../../icons/location-icon'
45
import ViewStopButton from '../../viewers/view-stop-button'
5-
import { getPlaceName, isTransit } from '../../../util/itinerary'
6+
import {
7+
getCompanyForNetwork,
8+
getModeStringForCompany,
9+
getPlaceName,
10+
isTransit
11+
} from '../../../util/itinerary'
612
import { formatTime } from '../../../util/time'
713

814
import TransitLegBody from './transit-leg-body'
@@ -11,7 +17,7 @@ import AccessLegBody from './access-leg-body'
1117
// TODO: make this a prop
1218
const defaultRouteColor = '#008'
1319

14-
export default class PlaceRow extends Component {
20+
class PlaceRow extends Component {
1521
_createLegLine (leg) {
1622
switch (leg.mode) {
1723
case 'WALK': return <div className='leg-line leg-line-walk' />
@@ -31,7 +37,7 @@ export default class PlaceRow extends Component {
3137

3238
/* eslint-disable complexity */
3339
render () {
34-
const { customIcons, leg, legIndex, place, time, timeOptions, followsTransit, previousLeg } = this.props
40+
const { config, customIcons, leg, legIndex, place, time, timeOptions, followsTransit, previousLeg } = this.props
3541
const stackIcon = (name, color, size) => <i className={`fa fa-${name} fa-stack-1x`} style={{ color, fontSize: size + 'px' }} />
3642

3743
let icon
@@ -59,6 +65,10 @@ export default class PlaceRow extends Component {
5965
}
6066

6167
const interline = leg && leg.interlineWithPreviousLeg
68+
// TODO: This changeVehicles condition might should be showing the full stop
69+
// place name because it could be helpful to give the user the stop viewer to
70+
// click on in order to see what time the next vehicle will be arriving to the
71+
// shared stop.
6272
const changeVehicles = previousLeg && previousLeg.to.stopId === leg.from.stopId && isTransit(previousLeg.mode) && isTransit(leg.mode)
6373
const special = interline || changeVehicles
6474
return (
@@ -80,7 +90,7 @@ export default class PlaceRow extends Component {
8090
? <div className='interline-name'>Stay on Board at <b>{place.name}</b></div>
8191
: changeVehicles
8292
? <div className='interline-name'>Change Vehicles at <b>{place.name}</b></div>
83-
: <div>{getPlaceName(place)}</div>
93+
: <div>{getPlaceName(place, config.companies)}</div>
8494
}
8595
</div>
8696

@@ -92,22 +102,11 @@ export default class PlaceRow extends Component {
92102
</div>
93103
)}
94104

95-
{/* Place subheading: rented bike pickup */}
96-
{leg && leg.rentedBike && (
97-
<div className='place-subheader'>
98-
Pick up shared bike
99-
</div>
105+
{/* Place subheading: rented vehicle (e.g., scooter, bike, car) pickup */}
106+
{leg && (leg.rentedVehicle || leg.rentedBike || leg.rentedCar) && (
107+
<RentedVehicleLeg config={config} leg={leg} />
100108
)}
101109

102-
{/* Place subheading: rented car pickup */}
103-
{leg && leg.rentedCar && (
104-
<div className='place-subheader'>
105-
Pick up {leg.from.networks ? leg.from.networks.join('/') : 'rented car'} {leg.from.name}
106-
</div>
107-
)}
108-
109-
<RentedVehicleLeg leg={leg} />
110-
111110
{/* Show the leg, if present */}
112111
{leg && (
113112
leg.transitLeg
@@ -121,6 +120,7 @@ export default class PlaceRow extends Component {
121120
)
122121
: (/* This is an access (e.g. walk/bike/etc.) leg */
123122
<AccessLegBody
123+
config={config}
124124
customIcons={customIcons}
125125
followsTransit={followsTransit}
126126
leg={leg}
@@ -137,6 +137,20 @@ export default class PlaceRow extends Component {
137137
}
138138
}
139139

140+
// connect to the redux store
141+
142+
const mapStateToProps = (state, ownProps) => {
143+
return {
144+
// Pass config in order to give access to companies definition (used to
145+
// determine proper place names for rental vehicles).
146+
config: state.otp.config
147+
}
148+
}
149+
150+
const mapDispatchToProps = { }
151+
152+
export default connect(mapStateToProps, mapDispatchToProps)(PlaceRow)
153+
140154
/**
141155
* A component to display vehicle rental data. The word "Vehicle" has been used
142156
* because a future refactor is intended to combine car rental, bike rental
@@ -146,8 +160,8 @@ export default class PlaceRow extends Component {
146160
*/
147161
class RentedVehicleLeg extends PureComponent {
148162
render () {
149-
const {leg} = this.props
150-
if (!leg || !leg.rentedVehicle) return null
163+
const { config, leg } = this.props
164+
const configCompanies = config.companies || []
151165
if (leg.mode === 'WALK') {
152166
return (
153167
<div className='place-subheader'>
@@ -156,14 +170,23 @@ class RentedVehicleLeg extends PureComponent {
156170
)
157171
}
158172

159-
if (leg.rentedVehicleData) {
173+
if (leg.rentedVehicle || leg.rentedBike || leg.rentedCar) {
174+
// console.log(leg.from.networks, configCompanies)
175+
const companies = leg.from.networks.map(n => getCompanyForNetwork(n, configCompanies))
176+
const companyLabel = companies.map(co => co.label).join('/')
177+
const modeString = getModeStringForCompany(companies[0])
178+
// Only show vehicle name for car rentals. For bikes and eScooters, these
179+
// IDs/names tend to be less relevant (or entirely useless) in this context.
180+
const vehicleName = leg.rentedCar ? ` ${leg.from.name}` : ''
181+
// e.g., Pick up REACHNOW rented car XYZNDB OR
182+
// Pick up SPIN eScooter
160183
return (
161184
<div className='place-subheader'>
162-
Pick up {leg.rentedVehicleData.companies.join('/')} vehicle {leg.from.name}
185+
Pick up {companyLabel} {modeString}{vehicleName}
163186
</div>
164187
)
165188
}
166-
189+
// FIXME: Under what conditions would this be returned?
167190
return (
168191
<div className='place-subheader'>
169192
Continue riding from {leg.from.name}

lib/util/itinerary.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,21 +390,55 @@ export function getLegIcon (leg, customIcons) {
390390
}
391391
let iconStr = leg.mode
392392
if (iconStr === 'CAR' && leg.rentedCar) {
393-
iconStr = leg.rentedCarData.companies[0]
393+
iconStr = leg.from.networks[0]
394394
} else if (iconStr === 'CAR' && leg.tncData) {
395395
iconStr = leg.tncData.company
396396
} else if (iconStr === 'BICYCLE' && leg.rentedBike) {
397-
// placeholder for future vehicle rental refactor
398-
} else if (iconStr === 'MICROMOBILITY' && leg.rentedVehicle && leg.rentedVehicleData) {
399-
iconStr = leg.rentedVehicleData.companies[0]
397+
iconStr = leg.from.networks[0]
398+
} else if (iconStr === 'MICROMOBILITY' && leg.rentedVehicle) {
399+
iconStr = leg.from.networks[0]
400400
}
401401

402402
return getIcon(iconStr, customIcons)
403403
}
404404

405-
export function getPlaceName (place) {
405+
export function getCompanyForNetwork (networkString, companies = []) {
406+
const company = companies.find(co => co.id === networkString)
407+
if (!company) {
408+
console.warn(`No company found in config.yml that matches rented vehicle network: ${networkString}`, companies)
409+
}
410+
return company
411+
}
412+
413+
export function getModeStringForCompany (company) {
414+
switch (company.modes) {
415+
case 'CAR_RENT':
416+
return 'rented car'
417+
case 'MICROMOBILITY_RENT':
418+
return 'eScooter'
419+
case 'BICYCLE_RENT':
420+
return 'bike'
421+
// If company offers more than one mode, default to `vehicle` string.
422+
default:
423+
return 'vehicle'
424+
}
425+
}
426+
427+
export function getPlaceName (place, companies) {
406428
// If address is provided (i.e. for carshare station, use it)
407-
return place.address ? place.address.split(',')[0] : place.name
429+
if (place.address) return place.address.split(',')[0]
430+
if (place.networks && place.vertexType === 'VEHICLERENTAL') {
431+
// For vehicle rental pick up, do not use the place name. Rather, use
432+
// company name + vehicle type (e.g., SPIN eScooter). Place name is often just
433+
// a UUID that has no relevance to the actual vehicle. For bikeshare, however,
434+
// there are often hubs or bikes that have relevant names to the user.
435+
const company = getCompanyForNetwork(place.networks[0], companies)
436+
if (company) {
437+
return `${company.label} ${getModeStringForCompany(company)}`
438+
}
439+
}
440+
// Default to place name
441+
return place.name
408442
}
409443

410444
export function getTNCLocation (leg, type) {

0 commit comments

Comments
 (0)