Maps, Geolocalization and Optimization with Maptimize
Playing with maps is great fun and here is walk through from basics to advanced use of geolocalization and how to optimize your maps with Maptimize

During this tutorial you will :
- start geo localizing your addresses with server side technologies and caching system
- add front side tools (in Javascript) to keep things lighter and nicer for the end user
- keep both side geo localization working nicely together and in an unobtrusive way
- map your localized object while keeping optimized, using Maptimize
- show details when clicking on markers and clusters
- bonus, use Maptimize makers when creating / editing entries
UPDATE: Thanks to Sébastien Gruhier from Xilinus I’ve made couple of change to improve various codes and this article in general.
UPDATE 2 – 2009-05-04: Sébastien Gruhier from Xilinus has been rocking again and helped me out improving few aspects.
UPDATE 3 – 2009-05-04: Improve maptimize configuration and task to be more flexible and portable
Feeling in hurry ?
I’ve made a basic demo running on Rails 2.3.2 to cover this tutorial : Maptimize – Demo 1 so feel free to check it out directly. Make sure to check the README.textile to get it working.
With more time, let’s get a step by step walk through
For this integration I’ll assume that you have a Rails application up-and-running (for me using Rails 2.3.2) with a model collecting address details, in my case, “business”
Geolocalize your addresses
You first gonna need to add the lat and lng float attributes in your database but also a geolocalized_address string attribute. This attribute will be used to cache the address and prevent unnecessary updates.
From the demo :
class AddGeolocalizedSupportToBusinesses < ActiveRecord::Migration
def self.up
change_table(:businesses) do |t|
t.float :lat, :lng
t.string :geolocalized_address
end
end
def self.down
change_table(:businesses) do |t|
t.remove :lat, :lng, :geolocalized_address
end
end
end
Don’t forget to run the migration rake db:migrate
1. Geolocalize on server side with GeoKit

Make sure to install GeoKit gem and GeoKit rails plugin as well as configuring your KEYS in “config/initializers/geokit_config.rb“
GeoKit propose you a really simple way to automatically geolocalize your address :
class Business < ActiveRecord::Base acts_as_mappable :auto_geocode=>true end
In our case, we will do something a bit more advanced by caching the geolocalized address to prevent calling the Geolocalization Services when we don’t need to.
Here is the model that I would like to geolocalize :
class Business < ActiveRecord::Base
acts_as_mappable
before_validation :geocode_address
private
def geocode_address
return if self.address.nil? || (self.geolocalized_address == self.address && !self.lat.nil? && !self.lng.nil?)
geo=Geokit::Geocoders::MultiGeocoder.geocode(address)
errors.add(:address, "Could not Geocode address") if !geo.success
self.lat, self.lng, self.geolocalized_address = geo.lat, geo.lng, self.address if geo.success
end
end
2. Geolocalize on front-end side

As Google will recommend and warn when signing up for the API key their is a geocode request limitation per day based on your ip. Using front side geocoding will prevent you from reaching this limits as the geolocalization request will be done from your customers IP address.
Client side geolocalization will also save server load and allow your customers to adjust the position when needed.
For this purpose, I’m using AddressChooser from Maptimize , and as dealing with a single address field I’m gonna add the prototype / scriptaculous autocomplete example
NOTICE : AddressChooser is Javascript framework-agnostic and Mapping system independent, so if your needs differs from mine go check AddressChooser from Maptimize to get the full details.
Start by including the required Javascripts files :
- Prototype JS
- Scriptaculous Effects and Controls
- AddressChooser
- Your own JavaScripts
I actually start to use more and more the Google hosted ones to prevent end users from loading this libraries again and again so here is how I deal with it:
<script type="text/javascript" src="http://www.google.com/jsapi?key=<%= Geokit::Geocoders::google %>"></script> <%= javascript_include_tag 'gloader', 'effects', 'controls', 'addresschooser/proxy/googlemap', 'addresschooser/addresschooser', 'application' %>
GLoader? What’s that ?
GLoader is a tiny javascript files that take care of loading my required libraries, hosted by Google :
NOTE : ScriptAculoUs is not loaded this way as it can’t be loaded properly right now. To help getting this fixed please review, comments, rate, ... the following issue : google.load / auto-loading and scriptaculous modules .
// Load libraries
google.load("maps", "2");
google.load("prototype", "1.6.0.3");
// on page load complete, initialize the application
google.setOnLoadCallback(function() {
$(document.body).observe('onunload', GUnload);
new Application;
});
Add autocomplete CSS
Just add <%= stylesheet_link_tag 'autocomplete' %> in the head of your page
Update your HTML
Now we got our required javascripts files loaded, let’s update our HTML to support Maptimize.AddressChooser
You’ll need to add the following fields and divs to your creation form
<div id='suggests' class='auto_complete' style='display:none'></div> <div id="map" style="position: absolute; top: 10px; left: 400px; height: 350px; width: 350px;"></div> <%= f.hidden_field :lat %> <%= f.hidden_field :lng %>
While editing, we’ll need some slightly different fields to be able to stay unobtrusive but also take full advantage of the client side geolocalization.
<div id='suggests' class='auto_complete' style='display:none'></div> <div id="map" style="position: absolute; top: 10px; left: 400px; height: 350px; width: 350px;"></div> <%= f.hidden_field :current_lat, :value => @business.lat %> <%= f.hidden_field :current_lng, :value => @business.lng %>
Initialize Maptimize.AddressChooser
Now we’ve got the HTML ready, our CSS and the required libraries let’s initialize Maptimize.AddressChooser to turn on the magic.
I’ve made couple of changes to work nicely in an unobtrusive way and while creating or editing any entry
From the demo application :
Application = Class.create({
initialize: function() {
this.initAddressChooser('business', {street: 'address'});
},
initAddressChooser: function(object, options) {
// Init options
options = $H({
street: 'street',
submit: 'submit',
lat: 'lat',
lng: 'lng',
current_lat: 'current_lat',
current_lng: 'current_lng',
suggests: 'suggests'
}).merge(options);
// Check if current lat/lng are defined to use them
var current_lat, current_lng;
if ((current_lat = $(object+'_'+options.get('current_lat'))) && (current_lng = $(object+'_'+options.get('current_lng'))))
current_lng.insert({after: '<input type="hidden" id="'+object+'_'+options.get('lat')+'" name="'+object+'['+options.get('lat')+']" value="'+current_lat.value+'" /><input type="hidden" id="'+object+'_'+options.get('lng')+'" name="'+object+'['+options.get('lng')+']" value="'+current_lng.value+'" />'});
var street, submit;
if (!(street = $(object+'_'+options.get('street'))) || !(submit = $(object+'_'+options.get('submit')))) return(this);
// BEGIN AUTOCOMPLETE SETTINGS AND HACKS :)
// Create a local autocomplete without data. Data will be added dynamically according to map suggestions
var autocomplete = new Autocompleter.Local(street, options.get('suggests'), [],
{
afterUpdateElement: function(element, selectedElement) {
var index = selectedElement.up().immediateDescendants().indexOf(selectedElement);
widget.showPlacemark(index);
},
selector: function(instance) {
instance.changed = false;
return "<ul><li>" + instance.options.array.join('</li><li>') + "</li></ul>";
}
}
);
// Do not observe keyboard event
autocomplete.onObserverEvent = function() {}
// Wrap render to update map with selected placemarks
autocomplete.render = autocomplete.render.wrap(function(method) {
method();
widget.showPlacemark(this.index);
});
// END AUTOCOMPLETE SETTINGS AND HACKS :)
widget = new Maptimize.AddressChooser.Widget(
{ onInitialized: function(widget) {
// Add default controls
widget.getMap().setUIToDefault();
widget.initMap();
// Observe 'suggests:started' to display spinner and disable submit button
widget.addEventListener('suggests:started', function() {
street.addClassName('spinner');
});
// Observe 'suggests:found' to hide spinner and enable submit button if a placemark has been found
widget.addEventListener('suggests:found', function(placemarks) {
street.removeClassName('spinner');
street.focus();
// Reset autocomplete suggestions to new placemarks
autocomplete.options.array.clear();
if (placemarks && placemarks.length > 0) {
for (var i = 0; i < placemarks.length; i++) {
autocomplete.options.array.push(widget.getAddress(placemarks[i]));
}
// For autocomplete update
autocomplete.getUpdatedChoices();
autocomplete.show();
}
else {
autocomplete.hide();
}
});
street.focus();
},
street: object+'_'+options.get('street'),
lat: object+'_'+options.get('lat'),
lng: object+'_'+options.get('lng')
}
);
return(this);
}
});
3. Take advantage of the front side geolocalization

