-
Notifications
You must be signed in to change notification settings - Fork 51
/
ui.js
314 lines (287 loc) · 10.8 KB
/
ui.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import { push } from 'connected-react-router'
import coreUtils from '@opentripplanner/core-utils'
import { createAction } from 'redux-actions'
import { matchPath } from 'react-router'
import { getUiUrlParams } from '../util/state'
import { findRoute, setUrlSearch } from './api'
import { setMapCenter, setMapZoom, setRouterId } from './config'
import {
clearActiveSearch,
parseUrlQueryString,
setActiveSearch
} from './form'
import { clearLocation } from './map'
import { setActiveItinerary } from './narrative'
/**
* Wrapper function for history#push (or, if specified, replace, etc.)
* that preserves the current search or, if
* replaceSearch is provided (including an empty string), replaces the search
* when routing to a new URL path.
* @param {[type]} url path to route to
* @param {string} replaceSearch optional search string to replace current one
* @param {func} routingMethod the connected-react-router method to execute (defaults to push).
*/
export function routeTo (url, replaceSearch, routingMethod = push) {
return function (dispatch, getState) {
// Get search to preserve when routing to new path.
const { router } = getState()
const search = router ? router.location.search : window.location.search
let path = url
if (replaceSearch || replaceSearch === '') {
path = `${path}${replaceSearch}`
} else {
path = `${path}${search}`
}
dispatch(routingMethod(path))
}
}
/**
* Checks URL and redirects app to appropriate content (e.g., viewed
* route or stop).
*/
export function matchContentToUrl (location) {
return function (dispatch, getState) {
// This is a bit of a hack to make up for the fact that react-router does
// not always provide the match params as expected.
// https://github.com/ReactTraining/react-router/issues/5870#issuecomment-394194338
const root = location.pathname.split('/')[1]
const match = matchPath(location.pathname, {
path: `/${root}/:id`,
exact: true,
strict: false
})
const id = match && match.params && match.params.id
switch (root) {
case 'route':
if (id) {
dispatch(findRoute({ routeId: id }))
dispatch(setViewedRoute({ routeId: id }))
} else {
dispatch(setViewedRoute(null))
dispatch(setMainPanelContent(MainPanelContent.ROUTE_VIEWER))
}
break
case 'stop':
if (id) {
dispatch(setViewedStop({ stopId: id }))
} else {
dispatch(setViewedStop(null))
dispatch(setMainPanelContent(MainPanelContent.STOP_VIEWER))
}
break
case 'start':
case '@':
// Parse comma separated params (ensuring numbers are parsed correctly).
let [lat, lon, zoom, routerId] = id ? idToParams(id) : []
if (!lat || !lon) {
// Attempt to parse path if lat/lon not found. (Legacy UI otp.js used
// slashes in the pathname to specify lat, lon, etc.)
[,, lat, lon, zoom, routerId] = idToParams(location.pathname, '/')
}
console.log(lat, lon, zoom, routerId)
// Update map location/zoom and optionally override router ID.
if (+lat && +lon) dispatch(setMapCenter({ lat, lon }))
if (+zoom) dispatch(setMapZoom({ zoom }))
// If router ID is provided, override the default routerId.
if (routerId) dispatch(setRouterId(routerId))
dispatch(setMainPanelContent(null))
break
// For any other route path, just revert to default panel.
default:
dispatch(setMainPanelContent(null))
break
}
}
}
/**
* Split the path id into its parts (according to specified delimiter). Parse
* numbers if detected.
*/
function idToParams (id, delimiter = ',') {
return id.split(delimiter).map(s => isNaN(s) ? s : +s)
}
/**
* Event listener for responsive webapp that handles a back button press and
* sets the active search and itinerary according to the URL query params.
*/
export function handleBackButtonPress (e) {
return function (dispatch, getState) {
const state = getState()
const { activeSearchId } = state.otp
const uiUrlParams = getUiUrlParams(state)
// Get new search ID from URL after back button pressed.
// console.log('back button pressed', e)
const urlParams = coreUtils.query.getUrlParams()
const previousSearchId = urlParams.ui_activeSearch
const previousItinIndex = +urlParams.ui_activeItinerary || 0
const previousSearch = state.otp.searches[previousSearchId]
if (previousSearch) {
// If back button pressed and active search has changed, set search to
// previous search ID.
if (activeSearchId !== previousSearchId) {
dispatch(setActiveSearch(previousSearchId))
} else if (uiUrlParams.ui_activeItinerary !== previousItinIndex) {
// Active itinerary index has changed.
dispatch(setActiveItinerary({ index: previousItinIndex }))
}
} else {
// The back button was pressed, but there was no corresponding search
// found for the previous search ID. Derive search from URL params.
if (!previousSearchId && activeSearchId) {
// There is no search ID. Clear active search and from/to
dispatch(clearActiveSearch())
dispatch(clearLocation({ type: 'from' }))
dispatch(clearLocation({ type: 'to' }))
} else if (previousSearchId) {
console.warn(`No search found in state history for search ID: ${previousSearchId}. Replanning...`)
// Set query to the params found in the URL and perform routing query
// for search ID.
// Also, we don't want to update the URL here because that will funk with
// the browser history.
dispatch(parseUrlQueryString(urlParams))
}
}
}
}
export const setMobileScreen = createAction('SET_MOBILE_SCREEN')
/**
* Sets the main panel content according to the payload (one of the enum values
* of MainPanelContent) and routes the application to the correct path.
* @param {number} payload MainPanelContent value
*/
export function setMainPanelContent (payload) {
return function (dispatch, getState) {
const { otp, router } = getState()
if (otp.ui.mainPanelContent === payload) {
console.warn(`Attempt to route from ${otp.ui.mainPanelContent} to ${payload}. Doing nothing`)
// Do nothing if the panel is already set. This will guard against over
// enthusiastic routing and overwriting current/nested states.
return
}
dispatch(setPanel(payload))
switch (payload) {
case MainPanelContent.ROUTE_VIEWER:
dispatch(routeTo('/route'))
break
case MainPanelContent.STOP_VIEWER:
dispatch(routeTo('/stop'))
break
default:
// Clear route, stop, and trip viewer focus and route to root
dispatch(viewRoute(null))
dispatch(viewStop(null))
dispatch(setViewedTrip(null))
if (router.location.pathname !== '/') dispatch(routeTo('/'))
break
}
}
}
const setPanel = createAction('SET_MAIN_PANEL_CONTENT')
export const clearPanel = createAction('CLEAR_MAIN_PANEL')
// Stop/Route/Trip Viewer actions
export function setViewedStop (payload) {
return function (dispatch, getState) {
dispatch(viewStop(payload))
const path = payload && payload.stopId
? `/stop/${payload.stopId}`
: '/stop'
dispatch(routeTo(path))
}
}
const viewStop = createAction('SET_VIEWED_STOP')
export const setViewedTrip = createAction('SET_VIEWED_TRIP')
export function setViewedRoute (payload) {
return function (dispatch, getState) {
dispatch(viewRoute(payload))
const path = payload && payload.routeId
? `/route/${payload.routeId}`
: '/route'
dispatch(routeTo(path))
}
}
const viewRoute = createAction('SET_VIEWED_ROUTE')
export const toggleAutoRefresh = createAction('TOGGLE_AUTO_REFRESH')
// UI state enums
export const MainPanelContent = {
ROUTE_VIEWER: 1,
STOP_VIEWER: 2
}
export const MobileScreens = {
WELCOME_SCREEN: 1,
SET_INITIAL_LOCATION: 2,
SEARCH_FORM: 3,
SET_FROM_LOCATION: 4,
SET_TO_LOCATION: 5,
SET_OPTIONS: 6,
SET_DATETIME: 7,
RESULTS_SUMMARY: 8
}
/**
* Enum to describe the layout of the itinerary view
* (currently only used in batch results).
*/
export const ItineraryView = {
/** One itinerary is shown. (In mobile view, the map is hidden.) */
FULL: 'full',
/** One itinerary is shown, itinerary and map are focused on a leg. (The mobile view is split.) */
LEG: 'leg',
/** One itinerary leg is hidden. (In mobile view, the map is expanded.) */
LEG_HIDDEN: 'leg-hidden',
/** The list of itineraries is shown. (The mobile view is split.) */
LIST: 'list',
/** The list of itineraries is hidden. (In mobile view, the map is expanded.) */
LIST_HIDDEN: 'list-hidden'
}
const setPreviousItineraryView = createAction('SET_PREVIOUS_ITINERARY_VIEW')
/**
* Sets the itinerary view state (see values above) in the URL params
* (currently only used in batch results).
*/
export function setItineraryView (value) {
return function (dispatch, getState) {
const urlParams = coreUtils.query.getUrlParams()
const prevItineraryView = urlParams.ui_itineraryView || ItineraryView.LIST
// If the itinerary value is changed,
// set the desired ui query param, or remove it if same as default,
// and store the current view as previousItineraryView.
if (value !== urlParams.ui_itineraryView) {
if (value !== ItineraryView.LIST) {
urlParams.ui_itineraryView = value
} else if (urlParams.ui_itineraryView) {
delete urlParams.ui_itineraryView
}
dispatch(setUrlSearch(urlParams))
dispatch(setPreviousItineraryView(prevItineraryView))
}
}
}
/**
* Switch the mobile batch results view between full map view and the split state
* (itinerary list or itinerary leg view) that was in place prior.
*/
export function toggleBatchResultsMap () {
return function (dispatch, getState) {
const urlParams = coreUtils.query.getUrlParams()
const itineraryView = urlParams.ui_itineraryView || ItineraryView.LIST
if (itineraryView === ItineraryView.LEG) {
dispatch(setItineraryView(ItineraryView.LEG_HIDDEN))
} else if (itineraryView === ItineraryView.LIST) {
dispatch(setItineraryView(ItineraryView.LIST_HIDDEN))
} else {
const { previousItineraryView } = getState().otp.ui
dispatch(setItineraryView(previousItineraryView))
}
}
}
/**
* Takes the user back to the mobile search screen in mobile views.
*/
export function showMobileSearchScreen () {
return function (dispatch, getState) {
// Reset itinerary view state to show the list of results *before* clearing the search.
// (Otherwise, if the map is expanded, the search is not cleared.)
dispatch(setItineraryView(ItineraryView.LIST))
dispatch(clearActiveSearch())
dispatch(setMobileScreen(MobileScreens.SEARCH_FORM))
}
}