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:
- The beekeeper logs in.
- He adds the information for his hive.
- Using Google Maps API, he chooses the hive location on a map.
- He saves the newly created hive.
- 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.