For this purpose, we have to update our model to update the cached address when needed but also prevent from using the geolocalization on server-side if the client have already done it on front-side.
class Business < ActiveRecord::Base
attr_accessor :current_lat, :current_lng
acts_as_mappable
before_validation :geocode_address
private
def geocode_address
if (self.geolocalized_address != self.address)
if (self.current_lat == self.lat && self.current_lng == self.lng)
geo=Geokit::Geocoders::MultiGeocoder.geocode(address)
errors.add(:address, "Could not Geocode address") if !geo.success
self.lat, self.lng, self.geolocalized_address = geo.lat, geo.lng, self.address if geo.success
else
self.geolocalized_address = self.address
end
end
end
end
You will notice the 2 attributes accessors that will allow to check any client changes on the coordinates.
4. Map you object and keep optimized using Maptimize

When the time comes to work with maps (google maps in my case) and lot of markers, a problem comes up as you grow, TOO MUCH MARKERS!
Too much markers makes you maps loading horribly slow!
For this reasons, the experts at Xilinus came up with a elegant solution to deal with this, Maptimize
Maptimize take care of merging your markers as clusters to reduce your amount of visible items. That keep it much lighter to load but also nicer for the viewer.
I then recommend using Maptimize from the early stage. That keep your application easier to scale but also nicer to the end users.
Signup and config Maptimize
Once signed up, go in the “Security Settings” tab to get your MAP and API KEYS.
From here you could add this keys in your environment’s configuration but I feel that using a dedicated initializer will keep things cleaner and easier to maintain.
In the file config/initializers/maptimize_config.rb with :
# You'll need to add a cronjob to automatically synchronize your data with Maptimize running the following command line # # IMPORTANT: This will overwrite any file called *maptimize.csv* located in your app's tmp folder # If you like to change this behavior update lib/tasks/maptimize.rake # # rake maptimize:sync MAPTIMIZE_CSV_URL = "http://localhost:3000/businesses.csv" MAPTIMIZE_CSV_URL.freeze MAPTIMIZE_AUTHENTICITY_TOKEN = "AUTHENTICITY_TOKEN" MAPTIMIZE_AUTHENTICITY_TOKEN.freeze MAPTIMIZE_MAP_KEY = Rails.env.development? ? "DEVELOPMENT_KEY" : "PRODUCTION_KEY" MAPTIMIZE_MAP_KEY.freeze
Send your data to Maptimize
Create a CSV file generator
Thanks to rails, is this trivial.
Update your controller
In your controller update your index action, from the demo “businesses_controller.rb” :
# GET /businesses
# GET /businesses.xml
# GET /businesses.csv
def index
@businesses = Business.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @businesses }
format.csv # index.csv.erb
end
end
Add “app/views/businesses/index.csv.erb“
id,lat,lng <%- @businesses.each do |business| -%> <%= business.id.to_s + ',' + business.lat.to_s + ',' + business.lng.to_s %> <%- end -%>
Well, that’s done! If you check http://localhost:3000/businesses.csv you’ll see a simple CSV file containing the minimum data required by Maptimize
Send your CSV file to Maptimize
This is something that you may like to run in a cronjob to keep your data synchronized automatically.
From a bash shell console, just run :
IMPORTANT: This will overwrite any file called “maptimize.csv” located in your app’s tmp folder If you like to change this behavior update lib/tasks/maptimize.rake
rake maptimize:sync
If everything worked as expected, your command should ends with :
<?xml version="1.0" encoding="UTF-8"?> <import> <status>succeeded</status> </import>
Show your objects on a map
Now that Maptimize have our data, it’s time to use them and get our map
Include the CSS and Javascript files required to integrate Maptimize
In your head section :<link href="http://www.maptimize.com/stylesheets/cluster.css" rel="stylesheet" type="text/css" />In the bottom of your page, right after Google jsapi file
<script src="http://www.maptimize.com/api/v1/<%= MAPTIMIZE_MAP_KEY %>/embed.js" type="text/javascript"></script>
Display your map
Add a div that will contain your map
In the demo, I’ve added the div in the index file “app/views/businesses/index.html.erb
<div id="map" style="width: 900px; height: 500px;"></div>
Initialize the map
In my case I’ll update “public/javascripts/application.js” to include :
Application = Class.create({
initialize: function() {
this.initAddressChooser('business', {street: 'address'})
.initMap('map');
},
initAddressChooser: function(object, options) {
...
return(this);
},
initMap: function(map) {
if (!GBrowserIsCompatible() || !(map = $(map)))
return(this);
if (typeof Maptimize.Map == "undefined") {
map.update("Maptimize key is not correctly set, check file config/initializers/maptimize_config.rb or \
run 'rake bootstrap' to fill database with some data");
} else {
// Create a new google map
var map = new GMap2(map);
// Center on the world
map.setCenter(new GLatLng(47, 1), 2);
// Add controls
map.addControl(new GSmallMapControl());
// Attach maptimize service
window.maptimizeMap = new Maptimize.Map(map);
}
return(this);
}
});
Show me the real deal!
The see the goodness of Maptimize you need to have a good bunch of entry, for this purpose you can run :
rake bootstrap
It will load 500 entries in your database.
Done?
That’s it! we have a map with all our objects localized, merged when needed, ... it can’t get better!
5. Show details when clicking on markers and clusters

