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)
```