Class: Geo::Coord

Inherits:
Object
  • Object
show all
Defined in:
lib/geo/coord.rb,
lib/geo/coord/version.rb

Overview

Geo::Coord is main class of Geo module, representing (latitude, longitude) pair. It stores coordinates in floating-point degrees form, provides access to coordinate components, allows complex formatting and parsing of coordinate pairs and performs geodesy calculations in standard WGS-84 coordinate reference system.

Examples of usage

Creation:

# From lat/lng pair:
g = Geo::Coord.new(50.004444, 36.231389)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Or using keyword arguments form:
g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Keyword arguments also allow creation of Coord from components:
g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

For parsing API responses you’d like to use from_h, which accepts String and Symbol keys, any letter case, and knows synonyms (lng/lon/longitude):

g = Geo::Coord.from_h('LAT' => 50.004444, 'LON' => 36.231389)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

For math, you’d probably like to be able to initialize Coord with radians rather than degrees:

g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

There’s also family of parsing methods, with different applicability:

# Tries to parse (lat, lng) pair:
g = Geo::Coord.parse_ll('50.004444, 36.231389')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Tries to parse degrees/minutes/seconds:
g = Geo::Coord.parse_dms('50° 0′ 16″ N, 36° 13′ 53″ E')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Tries to do best guess:
g = Geo::Coord.parse('50.004444, 36.231389')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>
g = Geo::Coord.parse('50° 0′ 16″ N, 36° 13′ 53″ E')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Allows user to provide pattern:
g = Geo::Coord.strpcoord('50.004444, 36.231389', '%lat, %lng')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

Having Coord object, you can get its properties:

g = Geo::Coord.new(50.004444, 36.231389)
g.lat # => 50.004444
g.latd # => 50 -- latitude degrees
g.lath # => N -- latitude hemisphere
g.lngh # => E -- longitude hemishpere
g.phi  # => 0.8727421884291233 -- longitude in radians
g.latdms # => [50, 0, 15.998400000011316, "N"]
# ...and so on

Format and convert it:

g.to_s # => "50.004444,36.231389"
g.strfcoord('%latd°%latm′%lats″%lath %lngd°%lngm′%lngs″%lngh')
# => "50°0′16″N 36°13′53″E"

g.to_h(lat: 'LAT', lng: 'LON') # => {'LAT'=>50.004444, 'LON'=>36.231389}

Do simple geodesy math:

kharkiv = Geo::Coord.new(50.004444, 36.231389)
kyiv = Geo::Coord.new(50.45, 30.523333)

kharkiv.distance(kyiv) # => 410211.22377421556
kharkiv.azimuth(kyiv) # => 279.12614358262067
kharkiv.endpoint(410_211, 280) # => #<Geo::Coord 50.505975,30.531283>

Constant Summary collapse

VERSION =
'0.1.0'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(lat = nil, lng = nil, **opts) ⇒ Coord

Creates Coord object.

There are three forms of usage:

  • Coord.new(lat, lng) with lat and lng being floats;

  • Coord.new(lat: lat, lng: lng) – same as above, but with keyword arguments;

  • Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E') – for cases when you have coordinates components already parsed;

In keyword arguments form, any argument can be omitted and will be replaced with 0. But you can’t mix, for example, “whole” latitude key lat and partial longitude keys lngd, lngm and so on.

g = Geo::Coord.new(50.004444, 36.231389)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Or using keyword arguments form:
g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Keyword arguments also allow creation of Coord from components:
g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

# Providing defaults:
g = Geo::Coord.new(lat: 50.004444)
# => #<Geo::Coord 50°0'16"N 0°0'0"W>


322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/geo/coord.rb', line 322

def initialize(lat = nil, lng = nil, **opts)
  @globe = Globes::Earth.instance

  case
  when lat && lng
    _init(lat, lng)
  when opts.key?(:lat) || opts.key?(:lng)
    _init(opts[:lat], opts[:lng])
  when opts.key?(:latd) || opts.key?(:lngd)
    _init_dms(opts)
  else
    raise ArgumentError, "Can't create #{self.class} by provided data"
  end
end

Instance Attribute Details

#latObject (readonly) Also known as: latitude

Latitude, degrees, signed float.



97
98
99
# File 'lib/geo/coord.rb', line 97

def lat
  @lat
end

#lngObject (readonly) Also known as: longitude, lon

Longitude, degrees, signed float.



100
101
102
# File 'lib/geo/coord.rb', line 100

def lng
  @lng
end

Class Method Details

.from_h(hash) ⇒ Object

Creates Coord from hash, containing latitude and longitude.

