19Feb
By: Davoud On: February 19, 2015 In: Blog, Ruby Comments: 0

The Bee Informed Partnership (BIP) Project is a national project that aims to decrease winter mortality of managed honeybee colonies by helping beekeepers keep colonies alive through surveys and data collection. Our lab is helping development some of the software BIP needs to manage the data it is collecting. Beekeepers can register a hive, assign it to a scale and track daily cycles such as weight, humidity and temperature. Over time, the data collected on the new Web application will become a research tool for scientists to use to discover patterns that could shed some light on this significant problem.

As a very basic use case, we needed to implement a feature for creating a Bee Hive. Here is the scenario:

  1. The beekeeper logs in.
  2. He adds the information for his hive.
  3. Using Google Maps API, he chooses the hive location on a map.
  4. He saves the newly created hive.
  5. On the next page he sees his hive information in addition to a Google Map Marker pinned to the hive location on a map.

To facilitate implementing mentioned scenario we used RGeo Gem which is a Geospatial data library for Ruby. Using RGeo we are able to work with Geospatial objects directly from our Ruby models. Further, using activerecord-postgis-adapter gem we can save our geospatial objects in PostgreSQL using PostGIS addon. The process is pretty straightforward.

Basically, in our hive model we use:

class Hive < ActiveRecord::Base
    set_rgeo_factory_for_column(:current_location,
                              RGeo::Geographic.spherical_factory(:srid => 4326))
    ...
end

This defines current_location attribute on Hive model as a spherical object. Spherical objects have latitudes and longitude accessors. That line says, for the “current_location” field, use a spherical geographic coordinate system with spatial reference ID 4326. This means, computations done in Ruby will assume a spherical earth, and the spatial reference ID should be set to 4326 to match what PostGIS expects for a “geographic” column.

Then in our location creator, we have the following code:

require_relative "./coordinate_builder"
require 'rgeo'

class LocationCreator
  DEFAULT = "default"

  def self.create(lat_lng)
    return nil if lat_lng == DEFAULT

    lng, lat = CoordinateBuilder.parse(lat_lng)
    geographic_factory = RGeo::Geographic.spherical_factory
    geographic_factory.point(lng, lat)
  end
end

LocationCreator receives an string which is comprised of latitude and longitude and then it tries to create a spherical object. Later during hive creation, we assigned the return value of ‘create’ method to @hive.current_location:

def create
    Hive.transaction do
      @hive = Hive.new(hive_params)
      @hive.current_location = LocationCreator.create(params[:hive][:latlng])
      respond_to_create
    end
end

Later in the views, we use the following piece of code to fetch the latitude and longitude of the points in order to create a Google Maps marker for the hive and pin it on a map:

<% if @hive.current_location.present? %>
  var myLatlng = new google.maps.LatLng(<%= @hive.current_location.latitude %>, <%=@hive.current_location.longitude %>);
  var mapOptions = {
    zoom: 16,
    center: myLatlng,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  }
<% end  %>

We implemented the feature. Everyone was happy. The code was working on production and development environments. Our test was passing flawlessly on the development system. Also, the feature worked perfectly on every production server. Later we decided to host our project on Heroku. Here was when the problem with RGeo Objects started.

On Heroku, initially we used good naive Webrick which comes as the default web server. But Webrick is not scalable. For the sake of scalability, we considered switching our production server to Puma.

When switched to Puma, calling @hive.current_location.lon and @hive.current_location.lat in my Javascript views throws the error:

ActionView::Template::Error (undefined method 'lon' and 'lat' for #<RGeo::Cartesian::PointImpl:0x007f700846c970>)

Meanwhile, we tried to access the current_location latitude and longitude via heroku run rails console. The interesting thing was that fetching lat and long values on Heroku console returns correct values that we expected which meant that objects are correctly saved in the database. So the problem should be on the presentation side.


Problem: Apparently Rgeo::Geographic::SphericalPointImpl falls back to RGeo::Cartesian::PointImpl in our Javascript code.

Workaround:  We changed the javascript calls in the views from @hive.current_location.lat to @hive.current_location.y. Also for latitude, we did the same: @hive.current_location.lon to @hive.current_location.x.

due to the fact that RGeo::Cartesian::PointImpl supports .y and .x methods and not .latitude and .longitude methods.

This fixed the problem for us.

Final Solution: After spending more time hunting the problem we traced back the root of the problem. Our migration.

Initially, when we wanted to add current_location to our hives table we used the following migration:

class AddCurrentLocationToHives < ActiveRecord::Migration
  def change
    add_column :hives, :current_location, :point
  end
end

Apparently this is not the perfect way to define a geographic column.

To fix it, we added another migration:

class ChangeHivesCurrentLocation < ActiveRecord::Migration
  def change
  	add_column :hives, :location_temp, :point, geographic: true

	Hive.reset_column_information # make the new column available to model methods
	Hive.all.each do |hive|
	  hive.location_temp = hive.current_location
	  hive.save
	end

	remove_column :hives, :current_location
	rename_column :hives, :location_temp, :current_location
  end
end

The key point here is  geographic: true option that we are passing to the change_column method. In our case we wanted the current_location column to contain not just any two-dimensional coordinate, but specifically a latitude and longitude. To specify this, we will add the “geographic” constraint to it.

And then we migrated our data from old current_location column to the new column.

This was the ultimate solution to our problem.

[author] [author_image]http://masl.cis.gvsu.edu/wp-content/uploads/2015/02/david.jpeg[/author_image] [author_info]David Qorashi is a Graduate Assistant in the GVSU Mobile Applications and Services Lab. David is interested in all things software, loves programming in Ruby and is an experienced Ruby on Rails developer. When he’s not hacking AR Drones, he’s reading thick tomes on philosophy.[/author_info] [/author]

Davoud on EmailDavoud on GithubDavoud on GoogleDavoud on Linkedin
Davoud
David Qorashi is a Graduate Assistant in the GVSU Mobile Applications and Services Lab. David is interested in all things software, loves programming in Ruby and is an experienced Ruby on Rails developer. When he's not hacking AR Drones, he's reading thick tomes on philosophy.