Over the weekend I built a mobile app to explore React Native and the mobile development process. While thinking about what I should build, I considered my own commutes and how I wish they could be better (I was likely commuting when I was thinking about it!). I take the train to and from Chicago’s northside to the River North neighborhood every day and I assumed city transit was the most efficient option by cost and time but I wasn’t certain.
So I set off to build an app that would provide multiple transportation options with a given location and destination and determine which of those options were better optimized for cost and time savings.
I assumed the user didn’t have a personal car (because I don’t) and that they could only use transportation options in Chicago. Those options are; train/bus, car ride sharing, bike sharing, and walking.
In the end, my app’s flow turned out to be as follows:
- The app displays the user’s current location and asks them to input a destination address.
- With Google Location services, it autocompletes the destination address as the user is typing.
- The app returns each transportation options including cost, travel time, and links to routes.
- When the user toggles between “Cost Weighted” and “Time Weighted”, it marks the transportation option that’s optimized for that weight type.
But in the process of building the app, I came across an interesting problem: how to find the nearest location from an array of locations? Specifically, I needed to find the nearest Divvy bike station from the user’s current location and provide them with a link to view the route to that station.
Starting With Current Location
At this point in my development, I already had available the user’s current location. This was accomplished by storing the geolocation provided by the user’s device (after the user granted permission, of course). The following snippet demonstrates how I accomplished this using Expo’s Location module:
export const getCurrentLocation = (simulator) => { | |
return async (dispatch) => { | |
if (!simulator) { | |
let { status } = await Permissions.askAsync(Permissions.LOCATION); | |
if (status !== 'granted') { | |
dispatch(gotCurrentLocationError('Permission to access location was denied')) | |
} | |
let location = await Location.getCurrentPositionAsync({}); | |
dispatch(gotCurrentLocation(location)) | |
} else { | |
// Demo location for simulator | |
dispatch(gotCurrentLocation(chicagoFSA)) | |
} | |
} | |
} |
I’m passing a boolean value into my getCurrentLocation
thunk creator that allowed me to filter against the app being run in a simulator (which did not provide me accurate geolocation data). Then the Location.getCurrentPositionAsyc
promise returns the user’s location and dispatches it into to my Redux store.
If you’re not using Expo, you could also accomplish this with the React Native Geolocation service. Using either method, the end result would be the same; access to the latitude and longitude values of your user’s current location.
Dealing With Divvy Sations
In Chicago, the bike sharing service Divvy has many hundreds of bike stations across the city. If I used the Divvy station public API endpoint, I would need to search through over 500 city locations before I could find the nearest station. That seemed woefully inefficient.
Instead of searching through every existing Divvy bike station individually, I could narrow it down to a search radius based on the user’s current location. After some research, I discovered that the Google Places API allows for just that using the nearbysearch
endpoint and radius
flag.
So now my request looked like this:
The user’s current location was passed as lat
and lng
, with a search radius
of 1000 meters, and all point_of_interest
types for divvy
returned. For any given location in Chicago, this query returns between 5–20 stations within a location radius. Not exactly what I was after but I was getting closer.
Of those stations within a 1000 meter radius, we could then check for a distance value from our current location against each station. To do this, I employed the help of the Geolib library for distance calculations along with a couple of familiar Javascript array methods; map and sort.
I started with a thunk creator that received the user’s latitude and longitude for our Google places query:
export const getNearestDivvyStation = (lat, lng) => { | |
const url = `${GOOGLE_PLACES_URL}?location=${lat},${lng}&radius=1000&type=point_of_interest&keyword=divvy&key=${API_KEY}` | |
return async (dispatch) => { | |
const { data } = await axios.get(url) | |
} | |
} |
The data returned from the GET request (using Axios in this case) included an array of individual station locations. This gave me access to the latitude and longitude of each surrounding station but it came with a great deal of other information I didn’t really need in my Redux store. I only need the station’s latitude and longitude at geometry.location
and eventually the distance from our starting point.
const data = "results": [ | |
{ | |
"geometry": { | |
"location": { | |
"lat": 41.8921794, | |
"lng": -87.6366551, | |
}, | |
"viewport": { | |
"northeast": { | |
"lat": 41.89363007989272, | |
"lng": -87.63530692010728, | |
}, | |
"southwest": { | |
"lat": 41.89093042010728, | |
"lng": -87.63800657989272, | |
}, | |
}, | |
}, | |
"icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", | |
"id": "e45dcf68e59b9b8cdccbe762b514f1b52eb63718", | |
"name": "Divvy Station: Orleans St & Ohio St", | |
"place_id": "ChIJV6F9r7QsDogRrDXAEYAZjtI", | |
"rating": 1, | |
"reference": "CmRbAAAAj4nnE4ECd1upWIsgX6hA0a8bMQdSRjedOaOft9hzVbzQf4sUoouQ1xRxiBCPtaOHF5OMhGUyoKFBnUDT738a7mX-G5Fpg6URBQ38IlmOmCiQw83u6ePrNu0iX4EiE0beEhDPhVaLnDVn1Bhw2bM_74XVGhRKSFM_xpRv2Ji4KUYlre97Z4Gjgw", | |
"scope": "GOOGLE", | |
"types": [ | |
"point_of_interest", | |
"establishment", | |
], | |
"vicinity": "325 W Ohio St, Chicago", | |
}, | |
{ | |
"geometry": { | |
} | |
} | |
] |
I then mapped through each station and found just the latitude and longitude coordinates that I needed for each:
const coords = data.results.map((station) => { | |
const coord = station.geometry.location | |
}) |
So now the station data looks like this:
const coords = [ | |
{ | |
"lat": 41.8921794, | |
"lng": -87.6366551, | |
}, | |
{ | |
"lat": 41.894652, | |
"lng": -87.638362, | |
}, | |
{ | |
"lat": 41.89993, | |
"lng": -87.63443, | |
}, | |
{ | |
"lat": 41.8967844, | |
"lng": -87.63562809999999, | |
}, | |
{ | |
"lat": 41.8938449, | |
"lng": -87.6417762, | |
}, | |
] |
With each station coordinates, I could now calculate a distance value for each in the map and add that to my new array. By leveraging the Geolib library, I mapped over the location coordinates, calculated the distance value, and added that it to a new array of data:
// User's current location | |
const current = {lat, lng} | |
const closest = data.results.map((station) => { | |
const coord = station.geometry.location | |
return { coord, dist: geolib.getDistance(current, coord) } | |
}) |
The resulting array after adding the distance value to each station location now looked like this:
const distances = [ | |
{ | |
"coord": { | |
"lat": 41.8921794, | |
"lng": -87.6366551, | |
}, | |
"dist": 396, | |
}, | |
{ | |
"coord": { | |
"lat": 41.894652, | |
"lng": -87.638362, | |
}, | |
"dist": 88, | |
}, | |
{ | |
"coord": { | |
"lat": 41.89993, | |
"lng": -87.63443, | |
}, | |
"dist": 644, | |
}, | |
{ | |
"coord": { | |
"lat": 41.8967844, | |
"lng": -87.63562809999999, | |
}, | |
"dist": 329, | |
}, | |
{ | |
"coord": { | |
"lat": 41.8938449, | |
"lng": -87.6417762, | |
}, | |
"dist": 277, | |
} | |
] |
With our distances calculated, it was then just a matter of sorting for the closest station using the sort
method and taking the first (closest) location:
const closest = distance.sort( (a, b) => a.dist - b.dist )[0] |
Our Redux thunk that dispatches the closest Divvy bike station is now complete:
export const getNearestDivvyStation = (lat, lng) => { | |
const url = `${GOOGLE_PLACES_URL}?location=${lat},${lng}&radius=1000&type=point_of_interest&keyword=divvy&key=${API_KEY}` | |
return async (dispatch) => { | |
const { data } = await axios.get(url) | |
const current = {lat, lng} | |
const closest = data.results.map((station) => { | |
const coord = station.geometry.location | |
return { coord, dist: geolib.getDistance(current, coord) } | |
}) | |
.sort( (a, b) => a.dist - b.dist )[0] | |
dispatch(gotNearestDivvyStation(closest)) | |
} | |
} |
Conclusion
Working with API data can be challenging when you’re not getting exactly what you need to accomplish your task. But often you can solve the issue by working through the given data incrementally to transform it into the desired output. In this situation, the Google Places API allowed me to search within a specific radius area relatively easily but it took some creative data manipulation to be able to add a distance value as well.