Ruby Javascript
Javascript, Rails, Google Maps!
For anyone familiar with a certain little backpack wearing explorer, the layout of this post will seem eerily reminiscent of an episode. Not only because of her favorite tool, the map, but also because we are following her three step method: JavaScript, Rails, Goo-gle-maps! (I can hear her now)
Lately at Hashrocket we have had a number of projects heavily dependent on the Google Maps API so it seemed like a great time to discuss one of these implementations, as well as some of the key points when integrating with that API. However, before we can begin we need to "map" out our journey. First we will geolocate our position using the Google Map API and javascript. Then we will use those results to query our rails services API for additional markers in the area. Finally we will return to JavaScript and add the markers and events to our map. So, come on vĂ¡monos!
Every journey starts with the first step, and our first step is to include Google's JavaScript API URI path in our page heading.
= javascript_include_tag "http://maps.googleapis.com/maps/api/js?sensor=false"
This will give us all the Google-y goodness needed for our adventure.
JavaScript & Google Geocoding API - the First Stop
The Google Geocoding API is provided as part of the Google Maps API in order to retrieve geolocation data. The API provides access to the geocoder object through HTTP requests. The service can geolocate points by both a standard or a reverse lookup with either an address or coordinates. Once the Map API URI is included as a JavaScript tag, accessing the Geocoder object is simple but we need to look at the results set that is returned.
var geocoder = new google.maps.Geocoder();
geocoder.geocode({address: 'Jax Beach, FL'}, function(results, status) {
// Do something with the results
});
The API delivers back some valuable data in the form of a JSON object (XML is also an option) that can be easily parsed to provide some essential information. One side effect is address sanitization so the address Jax Beach, FL returns Jacksonville Beach, FL.
In addition to having a clean address the response includes the geometry details like bounds, location point and suggested viewport bounds. Both the bounds and viewport are points in the Northeast and Southwest quadrants and define the rectangle that our supplied address appears within. The API provides a couple methods to get to those boundary points: getNorthEast()
and getSouthWest()
. The two methods will extract the bounding box points so that we can use them in out ajax request.
var geocoder = new google.maps.Geocoder();
geocoder.geocode({address: 'Jax Beach, FL'}, function(results, status) {
var bounds = results[0].geometry.bounds,
center = results[0].geometry.location;
if (bounds) {
var ne = bounds.getNorthEast(),
sw = bounds.getSouthWest(),
data = { sw: [sw.lat(), sw.lng()], ne: [ne.lat(), ne.lng()]};
// ajax call to rails service API
}
});
The Rails API - The Other Side
Now that we have arrived on the other side of the tracks, we can discuss a couple of gems used to ease the implementation of our Rails API. Because querying the data comes before delivering it, we should start with the geokit-rails3 gem. This is a port of the geokit gem and provides a common interface for geolocation applications. It also allows us to make ActiveRecord models mappable simply by adding acts_as_mappable to our model. To query the database based on our supplied bounds, we use the geo_scope method. This is the gem's underlying method for scoped searches and we can chain methods to refine our result set like any other ActiveRecord query.
def geo_search(sw, ne)
self.geo_scope(bounds: [sw, ne]).where(active: true)
end
Once we have retrieved the data from our database we can use the jBuilder gem to easily generate the JSON views. The setup is simple for our scenario and we can abstract some of the formatting to an application helper. Here is the format we will use in our result set:
json.results do |json|
json.array!(searchables) do |json, result|
json.id result.id
json.type result_type(result)
json.name result.name
json.lat result.latitude
json.lng result.longitude
json.url url_for(result)
end
end
When you are testing the Rails service API with rspec, remember to add a call to render_views in your spec. Otherwise, the tests will not return the JSON views in the response.
describe SearchablesController do
render_views
# add Fabricated records for testing
context "search spots" do
it "returns our spot" do
get :index, { format: :json, sw: sw, ne: ne }
data = JSON.parse(response.body)
data['results'].count.should eq(1)
data['results'][0]['name'].should eq( 'Jacksonville Beach Pier' )
end
end
end
Putting Our Map Together
Now that we have our results, we need to add them to a map object. To do that we need to define a map and give it a few options. Note the map center point is being set through the options. This can also be set after the map is drawn using the setCenter()
method. (For the full list of options check out the Google Maps API Map Options)
var opts = {
zoom: 10,
max_zoom: 16,
scrollwheel: false,
center: new google.maps.LatLng(center.lat(), center.lng()),
mapTypeId: google.maps.MapTypeId.ROADMAP,
MapTypeControlOptions: {
MapTypeIds: [google.maps.MapTypeId.ROADMAP]
}
};
var map = new google.maps.Map($('#map'), opts);
At this point we need to iterate through our results set and add markers for each point to the map. The Google API also provides a Marker object to define a marker and these can be customized to level your application's needs, including the addition of custom images.
var marker = new google.maps.Marker({
id: result.id,
title: result.name,
position: new google.maps.LatLng(result.lat, result.lng),
cursor: 'pointer',
flat: false,
icon: new google.maps.MarkerImage('/assets/our_custom_icon.png',
new google.maps.Size(32, 35),
new google.maps.Point(0, 0),
new google.maps.Point(32 / 2, 35),
new google.maps.Size(32, 35))
})
);
And there you have it! A map that has all our results that fall within a boundary. What more could you want? Oh yeah, when you move the map or zoom in/out the bounds change but our results don't. For that we need to use the Google Map API events, most notably the "idle" event. This event is fired when the map display goes idle. Although there are specific events for panning and zoom, we have found that the "idle" event works best for this scenario.
google.maps.event.addListener(map, 'idle', function() {
// call your geolocation method
});
Our journey is now complete. We have a Google map that displays all the stored results that fall within a bounding box. The code here has been modified to fit neatly into a single post so its purpose is to be an example of Google Maps integration. We would suggest defining your map object using object literal notation so that the map functionality can be encapsulated and functions can be reused.
Swiper, go swiping and have your own adventure with JavaScript, Rails, Goo-gle maps!