geo-distance 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/Gemfile +7 -9
  2. data/Gemfile.lock +30 -13
  3. data/README.textile +28 -8
  4. data/VERSION +1 -1
  5. data/geo-distance.gemspec +40 -32
  6. data/lib/geo-distance.rb +14 -132
  7. data/lib/geo-distance/class_methods.rb +71 -0
  8. data/lib/geo-distance/conversion.rb +67 -0
  9. data/lib/geo-distance/conversion/meters.rb +29 -0
  10. data/lib/geo-distance/conversion/radians.rb +36 -0
  11. data/lib/geo-distance/core_ext.rb +12 -23
  12. data/lib/geo-distance/distance.rb +24 -0
  13. data/lib/geo-distance/formula.rb +53 -7
  14. data/lib/geo-distance/formula/flat.rb +21 -0
  15. data/lib/geo-distance/{haversine.rb → formula/haversine.rb} +18 -14
  16. data/lib/geo-distance/formula/n_vector.rb +30 -0
  17. data/lib/geo-distance/formula/spherical.rb +44 -0
  18. data/lib/geo-distance/formula/vincenty.rb +80 -0
  19. data/lib/geo-distance/scale.rb +18 -0
  20. data/spec/geo_distance/class_methods_spec.rb +70 -0
  21. data/spec/geo_distance/core_ext_spec.rb +65 -0
  22. data/spec/geo_distance/distance_spec.rb +74 -0
  23. data/spec/geo_distance/formula/flat_spec.rb +31 -0
  24. data/spec/geo_distance/formula/haversine_spec.rb +33 -0
  25. data/spec/geo_distance/formula/n_vector.rb +33 -0
  26. data/spec/geo_distance/formula/spherical_spec.rb +33 -0
  27. data/spec/geo_distance/formula/vincenty_spec.rb +33 -0
  28. data/spec/spec_helper.rb +0 -10
  29. metadata +97 -96
  30. data/lib/geo-distance/spherical.rb +0 -24
  31. data/lib/geo-distance/vincenty.rb +0 -79
  32. data/spec/core_ext_spec.rb +0 -9
  33. data/spec/dist_default_spec.rb +0 -23
  34. data/spec/dist_haversine_spec.rb +0 -21
  35. data/spec/dist_spherical_spec.rb +0 -21
  36. data/spec/dist_vincenty_spec.rb +0 -21
