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.
@@ -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
+
@@ -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
+