Archive for the 'Ruby on Rails' Category

Rails Utility Methods

Monday, October 20th, 2008 By: Daniel

Browsing the Rails API I found a few methods I wish I’d known about earlier.

current_page? in ActionView::Helpers::UrlHelper returns “true if the current request URI was generated by the given options.”

  current_page?(:action => 'process')
  # => false
 
  current_page?(:action => 'checkout')
  # => true

reverse_merge! in ActiveSupport::CoreExtensions::Hash::ReverseMerge “allows for reverse merging where its the keys in the calling hash that wins over those in the other_hash.”

So now instead of

  def setup(options = {})
    options = {:income => 0, :expenses => 0}.merge(options)
  end

I can do

  def setup(options = {})
    options.reverse_merge! :income => 0, :expenses => 0
  end

And with ActiveSupport::CoreExtensions::String::Conversions I can stop using the PHP feeling

  Date.parse("10/20/2008")

to the more rubyish

  "10/20/2008".to_date

Add Markers to a Google Map With Ruby on Rails and JSON

Thursday, September 25th, 2008 By: Wes Bangerter

This tutorial will guide you through creating a map using the Google Maps API that will be dynamically populated with markers as the user zooms or scrolls around the map.

For this example, we’re going to create and use a generic Location model.

Geocoding Your Addresses

Geocoding will translate an address into its approximate latitude and longitude.

We’ll use GeoKit, a Ruby on Rails plugin to geocode our addresses. Install it with:

script/plugin install svn://rubyforge.org/var/svn/geokit/trunk

Follow the instructions to obtain and install your own Google API key.

Generate the model, controller and views for our map locations:

script/generate scaffold location name:string address:string city:string \
state:string zip:string

We also need to edit the migration file for this model and add fields for the location’s latitude and longitude:

t.decimal :lat, :precision => 15, :scale => 12
t.decimal :lng, :precision => 15, :scale => 12

Replace the code in app/models/location.rb with:

class Location < ActiveRecord::Base
  acts_as_mappable
 
  validates_presence_of :name, :address, :city, :state, :zip, :lat, :lng
  before_validation_on_create :geocode_address
 
  private
    def geocode_address
      geo=GeoKit::Geocoders::MultiGeocoder.geocode("#{address} #{city} #{state} #{zip}")
      errors.add(:address, "Could not Geocode address") if !geo.success
      self.lat, self.lng = geo.lat,geo.lng if geo.success
    end
end

That’s all there is to geocoding, now any time we create a Location it will automatically be assigned a latitude and longitude.

Adding the Google Map

In app/views/locations/index.html.erb add:

<div id="map" style="width: 890px; height: 600px;"></div>

And in app/views/controllers/locations_controller.rb, change the index action to:

# GET /locations
# GET /locations.xml
# GET /locations.js
def index
  respond_to do |format|
    format.html do
      @locations = Location.find(:all)
    end
    format.xml  { render :xml => @locations }
    format.js do
      ne = params[:ne].split(',').collect{|e|e.to_f}  
      sw = params[:sw].split(',').collect{|e|e.to_f}
      @locations = Location.find(:all, :limit => 100, :bounds => [sw, ne])
      render :json => @locations.to_json
    end
  end
end

The index action will now respond to javascript requests with a JSON object containing the first 100 Locations inside of the map boundaries.

In your layout file, add this code inside of the <head> tag:

<% unless @locations.blank? %>
  <script
    src="http://maps.google.com/maps?file=api&v=2.x&key=<%= GeoKit::Geocoders::google -%>"
    type="text/javascript"></script>
  <%= javascript_include_tag 'prototype', 'maps' %>
<% end %>

And finally, create a public/javascripts/maps.js file with this code:

window.onunload = GUnload;
 
var map;
var markers = new Array();
 
Event.observe(window, 'load', function() {
  if (GBrowserIsCompatible()) {
    map = new GMap2(document.getElementById("map"));
    // Center the map on the US
    map.setCenter(new GLatLng(37.731145,-97.326092),4);
    GEvent.addListener(map,"moveend",function(){updateMap();});
    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl());
 
    updateMap();
  }
});
 