@@ -0,0 +1,67 @@
1
+ class GeoDistance
2
+ module Conversion
3
+ autoload :Meters, 'geo-distance/conversion/meters'
4
+ autoload :Radians, 'geo-distance/conversion/radians'
5
+
6
+ def self.included(base)
7
+ base.send :include, Meters
8
+ base.send :include, Radians
9
+ end
10
+
11
+ # return new GeoDistance instance with distance converted to specific unit
12
+ ::GeoDistance.units.each do |unit|
13
+ class_eval %{
14
+ def to_#{unit}
15
+ cloned = self.dup
16
+ un = GeoUnits.key :#{unit}
17
+ cloned.distance = in_meters * GeoUnits.meters_map[un]
18
+ cloned.unit = un
19
+ cloned
20
+ end
21
+ }
22
+ end
23
+
24
+ # in_ and as_ return distance as a Float
25
+ # to_xxx! and similar, modify distance (self) directly
26
+ (::GeoDistance.units - [:meters]).each do |unit|
27
+ class_eval %{
28
+ def in_#{unit}
29
+ dist = (unit == :radians) ? in_radians : distance
30
+ conv_unit = GeoUnits.key :#{unit}
31
+ convert_to_meters(dist) * GeoUnits.meters_map[conv_unit]
32
+ end
33
+ alias_method :as_#{unit}, :in_#{unit}
34
+
35
+ def to_#{unit}!
36
+ conv_unit = GeoUnits.key :#{unit}
37
+ self.distance = in_meters * GeoUnits.meters_map[conv_unit]
38
+ self.unit = conv_unit
39
+ self
40
+ end
41
+ alias_method :in_#{unit}!, :to_#{unit}!
42
+ alias_method :as_#{unit}!, :to_#{unit}!
43
+ }
44
+ end
45
+
46
+ ::GeoDistance.all_units.each do |unit|
47
+ class_eval %{
48
+ def #{unit}
49
+ un = GeoUnits.key :#{unit}
50
+ send(:"as_\#{un}")
51
+ end
52
+ }
53
+ end
54
+
55
+ protected
56
+
57
+ # distance delta
58
+ GeoDistance.units.each do |unit|
59
+ class_eval %{
60
+ def delta_#{unit}
61
+ unit = GeoUnits.key(:#{unit})
62
+ GeoDistance.earth_radius[:#{unit}] * distance
63
+ end
64
+ }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ class GeoDistance
2
+ module Conversion
3
+ module Meters
4
+ def in_meters
5
+ convert_to_meters distance
6
+ end
7
+ alias_method :to_meters, :in_meters
8
+ alias_method :as_meters, :in_meters
9
+
10
+ def to_meters!
11
+ self.distance = convert_to_meters distance
12
+ self.unit = :meters
13
+ self
14
+ end
15
+ alias_method :in_meters!, :to_meters!
16
+ alias_method :as_meters!, :to_meters!
17
+
18
+ def convert_to_meters dist
19
+ (unit == :meters) ? dist : distance / GeoUnits.meters_map[unit]
20
+ end
21
+
22
+ def to_meters!
23
+ @distance = in_meters
24
+ @unit = :meters
25
+ self
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ class GeoDistance
2
+ module Conversion
3
+ module Radians
4
+ def to_radians
5
+ cloned = self.dup
6
+ cloned.distance = in_radians
7
+ cloned.unit = :radians
8
+ cloned
9
+ end
10
+
11
+ def to_radians! lat = 0
12
+ @distance = in_radians(lat)
13
+ @unit = :radians
14
+ end
15
+
16
+ def radians_conversion_factor
17
+ unit.radians_ratio
18
+ end
19
+
20
+ # calculate the distance in radians for the given latitude
21
+ def in_radians lat = 0
22
+ (unit != :radians) ? distance.to_f / earth_factor(lat) : distance # radians_conversion_factor
23
+ end
24
+
25
+ protected
26
+
27
+ def earth_factor u = nil, lat = 0
28
+ (GeoDistance.earth_radius[u ||= unit] / 180) * latitude_factor(lat)
29
+ end
30
+
31
+ def latitude_factor latitude = 0
32
+ 90 / (90 - latitude)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,31 +1,20 @@
1
- class Float
2
- def round_to(x)
3
- (self * 10**x).round.to_f / 10**x
4
- end
5
-
6
- def ceil_to(x)
7
- (self * 10**x).ceil.to_f / 10**x
8
- end
9
-
10
- def floor_to(x)
11
- (self * 10**x).floor.to_f / 10**x
12
- end
13
-
14
- def rpd
15
- self * GeoDistance.radians_per_degree
16
- end
17
- alias_method :to_radians, :rpd
18
- end
1
+ require 'sugar-high/numeric'
19
2
 
20
- require 'geo-distance'
21
-
22
- class Integer
23
- ::GeoDistance.units.each do |unit|
3
+ module GeoDistanceExt
4
+ ::GeoDistance.all_units.each do |unit|
24
5
  class_eval %{
25
6
  def #{unit}
26
- GeoDistance::Distance.new(self, :#{unit})
7
+ GeoDistance.new(self, :#{unit})
27
8
  end
28
9
  }
29
10
  end
30
11
  end
31
12
 
13
+ class Fixnum
14
+ include GeoDistanceExt
15
+ end
16
+
17
+ class Float
18
+ include GeoDistanceExt
19
+ end
20
+
@@ -0,0 +1,24 @@
1
+ require 'geo-distance/class_methods'
2
+
3
+ class GeoDistance
4
+ include Comparable
5
+
6
+ include Conversion
7
+
8
+ attr_accessor :distance, :unit
9
+
10
+ def initialize distance, unit = :radians
11
+ @distance = distance
12
+ @unit = GeoUnits.key(unit)
13
+ end
14
+
15
+ alias_method :units, :unit
16
+
17
+ def <=> other
18
+ in_meters <=> other.in_meters
19
+ end
20
+
21
+ def number
22
+ distance.round_to(precision[unit])
23
+ end
24
+ end
@@ -1,16 +1,62 @@
1
- module GeoDistance
2
- EARTH_RADIUS = { :kilometers => 6378.135, :miles => 3963.1676 }
3
- # WGS-84 numbers
4
- EARTH_MAJOR_AXIS_RADIUS = { :kilometers => 6378.137, :miles => 3963.19059 }
5
- EARTH_MINOR_AXIS_RADIUS = { :kilometers => 6356.7523142, :miles => 3949.90276 }
6
-
1
+ class GeoDistance
2
+ include GeoUnits
3
+
7
4
  class DistanceFormula
8
- include Math
9
5
  extend Math
6
+ extend GeoUnits
10
7
 
11
8
  def initialize
12
9
  raise NotImplementedError
10
+ end
11
+
12
+ # use underlying distance formula
13
+ def self.geo_distance *args
14
+ GeoDistance.new distance(args), get_units(args.last_option)
15
+ end
16
+
17
+ # used to convert various argument types into GeoPoints
18
+ def self.get_points(*args)
19
+ args.flatten!
20
+ options = args.delete(args.last_option) || {}
21
+ units = options[:units] || GeoDistance.default_units
22
+
23
+ case args.size
24
+ when 2
25
+ [GeoPoint.new(args.first), GeoPoint.new(args.last), units]
26
+ when 4
27
+ [GeoPoint.new(args[0..1]), GeoPoint.new(args[2..3]), units]
28
+ else
29
+ raise "Distance from point A to B, must be given either as 4 arguments (lat1, lng1, lat2, lng2) or 2 arguments: (pointA, pointB), was: #{args}"
30
+ end
31
+ end
32
+
33
+ # used to get the units for how to calculate the distance
34
+ def self.get_units options = {}
35
+ GeoUnits.key(options[:units] || :kms)
13
36
  end
37
+
38
+ # def self.degrees_to_radians(degrees)
39
+ # # degrees.to_f / 180.0 * Math::PI
40
+ # degrees.to_f * radians_per_degree
41
+ # end
42
+
43
+ # def self.units_sphere_multiplier(units)
44
+ # earth_radius_map[GeoUnit.key units]
45
+ # end
46
+ #
47
+ # def self.units_per_latitude_degree(units)
48
+ # GeoUnits.radian_multiplier[units.to_sym]
49
+ # end
50
+ #
51
+ # def self.units_per_longitude_degree(lat, units)
52
+ # miles_per_longitude_degree = (latitude_degrees * Math.cos(lat * pi_div_rad)).abs
53
+ # case units
54
+ # when :kms
55
+ # miles_per_longitude_degree * kms_per_mile
56
+ # when :miles
57
+ # miles_per_longitude_degree
58
+ # end
59
+ # end
14
60
  end
15
61
  end
16
62
 
@@ -0,0 +1,21 @@
1
+ require 'geo-distance/core_ext'
2
+ require 'geo-distance/formula'
3
+
4
+ class GeoDistance
5
+ class Flat < DistanceFormula
6
+
7
+ # Calculate distance using Flat earth formula, returns distance in whatever unit is specified (kms by default)
8
+ def self.distance *args
9
+ from, to, units = get_points(args)
10
+
11
+ return 0 if from == to # return 0.0 if points are have the same coordinates
12
+
13
+ Math.sqrt(
14
+ (units_per_latitude_degree(units) * (from.lat - to.lat))**2 +
15
+ (units_per_longitude_degree(from.lat, units) * (from.lng - to.lng))**2
16
+ )
17
+ end
18
+ end
19
+ end
20
+
21
+
@@ -33,24 +33,28 @@
33
33
  require 'geo-distance/core_ext'
34
34
  require 'geo-distance/formula'
35
35
 
36
- module GeoDistance
36
+ class GeoDistance
37
37
  class Haversine < DistanceFormula
38
38
  # given two lat/lon points, compute the distance between the two points using the haversine formula
39
39
  # the result will be a Hash of distances which are key'd by 'mi','km','ft', and 'm'
40
-
41
- def self.distance( lat1, lon1, lat2, lon2)
42
- dlon = lon2 - lon1
43
- dlat = lat2 - lat1
44
-
45
- a = calc(dlat, lat1, lat2, dlon)
46
- c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
47
-
48
- GeoDistance::Distance.new c
40
+
41
+ def self.distance *args
42
+ begin
43
+ from, to, units = get_points(args)
44
+
45
+ lat1, lon1, lat2, lon2 = [from.lat, from.lng, to.lat, to.lng]
46
+ dlon = lon2 - lon1
47
+ dlat = lat2 - lat1
48
+
49
+ a = (Math.sin(dlat.rpd/2))**2 + Math.cos(lat1.rpd) * Math.cos((lat2.rpd)) * (Math.sin(dlon.rpd/2))**2
50
+ c = (2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a)))
51
+ c = c.to_deg
52
+
53
+ units ? c.radians_to(units) : c
54
+ rescue Errno::EDOM
55
+ 0.0
56
+ end
49
57
  end
50
-
51
- def self.calc dlat, lat1, lat2, dlon
52
- (Math.sin(dlat.rpd/2))**2 + Math.cos(lat1.rpd) * Math.cos((lat2.rpd)) * (Math.sin(dlon.rpd/2))**2
53
- end
54
58
  end
55
59
 
56
60
  end
@@ -0,0 +1,30 @@
1
+ class GeoDistance
2
+ class NVector < DistanceFormula
3
+ def self.distance *args
4
+ from, to, units = get_points(args)
5
+ lat1, lon1, lat2, lon2 = [from.lat, from.lng, to.lat, to.lng]
6
+
7
+ sin_x1 = Math.sin(lon1)
8
+ cos_x1 = Math.cos(lon1)
9
+
10
+ sin_y1 = Math.sin(lat1)
11
+ cos_y1 = Math.cos(lat1)
12
+
13
+ sin_x2 = Math.sin(lon2)
14
+ cos_x2 = Math.cos(lon2)
15
+
16
+ sin_y2 = Math.sin(lat2)
17
+ cos_y2 = Math.cos(lat2)
18
+
19
+ cross_prod = (cos_y1*cos_x1 * cos_y2*cos_x2) + (cos_y1*sin_x1 * cos_y2*sin_x2) + (sin_y1 * sin_y2)
20
+
21
+ c = Math.acos(cross_prod)
22
+
23
+ # puts "c: #{c}"
24
+ # radians_per_degree
25
+ c = (c * 0.0201324).to_deg
26
+
27
+ units ? c.radians_to(units) : c
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ require 'geo-distance/core_ext'
2
+ require 'geo-distance/formula'
3
+
4
+ # module GeoDistance
5
+ # class Spherical < DistanceFormula
6
+ # def self.distance( lat1, lon1, lat2, lon2)
7
+ # from_longitude = lon1.to_radians
8
+ # from_latitude = lat1.to_radians
9
+ # to_longitude = lon2.to_radians
10
+ # to_latitude = lat2.to_radians
11
+ #
12
+ # c = Math.acos(
13
+ # Math.sin(from_latitude) *
14
+ # Math.sin(to_latitude) +
15
+ #
16
+ # Math.cos(from_latitude) *
17
+ # Math.cos(to_latitude) *
18
+ # Math.cos(to_longitude - from_longitude)
19
+ # ) #* EARTH_RADIUS[units.to_sym]
20
+ #
21
+ # GeoDistance::Distance.new c
22
+ # end
23
+ # end
24
+ # end
25
+
26
+ class GeoDistance
27
+ class Spherical < DistanceFormula
28
+ def self.distance *args
29
+ from, to, units = get_points(args)
30
+
31
+ return 0.0 if from == to #return 0.0 if points are have the same coordinates
32
+
33
+ c = Math.acos(
34
+ Math.sin(degrees_to_radians(from.lat)) * Math.sin(degrees_to_radians(to.lat)) +
35
+ Math.cos(degrees_to_radians(from.lat)) * Math.cos(degrees_to_radians(to.lat)) *
36
+ Math.cos(degrees_to_radians(to.lng) - degrees_to_radians(from.lng))
37
+ ).to_deg
38
+
39
+ units ? c.radians_to(units) : c
40
+ rescue Errno::EDOM
41
+ 0.0
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ require 'geo-distance/core_ext'
2
+ require 'geo-distance/formula'
3
+
4
+ class GeoDistance
5
+ class Vincenty < DistanceFormula
6
+
7
+ # Calculate the distance between two Locations using the Vincenty formula
8
+ #
9
+ def self.distance *args
10
+ begin
11
+ from, to, units = get_points(args)
12
+ lat1, lon1, lat2, lon2 = [from.lat, from.lng, to.lat, to.lng]
13
+
14
+ from_longitude = lon1.to_radians
15
+ from_latitude = lat1.to_radians
16
+ to_longitude = lon2.to_radians
17
+ to_latitude = lat2.to_radians
18
+
19
+ earth_major_axis_radius = earth_major_axis_radius_map[:kilometers]
20
+ earth_minor_axis_radius = earth_minor_axis_radius_map[:kilometers]
21
+
22
+ f = (earth_major_axis_radius - earth_minor_axis_radius) / earth_major_axis_radius
23
+
24
+ l = to_longitude - from_longitude
25
+ u1 = atan((1-f) * tan(from_latitude))
26
+ u2 = atan((1-f) * tan(to_latitude))
27
+ sin_u1 = sin(u1)
28
+ cos_u1 = cos(u1)
29
+ sin_u2 = sin(u2)
30
+ cos_u2 = cos(u2)
31
+
32
+ lambda = l
33
+ lambda_p = 2 * Math::PI
34
+ iteration_limit = 20
35
+ while (lambda-lambda_p).abs > 1e-12 && (iteration_limit -= 1) > 0
36
+ sin_lambda = sin(lambda)
37
+ cos_lambda = cos(lambda)
38
+ sin_sigma = sqrt((cos_u2*sin_lambda) * (cos_u2*sin_lambda) +
39
+ (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda) * (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda))
40
+ return 0 if sin_sigma == 0 # co-incident points
41
+ cos_sigma = sin_u1*sin_u2 + cos_u1*cos_u2*cos_lambda
42
+ sigma = atan2(sin_sigma, cos_sigma)
43
+ sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma
44
+ cosSqAlpha = 1 - sin_alpha*sin_alpha
45
+ cos2SigmaM = cos_sigma - 2*sin_u1*sin_u2/cosSqAlpha
46
+
47
+ cos2SigmaM = 0 if cos2SigmaM.nan? # equatorial line: cosSqAlpha=0 (§6)
48
+
49
+ c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
50
+ lambda_p = lambda
51
+ lambda = l + (1-c) * f * sin_alpha *
52
+ (sigma + c*sin_sigma*(cos2SigmaM+c*cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)))
53
+ end
54
+ # formula failed to converge (happens on antipodal points)
55
+ # We'll call Haversine formula instead.
56
+ return Haversine.distance(from, to, units) if iteration_limit == 0
57
+
58
+ uSq = cosSqAlpha * (earth_major_axis_radius**2 - earth_minor_axis_radius**2) / (earth_minor_axis_radius**2)
59
+ a = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
60
+ b = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
61
+ delta_sigma = b*sin_sigma*(cos2SigmaM+b/4*(cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)-
62
+ b/6*cos2SigmaM*(-3+4*sin_sigma*sin_sigma)*(-3+4*cos2SigmaM*cos2SigmaM)))
63
+
64
+ c = earth_minor_axis_radius * a * (sigma-delta_sigma)
65
+
66
+ c = (c / unkilometer).to_deg
67
+
68
+ units ? c.radians_to(units) : c
69
+ rescue Errno::EDOM
70
+ 0.0
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def self.unkilometer
77
+ 6378.135
78
+ end
79
+ end
80
+ end