Taking an existing project to the next level Part Three

Where the rubber meets the road on Google Maps

Matthew Madden
Chingu
7 min readSep 17, 2017

--

This is the third part in a four part series. Click HERE to start from the beginning. In the last article, Greg covered the inclusion of Redux in our application and how having this in place provide a solid foundation on which to add new features.

One of the “asks” from our volunteers was the ability to track the neighborhood where a given animal was lost or found. I believe they had either a text field or perhaps a drop-down list in mind when they made that request, but neighborhood boundaries aren’t always perfectly defined, and a text field would leave too much room for inconsistency. Also, these methods seem so… 15 years ago. I imagined an integrated Google Map with the ability to drop a pin on the exact spot where the dog was seen and somehow based on those coordinates, get the name of the neighborhood back.

Having only limited experience with the Google Maps API, I wasn’t certain that we could achieve what we wanted, so with my wild idea in hand, I ventured into their docs. It took about five minutes to find an example that came close enough to what I was trying to accomplish for me to confidently move forward. I shared the possibility with Greg and we agreed to venture further down the Google Maps rabbit hole.

Obstacles in our path

The way React functions in itself obstructs the way Google wants to load their Maps API. Because React is rendering a stack of components, there isn’t an easy way to ensure the script will have loaded by the time it’s expected to render something on the page unless the API is loaded up front. Being a conscientious developer, I imposed the condition that the API should not load until it’s actually needed on the page. No map… no call. Fortunately (or so we thought) there are several packages available to accomplish the asynchronous loading along with well developed map features. I scoured several of them looking for the ability to plug into predefined geoJSON data (for the polygon regions) and the ability to work with markers.

Rather than building directly into the application, we decided to work in a separate corner on the JS loading and map functionality and tackle the integration with Redux later. We had maps loading on the page in no time, but we quickly hit roadblocks when trying to utilize specific marker features and in particular when we tried to get region information back in response to a click. The more we read and coded the maps, the more we realized a few things.

  1. The Google Maps API is pretty easy to work with and has some amazing features like the containsLocation() method.
  2. Having the functionality of the API obfuscated away inside a neat package can be quite limiting.
  3. It’s not that hard to use an independent asynchronous JS loader (more on that later).

Map packages like any of the ones we tried are probably perfectly suited to 99% of the cases the might be used and might well have been successfully forced into service here, but we ultimately decided to abandon them and code the functionality on our own.

Name that Neighborhood

I found a website where I could draw out my own polygons and copy down the GeoJSON data. There are several out there. I modified the JSON to include the name of the neighborhood that the polygon would cover. Below is a tiny sample of what the file looks like. You have to imagine it with about 40 or 50 sets of coordinates representing each region.

...“name”: “Riverpoint”,“polygon”: [{“lng”: -86.67359478771687,“lat”: 36.16984061599069},
{
"lng": -86.67355187237263,"lat": 36.17351282749545}...

Changes to this file over time will be fairly minimal and I’m grateful for that as it’s tedious work trying to make the borders between regions fit exactly together. Ultimately this should all sit inside the Redux store somewhere so every page has instant access. The code to pull the correct name couldn’t be simpler…

for(let i=0; i<regions.length;i++) {currentPoly = new google.maps.Polygon({paths:    regions[i].polygon});if(google.maps.geometry.poly.containsLocation(latLng, currentPoly)) {regionName = regions[i].name;}}

It’s a simple loop that iterates over each region and sends those coordinates along with the coordinates of the last click off to google in the containsLocation() method which returns a simple true/false. We’ll grab the name and store it in the database along with the coordinates so we have all that at hand for future map renders and queries.

Not so fast…

Obviously the speed of this method suffers depending on the number of regions we’ll have to test. At this point we’re only testing with three regions, so some adjustments might need to be made if performance takes a hit. We have a default “Outside defined regions” that gets recorded when the coordinates of the click don’t fall inside a defined area. This way, if an animal is picked up outside a defined polygon, we won’t have the neighborhood name, but we will still have the exact coordinates which is ultimately far more valuable.

Put a pin in it

Compared to collecting the name of the region where a marker get’s dropped, getting the marker itself to display is a piece of cake… mmmm cake. We use two methods.

placeMarkerAndPanTo(latLng, map) {if(marker !== undefined) {marker.setMap(null)}marker = new google.maps.Marker({position: latLng,map,icon: require(`./images/mapIcons/${this.props.newHistory.status}${this.props.currentAnimal.type}Icon.png`)});map.panTo(latLng);}

This first one requires only the coordinates of the click and a reference for the map to which it is applied. I made some fancy icons that would reflect the status (lost or found) and type (cat or dog) of animal. As soon as the marker is dropped, the map centers on that location.

I imagined a scenario where someone would drop the marker and then switch “lost” to “found” or “cat” to “dog” before submitting the form. I needed the marker icon to update accordingly. Hence the method below which responds to those specific form actions.

replaceMarkerIcon(latLng, map, status, type) {marker.setMap(null)marker = new google.maps.Marker({position: latLng,map,icon: require(`./images/mapIcons/${status}${type}Icon.png`)});}

The latLng coordinates and map properties are the same as in the previous method. The status and type props come from the form handlers. After that is collected for this method, the update is a simple matter of doing away with the existing marker with marker.setMap(null) , and then a new one is put in it’s place.

At this point we hadn’t included the maps into the app itself, but here is a preview including some indicators for the currently selected region, status, and type of animal which gives it a nice responsive feel.

Excited at the prospect of bringing “real” data into the maps and recording “real” data to the db based on map interactions, we ventured into the swamp of including the maps on the three pages where we wanted them and found ourselves stuck.

You can’t get there from here

In retrospect, given how easy the solution to this problem is, it’s a little embarrassing to mention what a huge stumbling block this was. We could not figure out how to combine both Redux and the asynchronous script loader on the same React component… just couldn’t do it.

We had this…

export default scriptLoader(["https://maps.googleapis.com/maps/api/js?key=supersecretapikey&libraries=geometry"])(Add)

…and something like this…

export default connect(state => {return {currentAnimal: state.animal.currentAnimal}})(Add);

…and the solution was…

const LoadConnector = connect(state => {return {currentAnimal: state.animal.currentAnimal}})(Add)export default scriptLoader([“https://maps.googleapis.com/maps/api/js?key=supersecretapikey&libraries=geometry"])(LoadConnector)

I can only hope that someone else facing this problem finds this article and is spared a little misery. From here the process of loading the maps went fairly smoothly. We did take advantage of the React application life-cycle methods quite a bit. Staging the order in which the map loads, means that the map itself will not have to re-render on the page when new markers are added or an event fires on a form.

Results

I’ve already shown the map functionality to my primary contact with the non-profit. It’s always fun to amaze people with technology, even if it’s something like Google Maps which I’m certain their already familiar, but maybe haven’t thought about how it can be used to better help solve their specific problem. The interactivity that it brings to the application alone makes it a lot more fun to use.

Next Steps

Having applied maps to almost all of the pages we wanted it on, we realized that there was an opportunity to provide a history of sorts for each animal. Instead of simply overwriting the most recent location and status data, we thought we should create a new instance when updates were made. Our hope was that this would create a more complete picture of each animal for the volunteers and potential adopters. A story of sorts for every animal they interact with. Of course allowing for that kind of tracking over time would require a significant adjustment to the way we were structuring our data. That restructuring is the subject of our next and final article.

--

--