Skip to content

Commit b378eb2

Browse files
feat(route-viewer): support filtering routes by agency or mode
1 parent ade744e commit b378eb2

File tree

3 files changed

+127
-41
lines changed

3 files changed

+127
-41
lines changed

lib/components/viewers/route-viewer.js

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,32 @@ class RouteViewer extends Component {
3333
static propTypes = {
3434
hideBackButton: PropTypes.bool,
3535
routes: PropTypes.object
36-
}
36+
};
37+
38+
state = {
39+
agency: null,
40+
mode: null
41+
};
3742

38-
_backClicked = () => this.props.setMainPanelContent(null)
43+
_backClicked = () => this.props.setMainPanelContent(null);
3944

4045
componentDidMount () {
4146
this.props.findRoutes()
4247
}
4348

49+
/**
50+
* Handle filter dropdown change. Id of the filter is equivalent to the key in the
51+
* route object
52+
*/
53+
onFilterChange = (event) => {
54+
const { target } = event
55+
const { id, value } = target
56+
// id will be either 'agency' or 'mode' based on the dropdown used
57+
this.setState({
58+
[id]: value
59+
})
60+
};
61+
4462
render () {
4563
const {
4664
findRoute,
@@ -51,11 +69,36 @@ class RouteViewer extends Component {
5169
transitOperators,
5270
viewedRoute
5371
} = this.props
54-
const sortedRoutes = routes
55-
? Object.values(routes).sort(
56-
coreUtils.route.makeRouteComparator(transitOperators)
72+
const { agency, mode } = this.state
73+
const routesArray = routes ? Object.values(routes) : []
74+
75+
const filteredAndSortedRoutes = routesArray
76+
.filter(
77+
(route) =>
78+
// If the filter isn't defined, don't check.
79+
(!agency || agency === route.agencyName) &&
80+
(!mode || mode === route.mode)
81+
)
82+
.sort(coreUtils.route.makeRouteComparator(transitOperators))
83+
84+
// get agency and mode lists from route list
85+
const agencies = Array.from(
86+
new Set(routesArray.map((route) => route.agencyName || route.agency.name))
87+
)
88+
.filter((agency) => agency !== undefined)
89+
.sort()
90+
91+
const modes = Array.from(
92+
new Set(
93+
routesArray
94+
.filter((route) => route.mode !== undefined)
95+
.map((route) => route.mode.toLowerCase())
5796
)
58-
: []
97+
)
98+
// Correct capitalization
99+
.map((mode) => mode[0].toUpperCase() + mode.substr(1).toLowerCase())
100+
.sort()
101+
59102
return (
60103
<div className='route-viewer'>
61104
{/* Header Block */}
@@ -74,32 +117,51 @@ class RouteViewer extends Component {
74117
<div className='header-text'>
75118
{languageConfig.routeViewer || 'Route Viewer'}
76119
</div>
77-
<div className=''>
78-
{languageConfig.routeViewerDetails}
120+
<div className=''>{languageConfig.routeViewerDetails}</div>
121+
<div className='search-and-filter'>
122+
<Icon type='filter' />
123+
<select id='agency' onBlur={this.onFilterChange} onChange={this.onFilterChange}>
124+
<option value=''>All agencies</option>
125+
{agencies.map((agency) => (
126+
<option key={agency}>{agency}</option>
127+
))}
128+
</select>
129+
<select id='mode' onBlur={this.onFilterChange} onChange={this.onFilterChange}>
130+
<option value=''>All modes</option>
131+
{modes.map((mode) => (
132+
<option key={mode} value={mode.toUpperCase()}>
133+
{mode}
134+
</option>
135+
))}
136+
</select>
79137
</div>
80138
<div style={{ clear: 'both' }} />
81139
</div>
82140

83141
<div className='route-viewer-body'>
84-
{sortedRoutes
85-
.map(route => {
86-
// Find operator based on agency_id (extracted from OTP route ID).
87-
const operator = coreUtils.route.getTransitOperatorFromOtpRoute(
142+
{filteredAndSortedRoutes.map((route) => {
143+
// Find operator based on agency_id (extracted from OTP route ID).
144+
const operator =
145+
coreUtils.route.getTransitOperatorFromOtpRoute(
88146
route,
89147
transitOperators
90148
) || {}
91-
return (
92-
<RouteRow
93-
findRoute={findRoute}
94-
isActive={viewedRoute && viewedRoute.routeId === route.id}
95-
key={route.id}
96-
operator={operator}
97-
route={route}
98-
setViewedRoute={setViewedRoute}
99-
/>
100-
)
101-
})
102-
}
149+
return (
150+
<RouteRow
151+
findRoute={findRoute}
152+
isActive={viewedRoute && viewedRoute.routeId === route.id}
153+
key={route.id}
154+
operator={operator}
155+
route={route}
156+
setViewedRoute={setViewedRoute}
157+
/>
158+
)
159+
})}
160+
{(agency || mode) && filteredAndSortedRoutes.length === 0 && (
161+
<span className='noRoutesFoundMessage'>
162+
No routes match your filter!
163+
</span>
164+
)}
103165
</div>
104166
</div>
105167
)
@@ -177,9 +239,10 @@ class RouteRow extends PureComponent {
177239
// Attempt to split route name if splitter is defined for operator (to
178240
// remove short name value from start of long name value).
179241
const nameParts = route.longName.split(longNameSplitter)
180-
longName = (longNameSplitter && nameParts.length > 1)
181-
? nameParts[1]
182-
: route.longName
242+
longName =
243+
longNameSplitter && nameParts.length > 1
244+
? nameParts[1]
245+
: route.longName
183246
// If long name and short name are identical, set long name to be an empty
184247
// string.
185248
if (longName === route.shortName) longName = ''
@@ -191,14 +254,17 @@ class RouteRow extends PureComponent {
191254
const { isActive, operator, route } = this.props
192255
const { ModeIcon } = this.context
193256

194-
const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {}
257+
const { defaultRouteColor, defaultRouteTextColor, longNameSplitter } =
258+
operator || {}
195259
const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}`
196260
// NOTE: text color is not a part of short response route object, so there
197261
// is no way to determine from OTP what the text color should be if the
198262
// background color is, say, black. Instead, determine the appropriate
199263
// contrast color and use that if no text color is available.
200264
const contrastColor = getContrastYIQ(backgroundColor)
201-
const color = `#${defaultRouteTextColor || route.textColor || contrastColor}`
265+
const color = `#${defaultRouteTextColor ||
266+
route.textColor ||
267+
contrastColor}`
202268
// Default long name is empty string (long name is an optional GTFS value).
203269
const longName = this.getCleanRouteLongName({ longNameSplitter, route })
204270
return (
@@ -208,12 +274,9 @@ class RouteRow extends PureComponent {
208274
onClick={this._onClick}
209275
>
210276
<RouteRowElement>
211-
{operator && operator.logo &&
212-
<OperatorImg
213-
alt={`${operator.name} logo`}
214-
src={operator.logo}
215-
/>
216-
}
277+
{operator && operator.logo && (
278+
<OperatorImg alt={`${operator.name} logo`} src={operator.logo} />
279+
)}
217280
</RouteRowElement>
218281
<ModeIconElement>
219282
<ModeIcon height={22} mode={getModeFromRoute(route)} width={22} />
@@ -226,13 +289,19 @@ class RouteRow extends PureComponent {
226289
<b>{route.shortName}</b> {longName}
227290
</RouteNameElement>
228291
</RouteRowButton>
229-
<VelocityTransitionGroup enter={{animation: 'slideDown'}} leave={{animation: 'slideUp'}}>
292+
<VelocityTransitionGroup
293+
enter={{ animation: 'slideDown' }}
294+
leave={{ animation: 'slideUp' }}
295+
>
230296
{isActive && (
231297
<RouteDetails>
232-
{route.url
233-
? <a href={route.url} target='_blank'>Route Details</a>
234-
: 'No route URL provided.'
235-
}
298+
{route.url ? (
299+
<a href={route.url} target='_blank'>
300+
Route Details
301+
</a>
302+
) : (
303+
'No route URL provided.'
304+
)}
236305
</RouteDetails>
237306
)}
238307
</VelocityTransitionGroup>

lib/components/viewers/viewers.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,19 @@
268268
float: right;
269269
width: 50px;
270270
}
271+
272+
/* Route Viewer Updates */
273+
.search-and-filter {
274+
margin-top: 5px;
275+
}
276+
.search-and-filter select {
277+
max-width: 125px;
278+
margin: 0 5px;
279+
}
280+
281+
.route-viewer-body .noRoutesFoundMessage {
282+
display: flex;
283+
align-items: center;
284+
justify-content: center;
285+
padding-top: 10px;
286+
}

lib/reducers/create-otp-reducer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ function createOtpReducer (config) {
252252
// validate the initial state
253253
validateInitialState(initialState)
254254

255+
// eslint-disable-next-line complexity
255256
return (state = initialState, action) => {
256257
const searchId = action.payload && action.payload.searchId
257258
const requestId = action.payload && action.payload.requestId
@@ -899,7 +900,7 @@ function createOtpReducer (config) {
899900
// Otherwise, overwrite only this route
900901
return update(state, {
901902
transitIndex: {
902-
routes: { [action.payload.id]: { $set: action.payload } }
903+
routes: { [action.payload.id]: { $merge: action.payload } }
903904
}
904905
})
905906
case 'FIND_PATTERNS_FOR_ROUTE_RESPONSE':

0 commit comments

Comments
 (0)