About the Author:

Advanced Google Maps with JavaScript: Filtering and Displaying Information

September 1st, 2016

In our previous post about building interactive maps with Python and Javascript we learned how to create a GeoJSON dataset of campgrounds and display information on a Google Map with markers, using the Google Maps JavaScript API. We’re going to build on that foundation in today’s post by showing you how to use some more advanced techniques that are not discussed in Google’s documentation. To create a more customized user experience, we are going to learn how to filter markers and how to display marker GeoJSON outside of the Google Maps object.

We will be building out this Yelp-like interface for finding campgrounds. Similar to Yelp, you can filter your results, and when you click a marker on the map the information for that campground will be displayed below the map. Try it out!

See the Pen Map with Buttons and Display by Sev (@sevleonard) on CodePen.

Filtering Markers

In the previous post we used the loadGeoJson method to populate the markers on our map. For this example, we are going to use the jQuery getJson method.

$.getJSON(geojson_url, function(result) {
   // get the features array from the FeatureCollection 
   data = result['features']
   $.each(data, function(key, val) {
      // create markers
   })
})

We’ve made a modification to the GeoJSON file we were previously using to include specific properties for the amenity fields. These properties are what we will use to filter the markers.

geojson2

To keep track of what amenities the user wants to filter on lets create a list of possible filters, using the same names as we have for the new property fields:

// start out with filter features set to false, so no filtering happens by default
var filters = {shower:false, vault:false, flush:false}

Now, whenever we want to set marker visibility based on filter settings, we simply iterate through the array of markers and compare the properties field to the filters that are set to ‘true’:

// get a subset of the filters that are set to true
var get_set_options = function() {
  ret_array = []
  for (option in filters) {
    if (filters[option]) {
      ret_array.push(option)
    }
  }
  return ret_array;
}

var filter_markers = function() {  
  set_filters = get_set_options()
  
  // for each marker, check to see if all required options are set
  for (i = 0; i < markers.length; i++) {
    marker = markers[i];

    // start the filter check assuming the marker will be displayed
    // if any of the required features are missing, set 'keep' to false
    // to discard this marker
    keep=true
    for (opt=0; opt<set_filters.length; opt++) {
      if (!marker.properties[set_filters[opt]]) {
        keep = false;
      }
    }
    marker.setVisible(keep)
  }
}

Since we only want to check the properties that are set to ‘true’ in the filters list, we first call ‘get_set_options’ to return that subset of filters. We then iterate through the markers and check that the filtered value is ‘true’ in the GeoJSON, and if not we set ‘keep’ to ‘false’ to make the marker invisible, thus filtering it out of the current map view.

Because we matched the values in the ‘properties’ field to the names in the ‘filters’ list we simply have to check marker.properties using the name from the ‘filters’ list. A single iteration of the above would look like

marker.properties[filters[opt]]

// for opt = 0
// set_filters[0] -> 'shower'
// marker.properties -> 'flush':true, 'shower':true, 'vault':false
// marker.properties['shower'] evaluates to true

Setting the Filters

Now that we’ve seen how to filter markers, we need to connect the filter selection the UI so users can set and unset filter options. First, we need to provide a way for the user to select different features to filter on.

See the Pen Map with Buttons by Sev (@sevleonard) on CodePen.

Instead of using buttons, we will use checkboxes styled as buttons using a ‘label’ entity. This gives the UI a clean look while providing boolean functionality to use for setting the filters. We’ll use CSS as detailed in this StackOverflow post to style the checkboxes as buttons.

 <input type="checkbox" name="filter" id="shower" class='chk-btn'>
 <label for='shower'>Shower</label>

<input type="checkbox" name="filter" id="flush" class='chk-btn'>
 <label for='flush'>Flush Toilet</label>
 
 <input type="checkbox" name="filter" id="vault" class='chk-btn'>
 <label for='vault'>Vault Toilet</label>

