Main

Services

Clients

 
Posted over 2 years ago by Nazar Aziz

Google Maps on Ruby on Rails

5964 reads - 6 comments

Introduction

To summarise:

Goals

  1. First goal is easy: employ Google Maps in pantherFotos so that my members can easily place Google Map markers on their photos.
  2. Develop an abstract/interfaced solution that would allow me to present three types of Google Maps interface: an Editor Interface (allows the user to click on a map point and retrieve the X/Y coordinates, a Small Map Interface (small google map window with limited controls) and finally a larger Google Maps interface that utilises all Google Map controls and provides a popup window which displays images.
  3. Coming from a Delphi background I wanted to develop an Object Oriented solution that would allow me to manage all of the above three presentations.
  4. Coming from a Client/Server background... I wanted the Google Map instances to act as clients that query my web server for specific data to render. This data had to be delivered efficiently and quickly.

Develop an Abstract and Interfaced Solution

As described in point 2, I required three presentations of Google Maps to my members, with each presentation offering different controls and window sizes. Ideally I wanted to develop a base class that would instantiate the Google Maps object, set default settings and render map points. In Delphi speak, this composite base class would provide virtual and abstract methods that can be overridden in children classes to alter desired behaviour.

Sounds good in theory but can JavaScript do this? My limited experience of JS told me that it can do popup messages and windows (don't we all know it!), navigate a page's DOM and so on... but classes and inheritance?

Well... yes. I was quite surprised after some research and finding Prototype how far JS has come along. Prototype.js defines several JS classes and concepts that allow a great degree of class inheritance and most importantly to me, method overriding. A primer on JavaScript inheritance with Prototyping(not to be confused with Prototype.js) can be found here and here.

Prototype.js defines a Class as:
var Class = {
create: function() {
return function() {
this.initialize.apply(this, arguments);}
Which now allows me to define the GoogleBaseMap class as:
var GoogleBaseMap = Class.create();
GoogleBaseMap.prototype = {
initialize: function() {
this.init.call(arguments);
},
init: function(map_div) {
if (map_div != undefined) {
this.centered = false;
this.rescale = true;
//create the google map object
if (GBrowserIsCompatible()) {
this.map = new GMap2($(map_div));
return this;
} else {
alert('Google Maps not supported on this browser.');
return false;
}
} else {
return false;
}
},
//surface Google's map methods
addControl: function(obj) {
this.map.addControl(obj);
},
setCenterCoords: function( lat, lon, level){
this.map.setCenter(GPoint(lat,lon), level);
},
setCenter: function(point, level) {
this.map.centerAndZoom(point,level);
},
//abstract methods...don't call
setupMap: function() {
alert('Called abstract method. Call appropriate class method instead');
},
setup: function() {
this.addScaleControl();
this.addMapTypeControl();
this.map.enableDoubleClickZoom();
this.map.enableContinuousZoom();
},
//controls
addSmallMapControl: function() {
this.addControl(new GSmallMapControl());
},
addLargeMapControl: function() {
this.addControl(new GLargeMapControl());
},
addMapTypeControl: function() {
this.addControl(new GMapTypeControl())
},
addSmallZoomControl: function() {
this.addControl(new GSmallZoomControl());
},
addScaleControl: function() {
this.addControl(new GScaleControl());
},
addOverviewControl: function() {
this.addControl(new GOverviewMapControl());
},
// virtual methods
addMarker: function(row) {
coords = row.split('^');
marker = new GMarker(new GLatLng(coords[0],coords[1]), {title: coords[2] });
return marker;
},
getMapType: function() {
return G_NORMAL_MAP;
},
getZoomLevel: function(bounds) {
return this.map.getBoundsZoomLevel(bounds);
},
//methods
queryMarkerData: function (qryUrl){
this.queryUrl = qryUrl;
GDownloadUrl(this.queryUrl, this.onDataLoad.bind(this) )
},
requeryMarkerData: function() {
this.rescale = false;
if (this.queryUrl) {this.queryMarkerData(this.queryUrl);}
},
//events
onDataLoad: function(data, responseCode) {
if (data != '-1') {
markers = data.split(';');
if (markers.length > 0) {
batch = [];
for (var i = 0; i < markers.length; i++) {
if (marker = this.addMarker(markers[i])) { batch.push(marker); }
}
//center map on first marker
this.map.clearOverlays();
if (batch.length > 0) {
if (this.rescale) { this.map.setCenter(batch[0].getPoint(), 7);}
//
var bounds = new GLatLngBounds();
var t = this;
batch.each( function(value,index){
bounds.extend(value.getPoint());
t.map.addOverlay(value);
}
)
if (this.rescale) {t.map.setCenter(bounds.getCenter(), t.getZoomLevel(bounds), t.getMapType());}
}
}
} else {
this.map.setCenter(new GLatLng(53,5),5);
}
}
};
The purpose of GoogleBaseMap is:
  1. Define a base class that instantiates a Google Maps object.
  2. Define a base class that provides initialisation and data processing routines that can be used by inherited classes.
  3. Define methods that can be overridden to alter behaviour.
This now allows me to call a Google Map object from my Rails view:
  function loadMap() {
if (mapObj = new GoogleMapViewer('google_map')) {
mapObj.setupMap();
mapObj.queryMarkerData('http://pantherfotos.com/get_your_marker_data_here');
}
}
as a JavaScript in a RHTML View. Using the above function loadMap(), the important methods of GoogleBaseMap to note are:
  • init acts as the class constructor.
  • The critical method here is queryMarkerData, which receives a URL which is queried to return Google Map marker data. queryMarkerData call's Google Maps' GDownloadURL, which takes a URL and a callback method as parameters. Once GDownloadURL retrieves this data OnDataLoad is called to render map markers.
  • OnDataLoad processes the queried data and itself calls virtual methods that are overridden by children classes.

Inheriting from the Base Class to Define Concrete Classes

My first inherited class will be:
var GoogleLargeMap = Class.create();
GoogleLargeMap.prototype=Object.extend(
new GoogleBaseMap(),
{
initialize: function(map_div) {
this.init(map_div);
},
setupMap: function() {
this.setup();
this.addLargeMapControl();
this.addOverviewControl();
}
}
);
This class defines what a LargeMap is. In this case a LargeMap class (and all subsequent inherited classes) will contain a LargeMap and Overview controls. GoogleLargeMap is in turn is finally used by:
var GoogleMapViewer = Class.create();
GoogleMapViewer.prototype=Object.extend(
new GoogleLargeMap(),
{
initialize: function(map_div) {
this.init(map_div);
},
//override
addMarker: function(row) {
coords = row.split('^');
var marker = new GMarker(new GLatLng(coords[0],coords[1]), {title: coords[2] });
marker.photo_id = coords[3];
//register listener on marker
GEvent.addListener(marker, "click", function() {
marker.openInfoWindowHtml("Loading details...);
//
queryURL = '/photos/get_photo_window_info/'+marker.photo_id;
GDownloadUrl(queryURL, function(data, responseCode) {
marker.openInfoWindowHtml(data);
});
});
return marker;
}
}
);
Which is then called from an RHTML view as a JavaScript:
function loadMap() {
if (mapObj = new GoogleMapViewer('google_map')) {
mapObj.setupMap();
mapObj.queryMarkerData('http://pantherfotos.com/marker_data');
}
}
Note the following:
  • The setupMap call is routed to GoogleLargeMap.setupMap (which in effect calls the base setup method then overrides this with specific behaviour for this class).
  • GoogleBaseMap defines queryMarkerData in such a way that I only need to override the addMarker method if I wish to deviate from the base class's map marker rendering behaviour. This is the case here where the GoogleMapViewer class hooks an Info Window on each rendered map marker. This allows me to show a photo against all markers. This is demonstrated in the FotoMap section of pantherFotos.

Client/Server Google Maps.

When researching on how to use Google Maps and specifically how to tell the Google Maps object what to render I did stumble onto some questionable code. These examples (mostly PHP) would employ PHP to build an HTML page and JavaScript sections which would call mapObject.AddOverlay(point) in a loop.
Having used Ruby on Rails to contruct my maps page, I did not want to use this same method (for obvious sanity reasons). Ideally I wanted to abstract the marker data from the Google Map object. This solution would allow me to define one Google Map object that I can then re-use in rendering various data (i.e. a photographer's markers, a club's markers, all markers and so on). The Google Map object shouldn't care what the data was... it should just render it.

This is the purpose of the queryMarkerData in the GoogleBaseMap class. It receives a URL from which is fetches the data. The next issue was how to transfer the data. I had two options: use XML or make up my own format. I chose the later as I wanted compact and efficient method to pass hundreds if not thousands of marker coordinates to the Google Map object.

The following Ruby module was defined to construct the queries data:
module MapsHelper
def markers_to_markup(markers)
if markers.length > 0
out = []
markers.each{ |m|
out << "#{m.lat}^#{m.long}^#{m.title.gsub('^',' ')}^#{m.markable_id}"
}
return out.join(';')
else
return '-1'
end
end
end
This module is included in any Rails controller that is required to provide marker data. This generates the following output which is then parsed and rendered by GoogleBaseMap.OnDataLoad:
51.3897332^-2.96580433^tree^457;51.35791418^-2.99922466^Birnbeck Pier^455;

In Conclusion...

My final solution includes three Rails partials (one for each map type) that can be called by passing the URL as a local. These partials then construct the Google Maps object, pass it the URL which then renders my map markers. These partials can render any data source as can be seen here, here, here, here and here.

The Rails magic kicked in when I created a new plugin, acts_as_markable which with a single line of code will enable any Rails Model to support Google Map markers.

There is a drawback that has to be mentioned. Although the data sent back during OnDataLoad is compact and efficient, it isn't very extensible as the data format is proprietary to the Google Map object on pantherFotos. That data, cannot for example, be used by other sites. A more extensible solution could be achieved using GeoRSS, which will be the focus of my next article...

Nazar started programming on a Zx Spectrum in 1983, when the majority of games were supplied by magazines as source code and had to be keyed in by hand. Nazar started developing professionally in 1995, starting with Oracle Forms 3 and progressing to Delphi in 1998. He founded Panther Software Publishing in 2001 and has since developed and supplied numerous bespoke solutions to various sectors of industry, ranging from: Insurance, Banking, Facilities Management, Health Care, Engineering, Document Control and Procurement.

Panther Software has been specialising in developing bespoke database driven web applications using Ruby on Rails and AJAX since 2006. Contact us for your web application requirements.

Digg it! Slashdot Del.icio.us Technorati Google Bookmarks Reddit! Dzone Yahoo! MyWeb

Comments closed for this article

What others have said:

 
example code ?
By: chris

Hi,
Very cool!
Do you happen to have some example code (javascript and rails) to share ? zip, svn, … ?

Posted about 1 year ago