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