Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

nice improvements by @ryangreenberg

  • Loading branch information...
commit 3dcfe843c2f600e29bf6399087682e3fd4aa8217 2 parents 00bd23b + b464f1b
@kristianmandrup authored
View
48 README.textile
@@ -1,33 +1,52 @@
-h1. Haversine - geo distance calculations
+h1. Haversine Distance
-Calculates the haversine distance between two locations using longitude and latitude.
-This is done using Math formulas without resorting to Active Record or SQL DB functionality
+This gem calculates the Haversine distance between two points given their longitude and latitude. This is done using trigonometry without ActiveRecord or SQL. See http://en.wikipedia.org/wiki/Haversine_formula for details on the Haversine formula.
-This is meant to ba a replacement for all those geocode and geolocation utils out there that use built in SQL DB functionality for their calculations!
+This is a replacement for all geo libraries that use built-in SQL DB functionality for their calculations.
h2. Install & Usage
-<pre>require 'haversine'
+Install this gem with @gem install haversine@. Calling @Haversine.distance@ with four lat/lon coordinates returns a @Haversine::Distance@ object which can provide output in kilometers, meters, miles, or feet.
-it "should work" do
- lon1 = -104.88544
- lat1 = 39.06546
+<pre>
+require 'haversine'
- lon2 = -104.80
- lat2 = lat1
+distance = Haversine.distance(35.61488, 139.5813, 48.85341, 2.3488)
- dist = Haversine.distance( lat1, lon1, lat2, lon2 )
+distance.class => Haversine::Distance
+distance.to_miles => 6032.71091869803
+distance.to_kilometers => 9715.47049115903
+distance.to_meters => 9715470.49115903
+distance.to_feet => 31852713.6507256
+</pre>
+
+Convenience aliases for the measurements exist:
+<pre>
+distance.to_kilometers == distance.to_km
+distance.to_meters == distance.to_m
+distance.to_miles == distance.to_mi
+distance.to_feet == distance.to_ft
+</pre>
- puts "the distance from #{lat1}, #{lon1} to #{lat2}, #{lon2} is: #{dist[:meters].number} meters"
+Note that @to_m@ is the distance in meters, not miles.
puts "#{dist[:feet]}"
puts "#{dist.meters}"
puts "#{dist[:km]}"
puts "#{dist[:miles]}"
+ puts "#{dist.to_mi}"
puts "#{dist.to_miles_}"
dist[:km].to_s.should match(/7\.376*/)
- dist.to_kms.to_s.should match(/7\.376*/)
+ dist.to_km.to_s.should match(/7\.376*/)
end
+
+If you have lat/lon pairs stored in an array, you can alternately provide two arrays when calling @Haversine.distance@:
+
+<pre>
+require 'haversine'
+new_york_city = [40.71427, -74.00597]
+santiago_chile = [-33.42628, -70.56656]
+Haversine.distance(new_york_city, santiago_chile).to_miles => 5123.73
</pre>
Note: Haversine is used in the "geo_magic":https://github.com/kristianmandrup/geo_magic gem
@@ -45,5 +64,4 @@ h2. Contributing to haversine
h2. Copyright
Copyright (c) 2011 Kristian Mandrup. See LICENSE.txt for
-further details.
-
+further details.
View
2  Rakefile
@@ -19,7 +19,7 @@ Jeweler::Tasks.new do |gem|
gem.description = %Q{Calculates the haversine distance between two locations using longitude and latitude.
This is done using Math formulas without resorting to Active Record or SQL DB functionality}
gem.email = "kmandrup@gmail.com"
- gem.authors = ["Kristian Mandrup"]
+ gem.authors = ["Kristian Mandrup", "Ryan Greenberg"]
# Include your dependencies below. Runtime dependencies are required when using your gem,
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
View
135 lib/haversine.rb
@@ -10,7 +10,7 @@
#
# LICENSE: GNU Affero GPL v3
# The ruby implementation of the Haversine formula is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
+# it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
@@ -27,130 +27,39 @@
#
# This formula can compute accurate distances between two points given latitude and longitude, even for
# short distances.
-
-# PI = 3.1415926535
-require 'haversine/core_ext'
+require 'haversine/distance'
-class Haversine
+module Haversine
- RAD_PER_DEG = 0.017453293 # PI/180
-
- # this is global because if computing lots of track point distances, it didn't make
- # sense to new a Hash each time over potentially 100's of thousands of points
+ RAD_PER_DEG = Math::PI / 180
- class << self
- def units
- [:miles, :km, :feet, :meters]
- end
- end
-
- class Distance
- attr_reader :distance
-
- def initialize distance
- @distance = distance
- end
-
- def [] key
- method = :"delta_#{key}"
- raise ArgumentError, "Invalid unit key #{key}" if !respond_to? method
- Distance.send "in_#{key}", send(method)
- end
-
- Haversine.units.each do |unit|
- class_eval %{
- def #{unit}
- self[:#{unit}]
- end
- }
- end
-
- protected
-
- # the great circle distance d will be in whatever units R is in
-
- Rmiles = 3956 # radius of the great circle in miles
- Rkm = 6371 # radius in kilometers...some algorithms use 6367
- Rfeet = Rmiles * 5282 # radius in feet
- Rmeters = Rkm * 1000 # radius in meters
-
- # delta between the two points in miles
- def delta_miles
- Rmiles * distance
- end
-
- # delta in kilometers
- def delta_km
- Rkm * distance
- end
-
- def delta_feet
- Rfeet * distance
- end
-
- def delta_meters
- Rmeters * distance
- end
-
-
- class << self
- Haversine.units.each do |unit|
- class_eval %{
- def in_#{unit} number
- Unit.new :#{unit}, number
- end
- }
- end
+ # given two lat/lon points, compute the distance between the two points using the haversine formula
+ def self.distance(lat1, lon1, lat2=nil, lon2=nil)
+ # Accept two arrays of points in addition to four coordinates
+ if lat1.is_a?(Array) && lon1.is_a?(Array)
+ lat2, lon2 = lon1
+ lat1, lon1 = lat1
+ elsif lat2.nil? || lon2.nil?
+ raise ArgumentError
end
-
- class Unit
- attr_accessor :name, :number
-
- def initialize name, number = 0
- @name = name
- @number = number
- end
- def number
- @number.round_to(precision[name])
- end
-
- def to_s
- "#{number} #{name}"
- end
-
- private
-
- def precision
- {
- :feet => 0,
- :meters => 2,
- :km => 4,
- :miles => 4
- }
- end
- end
- end
-
- # given two lat/lon points, compute the distance between the two points using the haversine formula
- # the result will be a Hash of distances which are key'd by 'mi','km','ft', and 'm'
-
- def self.distance( lat1, lon1, lat2, lon2, units = :meters )
dlon = lon2 - lon1
dlat = lat2 - lat1
a = calc(dlat, lat1, lat2, dlon)
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
- Distance.new c
- end
+ Haversine::Distance.new(c)
+ end
- def self.calc dlat, lat1, lat2, dlon
- (Math.sin(dlat.rpd/2))**2 + Math.cos(lat1.rpd) * Math.cos((lat2.rpd)) * (Math.sin(dlon.rpd/2))**2
+ # TODO How can this be more descriptively named?
+ def self.calc(dlat, lat1, lat2, dlon)
+ (Math.sin(rpd(dlat)/2))**2 + Math.cos(rpd(lat1)) * Math.cos((rpd(lat2))) * (Math.sin(rpd(dlon)/2))**2
end
-
- def self.wants? unit_opts, unit
- unit_opts == unit || unit_opts[unit]
+
+ # Radians per degree
+ def self.rpd(num)
+ num * RAD_PER_DEG
end
-end
+end
View
19 lib/haversine/core_ext.rb
@@ -1,19 +0,0 @@
-class Float
- def round_to(x)
- (self * 10**x).round.to_f / 10**x
- end
-
- def ceil_to(x)
- (self * 10**x).ceil.to_f / 10**x
- end
-
- def floor_to(x)
- (self * 10**x).floor.to_f / 10**x
- end
-
- RAD_PER_DEG = 0.017453293 # PI/180
-
- def rpd
- self * RAD_PER_DEG
- end
-end
View
40 lib/haversine/distance.rb
@@ -0,0 +1,40 @@
+module Haversine
+ class Distance
+ include Comparable
+
+ GREAT_CIRCLE_RADIUS_MILES = 3956
+ GREAT_CIRCLE_RADIUS_KILOMETERS = 6371 # some algorithms use 6367
+ GREAT_CIRCLE_RADIUS_FEET = GREAT_CIRCLE_RADIUS_MILES * 5280
+ GREAT_CIRCLE_RADIUS_METERS = GREAT_CIRCLE_RADIUS_KILOMETERS * 1000
+
+ attr_reader :great_circle_distance
+
+ def initialize(great_circle_distance)
+ @great_circle_distance = great_circle_distance
+ end
+
+ def to_miles
+ @great_circle_distance * GREAT_CIRCLE_RADIUS_MILES
+ end
+ alias_method :to_mi, :to_miles
+
+ def to_kilometers
+ @great_circle_distance * GREAT_CIRCLE_RADIUS_KILOMETERS
+ end
+ alias_method :to_km, :to_kilometers
+
+ def to_meters
+ @great_circle_distance * GREAT_CIRCLE_RADIUS_METERS
+ end
+ alias_method :to_m, :to_meters
+
+ def to_feet
+ @great_circle_distance * GREAT_CIRCLE_RADIUS_FEET
+ end
+ alias_method :to_ft, :to_feet
+
+ def <=>(other)
+ great_circle_distance <=> other.great_circle_distance
+ end
+ end
+end
View
19 spec/distance_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Haversine::Distance do
+ describe "<=>" do
+ context "equality" do
+ it "is true when the great circle distance is equal" do
+ dist1 = Haversine::Distance.new(0)
+ dist2 = Haversine::Distance.new(0)
+ (dist1 == dist2).should be_true
+ end
+
+ it "is false when the great circle distance is not equal" do
+ dist1 = Haversine::Distance.new(0)
+ dist2 = Haversine::Distance.new(1)
+ (dist1 == dist2).should be_false
+ end
+ end
+ end
+end
View
46 spec/haversine_spec.rb
@@ -1,21 +1,31 @@
require 'spec_helper'
-describe "Haversine" do
- it "should work" do
- lon1 = -104.88544
- lat1 = 39.06546
-
- lon2 = -104.80
- lat2 = lat1
-
- dist = Haversine.distance( lat1, lon1, lat2, lon2 )
-
- puts "the distance from #{lat1}, #{lon1} to #{lat2}, #{lon2} is: #{dist[:meters].number} meters"
-
- puts "#{dist[:feet]}"
- puts "#{dist.meters}"
- puts "#{dist[:km]}"
- puts "#{dist[:miles]}"
- dist[:km].to_s.should match(/7\.376*/)
+describe Haversine do
+ describe "#self.distance" do
+ it "returns Haversine::Distance" do
+ Haversine.distance(0,0,0,0).should be_a(Haversine::Distance)
+ end
+
+ it "accepts 4 numbers or 2 arrays as arguments" do
+ new_york_city = [40.71427, -74.00597]
+ santiago_chile = [-33.42628, -70.56656]
+ point_dist = Haversine.distance(new_york_city[0], new_york_city[1], santiago_chile[0], santiago_chile[1])
+ array_dist = Haversine.distance(new_york_city, santiago_chile)
+
+ point_dist.should be_a(Haversine::Distance)
+ array_dist.should be_a(Haversine::Distance)
+ point_dist.to_m.should == array_dist.to_m
+ end
+
+ it "calculates the distance between the provided lat/lon pairs" do
+ Haversine.distance(0,0,0,0).to_miles.should == 0
+ round_to(6, Haversine.distance(0,0,0,360).to_miles).should == 0
+ round_to(6, Haversine.distance(0,0,360,0).to_miles).should == 0
+ end
+ end
+
+ # Helpers
+ def round_to(precision, num)
+ (num * 10**precision).round.to_f / 10**precision
end
-end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.