Notice that we have set the name to ‘filter’ for the checkbox inputs. By using a uniform ‘name’ attribute for all of our filter checkboxes we can use a single function to handle all of their change events:

$('input[name=filter]').change(function (e) {
      // update filters list
});

On the ‘change’ event, we can get the ‘id’ attribute of the current checkbox to set the filter, noticing that we used the same value in ‘id’ as we did for our ‘filters’ list. Because we matched these values, we can query the ‘filters’ list for the ‘id’ value of the current checkbox (accessed via ‘this’) and toggle the value of the filter.

// filters list: var filters = {shower:false, vault:false, flush:false}
var map_filter = function(id_val) {
   if (filters[id_val]) 
      filters[id_val] = false
   else
      filters[id_val] = true
}

$('input[name=filter]').change(function (e) {
   map_filter(this.id);
})

We now have a way for the user to set the filters via button-styled checkboxes, and for the filters to be applied to the markers using the ‘filter_markers’ function. We just need a connection between the two to perform the filtering.

<button class="btn btn-blue" id="search_campgrounds">Find Campgrounds</button>

When the user clicks ‘Find Campgrounds’ we execute the ‘filter_markers’ function.

$('#search_campgrounds').on('click', function() {
  filter_markers()
})

Try it out!

See the Pen Map with Buttons by Sev (@sevleonard) on CodePen.

Displaying Marker Info

Info windows are helpful for showing details for a point on the map, but they also hide the surrounding area. Since our users probably want to see the area around the campground it would be better to display the marker ‘description’ in a different place that doesn’t obscure the map.

To do this, lets revisit the ‘loadMarkers’ function from the previous codepen, in particular the ‘addListener’ function:

function loadMarkers() {
   ...
   var descriptionText = feature.getProperty('description')
   var titleText = feature.getProperty('title')
   var markerInfo = "<div><h3>" + titleText + "</h3>Amenities: " + descriptionText + "</div>"
   ...
   marker.addListener('click', function() {
      infoWindow.close()
      infoWindow.setContent(markerInfo)
      infoWindow.open(map, marker)
   });
   ...
}

Instead of setting an infoWindow when we click a marker, we want to display the markerInfo in another part of the DOM. First lets provide a home for that information:

 <div id="campground_info"></div>

Since we’ve already created the HTML formatting for the markerInfo, we can simply set the html attribute of the campground_info div in the ‘click’ listener, instead of creating the infoWindow:

marker.addListener('click', function() {
   $('#campground_info').html(markerInfo)
});

Now, when we click on each campground we will see the ‘description’ information displayed below the map in the campground_info div:

See the Pen Map with Buttons and Display by Sev (@sevleonard) on CodePen.

Summary

We’ve learned how to add filtering to Google Maps markers and display marker information outside of the map. By using GeoJSON properties in the ‘id’ fields of our filter checkboxes and for the values in our ‘filters’ list we were able to quickly put together this prototype.  Standardizing values in this way really simplifies the coding process, and results in code that reads easily. We can leverage this because we own the entire stack from GeoJSON generation to the front end – be careful about using these techniques if someone else is providing the source information, as fields in the GeoJSON could change. In that case you would want to provide a translation function to provide a consistent interface to your UI regardless of changes in the underlying GeoJSON.

Some fun extras you could try out with the code we developed in this post:

  • Change the marker, the marker label, or animate the marker on click. This will help the user keep track of which campground’s information they are seeing in the campground_info div.
  • What if we couldn’t change the GeoJSON? What are other ways you could filter based on amenities without the ‘flush’,’shower’,and ‘vault’ properties? (hint: consider the other ‘properties’ fields).
  • Add a button to clear the filters so the user doesn’t have to un-check filters.

About the Author:

Interactive Mapping with Python, GeoJSON, and JavaScript

August 19th, 2016