function updateMap() {
  var bounds = map.getBounds();
  var southWest = bounds.getSouthWest();
  var northEast = bounds.getNorthEast();
 
  // Send an AJAX request for our locations
  new Ajax.Request('/locations.js', {
    method:'get',
    parameters: {sw: southWest.toUrlValue(), ne: northEast.toUrlValue()},
    onSuccess: function(transport){
      // Remove markers outside of our maps boundaries.
      if(markers.length > 0){
        removeMarkersOutsideOfMapBounds();
      }
 
      // Add our new markers to the map (unless they are already on the map.)
      var json = transport.responseText.evalJSON();
      json.each(function(i) {
        id = i.location.id;
        if(!markers[id] || markers[id] == null){
          // Marker doesnt exist, add it.
          markers[id] = createMarker(i.location);
          map.addOverlay(markers[id]);
        }
      });      
    }
  });
}
 
function createMarkerClickHandler(marker, location) {
  return function() {
    marker.openInfoWindowHtml(
      '<div><strong>' + location.name + '</strong><br/> ' +
      location.address + '<br/>' + location.city + ', ' +
      location.state + ' ' + location.zip + '</div>'
    );
    return false;
  };
}
 
function createMarker(location) {
  var latlng = new GLatLng(location.lat, location.lng);
  var marker = new GMarker(latlng);
  var handler = createMarkerClickHandler(marker, location);
  GEvent.addListener(marker, "click", handler);
  return marker;
}
 
function removeMarkersOutsideOfMapBounds() {
  for(i in markers) {
    if(i > 0 && markers[i] && !map.getBounds().containsLatLng(markers[i].getLatLng())) {
      map.removeOverlay(markers[i]);
      markers[i] = null;
    }
  }
}

The updateMap() function is run after the page initially loads and each time the user moves or zooms the map. It sends an AJAX request to the server with the maps boundaries, and the server returns a JSON object of the locations within those boundaries. After it receives the JSON object, it will add new locations to the map (it skips locations that have already been mapped) and removes locations that are no longer within the visible map boundaries.

A sample app containing all of the code can be downloaded here: map-sample-code.zip

Ruby Timing Shortcut

Tuesday, June 10th, 2008 By: Daniel

Recently I was trying to optimize my code a little and needed a quick way to compare the speed of different code snippets. Of course Rails comes with the Benchmark module for doing just that. It is, however, in my opinion a bit clunky for quick tests. Look at all the typing it takes just to find the average speed of a snippet over 100 iterations:

Benchmark.bm do |x|
  x.report do
    100.times do
      (1...1000).to_a.sum
    end
  end
end

So I tossed this into my ~/.irbrc file:

class Integer
  def ti(&block)
    Benchmark.bm do |x|
      x.report do
        self.times do
          yield
        end
      end
    end
  end
end

And now I can “time it” like this:

>> 100.ti {(1...1000).to_a.sum}
      user     system      total        real
  0.540000   0.050000   0.590000 (  0.597664)
=> true

Much nicer!

Fragment caching with Radiant CMS

Tuesday, June 10th, 2008 By: Wes Bangerter

We’re in the process of converting our website to Radiant CMS, and one of the new things on the site is a “Blog Blurbs” section at the bottom of every page that lists our latest blog post. Our blog is in Wordpress, so I setup the RSS Reader extension in Radiant to fetch the posts. Everything worked great, except that page loading was noticeably slower. After the page gets cached everything is fine but this is included on every page so going through the site when there were not cached pages was really frustrating. I thought about modifying the RSS Reader extension to cache our blog blurbs, but I figured a more general approach would work better, so I came up with a fragment caching extension. We’re using it like this:

<r:cache name="rss_fragment" time="60">
  <r:feed:items url="[feed_url]" limit="1">
    <h3><r:feed:link /></h3>
    <p><r:feed:content max_length="300" no_html="true" /></p>
  </r:feed:items>
</r:cache>

The code is at github.