geo_coord 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +199 -0
- data/StdlibProposal.md +175 -0
- data/geo_coord.gemspec +38 -0
- data/lib/geo/coord/globes.rb +165 -0
- data/lib/geo/coord/version.rb +5 -0
- data/lib/geo/coord.rb +667 -0
- metadata +163 -0
data/lib/geo/coord.rb
ADDED
@@ -0,0 +1,667 @@
|
|
1
|
+
# Geo::Coord is Ruby's library for handling [lat, lng] pairs of
|
2
|
+
# geographical coordinates. It provides most of basic functionality
|
3
|
+
# you may expect (storing and representing coordinate pair), as well
|
4
|
+
# as some geodesy math, like distances and azimuth, and comprehensive
|
5
|
+
# parsing/formatting features.
|
6
|
+
#
|
7
|
+
# See +Geo::Coord+ class docs for full description and usage examples.
|
8
|
+
#
|
9
|
+
module Geo
|
10
|
+
# Geo::Coord is main class of Geo module, representing
|
11
|
+
# +(latitude, longitude)+ pair. It stores coordinates in floating-point
|
12
|
+
# degrees form, provides access to coordinate components, allows complex
|
13
|
+
# formatting and parsing of coordinate pairs and performs geodesy
|
14
|
+
# calculations in standard WGS-84 coordinate reference system.
|
15
|
+
#
|
16
|
+
# == Examples of usage
|
17
|
+
#
|
18
|
+
# Creation:
|
19
|
+
#
|
20
|
+
# # From lat/lng pair:
|
21
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
22
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
23
|
+
#
|
24
|
+
# # Or using keyword arguments form:
|
25
|
+
# g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
|
26
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
27
|
+
#
|
28
|
+
# # Keyword arguments also allow creation of Coord from components:
|
29
|
+
# g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
|
30
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
31
|
+
#
|
32
|
+
# For parsing API responses you'd like to use +from_h+,
|
33
|
+
# which accepts String and Symbol keys, any letter case,
|
34
|
+
# and knows synonyms (lng/lon/longitude):
|
35
|
+
#
|
36
|
+
# g = Geo::Coord.from_h('LAT' => 50.004444, 'LON' => 36.231389)
|
37
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
38
|
+
#
|
39
|
+
# For math, you'd probably like to be able to initialize
|
40
|
+
# Coord with radians rather than degrees:
|
41
|
+
#
|
42
|
+
# g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
|
43
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
44
|
+
#
|
45
|
+
# There's also family of parsing methods, with different applicability:
|
46
|
+
#
|
47
|
+
# # Tries to parse (lat, lng) pair:
|
48
|
+
# g = Geo::Coord.parse_ll('50.004444, 36.231389')
|
49
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
50
|
+
#
|
51
|
+
# # Tries to parse degrees/minutes/seconds:
|
52
|
+
# g = Geo::Coord.parse_dms('50° 0′ 16″ N, 36° 13′ 53″ E')
|
53
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
54
|
+
#
|
55
|
+
# # Tries to do best guess:
|
56
|
+
# g = Geo::Coord.parse('50.004444, 36.231389')
|
57
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
58
|
+
# g = Geo::Coord.parse('50° 0′ 16″ N, 36° 13′ 53″ E')
|
59
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
60
|
+
#
|
61
|
+
# # Allows user to provide pattern (see below for pattern language):
|
62
|
+
# g = Geo::Coord.strpcoord('50.004444, 36.231389', '%lat, %lng')
|
63
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
64
|
+
#
|
65
|
+
# Having Coord object, you can get its properties:
|
66
|
+
#
|
67
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
68
|
+
# g.lat # => 50.004444
|
69
|
+
# g.latd # => 50 -- latitude degrees
|
70
|
+
# g.lath # => N -- latitude hemisphere
|
71
|
+
# g.lngh # => E -- longitude hemishpere
|
72
|
+
# g.phi # => 0.8727421884291233 -- longitude in radians
|
73
|
+
# g.latdms # => [50, 0, 15.998400000011316, "N"]
|
74
|
+
# # ...and so on
|
75
|
+
#
|
76
|
+
# Format and convert it:
|
77
|
+
#
|
78
|
+
# g.to_s # => "50.004444,36.231389"
|
79
|
+
# g.strfcoord('%latd°%latm′%lats″%lath %lngd°%lngm′%lngs″%lngh')
|
80
|
+
# # => "50°0′16″N 36°13′53″E"
|
81
|
+
#
|
82
|
+
# g.to_h(lat: 'LAT', lng: 'LON') # => {'LAT'=>50.004444, 'LON'=>36.231389}
|
83
|
+
#
|
84
|
+
# Do simple geodesy math:
|
85
|
+
#
|
86
|
+
# kharkiv = Geo::Coord.new(50.004444, 36.231389)
|
87
|
+
# kyiv = Geo::Coord.new(50.45, 30.523333)
|
88
|
+
#
|
89
|
+
# kharkiv.distance(kyiv) # => 410211.22377421556
|
90
|
+
# kharkiv.azimuth(kyiv) # => 279.12614358262067
|
91
|
+
# kharkiv.endpoint(410_211, 280) # => #<Geo::Coord 50.505975,30.531283>
|
92
|
+
#
|
93
|
+
class Coord
|
94
|
+
# Latitude, degrees, signed float.
|
95
|
+
attr_reader :lat
|
96
|
+
|
97
|
+
# Longitude, degrees, signed float.
|
98
|
+
attr_reader :lng
|
99
|
+
|
100
|
+
alias latitude lat
|
101
|
+
alias longitude lng
|
102
|
+
alias lon lng
|
103
|
+
|
104
|
+
class << self
|
105
|
+
# @private
|
106
|
+
LAT_KEYS = %i[lat latitude].freeze # :nodoc:
|
107
|
+
# @private
|
108
|
+
LNG_KEYS = %i[lng lon long longitude].freeze # :nodoc:
|
109
|
+
|
110
|
+
# Creates Coord from hash, containing latitude and longitude.
|
111
|
+
#
|
112
|
+
# This methos designed as a way for parsing responses from APIs and
|
113
|
+
# databases, so, it tries to be pretty liberal on its input:
|
114
|
+
# - accepts String or Symbol keys;
|
115
|
+
# - accepts any letter case;
|
116
|
+
# - accepts several synonyms for latitude ("lat" and "latitude")
|
117
|
+
# and longitude ("lng", "lon", "long", "longitude").
|
118
|
+
#
|
119
|
+
# g = Geo::Coord.from_h('LAT' => 50.004444, longitude: 36.231389)
|
120
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
121
|
+
#
|
122
|
+
def from_h(hash)
|
123
|
+
h = hash.map { |k, v| [k.to_s.downcase.to_sym, v] }.to_h
|
124
|
+
lat = h.values_at(*LAT_KEYS).compact.first or
|
125
|
+
raise(ArgumentError, "No latitude value found in #{hash}")
|
126
|
+
lng = h.values_at(*LNG_KEYS).compact.first or
|
127
|
+
raise(ArgumentError, "No longitude value found in #{hash}")
|
128
|
+
|
129
|
+
new(lat, lng)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Creates Coord from φ and λ (latitude and longitude in radians).
|
133
|
+
#
|
134
|
+
# g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
|
135
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
136
|
+
#
|
137
|
+
def from_rad(phi, la)
|
138
|
+
new(phi * 180 / Math::PI, la * 180 / Math::PI)
|
139
|
+
end
|
140
|
+
|
141
|
+
# @private
|
142
|
+
INT_PATTERN = '[-+]?\d+'.freeze # :nodoc:
|
143
|
+
# @private
|
144
|
+
UINT_PATTERN = '\d+'.freeze # :nodoc:
|
145
|
+
# @private
|
146
|
+
FLOAT_PATTERN = '[-+]?\d+(?:\.\d*)?'.freeze # :nodoc:
|
147
|
+
# @private
|
148
|
+
UFLOAT_PATTERN = '\d+(?:\.\d*)?'.freeze # :nodoc:
|
149
|
+
|
150
|
+
# @private
|
151
|
+
DEG_PATTERN = '[ °d]'.freeze # :nodoc:
|
152
|
+
# @private
|
153
|
+
MIN_PATTERN = "['′m]".freeze # :nodoc:
|
154
|
+
# @private
|
155
|
+
SEC_PATTERN = '["″s]'.freeze # :nodoc:
|
156
|
+
|
157
|
+
# @private
|
158
|
+
LL_PATTERN = /^(#{FLOAT_PATTERN})\s*[,; ]\s*(#{FLOAT_PATTERN})$/ # :nodoc:
|
159
|
+
|
160
|
+
# @private
|
161
|
+
DMS_PATTERN = # :nodoc:
|
162
|
+
/^\s*
|
163
|
+
(?<latd>#{INT_PATTERN})#{DEG_PATTERN}\s*
|
164
|
+
((?<latm>#{UINT_PATTERN})#{MIN_PATTERN}\s*
|
165
|
+
((?<lats>#{UFLOAT_PATTERN})#{SEC_PATTERN}\s*)?)?
|
166
|
+
(?<lath>[NS])?
|
167
|
+
\s*[,; ]\s*
|
168
|
+
(?<lngd>#{INT_PATTERN})#{DEG_PATTERN}\s*
|
169
|
+
((?<lngm>#{UINT_PATTERN})#{MIN_PATTERN}\s*
|
170
|
+
((?<lngs>#{UFLOAT_PATTERN})#{SEC_PATTERN}\s*)?)?
|
171
|
+
(?<lngh>[EW])?
|
172
|
+
\s*$/x
|
173
|
+
|
174
|
+
# Parses Coord from string containing float latitude and longitude.
|
175
|
+
# Understands several types of separators/spaces between values.
|
176
|
+
#
|
177
|
+
# Geo::Coord.parse_ll('-50.004444 +36.231389')
|
178
|
+
# # => #<Geo::Coord -50.004444,36.231389>
|
179
|
+
#
|
180
|
+
# If parse_ll is not wise enough to understand your data, consider
|
181
|
+
# using ::strpcoord.
|
182
|
+
#
|
183
|
+
def parse_ll(str)
|
184
|
+
str.match(LL_PATTERN) do |m|
|
185
|
+
return new(m[1].to_f, m[2].to_f)
|
186
|
+
end
|
187
|
+
raise ArgumentError, "Can't parse #{str} as lat, lng"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Parses Coord from string containing latitude and longitude in
|
191
|
+
# degrees-minutes-seconds-hemisphere format. Understands several
|
192
|
+
# types of separators, degree, minute, second signs, as well as
|
193
|
+
# explicit hemisphere and no-hemisphere (signed degrees) formats.
|
194
|
+
#
|
195
|
+
# Geo::Coord.parse_dms('50°0′16″N 36°13′53″E')
|
196
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
197
|
+
#
|
198
|
+
# If parse_dms is not wise enough to understand your data, consider
|
199
|
+
# using ::strpcoord.
|
200
|
+
#
|
201
|
+
def parse_dms(str)
|
202
|
+
str.match(DMS_PATTERN) do |m|
|
203
|
+
return new(
|
204
|
+
latd: m[:latd], latm: m[:latm], lats: m[:lats], lath: m[:lath],
|
205
|
+
lngd: m[:lngd], lngm: m[:lngm], lngs: m[:lngs], lngh: m[:lngh]
|
206
|
+
)
|
207
|
+
end
|
208
|
+
raise ArgumentError, "Can't parse #{str} as degrees-minutes-seconds"
|
209
|
+
end
|
210
|
+
|
211
|
+
# Tries its best to parse Coord from string containing it (in any
|
212
|
+
# known form).
|
213
|
+
#
|
214
|
+
# Geo::Coord.parse('-50.004444 +36.231389')
|
215
|
+
# # => #<Geo::Coord -50.004444,36.231389>
|
216
|
+
# Geo::Coord.parse('50°0′16″N 36°13′53″E')
|
217
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
218
|
+
#
|
219
|
+
# If you know exact form in which coordinates are
|
220
|
+
# provided, it may be wider to consider parse_ll, parse_dms or
|
221
|
+
# even ::strpcoord.
|
222
|
+
def parse(str)
|
223
|
+
# rubocop:disable Style/RescueModifier
|
224
|
+
parse_ll(str) rescue (parse_dms(str) rescue nil)
|
225
|
+
# rubocop:enable Style/RescueModifier
|
226
|
+
end
|
227
|
+
|
228
|
+
# @private
|
229
|
+
PARSE_PATTERNS = { # :nodoc:
|
230
|
+
'%latd' => "(?<latd>#{INT_PATTERN})",
|
231
|
+
'%latm' => "(?<latm>#{UINT_PATTERN})",
|
232
|
+
'%lats' => "(?<lats>#{UFLOAT_PATTERN})",
|
233
|
+
'%lath' => '(?<lath>[NS])',
|
234
|
+
|
235
|
+
'%lat' => "(?<lat>#{FLOAT_PATTERN})",
|
236
|
+
|
237
|
+
'%lngd' => "(?<lngd>#{INT_PATTERN})",
|
238
|
+
'%lngm' => "(?<lngm>#{UINT_PATTERN})",
|
239
|
+
'%lngs' => "(?<lngs>#{UFLOAT_PATTERN})",
|
240
|
+
'%lngh' => '(?<lngh>[EW])',
|
241
|
+
|
242
|
+
'%lng' => "(?<lng>#{FLOAT_PATTERN})"
|
243
|
+
}.freeze
|
244
|
+
|
245
|
+
# Parses +str+ into Coord with provided +pattern+.
|
246
|
+
#
|
247
|
+
# Example:
|
248
|
+
#
|
249
|
+
# Geo::Coord.strpcoord('-50.004444/+36.231389', '%lat/%lng')
|
250
|
+
# # => #<Geo::Coord -50.004444,36.231389>
|
251
|
+
#
|
252
|
+
# List of parsing flags:
|
253
|
+
#
|
254
|
+
# %lat :: Full latitude, float
|
255
|
+
# %latd :: Latitude degrees, integer, may be signed (instead of
|
256
|
+
# providing hemisphere info
|
257
|
+
# %latm :: Latitude minutes, integer, unsigned
|
258
|
+
# %lats :: Latitude seconds, float, unsigned
|
259
|
+
# %lath :: Latitude hemisphere, "N" or "S"
|
260
|
+
# %lng :: Full longitude, float
|
261
|
+
# %lngd :: Longitude degrees, integer, may be signed (instead of
|
262
|
+
# providing hemisphere info
|
263
|
+
# %lngm :: Longitude minutes, integer, unsigned
|
264
|
+
# %lngs :: Longitude seconds, float, unsigned
|
265
|
+
# %lngh :: Longitude hemisphere, "N" or "S"
|
266
|
+
#
|
267
|
+
def strpcoord(str, pattern)
|
268
|
+
pattern = PARSE_PATTERNS.inject(pattern) do |memo, (pfrom, pto)|
|
269
|
+
memo.gsub(pfrom, pto)
|
270
|
+
end
|
271
|
+
if (m = Regexp.new('^' + pattern).match(str))
|
272
|
+
h = m.names.map { |n| [n.to_sym, _extract_match(m, n)] }.to_h
|
273
|
+
new(h)
|
274
|
+
else
|
275
|
+
raise ArgumentError, "Coordinates str #{str} can't be parsed by pattern #{pattern}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
def _extract_match(match, name)
|
282
|
+
return nil unless match[name]
|
283
|
+
val = match[name]
|
284
|
+
name.end_with?('h') ? val : val.to_f
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Creates Coord object.
|
289
|
+
#
|
290
|
+
# There are three forms of usage:
|
291
|
+
# - <tt>Coord.new(lat, lng)</tt> with +lat+ and +lng+ being floats;
|
292
|
+
# - <tt>Coord.new(lat: lat, lng: lng)</tt> -- same as above, but
|
293
|
+
# with keyword arguments;
|
294
|
+
# - <tt>Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')</tt> -- for
|
295
|
+
# cases when you have coordinates components already parsed;
|
296
|
+
#
|
297
|
+
# In keyword arguments form, any argument can be omitted and will be
|
298
|
+
# replaced with 0. But you can't mix, for example, "whole" latitude
|
299
|
+
# key +lat+ and partial longitude keys +lngd+, +lngm+ and so on.
|
300
|
+
#
|
301
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
302
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
303
|
+
#
|
304
|
+
# # Or using keyword arguments form:
|
305
|
+
# g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
|
306
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
307
|
+
#
|
308
|
+
# # Keyword arguments also allow creation of Coord from components:
|
309
|
+
# g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
|
310
|
+
# # => #<Geo::Coord 50.004444,36.231389>
|
311
|
+
#
|
312
|
+
# # Providing defaults:
|
313
|
+
# g = Geo::Coord.new(lat: 50.004444)
|
314
|
+
# # => #<Geo::Coord 50.004444,0.000000>
|
315
|
+
#
|
316
|
+
def initialize(lat = nil, lng = nil, **opts)
|
317
|
+
@globe = Globes::Earth.instance
|
318
|
+
|
319
|
+
case
|
320
|
+
when lat && lng
|
321
|
+
_init(lat, lng)
|
322
|
+
when opts.key?(:lat) || opts.key?(:lng)
|
323
|
+
_init(opts[:lat], opts[:lng])
|
324
|
+
when opts.key?(:latd) || opts.key?(:lngd)
|
325
|
+
_init_dms(opts)
|
326
|
+
else
|
327
|
+
raise ArgumentError, "Can't create #{self.class} by provided data"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Compares with +other+.
|
332
|
+
#
|
333
|
+
# Note, that comparison includes comparing floating point values,
|
334
|
+
# so, when two "almost exactly same" coord pairs are calculated using
|
335
|
+
# different methods, you can rarely expect them to be _exactly_ equal.
|
336
|
+
#
|
337
|
+
# Also, note that no greater/lower relation is defined on Coord, so,
|
338
|
+
# for example, you can't just sort an array of Coord.
|
339
|
+
def ==(other)
|
340
|
+
other.is_a?(self.class) && other.lat == lat && other.lng == lng
|
341
|
+
end
|
342
|
+
|
343
|
+
# Returns latitude degrees (unsigned integer).
|
344
|
+
def latd
|
345
|
+
lat.abs.to_i
|
346
|
+
end
|
347
|
+
|
348
|
+
# Returns latitude minutes (unsigned integer).
|
349
|
+
def latm
|
350
|
+
(lat.abs * 60).to_i % 60
|
351
|
+
end
|
352
|
+
|
353
|
+
# Returns latitude seconds (unsigned float).
|
354
|
+
def lats
|
355
|
+
(lat.abs * 3600) % 3600
|
356
|
+
end
|
357
|
+
|
358
|
+
# Returns latitude hemisphere (upcase letter 'N' or 'S').
|
359
|
+
def lath
|
360
|
+
lat > 0 ? 'N' : 'S'
|
361
|
+
end
|
362
|
+
|
363
|
+
# Returns longitude degrees (unsigned integer).
|
364
|
+
def lngd
|
365
|
+
lng.abs.to_i
|
366
|
+
end
|
367
|
+
|
368
|
+
# Returns longitude minutes (unsigned integer).
|
369
|
+
def lngm
|
370
|
+
(lng.abs * 60).to_i % 60
|
371
|
+
end
|
372
|
+
|
373
|
+
# Returns longitude seconds (unsigned float).
|
374
|
+
def lngs
|
375
|
+
(lng.abs * 3600) % 60
|
376
|
+
end
|
377
|
+
|
378
|
+
# Returns longitude hemisphere (upcase letter 'E' or 'W').
|
379
|
+
def lngh
|
380
|
+
lng > 0 ? 'E' : 'W'
|
381
|
+
end
|
382
|
+
|
383
|
+
# Returns latitude components: degrees, minutes, seconds and optionally
|
384
|
+
# a hemisphere:
|
385
|
+
#
|
386
|
+
# # Nothern hemisphere:
|
387
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
388
|
+
#
|
389
|
+
# g.latdms # => [50, 0, 15.998400000011316, "N"]
|
390
|
+
# g.latdms(true) # => [50, 0, 15.998400000011316]
|
391
|
+
#
|
392
|
+
# # Southern hemisphere:
|
393
|
+
# g = Geo::Coord.new(-50.004444, 36.231389)
|
394
|
+
#
|
395
|
+
# g.latdms # => [50, 0, 15.998400000011316, "S"]
|
396
|
+
# g.latdms(true) # => [-50, 0, 15.998400000011316]
|
397
|
+
#
|
398
|
+
def latdms(nohemisphere = false)
|
399
|
+
nohemisphere ? [latsign * latd, latm, lats] : [latd, latm, lats, lath]
|
400
|
+
end
|
401
|
+
|
402
|
+
# Returns longitude components: degrees, minutes, seconds and optionally
|
403
|
+
# a hemisphere:
|
404
|
+
#
|
405
|
+
# # Eastern hemisphere:
|
406
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
407
|
+
#
|
408
|
+
# g.lngdms # => [36, 13, 53.00040000000445, "E"]
|
409
|
+
# g.lngdms(true) # => [36, 13, 53.00040000000445]
|
410
|
+
#
|
411
|
+
# # Western hemisphere:
|
412
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
413
|
+
#
|
414
|
+
# g.lngdms # => [36, 13, 53.00040000000445, "E"]
|
415
|
+
# g.lngdms(true) # => [-36, 13, 53.00040000000445]
|
416
|
+
#
|
417
|
+
def lngdms(nohemisphere = false)
|
418
|
+
nohemisphere ? [lngsign * lngd, lngm, lngs] : [lngd, lngm, lngs, lngh]
|
419
|
+
end
|
420
|
+
|
421
|
+
# Latitude in radians. Geodesy formulae almost alwayse use greek Phi
|
422
|
+
# for it.
|
423
|
+
def phi
|
424
|
+
deg2rad(lat)
|
425
|
+
end
|
426
|
+
|
427
|
+
alias φ phi
|
428
|
+
|
429
|
+
# Latitude in radians. Geodesy formulae almost alwayse use greek Lambda
|
430
|
+
# for it; we are using shorter name for not confuse with Ruby's +lambda+
|
431
|
+
# keyword.
|
432
|
+
def la
|
433
|
+
deg2rad(lng)
|
434
|
+
end
|
435
|
+
|
436
|
+
alias λ la
|
437
|
+
|
438
|
+
# Returns a string represent coordinates object.
|
439
|
+
#
|
440
|
+
# g.inspect # => "#<Geo::Coord 50.004444,36.231389>"
|
441
|
+
#
|
442
|
+
def inspect
|
443
|
+
'#<%s %s>' % [self.class.name, to_s]
|
444
|
+
end
|
445
|
+
|
446
|
+
# Returns a string representing coordinates.
|
447
|
+
#
|
448
|
+
# g.to_s # => "50.004444,36.231389"
|
449
|
+
#
|
450
|
+
def to_s
|
451
|
+
'%f,%f' % [lat, lng]
|
452
|
+
end
|
453
|
+
|
454
|
+
# Returns a two-element array of latitude and longitude.
|
455
|
+
#
|
456
|
+
# g.to_a # => [50.004444, 36.231389]
|
457
|
+
#
|
458
|
+
def to_a
|
459
|
+
[lat, lng]
|
460
|
+
end
|
461
|
+
|
462
|
+
# Returns hash of latitude and longitude. You can provide your keys
|
463
|
+
# if you want:
|
464
|
+
#
|
465
|
+
# g.to_h
|
466
|
+
# # => {:lat=>50.004444, :lng=>36.231389}
|
467
|
+
# g.to_h(lat: 'LAT', lng: 'LNG')
|
468
|
+
# # => {'LAT'=>50.004444, 'LNG'=>36.231389}
|
469
|
+
#
|
470
|
+
def to_h(lat: :lat, lng: :lng)
|
471
|
+
{lat => self.lat, lng => self.lng}
|
472
|
+
end
|
473
|
+
|
474
|
+
# @private
|
475
|
+
INTFLAGS = '\+'.freeze # :nodoc:
|
476
|
+
# @private
|
477
|
+
FLOATUFLAGS = /\.0\d+/ # :nodoc:
|
478
|
+
# @private
|
479
|
+
FLOATFLAGS = /\+?#{FLOATUFLAGS}?/ # :nodoc:
|
480
|
+
|
481
|
+
# @private
|
482
|
+
DIRECTIVES = { # :nodoc:
|
483
|
+
/%(#{INTFLAGS})?latds/ => proc { |m| "%<latds>#{m[1]}i" },
|
484
|
+
'%latd' => '%<latd>i',
|
485
|
+
'%latm' => '%<latm>i',
|
486
|
+
/%(#{FLOATUFLAGS})?lats/ => proc { |m| "%<lats>#{m[1] || '.0'}f" },
|
487
|
+
'%lath' => '%<lath>s',
|
488
|
+
/%(#{FLOATFLAGS})?lat/ => proc { |m| "%<lat>#{m[1]}f" },
|
489
|
+
|
490
|
+
/%(#{INTFLAGS})?lngds/ => proc { |m| "%<lngds>#{m[1]}i" },
|
491
|
+
'%lngd' => '%<lngd>i',
|
492
|
+
'%lngm' => '%<lngm>i',
|
493
|
+
/%(#{FLOATUFLAGS})?lngs/ => proc { |m| "%<lngs>#{m[1] || '.0'}f" },
|
494
|
+
'%lngh' => '%<lngh>s',
|
495
|
+
/%(#{FLOATFLAGS})?lng/ => proc { |m| "%<lng>#{m[1]}f" }
|
496
|
+
}.freeze
|
497
|
+
|
498
|
+
# Formats coordinates according to directives in +formatstr+.
|
499
|
+
#
|
500
|
+
# Each directive starts with +%+ and can contain some modifiers before
|
501
|
+
# its name.
|
502
|
+
#
|
503
|
+
# Acceptable modifiers:
|
504
|
+
# - unsigned integers: none;
|
505
|
+
# - signed integers: <tt>+</tt> for mandatory sign printing;
|
506
|
+
# - floats: same as integers and number of digits modifier, like
|
507
|
+
# <tt>.03</tt>.
|
508
|
+
#
|
509
|
+
# List of directives:
|
510
|
+
#
|
511
|
+
# %lat :: Full latitude, floating point, signed
|
512
|
+
# %latds :: Latitude degrees, integer, signed
|
513
|
+
# %latd :: Latitude degrees, integer, unsigned
|
514
|
+
# %latm :: Latitude minutes, integer, unsigned
|
515
|
+
# %lats :: Latitude seconds, floating point, unsigned
|
516
|
+
# %lath :: Latitude hemisphere, "N" or "S"
|
517
|
+
# %lng :: Full longitude, floating point, signed
|
518
|
+
# %lngds :: Longitude degrees, integer, signed
|
519
|
+
# %lngd :: Longitude degrees, integer, unsigned
|
520
|
+
# %lngm :: Longitude minutes, integer, unsigned
|
521
|
+
# %lngs :: Longitude seconds, floating point, unsigned
|
522
|
+
# %lngh :: Longitude hemisphere, "E" or "W"
|
523
|
+
#
|
524
|
+
# Examples:
|
525
|
+
#
|
526
|
+
# g = Geo::Coord.new(50.004444, 36.231389)
|
527
|
+
# g.strfcoord('%+lat, %+lng')
|
528
|
+
# # => "+50.004444, +36.231389"
|
529
|
+
# g.strfcoord("%latd°%latm'%lath -- %lngd°%lngm'%lngh")
|
530
|
+
# # => "50°0'N -- 36°13'E"
|
531
|
+
#
|
532
|
+
def strfcoord(formatstr)
|
533
|
+
h = full_hash
|
534
|
+
|
535
|
+
DIRECTIVES.reduce(formatstr) do |memo, (from, to)|
|
536
|
+
memo.gsub(from) do
|
537
|
+
to = to.call(Regexp.last_match) if to.is_a?(Proc)
|
538
|
+
to % h
|
539
|
+
end
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Calculates distance to +other+ in SI units (meters). Vincenty
|
544
|
+
# formula is used.
|
545
|
+
#
|
546
|
+
# kharkiv = Geo::Coord.new(50.004444, 36.231389)
|
547
|
+
# kyiv = Geo::Coord.new(50.45, 30.523333)
|
548
|
+
#
|
549
|
+
# kharkiv.distance(kyiv) # => 410211.22377421556
|
550
|
+
#
|
551
|
+
def distance(other)
|
552
|
+
@globe.inverse(phi, la, other.phi, other.la).first
|
553
|
+
end
|
554
|
+
|
555
|
+
# Calculates azimuth (direction) to +other+ in degrees. Vincenty
|
556
|
+
# formula is used.
|
557
|
+
#
|
558
|
+
# kharkiv = Geo::Coord.new(50.004444, 36.231389)
|
559
|
+
# kyiv = Geo::Coord.new(50.45, 30.523333)
|
560
|
+
#
|
561
|
+
# kharkiv.azimuth(kyiv) # => 279.12614358262067
|
562
|
+
#
|
563
|
+
def azimuth(other)
|
564
|
+
rad2deg(@globe.inverse(phi, la, other.phi, other.la).last)
|
565
|
+
end
|
566
|
+
|
567
|
+
# Given distance in meters and azimuth in degrees, calculates other
|
568
|
+
# point on globe being on that direction/azimuth from current.
|
569
|
+
# Vincenty formula is used.
|
570
|
+
#
|
571
|
+
# kharkiv = Geo::Coord.new(50.004444, 36.231389)
|
572
|
+
# kharkiv.endpoint(410_211, 280)
|
573
|
+
# # => #<Geo::Coord 50.505975,30.531283>
|
574
|
+
#
|
575
|
+
def endpoint(distance, azimuth)
|
576
|
+
phi2, la2 = @globe.direct(phi, la, distance, deg2rad(azimuth))
|
577
|
+
Coord.from_rad(phi2, la2)
|
578
|
+
end
|
579
|
+
|
580
|
+
private
|
581
|
+
|
582
|
+
def _init(lat, lng)
|
583
|
+
lat = lat.to_f
|
584
|
+
lng = lng.to_f
|
585
|
+
|
586
|
+
unless (-90..90).cover?(lat)
|
587
|
+
raise ArgumentError, "Expected latitude to be between -90 and 90, #{lat} received"
|
588
|
+
end
|
589
|
+
|
590
|
+
unless (-180..180).cover?(lng)
|
591
|
+
raise ArgumentError, "Expected longitude to be between -180 and 180, #{lng} received"
|
592
|
+
end
|
593
|
+
|
594
|
+
@lat = lat
|
595
|
+
@lng = lng
|
596
|
+
end
|
597
|
+
|
598
|
+
# @private
|
599
|
+
LATH = {'N' => 1, 'S' => -1}.freeze # :nodoc:
|
600
|
+
# @private
|
601
|
+
LNGH = {'E' => 1, 'W' => -1}.freeze # :nodoc:
|
602
|
+
|
603
|
+
def _init_dms(opts) # rubocop:disable Metrics/AbcSize
|
604
|
+
lat = (
|
605
|
+
opts[:latd].to_i +
|
606
|
+
opts[:latm].to_i / 60.0 +
|
607
|
+
opts[:lats].to_i / 3600.0
|
608
|
+
) * guess_sign(opts[:lath], LATH)
|
609
|
+
lng = (
|
610
|
+
opts[:lngd].to_i +
|
611
|
+
opts[:lngm].to_i / 60.0 +
|
612
|
+
opts[:lngs].to_i / 3600.0
|
613
|
+
) * guess_sign(opts[:lngh], LNGH)
|
614
|
+
_init(lat, lng)
|
615
|
+
end
|
616
|
+
|
617
|
+
def guess_sign(h, hemishperes)
|
618
|
+
return 1 unless h
|
619
|
+
hemishperes[h] or
|
620
|
+
raise ArgumentError, "Unidentified hemisphere: #{h}"
|
621
|
+
end
|
622
|
+
|
623
|
+
def latsign
|
624
|
+
lat <=> 0
|
625
|
+
end
|
626
|
+
|
627
|
+
def lngsign
|
628
|
+
lng <=> 0
|
629
|
+
end
|
630
|
+
|
631
|
+
def latds
|
632
|
+
lat.to_i
|
633
|
+
end
|
634
|
+
|
635
|
+
def lngds
|
636
|
+
lng.to_i
|
637
|
+
end
|
638
|
+
|
639
|
+
def full_hash
|
640
|
+
{
|
641
|
+
latd: latd,
|
642
|
+
latds: latds,
|
643
|
+
latm: latm,
|
644
|
+
lats: lats,
|
645
|
+
lath: lath,
|
646
|
+
lat: lat,
|
647
|
+
|
648
|
+
lngd: lngd,
|
649
|
+
lngds: lngds,
|
650
|
+
lngm: lngm,
|
651
|
+
lngs: lngs,
|
652
|
+
lngh: lngh,
|
653
|
+
lng: lng
|
654
|
+
}
|
655
|
+
end
|
656
|
+
|
657
|
+
def rad2deg(r)
|
658
|
+
(r / Math::PI * 180 + 360) % 360
|
659
|
+
end
|
660
|
+
|
661
|
+
def deg2rad(d)
|
662
|
+
d * Math::PI / 180
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
require_relative 'coord/globes'
|