Interactive maps are a powerful tool for data visualization and user interfaces. Providing rich content based on geographic location is the cornerstone of many apps such as Yelp,  Zillow, and Road Trippers. In this post we are going to create an interactive map of campgrounds using the Google Maps API, JavaScript, and Python.

The codepen below shows what we’ll be building:

See the Pen Finished Map by Sev (@sevleonard) on CodePen.


What We Will Achieve Here

  1. Build a Google map with JavaScript.
  2. Convert geographic campground data from a CSV to GeoJSON so Google Maps can read it.
  3. Load the GeoJSON into Google Maps and set up click events and interactivity.
  4. Go camping at Crater Lake (optional).

Designing with Maps

A good map-based application, or really, any good application, begins with thinking about what functionality you want to provide and how users will interact with it. Here are some questions to consider while you’re specing things out.

  1. What geographic region do you want to map?
  2. What kind of map makes the most sense for your application? Topographic, traffic, congressional districts?
  3. What information do you have to add to the map?
  4. How will end-users interact with the map?

For this example, we want to build a map to help people find a place to camp near Crater Lake, Oregon. Crater Lake is a popular tourist destination and the deepest lake in the United States, though folks from Lake Tahoe may disagree. Disagreements aside, we know we want a map of the Crater Lake area and we want to display information about campgrounds.

Regarding the type of map to use, let’s think about what would be important to a person looking for a place to camp. Certainly a camper would want to know how they can get to Crater Lake from a campground, so we definitely should pick a map that shows roads. Perhaps we read a study of outdoor recreation and found that most campers prefer to camp in shaded areas. With satellite imagery campers could identify shaded campgrounds by looking at the tree cover, so that seems like a good bit of information to include in our map.

Using information from the National Forest Service, we have a dataset of eleven campgrounds closest to Crater Lake. Each data point includes the campground name and location with some additional features on campground amenities: showers and toilet type – vault or flush. We’ll display the data using map markers for each campground, displaying the amenities if the user clicks the marker to learn more, like this:

Now that we have an initial design, let’s get into the details of making it happen.

Setting up a map element

We’ll setup our campground finder as a web app that runs in the browser. We need a reference to the Google Maps API and a div element where the map will be displayed.

<script src="https://maps.googleapis.com/maps/api/js"></script>

<div id="map"></div>

For this example we are not specifying an API key, so you will see a warning “NoApiKeys” when running the code in codepen. If you are using the Google Maps API for your web application you should get an API key and note the usage limits. As long as you stay under 1,000 map loads / hr you should be good with the free plan.

One last thing before we move on – we need to set the size of the map div. If you forget to do this, you’ll find yourself staring at a blank page. Not super helpful for finding a place to camp.

#map {

  width: 600px;

  height: 500px;

}

Customizing the map

At this point our app isn’t very exciting. Just a blank HTML page. Now we’ll setup and display the map, recalling our design specs.

Map types – We decided we wanted to show roads and satellite images for our campground map. Conveniently this sort of map is one of the basic map types, hybrid.

Zoom Level – The initial level of detail is determined by setting the zoom level. A zoom level of 10 will give us enough detail to show all the campgrounds near Crater Lakes and the major roadways.

Center – as you would likely expect, this is the latitude, longitude where you’d like to center the map. Our map is centered at Crater Lake.

We’ll encapsulate these parameters in the map_options object:

map_options = {

zoom: 10,

mapTypeId: google.maps.MapTypeId.HYBRID,

center: {lat: 42.9446, lng: -122.1090}

}

Initializing the Map

Using the Map constructor, we pass the map_options and the map div to create a new object “map”.  We will refer to the map object later when we are ready to add the campgrounds.

map_document = document.getElementById('map')

map = new google.maps.Map(map_document,map_options);

Let’s set the map to draw when the page is loaded. To do this, we’ll add a call to addDomListener. The initMap function containing our map setup code will be called when the window is loaded.

