Finding the Distance Between Two GPS Coordinates - kristianmandrup/mongoid_geospatial GitHub Wiki

This will cover how to take standard WGS84 GPS coordinates from a CSV file and make it so that distances can be calculated easily between the two points using RGeo. For this example, I'll be using Rails 3.2.

In your Gemfile you'll need:

gem "mongoid_geospatial", :git => "git://github.com/kristianmandrup/mongoid_geospatial.git"
gem "rgeo"

First, you'll need a GPS file. What you are most likely to find online are WGS84 formatted coordinates so we will use them:

"id","City","ST","ZIP","A/C","FIPS","County","T/Z","DST?","Lat","Long","Type"
1,"Holtsville","NY","00501","631",36103,"Suffolk","EST","Y","40.8151","-73.0455","U"
38169,"San Diego","CA","92101","619",6073,"San Diego","PST","Y","32.7253","-117.1721",
```

I'm going to import this file into a `Location` model. The `Location` model will be my master lookup for zipcodes and city/state information. Here is my `location.rb` file:

```
class Location
  include Mongoid::Document
  include Mongoid::Geospatial

  field :city, type: String
  field :state, type: String
  field :zipcode, type: String

  geo_field :coords
  spatial_index :coords

  def to_geo
    self.coords
  end

end
```

Here is a Rake task to import all the locations from the csv file:

```
require "csv"

namespace :bootstrap do

  desc "Seed Locations into the database using zipcode data"
  task :locations => :environment do
    Location.delete_all
    puts "Seeding locations... (this will take a while)"
    location_count = 0

    csv_text = File.read("#{File.dirname(__FILE__)}/csv/zipinfo_zipcodes_with_headers_id_col.csv")
    csv = CSV.parse(csv_text, :headers => true)

    csv.each do |row|
      # "id","City","ST","ZIP","A/C","FIPS","County","T/Z","DST?","Lat","Long","Type"
      id = row[0]
      city = row[1]
      st = row[2]
      zip = row[3]
      timezone = row[7]
      dst = (row[8] == "Y")
      lat = row[9].to_f 
      lon = row[10].to_f 

      location = Location.create(city: city, state: st, zipcode: zip, coords: {:lat => lat, :lng => lon})

      location_count += 1
      puts "#{location_count} Locations (#{location_count * 100 / csv.length}% complete)" if location_count % 500 == 0

    end

    puts "#{location_count} locations seeded."

  end

end

```

To run the import type `rake bootstrap:locations`

Now, I want to assign a location to a second model, however, I'm going to do this without embedding a `Location` object because the data I have is de-normalized. The model in this example will be for appointments. Here is my `appointment.rb`:

```
class Appointment
  include Mongoid::Document
  include Mongoid::Geospatial
  include Mongoid::Timestamps

  field :name, type: String

  field :company, type: String
  field :email, type: String
  field :phone, type: String

  field :address, type: String
  field :address2, type: String
  field :city, type: String
  field :state, type: String
  field :zipcode, type: String

  field :notes, type: String
  field :when, type: String
  field :when_datetime, type: DateTime

  geo_field :current_coords
  spatial_index :current_coords

  field :distance, type: Float
  index({ distance: 1 })

  def to_geo
    self.current_coords
  end

end
```

Now, we need to make it so that we can calculate the distance between two points by making sure that we are doing two things:

1. Using the correct coordinate system so that we can use the more common notation e.g. (32.7253, -117.1721). You'll notice some boilerplate code to do this.

2. We must make sure all objects that include `Mongoid::Geospatial` have a `distance_from()` method and know which fields to use in the calculation. For this to work, each object must implement it's own `to_geo` method that returns the `Mongoid::Geospatial::Point` object that you wish to use in the calculation.

Here is my `mongoid-geospatial.rb` file. I've placed it in my `config/initializers` directory:

```
module Mongoid
  module Geospatial

    def self.meters_to_miles(meters)
      meters / 1609.34
    end

    def self.meters_to_km(meters)
      meters / 1000.0
    end

    def distance_from(obj, options = {:unit => :mi})
      if (!self.respond_to?(:to_geo) || !obj.respond_to?(:to_geo))
        puts "Both objects must implement the to_geo() method"
        return nil
      end
      self.to_geo.distance_from(obj.to_geo, options) if self.to_geo.is_a?(Mongoid::Geospatial::Point)
    end

    class Point

      def distance_from(obj, options = {})
        wgs84_proj4 = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
        wgs84_wkt = <<-WKT
          GEOGCS["WGS 84",
            DATUM["WGS_1984",
              SPHEROID["WGS 84",6378137,298.257223563,
                AUTHORITY["EPSG","7030"]],
              AUTHORITY["EPSG","6326"]],
            PRIMEM["Greenwich",0,
              AUTHORITY["EPSG","8901"]],
            UNIT["degree",0.01745329251994328,
              AUTHORITY["EPSG","9122"]],
            AUTHORITY["EPSG","4326"]]
        WKT

        wgs84_factory = RGeo::Geographic.spherical_factory(:srid => 4326, :proj4 => wgs84_proj4, :coord_sys => wgs84_wkt)

        point1 = wgs84_factory.point(self.x, self.y)
        point2 = wgs84_factory.point(obj.x, obj.y)

        distance = wgs84_factory.line(point1, point2).length

        distance = Mongoid::Geospatial.meters_to_miles(distance) if options[:unit] == :mi
        distance = Mongoid::Geospatial.meters_to_km(distance) if options[:unit] == :km
        distance = distance if options[:unit] == :m 

        distance
      end

    end

  end
end
```

Now, to calculate the distance we can do something like:

```
> seattle = Location.where(:city => 'Seattle').first
> my_appointment = Appointment.last
> seattle.distance_from(my_appointment)
or
> my_appointment.distance_from(seattle)
```

This will return the distance in miles by default. If you'd like to return using other units, you can do this:

```
> my_appointment.distance_from(seattle, :unit => :m)     # => meters
> my_appointment.distance_from(seattle, :unit => :km)    # => kilometers
> my_appointment.distance_from(seattle, :unit => :mi)    # => miles
```

You may also just as easily find the distance between two locations:

```
> miami = Location.where(:city => "Miami", :state => "FL").first
> seattle = Location.where(:city => 'Seattle').first
> miami.distance_from(seattle)
```