geo_calc 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +20 -0
- data/README.textile +181 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/geo_calc.gemspec +74 -0
- data/lib/geo_calc/calculations.rb +333 -0
- data/lib/geo_calc/core_ext.rb +228 -0
- data/lib/geo_calc/geo.rb +170 -0
- data/lib/geo_calc/geo_point.rb +103 -0
- data/lib/geo_calc/js/geo_calc.js +551 -0
- data/lib/geo_calc.rb +1 -0
- data/spec/geo_calc/calculations_spec.rb +174 -0
- data/spec/geo_calc/core_ext_spec.rb +272 -0
- data/spec/geo_calc/geo_point_spec.rb +228 -0
- data/spec/geo_calc/geo_spec.rb +99 -0
- data/spec/spec_helper.rb +12 -0
- metadata +118 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
module NumericCheckExt
|
2
|
+
def is_numeric? arg
|
3
|
+
arg.is_a? Numeric
|
4
|
+
end
|
5
|
+
|
6
|
+
alias_method :is_num?, :is_numeric?
|
7
|
+
|
8
|
+
def check_numeric! arg
|
9
|
+
raise ArgumentError, "Argument must be Numeric" if !is_numeric? arg
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module NumericGeoExt
|
14
|
+
def to_dms format = :dms, dp = nil
|
15
|
+
Geo.to_dms self, format, dp
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_lat_dms format = :dms, dp = nil
|
19
|
+
Geo.to_lat self, format, dp
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_lon_dms format = :dms, dp = nil
|
23
|
+
Geo.to_lon self, format, dp
|
24
|
+
end
|
25
|
+
|
26
|
+
# Converts numeric degrees to radians
|
27
|
+
def to_rad
|
28
|
+
self * Math::PI / 180
|
29
|
+
end
|
30
|
+
alias_method :to_radians, :to_rad
|
31
|
+
alias_method :as_rad, :to_rad
|
32
|
+
alias_method :as_radians, :to_rad
|
33
|
+
alias_method :in_rad, :to_rad
|
34
|
+
alias_method :in_radians, :to_rad
|
35
|
+
|
36
|
+
|
37
|
+
# Converts radians to numeric (signed) degrees
|
38
|
+
# latitude (north to south) from equator +90 up then -90 down (equator again) = 180 then 180 for south = 360 total
|
39
|
+
# longitude (west to east) east +180, west -180 = 360 total
|
40
|
+
def to_deg
|
41
|
+
self * 180 / Math::PI
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method :to_degrees, :to_deg
|
45
|
+
alias_method :as_deg, :to_deg
|
46
|
+
alias_method :as_degrees, :to_deg
|
47
|
+
alias_method :in_deg, :to_deg
|
48
|
+
alias_method :in_degrees, :to_deg
|
49
|
+
|
50
|
+
|
51
|
+
# Formats the significant digits of a number, using only fixed-point notation (no exponential)
|
52
|
+
#
|
53
|
+
# @param {Number} precision: Number of significant digits to appear in the returned string
|
54
|
+
# @returns {String} A string representation of number which contains precision significant digits
|
55
|
+
def to_precision precision
|
56
|
+
self.round(precision).to_s
|
57
|
+
|
58
|
+
# numb = self.abs # can't take log of -ve number...
|
59
|
+
# sign = self < 0 ? '-' : '';
|
60
|
+
#
|
61
|
+
# # can't take log of zero
|
62
|
+
# if (numb == 0)
|
63
|
+
# n = '0.'
|
64
|
+
# while (precision -= 1) > 0
|
65
|
+
# n += '0'
|
66
|
+
# end
|
67
|
+
# return n
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# scale = (Math.log(numb) * Math.log10e).ceil # no of digits before decimal
|
71
|
+
# n = (numb * (precision - scale)**10).round.to_s
|
72
|
+
# if (scale > 0) # add trailing zeros & insert decimal as required
|
73
|
+
# l = scale - n.length
|
74
|
+
#
|
75
|
+
# while (l -= 1) > 0
|
76
|
+
# n += '0'
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# if scale < n.length
|
80
|
+
# n = n.slice(0,scale) + '.' + n.slice(scale)
|
81
|
+
# else # prefix decimal and leading zeros if required
|
82
|
+
# while (scale += 1) < 0
|
83
|
+
# n = '0' + n
|
84
|
+
# end
|
85
|
+
# n = '0.' + n
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# sign + n
|
89
|
+
end
|
90
|
+
alias_method :to_fixed, :to_precision
|
91
|
+
|
92
|
+
def normalize_deg shift = 0
|
93
|
+
(self + shift) % 360
|
94
|
+
end
|
95
|
+
alias_method :normalize_degrees, :normalize_deg
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
module Math
|
100
|
+
def self.log10e
|
101
|
+
0.4342944819032518
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
module NumericLatLngExt
|
106
|
+
def to_lat
|
107
|
+
normalize_deg
|
108
|
+
end
|
109
|
+
alias_method :to_lng, :to_lat
|
110
|
+
|
111
|
+
def is_between? lower, upper
|
112
|
+
(lower..upper).cover? self
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class Array
|
117
|
+
def geo_point
|
118
|
+
GeoPoint.new to_lat_lng
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_lat_lng
|
122
|
+
raise "Array must contain at least two elements to be converted to latitude and longitude" if !(size >= 2)
|
123
|
+
[to_lat, to_lng]
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_lat
|
127
|
+
raise "Array must contain at least one element to return the latitude" if empty?
|
128
|
+
first.to_lat
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_lng
|
132
|
+
raise "Array must contain at least two elements to return the longitude" if !self[1]
|
133
|
+
self[1].to_lng
|
134
|
+
end
|
135
|
+
|
136
|
+
def trim
|
137
|
+
join.trim
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class Symbol
|
142
|
+
def self.lng_symbols
|
143
|
+
[:lon, :long, :lng, :longitude]
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.lat_symbols
|
147
|
+
[:lat, :latitude]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class Hash
|
152
|
+
def to_lat_lng
|
153
|
+
[to_lat, to_lng]
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_lat
|
157
|
+
v = Symbol.lat_symbols.select {|key| self[key] }
|
158
|
+
return self[v.first].to_lat if !v.empty?
|
159
|
+
raise "Hash must contain either of the keys: [:lat, :latitude] to be converted to a latitude"
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_lng
|
163
|
+
v = Symbol.lng_symbols.select {|key| self[key] }
|
164
|
+
return self[v.first].to_lng if !v.empty?
|
165
|
+
raise "Hash must contain either of the keys: [:lon, :long, :lng, :longitude] to be converted to a longitude"
|
166
|
+
end
|
167
|
+
|
168
|
+
def geo_point
|
169
|
+
GeoPoint.new to_lat_lng
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class String
|
174
|
+
def concat *args
|
175
|
+
args.inject(self) do |res, arg|
|
176
|
+
x = arg.is_a?(String) ? arg : arg.to_s
|
177
|
+
res << x
|
178
|
+
res
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def parse_dms
|
183
|
+
Geo.parse_dms self
|
184
|
+
end
|
185
|
+
|
186
|
+
def to_rad
|
187
|
+
parse_dms.to_rad
|
188
|
+
end
|
189
|
+
|
190
|
+
def trim
|
191
|
+
strip
|
192
|
+
end
|
193
|
+
|
194
|
+
def geo_clean
|
195
|
+
self.gsub(/^\(/, '').gsub(/\)$/, '').trim
|
196
|
+
end
|
197
|
+
|
198
|
+
def geo_point
|
199
|
+
GeoPoint.new to_lat_lng
|
200
|
+
end
|
201
|
+
|
202
|
+
def to_lat_lng
|
203
|
+
geo_clean.split(',').to_lat_lng
|
204
|
+
end
|
205
|
+
|
206
|
+
def to_lat
|
207
|
+
raise "An empty String has no latitude" if empty?
|
208
|
+
geo_clean.parse_dms.to_f.to_lat
|
209
|
+
end
|
210
|
+
|
211
|
+
def to_lng
|
212
|
+
raise "An empty String has no latitude" if empty?
|
213
|
+
geo_clean.parse_dms.to_f.to_lng
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
class Fixnum
|
220
|
+
include NumericGeoExt
|
221
|
+
include NumericLatLngExt
|
222
|
+
end
|
223
|
+
|
224
|
+
class Float
|
225
|
+
include NumericGeoExt
|
226
|
+
include NumericLatLngExt
|
227
|
+
end
|
228
|
+
|
data/lib/geo_calc/geo.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'geo_calc/core_ext'
|
2
|
+
|
3
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
4
|
+
# Geodesy representation conversion functions (c) Chris Veness 2002-2010
|
5
|
+
# - www.movable-type.co.uk/scripts/latlong.html
|
6
|
+
#
|
7
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
8
|
+
|
9
|
+
# Parses string representing degrees/minutes/seconds into numeric degrees
|
10
|
+
#
|
11
|
+
# This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally
|
12
|
+
# suffixed by compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W)
|
13
|
+
# or fixed-width format without separators (eg 0033709W). Seconds and minutes may be omitted.
|
14
|
+
# (Note minimal validation is done).
|
15
|
+
#
|
16
|
+
# @param {String|Number} dmsStr: Degrees or deg/min/sec in variety of formats
|
17
|
+
# @returns {Number} Degrees as decimal number
|
18
|
+
# @throws ArgumentError
|
19
|
+
|
20
|
+
module Geo
|
21
|
+
extend self
|
22
|
+
extend ::NumericCheckExt
|
23
|
+
include ::NumericCheckExt
|
24
|
+
|
25
|
+
def parse_dms dms_str
|
26
|
+
# check for signed decimal degrees without NSEW, if so return it directly
|
27
|
+
return dms_str if is_numeric?(dms_str)
|
28
|
+
|
29
|
+
# strip off any sign or compass dir'n & split out separate d/m/s
|
30
|
+
dms = dms_str.trim.gsub(/^-/,'').gsub(/[NSEW]$/i,'').split(/[^0-9.,]+/).map(&:trim).map(&:to_f)
|
31
|
+
return nil if dms.empty?
|
32
|
+
|
33
|
+
# and convert to decimal degrees...
|
34
|
+
deg = case dms.length
|
35
|
+
when 3 # interpret 3-part result as d/m/s
|
36
|
+
dms[0]/1 + dms[1]/60 + dms[2]/3600
|
37
|
+
when 2 # interpret 2-part result as d/m
|
38
|
+
dms[0]/1 + dms[1]/60
|
39
|
+
when 1 # just d (possibly decimal) or non-separated dddmmss
|
40
|
+
d = dms[0];
|
41
|
+
# check for fixed-width unseparated format eg 0033709W
|
42
|
+
d = "0#{d}" if (/[NS]/i.match(dms_str)) # - normalise N/S to 3-digit degrees
|
43
|
+
d = "#{d.slice(0,3)/1}#{deg.slice(3,5)/60}#{deg.slice(5)/3600}" if (/[0-9]{7}/.match(deg))
|
44
|
+
d
|
45
|
+
else
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
return nil if !deg
|
49
|
+
deg = (deg * -1) if (/^-|[WS]$/i.match(dms_str.trim)) # take '-', west and south as -ve
|
50
|
+
deg.to_f
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert decimal degrees to deg/min/sec format
|
54
|
+
# - degree, prime, double-prime symbols are added, but sign is discarded, though no compass
|
55
|
+
# direction is added
|
56
|
+
#
|
57
|
+
#
|
58
|
+
# @param {Number} deg: Degrees
|
59
|
+
# @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
60
|
+
# @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
61
|
+
# @returns {String} deg formatted as deg/min/secs according to specified format
|
62
|
+
# @throws {TypeError} deg is an object, perhaps DOM object without .value?
|
63
|
+
|
64
|
+
def to_dms deg, format = :dms, dp = nil
|
65
|
+
deg = begin
|
66
|
+
deg.to_f
|
67
|
+
rescue
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
return nil if !deg # give up here if we can't make a number from deg
|
71
|
+
|
72
|
+
# default values
|
73
|
+
format ||= :dms
|
74
|
+
dp = if dp.nil?
|
75
|
+
case format.to_sym
|
76
|
+
when :d
|
77
|
+
4
|
78
|
+
when :dm
|
79
|
+
2
|
80
|
+
else
|
81
|
+
0 # default
|
82
|
+
end
|
83
|
+
end
|
84
|
+
dp ||= 0
|
85
|
+
|
86
|
+
deg = deg.abs # (unsigned result ready for appending compass dir'n)
|
87
|
+
|
88
|
+
case format
|
89
|
+
when :d
|
90
|
+
d = deg.round(dp) # round degrees
|
91
|
+
ds = "0#{d}" if (d <100) # pad with leading zeros
|
92
|
+
ds = "0#{ds}" if (d <10)
|
93
|
+
dms = ds.to_s.concat("\u00B0") # add º symbol
|
94
|
+
when :dm
|
95
|
+
min = (deg*60).round(dp) # convert degrees to minutes & round
|
96
|
+
d = d.to_i
|
97
|
+
d = (min / 60).floor # get component deg/min
|
98
|
+
m = (min % 60).round(dp) # pad with trailing zeros
|
99
|
+
ds = d
|
100
|
+
ms = m
|
101
|
+
ds = "0#{d}" if (d<100) # pad with leading zeros
|
102
|
+
ds = "0#{d}" if (d<10)
|
103
|
+
ms = "0#{m}" if (m<10)
|
104
|
+
dms = ds.to_s.concat("\u00B0", ms, "\u2032") # add º, ' symbols
|
105
|
+
when :dms
|
106
|
+
sec = (deg * 3600).round # convert degrees to seconds & round
|
107
|
+
d = (sec / 3600).floor # get component deg/min/sec
|
108
|
+
m = ((sec / 60) % 60).floor
|
109
|
+
s = (sec % 60).round(dp) # pad with trailing zeros
|
110
|
+
ds = d
|
111
|
+
ms = m
|
112
|
+
ss = s
|
113
|
+
ds = "0#{d}" if (d < 100) # pad with leading zeros
|
114
|
+
ds = "0#{ds}" if (d < 10)
|
115
|
+
ms = "0#{m}" if (m < 10)
|
116
|
+
ss = "0#{s}" if (s < 10)
|
117
|
+
dms = ds.to_s.concat("\u00B0", ms, "\u2032", ss, "\u2033") # add º, ', " symbols
|
118
|
+
end
|
119
|
+
return dms
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Convert numeric degrees to deg/min/sec latitude (suffixed with N/S)
|
124
|
+
#
|
125
|
+
# @param {Number} deg: Degrees
|
126
|
+
# @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
127
|
+
# @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
128
|
+
# @returns {String} Deg/min/seconds
|
129
|
+
|
130
|
+
def to_lat deg, format = :dms, dp = 0
|
131
|
+
_lat = to_dms deg, format, dp
|
132
|
+
_lat == '' ? '' : _lat[1..-1] + (deg<0 ? 'S' : 'N') # knock off initial '0' for lat!
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Convert numeric degrees to deg/min/sec longitude (suffixed with E/W)
|
137
|
+
#
|
138
|
+
# @param {Number} deg: Degrees
|
139
|
+
# @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
140
|
+
# @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
141
|
+
# @returns {String} Deg/min/seconds
|
142
|
+
|
143
|
+
def to_lon deg, format = :dms, dp = 0
|
144
|
+
deg = (360 - deg) * -1 if deg % 360 > 180
|
145
|
+
lon = to_dms deg, format, dp
|
146
|
+
lon == '' ? '' : lon + (deg<0 ? 'W' : 'E')
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# Convert numeric degrees to deg/min/sec as a bearing (0º..360º)
|
151
|
+
#
|
152
|
+
# @param {Number} deg: Degrees
|
153
|
+
# @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
154
|
+
# @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
155
|
+
# @returns {String} Deg/min/seconds
|
156
|
+
|
157
|
+
def to_brng deg, format = :dms, dp = 0
|
158
|
+
deg = (deg.to_f + 360) % 360 # normalise -ve values to 180º..360º
|
159
|
+
brng = to_dms deg, format, dp
|
160
|
+
brng.gsub /360/, '0' # just in case rounding took us up to 360º!
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
include NumericCheckExt
|
166
|
+
end
|
167
|
+
|
168
|
+
# class String
|
169
|
+
# include ::Geo
|
170
|
+
# end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'geo_calc/calculations'
|
2
|
+
|
3
|
+
# Sample usage:
|
4
|
+
# p1 = GeoPoint.new(51.5136, -0.0983)
|
5
|
+
# p2 = GeoPoint.new(51.4778, -0.0015)
|
6
|
+
# dist = p1.distance_to(p2) # in km
|
7
|
+
# brng = p1.bearing_to(p2) # in degrees clockwise from north
|
8
|
+
# ... etc
|
9
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
10
|
+
#
|
11
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
12
|
+
# Note that minimal error checking is performed in this example code!
|
13
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
14
|
+
|
15
|
+
class GeoPoint
|
16
|
+
include GeoCalc
|
17
|
+
# Creates a point on the earth's surface at the supplied latitude / longitude
|
18
|
+
#
|
19
|
+
# Constructor
|
20
|
+
# - Numeric lat: latitude in numeric degrees
|
21
|
+
# - Numeric lon: longitude in numeric degrees
|
22
|
+
# - Numeric [rad=6371]: radius of earth if different value is required from standard 6,371km
|
23
|
+
|
24
|
+
attr_reader :lat, :lon, :unit, :radius
|
25
|
+
|
26
|
+
(Symbol.lng_symbols - [:lon]).each do |sym|
|
27
|
+
class_eval %{
|
28
|
+
alias_method :#{sym}, :lon
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
(Symbol.lat_symbols - [:lat]).each do |sym|
|
33
|
+
class_eval %{
|
34
|
+
alias_method :#{sym}, :lat
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def initialize *args
|
40
|
+
rad = args.delete(args.size) if is_numeric?(args.last) && args.last.is_between?(6350, 6380)
|
41
|
+
rad ||= 6371 # default
|
42
|
+
case args.size
|
43
|
+
when 1
|
44
|
+
create_from_one *args, rad
|
45
|
+
when 2
|
46
|
+
create_from_two *args, rad
|
47
|
+
else
|
48
|
+
raise "GeoPoint must be initialized with either one or to arguments defining the (latitude, longitude) coordinate on the map"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def [] key
|
53
|
+
case key
|
54
|
+
when Fixnum
|
55
|
+
raise ArgumentError, "Index must be 0 or 1" if !(0..1).cover?(key)
|
56
|
+
to_arr[key]
|
57
|
+
when String, Symbol
|
58
|
+
send(key) if respond_to? key
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Key must be a Fixnum (index) or a method name"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
alias_method :to_dms, :to_s
|
65
|
+
|
66
|
+
def to_lat_lng
|
67
|
+
[lat, lng]
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_arr
|
71
|
+
a = to_lat_lng
|
72
|
+
reverse_arr? ? a.reverse : a
|
73
|
+
end
|
74
|
+
|
75
|
+
def reverse_arr?
|
76
|
+
@reverse_arr
|
77
|
+
end
|
78
|
+
|
79
|
+
def reverse_arr!
|
80
|
+
@reverse_arr = true
|
81
|
+
end
|
82
|
+
|
83
|
+
def normal_arr!
|
84
|
+
@reverse_arr = false
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
include NumericCheckExt
|
90
|
+
|
91
|
+
def create_from_one points, rad = 6371
|
92
|
+
create_from_two *points.to_lat_lng, rad
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_from_two lat, lon, rad = 6371
|
96
|
+
rad ||= 6371 # earth's mean radius in km
|
97
|
+
@lat = lat.to_lat
|
98
|
+
@lon = lon.to_lng
|
99
|
+
@radius = rad
|
100
|
+
@unit = :degrees
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|