function initMap() {

   map_options = {

     zoom: 10,

     mapTypeId: google.maps.MapTypeId.HYBRID,

     center: {lat: 42.9446, lng: -122.1090}

   }

   

   map_document = document.getElementById('map')

   map = new google.maps.Map(map_document,map_options);

}

google.maps.event.addDomListener(window, 'load', initMap);

Alright! We are now all set to load a map. If you run the codepen you should see this map of Crater Lake and the surrounding area

See the Pen Finished Map by Sev (@sevleonard) on CodePen.

Getting data for our map

Now that we have a map, lets get some data to add to it.  Google Maps can read data in a variety of different formats, including GeoJSON, which we will be using in this example.  All we need now is a bit of Python code to convert the data to GeoJson. The code in the next section uses Python 3.4.3 and version 0.18.1 of the pandas library. You can follow along using the Jupyter notebook here

Data cleaning

When working with data it’s important to begin by reading it! We need to convert our data from a CSV format to the GeoJSON format that Google Maps can read. Using the Python pandas library, we can read data into a tabular structure called a DataFrame. We can use this object to observe, analyze and manipulate the data. The campground data is in a CSV format, we can use pandas read_csv to read this into a DataFrame.

# import the modules we need to convert the CSV data to GeoJSON
import pandas as pd

import numpy as np

from pandas import json

cg_data = pd.read_csv('campgrounds.csv')

# inspect the data, take a look at its shape (rows, columns) and the first

# few rows of data

cg_data.shape

>> (11, 6)

cg_data.head()

Each row represents a campground, with its name denoted by the facilityname field, a string. The latitude and longitude in numeric format are next, followed by the amenities – flush toilets, showers, and vault toilets. The amenities fields have three possible values: 0,1, and ‘\N’.  0 denotes False, 1 is True, and ‘\N’ indicates the data is not available.

We want to transform this data into valid GeoJSON, for example:

Example GeoJSON:

{

"type": "Feature",

"properties": {

"title":"Ainsworth State Park",

"description": "Flush toilet, shower"

}

"geometry": {

"type": "Point",

"coordinates":[-122.048974,45.59844]

}

}

We’ll use the facility name for the ‘title’ field, and create a string of the amenities for the ‘description’ field. A few lines of Python to change some column names and replace amenity field values should do the trick:

cg_data_clean = cg_data

# replace the amenity values with the appropriate strings

cg_data_clean = cg_data_clean.replace({'flush': {'1':'Flush toilet', '0':'', '\N':''}})

cg_data_clean = cg_data_clean.replace({'shower': {'1':'Shower', '0':'', '\N':''}})

cg_data_clean = cg_data_clean.replace({'vault': {'1':'Vault toilet', '0':'', '\N':''}})

# rename columns to be consistent with the GeoJSON field names

cg_data_clean = cg_data_clean.rename(columns={'facilityname': 'title',

                                             'facilitylatitude':'latitude',

                                             'facilitylongitude':'longitude'})

We can concatenate the amenity fields into a single ‘description’ field using the .join() function inside an apply. The apply function enables you to perform operations over rows (axis=0) or columns (axis=1) of a DataFrame. This leaves us with some extraneous leading and trailing commas for this data set, so we also have a helper function, clean_description, to strip leading and trailing commas.

# create description field

cg_data_clean['description'] = cg_data_clean[['flush','shower','vault']].apply(lambda x: ', '.join(x), axis=1)

# function to clean the leading / ending commas from the description. This won’t remove commas in between fields, though!

def clean_description(description):

   description = description.strip()

   while((description.startswith(',') or description.endswith(',')) and len(description) > -1):

       if description.endswith(',') :

           description = description[0:len(description)-1]

       if description.startswith(',') :

           description = description[1:len(description)]   

       description = description.strip()

   return description

# apply the clean_description function to all rows

cg_data_clean['description'] = cg_data_clean.description.apply(lambda x: clean_description(x))

