Distance calculation in Ruby using RGeo - Geospatial library for Ruby


RGeo is a gem for writing location-based applications in Ruby programming language.

It is used to:

  • Represent spatial and geolocation data objects such as points, lines, and polygons.
  • Perform standard spatial analysis operations such as finding intersections, creating buffers, and computing lengths and areas.
  • Correctly handle spherical geometry, and compute geographic projections for map display and data analysis.
  • Read and write location data in the WKT and WKB representations used by spatial databases.

Prerequisites:

  • Rails app.
  • PostgreSQL database with PostGIS extension enabled.
  • rgeo gem.
  • activerecord-postgis-adapter gem.

Installing the rgeo gem:

Before we install rgeo gem, we need to install Geos. For installing geos on Mac, execute brew install geos.

Add rgeo in the Gemfile and execute bundle install.

To verify that rgeo is installed correctly, we need to execute RGeo::Geos.supported? in the Rails console (rails c in the terminal).

If the response is true, it is installed successfully.

RGeo Calculations:

To store spatial data, we must create column with spatial type. PostGIS provides various spatial datatypes like point, linestring, polygon, etc.

The activerecord-postgis-adapter gem supports the above spatial type in ActiveRecord’s migration.

We can create spatial datatype columns in migration as below

create_table :spatial_table do |t|
  t.st_point :latlong, geographic: true
  t.geometry :area
  t.linestring :route
end 

We will create a simple model Locality with columns name and latlong as below.

rails generate model Locality name:string latlon:point

The migration file looks as below

class CreateLocalities < ActiveRecord::Migration
  def change
    create_table :localities do |t|
      t.string :name
      t.point :latlon
      t.timestamps
    end
  end
end
Distance between two points

We create two Locality entries as below.

saeloun_locality = Locality.create(
                     name: "Saeloun Office",
                     latlon: "POINT(-71.4313134 42.5891898)"
                   )
                   
liberty_locality = Locality.create(
                     name: "Statue of Liberty",
                     latlon: "POINT(-74.0466891 40.6892494)"  
                   )           

We can get the distance between the above two coordinates by using the distance method as below

saeloun_locality.distance(liberty_locality)

=> 303408.38530644414

The distance method returns the shortest distance in meters between two points. To the POINT function, we pass two numeric values longitude and then latitude.

Querying by location

The PostGIS database supports querying on spatial datatype.

We can figure out which of the above two localities created are closer to a particular coordinate.

For eg., Boston Public Garden is around 36.3 miles (58.42 km) from Saeloun locality and around 215 miles (346 km) from Statue of Liberty. Using the coordinates of Boston Public Garden we can identify which localities are within 62 miles (100 km) and which are greater than 62 miles.

Boston Public Garden coordinates are 42.3541675, -71.0726166. Where 42.3541675 is latitude and -71.0726166 is longitude.

Locality.where("ST_Distance(latlon, "+
  "'POINT(-71.0726166 42.3541675)') < 100000").
  map{ |ar| ar.name }
 
=> ["Saeloun Office"]

Locality.where("ST_Distance(latlon, "+
  "'POINT(-71.0726166 42.3541675)') > 100000").
  map{ |ar| ar.name }
  
=> ["Statue Of Liberty"]    

Here we have used ST_Distance function which calculates the distance between two points.

Distance between points using Rgeo factory

We can calculate the distance between two points by using their coordinates and calling the distance function.

 point1 = RGeo::Geographic.spherical_factory.point(-74.0466891, 40.6892494)
 point2 = RGeo::Geographic.spherical_factory.point(-71.4313134, 42.5891898)
 
 point1.distance(point2)
 => 303408.38530644414

The point function expects longitude and latitude as the first and second parameter respectively.

Point Inside a polygon or not

PostGIS database also supports storing polygon data in a table.

We will add the polygon datatype column to Locality model via migration as shown below:

rails generate migration add_polygon_to_localities

class AddPolygonToLocality < ActiveRecord::Migration[5.0]
  def change
    add_column :localities, :site_polygon, :st_polygon
  end
end  

Let’s say we create New York locality in Locality table which has polygon endpoints at

  • Saeloun Office
  • Statue of Liberty
  • Syracuse

Syracuse coordinates are (43.0352286, -76.1742994)

We create the New York locality in Locality table as below

site_polygon = "POLYGON ((-71.4313134 42.5891898, -74.0466891 40.6892494, -76.1742994 43.0352286,-71.4313134 42.5891898))"
new_york_locality = Locality.create(name: "New York", site_polygon: site_polygon)

Kingston lies between the polygon created above. And Virginia lies outside this polygon.

So we can check whether a point lies inside a polygon or not
using contains? method as below

kingston_coordinate = RGeo::Geographic.spherical_factory.point(-74.0166785, 41.9274042)

new_york_locality.site_polygon.contains?(kingston_coordinate)
=> true

virginia_coordinate = RGeo::Geographic.spherical_factory.point(-81.6640155, 37.9820796)

new_york_locality.site_polygon.contains?(virginia_coordinate)
=> false

We can also check whether a coordinate lies within any Locality, using the below query.

Locality.where("st_contains(site_polygon, '#{kingston_coordinate}')").first
=> #<Locality:0x00007fd0d74136b0
    id: 7,
    name: "New York",
    latlon: nil,
    created_at: Wed, 18 Sep 2019 11:25:40 EDT -04:00,
    updated_at: Wed, 18 Sep 2019 11:25:40 EDT -04:00,
    site_polygon: #<RGeo::Geos::CAPIPolygonImpl:0x3fe86b9d12e4 "POLYGON ((-71.4313134 42.5891898, -74.0466891 40.6892494, -76.1742994 43.0352286, -71.4313134 42.5891898))">>
    
Locality.where("st_contains(site_polygon, '#{virginia_coordinate}')").first
=> nil

Note we can index these spatial datatype colum to improve query performance.