Skip to content

Commit d525f0f

Browse files
author
David Emory
committed
feat(overlay): Implement first pass at stops overlay
1 parent 0bbaef4 commit d525f0f

File tree

5 files changed

+175
-0
lines changed

5 files changed

+175
-0
lines changed

lib/actions/api.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export function bikeRentalQuery () {
158158
}
159159
}
160160

161+
// Nearby Stops Query
162+
161163
export const receivedNearbyStopsResponse = createAction('NEARBY_STOPS_RESPONSE')
162164
export const receivedNearbyStopsError = createAction('NEARBY_STOPS_ERROR')
163165

@@ -192,6 +194,8 @@ export function findNearbyStops (params) {
192194
}
193195
}
194196

197+
// Routes at Stop query
198+
195199
export const receivedRoutesAtStopResponse = createAction('ROUTES_AT_STOP_RESPONSE')
196200
export const receivedRoutesAtStopError = createAction('ROUTES_AT_STOP_ERROR')
197201

@@ -216,3 +220,33 @@ export function findRoutesAtStop (stopId) {
216220
dispatch(receivedRoutesAtStopResponse({ stopId, routes }))
217221
}
218222
}
223+
224+
// Stops within Bounding Box Query
225+
226+
export const receivedStopsWithinBBoxResponse = createAction('STOPS_WITHIN_BBOX_RESPONSE')
227+
export const receivedStopsWithinBBoxError = createAction('STOPS_WITHIN_BBOX_ERROR')
228+
229+
export function findStopsWithinBBox (params) {
230+
return async function (dispatch, getState) {
231+
const otpState = getState().otp
232+
const api = otpState.config.api
233+
const paramStr = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')
234+
const url = `${api.host}:${api.port ? ':' + api.port : ''}${api.path}/index/stops?${paramStr}`
235+
let stops
236+
try {
237+
const response = await fetch(url)
238+
if (response.status >= 400) {
239+
const error = new Error('Received error from server')
240+
error.response = response
241+
throw error
242+
}
243+
stops = await response.json()
244+
} catch (err) {
245+
return dispatch(receivedStopsWithinBBoxError(err))
246+
}
247+
248+
dispatch(receivedStopsWithinBBoxResponse({ stops }))
249+
}
250+
}
251+
252+
export const clearStops = createAction('CLEAR_STOPS_OVERLAY')
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { PropTypes } from 'react'
2+
import { connect } from 'react-redux'
3+
import { MapLayer, Marker, Popup } from 'react-leaflet'
4+
import { divIcon } from 'leaflet'
5+
6+
import { hasTransit } from '../../util/itinerary'
7+
import { findStopsWithinBBox, clearStops } from '../../actions/api'
8+
9+
class StopsOverlay extends MapLayer {
10+
static propTypes = {
11+
minZoom: PropTypes.number,
12+
queryMode: PropTypes.string,
13+
stops: PropTypes.array,
14+
refreshStops: PropTypes.func
15+
}
16+
17+
static defaultProps = {
18+
minZoom: 15
19+
}
20+
21+
componentDidMount () {
22+
// set up pan/zoom listeners
23+
this.context.map.on('zoomend', () => { this._refreshStops() })
24+
this.context.map.on('dragend', () => { this._refreshStops() })
25+
}
26+
27+
_refreshStops () {
28+
if (this.context.map.getZoom() < this.props.minZoom) {
29+
this.props.clearStops()
30+
return
31+
}
32+
33+
const bounds = this.context.map.getBounds()
34+
const params = {
35+
minLat: bounds.getSouth(),
36+
maxLat: bounds.getNorth(),
37+
minLon: bounds.getWest(),
38+
maxLon: bounds.getEast()
39+
}
40+
this.props.refreshStops(params)
41+
}
42+
43+
createLeafletElement () {
44+
}
45+
46+
updateLeafletElement () {
47+
}
48+
49+
render () {
50+
const { minZoom, queryMode, stops } = this.props
51+
52+
// don't render if below zoom threshold or transit not currently selected
53+
if (this.context.map.getZoom() < minZoom || !hasTransit(queryMode)) return null
54+
55+
return (
56+
<div>
57+
{stops.map((stop) => {
58+
const icon = divIcon({
59+
iconSize: [20, 20],
60+
iconAnchor: [12, 15],
61+
popupAnchor: [1, -6],
62+
html: `<span class="fa-stack" style="opacity: 1.0" style={{ background: #00f }}>
63+
<i class="fa fa-circle fa-stack-1x" style="color: #ffffff"></i>
64+
<i class="fa fa-circle-o fa-stack-1x" style="color: #000000"></i>
65+
</span>`,
66+
className: ''
67+
})
68+
69+
const idArr = stop.id.split(':')
70+
71+
return (
72+
<Marker
73+
icon={icon}
74+
key={stop.id}
75+
position={[stop.lat, stop.lon]}
76+
>
77+
<Popup>
78+
<div>
79+
<div style={{ fontWeight: 'bold', fontSize: 18 }}>{stop.name}</div>
80+
<div><b>Agency:</b> {idArr[0]}</div>
81+
<div><b>Stop ID:</b> {idArr[1]}</div>
82+
</div>
83+
</Popup>
84+
</Marker>
85+
)
86+
})}
87+
</div>
88+
)
89+
}
90+
}
91+
92+
// connect to the redux store
93+
94+
const mapStateToProps = (state, ownProps) => {
95+
return {
96+
stops: state.otp.overlay.transit.stops,
97+
queryMode: state.otp.currentQuery.mode
98+
}
99+
}
100+
101+
const mapDispatchToProps = {
102+
refreshStops: findStopsWithinBBox,
103+
clearStops: clearStops
104+
}
105+
106+
export default connect(mapStateToProps, mapDispatchToProps)(StopsOverlay)

lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import BikeRentalOverlay from './components/map/bike-rental-overlay'
1717
import EndpointsOverlay from './components/map/endpoints-overlay'
1818
import ItineraryOverlay from './components/map/itinerary-overlay'
1919
import OsmBaseLayer from './components/map/osm-base-layer'
20+
import StopsOverlay from './components/map/stops-overlay'
2021

2122
import ItineraryCarousel from './components/narrative/itinerary-carousel'
2223
import NarrativeItineraries from './components/narrative/narrative-itineraries'
@@ -65,6 +66,7 @@ export {
6566
ItineraryCarousel,
6667
ItineraryOverlay,
6768
OsmBaseLayer,
69+
StopsOverlay,
6870

6971
// narrative components
7072
NarrativeItineraries,

lib/reducers/create-otp-reducer.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ function createOtpReducer (config, initialQuery) {
3838
overlay: {
3939
bikeRental: {
4040
stations: []
41+
},
42+
transit: {
43+
stops: []
4144
}
4245
}
4346
}
@@ -174,6 +177,24 @@ function createOtpReducer (config, initialQuery) {
174177
location: { nearbyStops: { $set: action.payload.stops.map(s => s.id) } },
175178
transitIndex: { stops: { $merge: stopLookup } }
176179
})
180+
case 'STOPS_WITHIN_BBOX_RESPONSE':
181+
return update(state, {
182+
overlay: {
183+
transit: {
184+
stops: {$set: action.payload.stops},
185+
pending: {$set: false}
186+
}
187+
}
188+
})
189+
case 'CLEAR_STOPS_OVERLAY':
190+
return update(state, {
191+
overlay: {
192+
transit: {
193+
stops: {$set: []},
194+
pending: {$set: false}
195+
}
196+
}
197+
})
177198
case 'ROUTES_AT_STOP_RESPONSE':
178199
return update(state, { transitIndex: { stops: { [action.payload.stopId]: { routes: { $set: action.payload.routes } } } } })
179200

lib/util/itinerary.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ export function isTransit (mode) {
1111
return transitModes.indexOf(mode) !== -1
1212
}
1313

14+
/**
15+
* @param {string} modesStr a comma-separated list of OTP modes
16+
* @return {boolean} whether any of the modes are transit modes
17+
*/
18+
export function hasTransit (modesStr) {
19+
let result = false
20+
modesStr.split(',').forEach(mode => {
21+
if (isTransit(mode)) result = true
22+
})
23+
return result
24+
}
25+
1426
export function isWalk (mode) {
1527
mode = mode || this.get('mode')
1628
return mode === 'WALK'

0 commit comments

Comments
 (0)