“it can’t get better!” I said? Well it can!
What about showing our object details when we click a marker? Let’s do it!
Update our Maptimize initialization to bind actions on click for clusters and markers
Application = Class.create({
...
initMap: function(map) {
if (!GBrowserIsCompatible() || !(map = $(map)))
return(this);
if (typeof Maptimize.Map == "undefined") {
map.update("Maptimize key is not correctly set, check file config/initializers/maptimize_config.rb or \
run 'rake bootstrap' to fill database with some data");
} else {
// Create a new google map
var map = new GMap2(map);
// Center on the world
map.setCenter(new GLatLng(47, 1), 2);
// Add controls
map.addControl(new GSmallMapControl());
// Attach maptimize service
var app = this;
window.maptimizeMap = new Maptimize.Map(map, {
onMarkerClicked: function(marker) {
return app.getMarkerDetails(marker, marker.getId());
},
onZoomMaxClusterClicked: function(cluster, ids) {
return app.getMarkerDetails(cluster, ids);
}
});
}
return(this);
},
getMarkerDetails: function(object, ids) {
return new Ajax.Request("/businesses/"+ids, {
method: 'GET',
onComplete: function(response) {
object.getGMarker().openInfoWindowHtml(response.responseText);
}
});
}
});
As you can see, the id of the marker will be used to call the show action of our object, “business” in this case.
What append when you click a cluster, the id string looks like “1,2,3“
For this we will update our action to support multiple ids and show the expected details
Update our controller
Looking at “app/controllers/businesses_controller.rb” we’ll change the show method as follow :
# GET /businesses/1
# GET /businesses/1.xml
# GET /businesses/1,2
def show
@businesses = Business.find(params[:id].split(','))
@business = @businesses.first
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @business }
format.js # show.js.erb
end
end
The split call will allow us to get more IDS at once, to keep our initial formats working properly I keep the single object as well.
Add the view “app/views/businesses/show.js.erb“
<%- @businesses.each do |business| -%> <p><%= business.name %><br /><small><%= business.address %></small></p> <%- end -%>
6. Bonus, use Maptimize makers when creating / editing entries
To use the Maptimized map with markers when creating / editing your entries, just do the tiny change in initAddressChooser from “/public/application.js“
Include this code just after widget.getMap().setUIToDefault();
// Attach maptimize plugin var maptimizeMap = new Maptimize.Map(widget.getMap());
Comments
-
Great post Seb. But some of the codes are messed up by the green arrow symbol. Please fix them.
-
Hi Arun, thanks for your feedback. Actually this green arrows are here to split long lines. If you like to see a nice and clean code, use the "show source" button
-
Hi Do you have a version online for me to have a look at? Thanks Mick
-
Hi Mick, There's actually no online demo and that would remove lot of features, specially touching the synchronization. You might need only couple minutes to get the app running and play with all features on your own. If you like to see some live exemple using maptimize, please refer the http://www.maptimize.com . Hope this helps,

Web application developer born in summer '83, I made my company