geo_coord 0.0.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.
- 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'
|