Thursday, August 23, 2012

Using the Maps API Autocomplete for User Profile Location

As a frontend engineer at Coursera, one of my current projects is to create shared profiles for our users. When users first sign up, all we ask them for is their name, and so that's all we can show to other users as well — like when they see each other's posts in the forums. We want to make it so that users can click on a classmate's name and find out more about them, like where they're from and what other classes they're taking, because that should make a classroom of thousands of strangers feel more intimate. Eventually, we also want to make it easy for users to search their classmates to find people to form in-person, location-specific study groups or language-specific discussion forums, because that will give users a mini community inside the class.

By the time users sign up for Coursera, they've already signed up for many other sites and likely filled out a profile information many times, and I wanted to make their Coursera profile editing experience as quick and easy as possible. Easier said than done! It's amazing how many debates and decisions can go into designing a single form.


Location, Location

For example: location. We want to let users share where they are—not their exact address, that'd be creepy—just their city and country. In my first draft of profiles, I started off with a single line text input for location, with a placeholder for "City, State, Country". But, I wanted to be able to query our database and easily find out which users are from what country or state, so I needed a good way of turning user entered locations into structured data.

Halfway down the path of implementing it myself, I discovered that the Google Maps API now offers an Autocomplete API and Autocomplete widget, and that it was exactly what I needed. When you attach the widget to a text input, it listens to a user typing and pops up suggestions (like Google instant search), lets them pick a suggestion, and it can call back your code with structured information about the picked address.

The Code

To use it for our profile editor form, I first created HTML for the single-line text input and a set of hidden inputs for each piece of structured location data that I cared about. We actually write our HTML in bracket-less Jade templates, but here's what it comes out as:

<input name="location" type="text" placeholder="City, State, Country">
<input name="location_city" type="hidden">
<input name="location_state" type="hidden">
<input name="location_country" type="hidden">
<input name="location_lat" type="hidden">
<input name="location_lng" type="hidden">
<input name="location_id" type="hidden">

Then in the JavaScript, I attach the Autocomplete widget to the location input and add an event listener for every time the user selects a new result. In the callback, I populate the hidden inputs with the relevant structured data. The results format for the Maps API is a bit complex, because they designed it to work for any address anywhere in the world (and not every address is divided into the same city/state divisions as US addresses), so you end up having to search through all the "address components" to find the component that most matches what you're interested in. To simplify that search, I wrote a wrapper function using Underscore's find method. Here's what that looks like:

var autocomplete = new google.maps.places.Autocomplete($('#input[name="location"')[0], {types: ['geocode']});
  
google.maps.event.addListener(autocomplete, 'place_changed', function() {
    var place = autocomplete.getPlace();
    $('input[name="location_id"]').val(place.id);
    $('input[name="location_lat"]').val(place.geometry.location.lat());
    $('input[name="location_lng"]').val(place.geometry.location.lng());
    $('input[name="location_country"]').val(findComponent(place, 'country'));
    $('input[name="location_state"]').val(findComponent(place, 'administrative_area_level_1'));
    $('input[name="location_city"]').val(findComponent(place, 'administrative_area_level_3') || findComponent(place, 'locality'));
});

function findComponent(result, type) {
  var component = _.find(result.address_components, function(component) {
    return _.include(component.types, type);
  });
  return component && component.short_name;
}

All the code is in this gist.


The Result

We launched the first iteration of profiles today, so you can try it our yourself, in your settings. Or you can check out this screenshot of what it looks like to a user when they're editing their profile.

When a classmate is viewing their profile (like you can view mine here, they'll see their location plus a little flag (courtesy of this free flag icons database), and that flag is based on the hidden country field.


Caveats

There are a few things to keep in mind with this approach:

  • If the user types their location but does not select it from the autocomplete, then we will not store the structured data. I could add in a pre-submit reverse geocode step in case that happens, but I'll wait until I see what users do.
  • If the user types a location that the Google Maps API simply doesn't know, then we won't have any structured data about it. That's okay- we'll still display what they typed on their profile, we just won't be able to do any clever location-based recommendations for them.
  • The Autocomplete widget includes the Google logo, and per the terms, that cannot be removed. I am okay with admitting that it is courtesy of Google, though. I think it improves the user experience enough that it's okay to share branding with them.
  • If the Maps API ever shuts down the Autocomplete widget (Google is on a bit of an API-killing roll these days), we will have to re-write the functionality ourselves. That's okay, we'll still have the data from before. There's no harm in saving time now while we can.
  • Per the Maps API terms of use, you shouldn't be storing the Places data forever - so you should store the place ID and refresh your cache every so often. Or at least, that's how I interpret the terms. Do your own interpreting, or hire a lawyer. :-)

All in all, it's a nice touch in our profile experience that should hopefully make it faster and more fun. Thoughts?.

No comments: