Rails callbacks from Google Maps events
One of the major difficulties I've had with my new Ruby on Rails (RoR) site is managing the interaction between the Javascript world of Google Maps API and the Ruby world of RoR. Ostensibly there should not be a great divide - RoR has RJS pages interacting directly with Javascript, and a Ruby call from Javascript is just a URL fetch with the right HTTP verb (GET, PUT, etc.). The fact that Ruby operates on the server and Javascript operates client side however, as well as the stack of clever detail-hiding Rails wizardry, means things aren't always straight forward.
The ym4r_gm Plugin (the delightful name comes from "Yahoo! Maps For Rails _ Google Maps", hinting heavily at the plugin's origins) for RoR aims to simplify the use of the Google Maps API from Rails. I made the mistake of assuming it does anything beyond simplifying the generation of the Google Maps API initialisation Javascript for your webpage. When you understand that it just consists of Ruby objects that spit out the Javascript for creating themselves, things start to make a lot more sense. Any management of the Javascript references themselves still has to be managed manually, and that includes interacting with them after initialisation.
I quickly found myself in a bind when I wanted to create map events and GMarkers (the pins and icons that appear on a Google Map) on the fly that dynamically fired actions on my RoR controllers. After significant head scratching, I came up with the following solution, which I hope saves someone else some hours of frustration:
If you have an event on the initial map, you can take advantage of the ym4r_gm convenience method event_init:
@map.event_init(@map, :dblclick,
"function(overlay, latlng) { myJavaScriptCode; }")
But if that Javascript code is not static, or more likely, needs to do some Ruby on Rails work to consult your model, or update some session variables or whatever, you need a way to call back to your controller. In the ActiveView world you'd use remote_function, but we're in the ActiveController world. All is not lost, we can still use the functions, as long as we include the right files:
include ActionView::Helpers::PrototypeHelper
include ActionView::Helpers::JavaScriptHelper
func_str = remote_function(:url => { :action => :dblclick_map_event } )
@map.event_init(@map, :dblclick,
"function(overlay, latlng) { " + func_str + " }")
That saves us hardcoding all that "new Ajax.Request" stuff, which is a good thing in my books.
But we're not done yet - by calling a Ruby action we've thrown away the Javascript parameters passed to us. In particular, chances are that we'll definitely need latlng to find out where the double click occurred. Here's the most pleasant way I found of combining the Javascript and Ruby elements:
include ActionView::Helpers::PrototypeHelper
include ActionView::Helpers::JavaScriptHelper
func_str = remote_function(:url => { :action => :dblclick_map_event,
:lat => "lat_ph", :lng => "lng_ph" } )
func_str[/lat_ph/] = "'+latlng.lat()+'" # replace placeholders
func_str[/lng_ph/] = "'+latlng.lng()+'"
@map.event_init(@map, :dblclick,
"function(overlay, latlng) { " + func_str + " }")
Here we insert a bit more Javascript into the Javascript generated by the remote_function function to pass the lat and lng values to the RoR action. In our RoR controller we'd access those values with params[:lat].to_f and params[:lng].to_f.
You can exploit the Javascript access even further by adding calls before or after the remote_function call. For example, I disabled the standard pan-on-double-click behaviour of Google Maps by calling map.setCenter() in my double click handler:
include ActionView::Helpers::PrototypeHelper
include ActionView::Helpers::JavaScriptHelper
func_str = "map.setCenter(map.getCenter());\n"
func_str += remote_function(:url => { :action => :dblclick_map_event,
:lat => "lat_ph", :lng => "lng_ph" } )
func_str[/lat_ph/] = "'+latlng.lat()+'" # replace placeholders
func_str[/lng_ph/] = "'+latlng.lng()+'"
@map.event_init(@map, :dblclick,
"function(overlay, latlng) { " + func_str + " }")
Okay, there's only one thing left to do. This is all fine and dandy for initialisation code, but how do we add events like this on the fly? I'm afraid this is where ym4r_gm lets us down. We can still use the convenience classes GIcon, GLatLng and GMarker, but we're going to have to code the event creation ourselves:
@map = Variable.new("map")
regionIcon = GIcon.new(:image => "images/maps/regionMarker.png",
:shadow => "images/maps/regionMarkerShadow.png",
:iconSize => GSize.new(64.0, 64.0),
:shadowSize => GSize.new(69.0, 69.0),
:iconAnchor => GPoint.new(32.0, 32.0),
:infoWindowAnchor => GPoint.new(32.0, 32.0))
point = GLatLng.new([lat, lng])
marker = GMarker.new(point,
:icon => regionIcon)
func_str = remote_function(:url => { :action => :dblclick_region_event,
:marker_id => "marker_id_ph" } )
func_str[/marker_id_ph/] = "'+gmarkers.indexOf(marker)+'" #replace placeholder string
@js_str += "var marker = #{MappingObject.javascriptify_variable(marker)};\n" +
"gmarkers[#{select_id.to_s}] = marker;\n" +
"map.addOverlay(marker);\n" +
"GEvent.addListener(marker, \"dblclick\", function(latlng) { #{func_str} });" +
"map.panTo(#{MappingObject.javascriptify_variable(point)});"
Here's what is going on:
@map = Variable.new("map")uses ym4r_gm to retrieve our previously created Google Map objectregionIcon = GIcon.newuses ym4r_gm to create the Javascript necessary to create a GIcon. Note that since we're doing this on the fly we don't have any existing Javascript objects and have to recreate this each time.point = GLatLng.newdittomarker = GMarker.newdittofunc_str[/marker_id_ph/] = "'+gmarkers.indexOf(marker)+'"this time I'm using the index of the Javascript object reference to the marker I just created as the parameter to the RoR action call. Note thatgmarkersis a global Javascript object for my page ('g' for Global, not Google), andmarkeris what I'll call the new Javascript GMarker.@js_str += "var marker = #{MappingObject.javascriptify_variable(marker)};\n"here we start to write the necessary Javascript. That's a Javascript object calledmarker, which is created using the ym4r_gm convenience methodjavascriptify_variableand passing it the Ruby object, also called marker. I learned how to usejavascriptify_variableby reading the ym4r_gm code."gmarkers[#{select_id.to_s}] = marker;\n"next the new Javascript object gets stored in the global Javascript array."map.addOverlay(marker);\n"we have to call straight Google Maps API Javascript to add the new marker to the map."GEvent.addListener(marker, \"dblclick\", function(latlng) { #{func_str} });"and also to add the event, substituting the result of ourremote_functioncall for the function content."map.panTo(#{MappingObject.javascriptify_variable(point)});"and finally using some of the same tricks to pan the map to the point the marker was just added.
Now that all this Javascript has been generated, how does it actually get executed? There are two ways. The first is the ym4r_gm intended way, which only works for map initialisation. As the ym4r_gm docs says, you simply need to include:
<%= GMap.header %>
<%= @map.to_html %>
in your RoR view, between the <head></head> tags and have your action render the view. That works fine for the initial page view, but of course our marker code should execute without having to reload the how page. That's where RoR's RJS pages come in.
Remember when I created the @js_str variable? Shortly after that I call render :action => "search_form". Instead of a normal erb view file however, I create a file called search_form.js.rjs, which allows me to include Javascript. Once I've done that, all I need to put in that file is this line:
page << @js_str
And the Javascript in @js_str will be executed in an AJAX call, dynamically updating your map. I've also added a page.replace_html and a page.visual_effect call to dynamically update the HTML sections of the page.
So there you have it. Dynamic Google Maps pages using RoR and ym4r_gm. I wish someone had written this before I tried to figure it out!
Comments
Thanks a lot!! You saved my day! I have been searching for how to use ym4r_gm event_init for hours, and the results are not good enough until I found this!
Thanks! ^^
Posted by: Animaster | April 8, 2010 9:30 PM
Same here, thanks a lot
Posted by: Kluthen | May 5, 2010 9:24 AM
THANKS a lot!! Helped me to get out of the "hole" (it's a hungarian saying) cheers!
Posted by: Peter | August 17, 2010 6:09 PM
This is amazing. Thanks!
Posted by: Rory | December 30, 2010 11:00 PM