We no longer need the individual amenity columns, having concatenated them into the description column, so we can drop those moving forward. We’re ready to convert the DataFrame to GeoJSON, which we will again call on our new friend apply to help with. Thanks to Geoff Boeing and his post on Exporting Python Data to GeoJSON for some insight here.

Recalling our GeoJSON example above, each campground will be a feature, part of a FeatureCollection object. The approach here is to create a collection object and add features to it as they are processed by the feature_from_row method.

# create the feature collection

collection = {'type':'FeatureCollection', 'features':[]}

# function to create a feature from each row and add it to the collection

def feature_from_row(title, latitude, longitude, description):

   feature = { 'type': 'Feature',

              'properties': { 'title': '', 'description': ''},

              'geometry': { 'type': 'Point', 'coordinates': []}

              }

   feature['geometry']['coordinates'] = [longitude, latitude]

   feature['properties']['title'] = title

   feature['properties']['description'] = description

   collection['features'].append(feature)

   return feature
# apply the feature_from_row function to populate the feature collection geojson_series = geojson_df.apply(lambda x: feature_from_row(x['title'],x['latitude'],x['longitude'],x['description']),

axis=1)

Looking at the ‘collection’ object we can see how the DataFrame was converted to GeoJSON. Here are the first two features in our FeatureCollection:

Using pandas json module we can write the collection out to a geojson file with the correct formatting. We’ll reference this file to pull in the campground data for the map.

with open('collection.geojson', 'w') as outfile:
   json.dump(collection, outfile)

Adding GeoJSON data to the map

We have our base map and our campground GeoJSON. We just need a little more JavaScript to pull all of it together. Google Maps has a super convenient loadGeoJson method that we can use to get our points onto the map right away:

geojson_url = 'https://raw.githubusercontent.com/gizm00/blog_code/master/appendto/python_maps/collection.geojson'

map.data.loadGeoJson(geojson_url, null, loadMarkers)

Running this in codepen, you should see markers added to the map. Those are the campgrounds! Hover over a campground to see its name given in the ‘title’ field.

The loadMarkers parameter in the loadGeoJson call above is a callback function we will use to add information to the markers. Once the GeoJson is loaded, loadMarkers gets called and the marker objects and pop up windows (info windows) are created. The InfoWindow content field is a string that accepts HTML tags, so you can format and style the pop up as you like. This last codepen includes the code for adding the infoWindow to the markers.

function loadMarkers() {
 console.log('creating markers')
 var infoWindow = new google.maps.InfoWindow()
 map.data.forEach(function(feature) {
   
   // geojson format is [longitude, latitude] but google maps marker position attribute
   // expects [latitude, longitude]
   var latitude = feature.getGeometry().get().lat()
   var longitude = feature.getGeometry().get().lng()
   var titleText = feature.getProperty('title')
   var descriptionText = feature.getProperty('description')

   var marker = new google.maps.Marker({
     position: {lat: latitude, lng:longitude},
     title: titleText,
     map: map
    });
   
   var markerInfo = "<div><h3>" + titleText + "</h3>Amenities: " + descriptionText + "</div>"
   // by default the infoWindow for each marker will stay open unless manually closed

   // using setContent and opening the window whenever a marker is clicked will

   // cause the prior infoWindow to close
   marker.addListener('click', function() {
         infoWindow.close()
         infoWindow.setContent(markerInfo)
         infoWindow.open(map, marker)
       });
   markers.push(marker)
 });
}

We’re all done! Try it out and see which campground you’d prefer. One last note, we’re keeping an array of the current markers for later use when it will be needed for functions like filtering.

Summary

Congratulations! You now know how to clean and format raw data and use it to create an interactive map. Maybe you can think of some additional information you’d like to add to the map like current weather or cell phone coverage. You can follow the same procedures we used for cleaning the campground data to other information sources, and adding these features to the ‘description’ GeoJSON field to easily include it on your map. Happy mapping!