This methos designed as a way for parsing responses from APIs and databases, so, it tries to be pretty liberal on its input:

  • accepts String or Symbol keys;

  • accepts any letter case;

  • accepts several synonyms for latitude (“lat” and “latitude”) and longitude (“lng”, “lon”, “long”, “longitude”).

    g = Geo::Coord.from_h('LAT' => 50.004444, longitude: 36.231389)
    # => #<Geo::Coord 50°0'16"N 36°13'53"E>
    


124
125
126
127
128
129
130
131
132
# File 'lib/geo/coord.rb', line 124

def from_h(hash)
  h = hash.map { |k, v| [k.to_s.downcase.to_sym, v] }.to_h
  lat = h.values_at(*LAT_KEYS).compact.first or
    raise(ArgumentError, "No latitude value found in #{hash}")
  lng = h.values_at(*LNG_KEYS).compact.first or
    raise(ArgumentError, "No longitude value found in #{hash}")

  new(lat, lng)
end

.from_rad(phi, la) ⇒ Object

Creates Coord from φ and λ (latitude and longitude in radians).

g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
# => #<Geo::Coord 50°0'16"N 36°13'53"E>


139
140
141
# File 'lib/geo/coord.rb', line 139

def from_rad(phi, la)
  new(phi * 180 / Math::PI, la * 180 / Math::PI)
end

.parse(str) ⇒ Object

Tries its best to parse Coord from string containing it (in any known form).

Geo::Coord.parse('-50.004444 +36.231389')
# => #<Geo::Coord 50°0'16"S 36°13'53"E>
Geo::Coord.parse('50°0′16″N 36°13′53″E')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

If you know exact form in which coordinates are provided, it may be wider to consider parse_ll, parse_dms or even ::strpcoord.



231
232
233
234
235
# File 'lib/geo/coord.rb', line 231

def parse(str)
  # rubocop:disable Style/RescueModifier
  parse_ll(str) rescue (parse_dms(str) rescue nil)
  # rubocop:enable Style/RescueModifier
end

.parse_dms(str) ⇒ Object

Parses Coord from string containing latitude and longitude in degrees-minutes-seconds-hemisphere format. Understands several types of separators, degree, minute, second signs, as well as explicit hemisphere and no-hemisphere (signed degrees) formats.

Geo::Coord.parse_dms('50°0′16″N 36°13′53″E')
# => #<Geo::Coord 50°0'16"N 36°13'53"E>

If parse_dms is not wise enough to understand your data, consider using ::strpcoord.

Raises:

  • (ArgumentError)


210
211
212
213
214
215
216
217
218
# File 'lib/geo/coord.rb', line 210

def parse_dms(str)
  str.match(DMS_PATTERN) do |m|
    return new(
      latd: m[:latd], latm: m[:latm], lats: m[:lats], lath: m[:lath],
      lngd: m[:lngd], lngm: m[:lngm], lngs: m[:lngs], lngh: m[:lngh]
    )
  end
  raise ArgumentError, "Can't parse #{str} as degrees-minutes-seconds"
end

.parse_ll(str) ⇒ Object

Parses Coord from string containing float latitude and longitude. Understands several types of separators/spaces between values.

Geo::Coord.parse_ll('-50.004444 +36.231389')
# => #<Geo::Coord 50°0'16"S 36°13'53"E>

If parse_ll is not wise enough to understand your data, consider using ::strpcoord.

Raises:

  • (ArgumentError)


192
193
194
195
196
197
# File 'lib/geo/coord.rb', line 192

def parse_ll(str)
  str.match(LL_PATTERN) do |m|
    return new(m[1].to_f, m[2].to_f)
  end
  raise ArgumentError, "Can't parse #{str} as lat, lng"
end

.strpcoord(str, pattern) ⇒ Object

Parses str into Coord with provided pattern.

Example:

Geo::Coord.strpcoord('-50.004444/+36.231389', '%lat/%lng')
# => #<Geo::Coord -50.004444,36.231389>

List of parsing flags:

%lat

Full latitude, float

%latd

