Skip to content

Commit c121587

Browse files
Merge pull request #339 from opentripplanner/mobile-batch-results
Mobile batch results
2 parents 3b0ac43 + cabab54 commit c121587

16 files changed

+617
-188
lines changed

Diff for: example.js

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import createLogger from 'redux-logger'
1313

1414
// import OTP-RR components
1515
import {
16+
BatchResultsScreen,
1617
BatchRoutingPanel,
1718
BatchSearchScreen,
1819
CallTakerControls,
@@ -21,6 +22,7 @@ import {
2122
DefaultItinerary,
2223
DefaultMainPanel,
2324
FieldTripWindows,
25+
MobileResultsScreen,
2426
MobileSearchScreen,
2527
ResponsiveWebapp,
2628
createCallTakerReducer,
@@ -76,6 +78,7 @@ const TermsOfStorage = () => (
7678
// - MainControls (optional)
7779
// - MainPanel (required)
7880
// - MapWindows (optional)
81+
// - MobileResultsScreen (required)
7982
// - MobileSearchScreen (required)
8083
// - ModeIcon (required)
8184
// - TermsOfService (required if otpConfig.persistence.strategy === 'otp_middleware')
@@ -96,6 +99,9 @@ const components = {
9699
<FieldTripWindows />
97100
</>
98101
: null,
102+
MobileResultsScreen: isBatchRoutingEnabled
103+
? BatchResultsScreen
104+
: MobileResultsScreen,
99105
MobileSearchScreen: isBatchRoutingEnabled
100106
? BatchSearchScreen
101107
: MobileSearchScreen,

Diff for: lib/actions/ui.js

+84-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import coreUtils from '@opentripplanner/core-utils'
33
import { createAction } from 'redux-actions'
44
import { matchPath } from 'react-router'
55

6-
import { findRoute } from './api'
6+
import { findRoute, setUrlSearch } from './api'
77
import { setMapCenter, setMapZoom, setRouterId } from './config'
8-
import { clearActiveSearch, parseUrlQueryString, setActiveSearch } from './form'
8+
import {
9+
clearActiveSearch,
10+
parseUrlQueryString,
11+
setActiveSearch
12+
} from './form'
913
import { clearLocation } from './map'
1014
import { setActiveItinerary } from './narrative'
1115
import { getUiUrlParams } from '../util/state'
@@ -230,3 +234,81 @@ export const MobileScreens = {
230234
SET_DATETIME: 7,
231235
RESULTS_SUMMARY: 8
232236
}
237+
238+
/**
239+
* Enum to describe the layout of the itinerary view
240+
* (currently only used in batch results).
241+
*/
242+
export const ItineraryView = {
243+
DEFAULT: 'list',
244+
/** One itinerary is shown. (In mobile view, the map is hidden.) */
245+
FULL: 'full',
246+
/** One itinerary is shown, itinerary and map are focused on a leg. (The mobile view is split.) */
247+
LEG: 'leg',
248+
/** One itinerary leg is hidden. (In mobile view, the map is expanded.) */
249+
LEG_HIDDEN: 'leg-hidden',
250+
/** The list of itineraries is shown. (The mobile view is split.) */
251+
LIST: 'list',
252+
/** The list of itineraries is hidden. (In mobile view, the map is expanded.) */
253+
LIST_HIDDEN: 'list-hidden'
254+
}
255+
256+
const setPreviousItineraryView = createAction('SET_PREVIOUS_ITINERARY_VIEW')
257+
258+
/**
259+
* Sets the itinerary view state (see values above) in the URL params
260+
* (currently only used in batch results).
261+
*/
262+
export function setItineraryView (value) {
263+
return function (dispatch, getState) {
264+
const urlParams = coreUtils.query.getUrlParams()
265+
const prevItineraryView = urlParams.ui_itineraryView || ItineraryView.DEFAULT
266+
267+
// If the itinerary value is changed,
268+
// set the desired ui query param, or remove it if same as default,
269+
// and store the current view as previousItineraryView.
270+
if (value !== urlParams.ui_itineraryView) {
271+
if (value !== ItineraryView.DEFAULT) {
272+
urlParams.ui_itineraryView = value
273+
} else if (urlParams.ui_itineraryView) {
274+
delete urlParams.ui_itineraryView
275+
}
276+
277+
dispatch(setUrlSearch(urlParams))
278+
dispatch(setPreviousItineraryView(prevItineraryView))
279+
}
280+
}
281+
}
282+
283+
/**
284+
* Switch the mobile batch results view between full map view and the split state
285+
* (itinerary list or itinerary leg view) that was in place prior.
286+
*/
287+
export function toggleBatchResultsMap () {
288+
return function (dispatch, getState) {
289+
const urlParams = coreUtils.query.getUrlParams()
290+
const itineraryView = urlParams.ui_itineraryView || ItineraryView.DEFAULT
291+
292+
if (itineraryView === ItineraryView.LEG) {
293+
dispatch(setItineraryView(ItineraryView.LEG_HIDDEN))
294+
} else if (itineraryView === ItineraryView.LIST) {
295+
dispatch(setItineraryView(ItineraryView.LIST_HIDDEN))
296+
} else {
297+
const { previousItineraryView } = getState().otp.ui
298+
dispatch(setItineraryView(previousItineraryView))
299+
}
300+
}
301+
}
302+
303+
/**
304+
* Takes the user back to the mobile search screen in mobile views.
305+
*/
306+
export function showMobileSearchScreen () {
307+
return function (dispatch, getState) {
308+
// Reset itinerary view state to show the list of results *before* clearing the search.
309+
// (Otherwise, if the map is expanded, the search is not cleared.)
310+
dispatch(setItineraryView(ItineraryView.LIST))
311+
dispatch(clearActiveSearch())
312+
dispatch(setMobileScreen(MobileScreens.SEARCH_FORM))
313+
}
314+
}

Diff for: lib/components/map/bounds-updating-overlay.js

+39-7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ function extendBoundsByPlaces (bounds, places = []) {
2222
})
2323
}
2424

25+
/** Padding around itinerary bounds and map bounds. */
26+
const BOUNDS_PADDING = [30, 30]
27+
2528
/**
2629
* This MapLayer component will automatically update the leaflet bounds
2730
* depending on what data is in the redux store. This component does not
@@ -42,6 +45,29 @@ class BoundsUpdatingOverlay extends MapLayer {
4245

4346
componentWillUnmount () {}
4447

48+
_fitItineraryViewToMap (newProps, bounds, map) {
49+
// If itineraryView has changed (currently: only in mobile batch results),
50+
// force a resize of the map before re-fitting the active itinerary or active leg,
51+
// and do that after a delay to ensure that canvas heights have stabilized in the DOM.
52+
setTimeout(() => {
53+
map.invalidateSize(true)
54+
55+
const { activeLeg, itinerary } = newProps
56+
if (itinerary) {
57+
if (activeLeg !== null) {
58+
// Fit to active leg if set.
59+
map.fitBounds(
60+
getLeafletLegBounds(itinerary.legs[activeLeg]),
61+
{ ITINERARY_MAP_PADDING: BOUNDS_PADDING }
62+
)
63+
} else {
64+
// Fit to whole itinerary otherwise.
65+
map.fitBounds(bounds, { ITINERARY_MAP_PADDING: BOUNDS_PADDING })
66+
}
67+
}
68+
}, 250)
69+
}
70+
4571
/* eslint-disable-next-line complexity */
4672
updateBounds (oldProps, newProps) {
4773
// TODO: maybe setting bounds ought to be handled in map props...
@@ -55,8 +81,6 @@ class BoundsUpdatingOverlay extends MapLayer {
5581
const { map } = newProps.leaflet
5682
if (!map) return
5783

58-
const padding = [30, 30]
59-
6084
// Fit map to to entire itinerary if active itinerary bounds changed
6185
const newFrom = newProps.query && newProps.query.from
6286
const newItinBounds = newProps.itinerary && getLeafletItineraryBounds(newProps.itinerary)
@@ -69,11 +93,17 @@ class BoundsUpdatingOverlay extends MapLayer {
6993
const oldIntermediate = oldProps.query && oldProps.query.intermediatePlaces
7094
const newIntermediate = newProps.query && newProps.query.intermediatePlaces
7195
const intermediateChanged = !isEqual(oldIntermediate, newIntermediate)
72-
if (
96+
97+
// Also refit map if itineraryView prop has changed.
98+
const itineraryViewChanged = oldProps.itineraryView !== newProps.itineraryView
99+
100+
if (itineraryViewChanged) {
101+
this._fitItineraryViewToMap(newProps, newItinBounds, map)
102+
} else if (
73103
(!oldItinBounds && newItinBounds) ||
74104
(oldItinBounds && newItinBounds && !oldItinBounds.equals(newItinBounds))
75105
) {
76-
map.fitBounds(newItinBounds, { padding })
106+
map.fitBounds(newItinBounds, { padding: BOUNDS_PADDING })
77107
// Pan to to itinerary leg if made active (clicked); newly active leg must be non-null
78108
} else if (
79109
newProps.itinerary &&
@@ -82,7 +112,7 @@ class BoundsUpdatingOverlay extends MapLayer {
82112
) {
83113
map.fitBounds(
84114
getLeafletLegBounds(newProps.itinerary.legs[newProps.activeLeg]),
85-
{ padding }
115+
{ padding: BOUNDS_PADDING }
86116
)
87117

88118
// If no itinerary update but from/to locations are present, fit to those
@@ -108,9 +138,8 @@ class BoundsUpdatingOverlay extends MapLayer {
108138
map.fitBounds([
109139
[left, bottom],
110140
[right, top]
111-
], { padding })
141+
], { padding: BOUNDS_PADDING })
112142
}
113-
114143
// If only from or to is set, pan to that
115144
} else if (newFrom && fromChanged) {
116145
map.panTo([newFrom.lat, newFrom.lon])
@@ -141,10 +170,13 @@ class BoundsUpdatingOverlay extends MapLayer {
141170

142171
const mapStateToProps = (state, ownProps) => {
143172
const activeSearch = getActiveSearch(state.otp)
173+
const urlParams = coreUtils.query.getUrlParams()
174+
144175
return {
145176
activeLeg: activeSearch && activeSearch.activeLeg,
146177
activeStep: activeSearch && activeSearch.activeStep,
147178
itinerary: getActiveItinerary(state.otp),
179+
itineraryView: urlParams.ui_itineraryView,
148180
popupLocation: state.otp.ui.mapPopupLocation,
149181
query: state.otp.currentQuery
150182
}

Diff for: lib/components/map/default-map.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import BaseMap from '@opentripplanner/base-map'
2+
import coreUtils from '@opentripplanner/core-utils'
23
import React, { Component } from 'react'
34
import { connect } from 'react-redux'
45
import styled from 'styled-components'
@@ -119,6 +120,7 @@ class DefaultMap extends Component {
119120
bikeRentalStations,
120121
carRentalQuery,
121122
carRentalStations,
123+
itineraryView,
122124
mapConfig,
123125
mapPopupLocation,
124126
vehicleRentalQuery,
@@ -146,16 +148,23 @@ class DefaultMap extends Component {
146148
center={center}
147149
maxZoom={mapConfig.maxZoom}
148150
onClick={this.onMapClick}
149-
popup={popup}
150151
onPopupClosed={this.onPopupClosed}
152+
popup={popup}
151153
zoom={mapConfig.initZoom || 13}
152154
>
153155
{/* The default overlays */}
154156
<BoundsUpdatingOverlay />
155157
<EndpointsOverlay />
156158
<RouteViewerOverlay />
157159
<StopViewerOverlay />
158-
<TransitiveOverlay />
160+
{/*
161+
HACK: Use the key prop to force a remount and full resizing of transitive
162+
if the map container size changes,
163+
per https://linguinecode.com/post/4-methods-to-re-render-react-component
164+
Without it, transitive resolution will not match the map,
165+
and transitive will appear blurry after e.g. the narrative is expanded.
166+
*/}
167+
<TransitiveOverlay key={itineraryView ? `transitive-${itineraryView}` : 'transitive-default'} />
159168
<TripViewerOverlay />
160169
<ElevationPointMarker />
161170

@@ -206,9 +215,12 @@ const mapStateToProps = (state, ownProps) => {
206215
const overlays = state.otp.config.map && state.otp.config.map.overlays
207216
? state.otp.config.map.overlays
208217
: []
218+
const urlParams = coreUtils.query.getUrlParams()
219+
209220
return {
210221
bikeRentalStations: state.otp.overlay.bikeRental.stations,
211222
carRentalStations: state.otp.overlay.carRental.stations,
223+
itineraryView: urlParams.ui_itineraryView,
212224
mapConfig: state.otp.config.map,
213225
mapPopupLocation: state.otp.ui.mapPopupLocation,
214226
overlays,

0 commit comments

Comments
 (0)