Latitude degrees, integer, may be signed (instead of providing hemisphere info

%latm

Latitude minutes, integer, unsigned

%lats

Latitude seconds, float, unsigned

%lath

Latitude hemisphere, “N” or “S”

%lng

Full longitude, float

%lngd

Longitude degrees, integer, may be signed (instead of providing hemisphere info

%lngm

Longitude minutes, integer, unsigned

%lngs

Longitude seconds, float, unsigned

%lngh

Longitude hemisphere, “N” or “S”

Raises:

  • (ArgumentError)


276
277
278
279
280
281
282
283
# File 'lib/geo/coord.rb', line 276

def strpcoord(str, pattern)
  pattern = PARSE_PATTERNS.inject(pattern) do |memo, (pfrom, pto)|
    memo.gsub(pfrom, pto)
  end
  match = Regexp.new('^' + pattern).match(str)
  raise ArgumentError, "Coordinates str #{str} can't be parsed by pattern #{pattern}" unless match
  new(match.names.map { |n| [n.to_sym, _extract_match(match, n)] }.to_h)
end

Instance Method Details

#==(other) ⇒ Object

Compares with other.

Note, that comparison includes comparing floating point values, so, when two “almost exactly same” coord pairs are calculated using different methods, you can rarely expect them to be exactly equal.

Also, note that no greater/lower relation is defined on Coord, so, for example, you can’t just sort an array of Coord.



345
346
347
# File 'lib/geo/coord.rb', line 345

def ==(other)
  other.is_a?(self.class) && other.lat == lat && other.lng == lng
end

#azimuth(other) ⇒ Object

Calculates azimuth (direction) to other in degrees. Vincenty formula is used.

kharkiv = Geo::Coord.new(50.004444, 36.231389)
kyiv = Geo::Coord.new(50.45, 30.523333)

kharkiv.azimuth(kyiv) # => 279.12614358262067


589
590
591
# File 'lib/geo/coord.rb', line 589

def azimuth(other)
  rad2deg(@globe.inverse(phi, la, other.phi, other.la).last)
end

#distance(other) ⇒ Object

Calculates distance to other in SI units (meters). Vincenty formula is used.

kharkiv = Geo::Coord.new(50.004444, 36.231389)
kyiv = Geo::Coord.new(50.45, 30.523333)

kharkiv.distance(kyiv) # => 410211.22377421556


577
578
579
# File 'lib/geo/coord.rb', line 577

def distance(other)
  @globe.inverse(phi, la, other.phi, other.la).first
end

#endpoint(distance, azimuth) ⇒ Object

Given distance in meters and azimuth in degrees, calculates other point on globe being on that direction/azimuth from current. Vincenty formula is used.

kharkiv = Geo::Coord.new(50.004444, 36.231389)
kharkiv.endpoint(410_211, 280)
# => #<Geo::Coord 50°30'22"N 30°31'53"E>


601
602
603
604
# File 'lib/geo/coord.rb', line 601

def endpoint(distance, azimuth)
  phi2, la2 = @globe.direct(phi, la, distance, deg2rad(azimuth))
  Coord.from_rad(phi2, la2)
end

#inspectObject

Returns a string represent coordinates object.

g.inspect  # => "#<Geo::Coord 50.004444,36.231389>"


448
449
450
# File 'lib/geo/coord.rb', line 448

def inspect
  strfcoord(%{#<#{self.class.name} %latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh>})
end

#laObject Also known as: λ

Latitude in radians. Geodesy formulae almost alwayse use greek Lambda for it; we are using shorter name for not confuse with Ruby’s lambda keyword.



438
439
440
# File 'lib/geo/coord.rb', line 438

def la
  deg2rad(lng)
end

#latdObject

Returns latitude degrees (unsigned integer).



350
351
352
# File 'lib/geo/coord.rb', line 350

def latd
  lat.abs.to_i
end

#latdms(nohemisphere = false) ⇒ Object

Returns latitude components: degrees, minutes, seconds and optionally a hemisphere:

# Nothern hemisphere:
g = Geo::Coord.new(50.004444, 36.231389)

g.latdms        # => [50, 0, 15.9984, "N"]
g.latdms(true)  # => [50, 0, 15.9984]

# Southern hemisphere:
g = Geo::Coord.new(-50.004444, 36.231389)

g.latdms        # => [50, 0, 15.9984, "S"]
g.latdms(true)  # => [-50, 0, 15.9984]


404
405
406
# File 'lib/geo/coord.rb', line 404

def latdms(nohemisphere = false)
  nohemisphere ? [latsign * latd, latm, lats] : [latd, latm, lats, lath]
end

#lathObject

Returns latitude hemisphere (upcase letter ‘N’ or ‘S’).



365
366
367
# File 'lib/geo/coord.rb', line 365

def lath
  lat > 0 ? 'N' : 'S'
end

#latlngObject

Returns a two-element array of latitude and longitude.

g.latlng   # => [50.004444, 36.231389]


466
467
468
# File 'lib/geo/coord.rb', line 466

def latlng
  [lat, lng]
end

#latmObject

Returns latitude minutes (unsigned integer).



355
356
357
# File 'lib/geo/coord.rb', line 355

def latm
  (lat.abs * 60).to_i % 60
end

#latsObject

Returns latitude seconds (unsigned float).



360
361
362
# File 'lib/geo/coord.rb', line 360

def lats
  (lat.abs * 3600) % 60
end

#lngdObject

Returns longitude degrees (unsigned integer).



370
371
372
# File 'lib/geo/coord.rb', line 370

def lngd
  lng.abs.to_i
end

#lngdms(nohemisphere = false) ⇒ Object

Returns longitude components: degrees, minutes, seconds and optionally a hemisphere:

# Eastern hemisphere:
g = Geo::Coord.new(50.004444, 36.231389)

g.lngdms        # => [36, 13, 53.0004, "E"]
g.lngdms(true)  # => [36, 13, 53.0004]

# Western hemisphere:
g = Geo::Coord.new(50.004444, 36.231389)

g.lngdms        # => [36, 13, 53.0004, "E"]
g.lngdms(true)  # => [-36, 13, 53.0004]


423
424
425
# File 'lib/geo/coord.rb', line 423

def lngdms(nohemisphere = false)
  nohemisphere ? [lngsign * lngd, lngm, lngs] : [lngd, lngm, lngs, lngh]
end

#lnghObject

Returns longitude hemisphere (upcase letter ‘E’ or ‘W’).



385
386
387
# File 'lib/geo/coord.rb', line 385

def lngh
  lng > 0 ? 'E' : 'W'
end

#lnglatObject

Returns a two-element array of longitude and latitude (reverse order to latlng).

g.lnglat   # => [36.231389, 50.004444]


474
475
476
# File 'lib/geo/coord.rb', line 474

def lnglat
  [lng, lat]
end

#lngmObject

Returns longitude minutes (unsigned integer).



375
376
377
# File 'lib/geo/coord.rb', line 375

def lngm
  (lng.abs * 60).to_i % 60
end

#lngsObject

Returns longitude seconds (unsigned float).



380
381
382
# File 'lib/geo/coord.rb', line 380

def lngs
  (lng.abs * 3600) % 60
end

#phiObject Also known as: φ

Latitude in radians. Geodesy formulae almost alwayse use greek Phi for it.



429
430
431
# File 'lib/geo/coord.rb', line 429

def phi
  deg2rad(lat)
end

#strfcoord(formatstr) ⇒ Object

Formats coordinates according to directives in formatstr.

Each directive starts with % and can contain some modifiers before its name.

Acceptable modifiers:

  • unsigned integers: none;

  • signed integers: + for mandatory sign printing;

  • floats: same as integers and number of digits modifier, like .03.

List of directives:

%lat

Full latitude, floating point, signed

%latds

Latitude degrees, integer, signed

%latd

Latitude degrees, integer, unsigned

%latm

Latitude minutes, integer, unsigned

%lats

Latitude seconds, floating point, unsigned

%lath

Latitude hemisphere, “N” or “S”

%lng

Full longitude, floating point, signed

%lngds

Longitude degrees, integer, signed

%lngd

Longitude degrees, integer, unsigned

%lngm

Longitude minutes, integer, unsigned

%lngs

Longitude seconds, floating point, unsigned

%lngh

Longitude hemisphere, “E” or “W”

Examples:

g = Geo::Coord.new(50.004444, 36.231389)
g.strfcoord('%+lat, %+lng')
# => "+50.004444, +36.231389"
g.strfcoord("%latd°%latm'%lath -- %lngd°%lngm'%lngh")
# => "50°0'N -- 36°13'E"

strfcoord handles seconds rounding implicitly:

pos = Geo::Coord.new(0.033333, 91.333333)
pos.lats # => 0.599988e2
pos.strfcoord('%latd %latm %.05lats') # => "0 1 59.99880"
pos.strfcoord('%latd %latm %lats')  # => "0 2 0"


555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/geo/coord.rb', line 555

def strfcoord(formatstr)
  h = full_hash

  DIRECTIVES.reduce(formatstr) do |memo, (from, to)|
    memo.gsub(from) do
      to = to.call(Regexp.last_match) if to.is_a?(Proc)
      res = to % h
      res, carrymin = guard_seconds(to, res)
      h[carrymin] += 1 if carrymin
      res
    end
  end
end

#to_h(lat: :lat, lng: :lng) ⇒ Object

Returns hash of latitude and longitude. You can provide your keys if you want:

g.to_h
# => {:lat=>50.004444, :lng=>36.231389}
g.to_h(lat: 'LAT', lng: 'LNG')
# => {'LAT'=>50.004444, 'LNG'=>36.231389}


486
487
488
# File 'lib/geo/coord.rb', line 486

def to_h(lat: :lat, lng: :lng)
  {lat => self.lat, lng => self.lng}
end

#to_s(dms: true) ⇒ Object

Returns a string representing coordinates.

g.to_s              # => "50°0'16\"N 36°13'53\"E"
g.to_s(dms: false)  # => "50.004444,36.231389"


457
458
459
460
# File 'lib/geo/coord.rb', line 457

def to_s(dms: true)
  format = dms ? %{%latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh} : '%lat,%lng'
  strfcoord(format)
end