class Geo::Coord

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>

Constants

LAT_RANGE_ERROR
LNG_RANGE_ERROR
VERSION

Attributes

lat[R]

Latitude, degrees, signed float.

latitude[R]

Latitude, degrees, signed float.

lng[R]

Longitude, degrees, signed float.

lon[R]

Longitude, degrees, signed float.

longitude[R]

Longitude, degrees, signed float.

Public Class Methods

from_h(hash) click to toggle source

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>
    
# File lib/geo/coord.rb, line 127
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) click to toggle source

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>
# File lib/geo/coord.rb, line 142
def from_rad(phi, la)
  new(phi * 180 / Math::PI, la * 180 / Math::PI)
end
new(lat = nil, lng = nil, **kwargs) click to toggle source

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>
# File lib/geo/coord.rb, line 327
def initialize(lat = nil, lng = nil, **kwargs) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  @globe = Globes::Earth.instance

  # It is probably can be clearer with Ruby 2.7+ pattern-matching... or with less permissive
  # protocol :)
  kwargs = lat if lat.is_a?(Hash) && kwargs.empty? # Ruby 3.0

  case
  when lat && lng
    _init(lat, lng)
  when kwargs.key?(:lat) || kwargs.key?(:lng)
    _init(*kwargs.values_at(:lat, :lng))
  when kwargs.key?(:latd) || kwargs.key?(:lngd)
    _init_dms(**kwargs)
  else
    raise ArgumentError, "Can't create #{self.class} by provided data: (#{lat}, #{lng}, **#{kwargs}"
  end
end
parse(str) click to toggle source

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.

# File lib/geo/coord.rb, line 234
def parse(str)
  # rubocop:disable Style/RescueModifier
  parse_ll(str) rescue (parse_dms(str) rescue nil)
  # rubocop:enable Style/RescueModifier
end
parse_dms(str) click to toggle source

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.

# File lib/geo/coord.rb, line 213
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) click to toggle source

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.

# File lib/geo/coord.rb, line 195
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) click to toggle source

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”

# File lib/geo/coord.rb, line 279
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

Private Class Methods

_extract_match(match, name) click to toggle source
# File lib/geo/coord.rb, line 291
def _extract_match(match, name)
  return nil unless match[name]

  val = match[name]
  name.end_with?('h') ? val : val.to_f
end

Public Instance Methods

==(other) click to toggle source

Compares with other.

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

# File lib/geo/coord.rb, line 350
def ==(other)
  other.is_a?(self.class) && other.lat == lat && other.lng == lng
end
azimuth(other) click to toggle source

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
# File lib/geo/coord.rb, line 594
def azimuth(other)
  rad2deg(@globe.inverse(phi, la, other.phi, other.la).last)
end
distance(other) click to toggle source

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
# File lib/geo/coord.rb, line 582
def distance(other)
  @globe.inverse(phi, la, other.phi, other.la).first
end
endpoint(distance, azimuth) click to toggle source

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>
# File lib/geo/coord.rb, line 606
def endpoint(distance, azimuth)
  phi2, la2 = @globe.direct(phi, la, distance, deg2rad(azimuth))
  Coord.from_rad(phi2, la2)
end
inspect() click to toggle source

Returns a string represent coordinates object.

g.inspect  # => "#<Geo::Coord 50.004444,36.231389>"
# File lib/geo/coord.rb, line 453
def inspect
  strfcoord(%{#<#{self.class.name} %latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh>})
end
la() click to toggle source

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.

# File lib/geo/coord.rb, line 443
def la
  deg2rad(lng)
end
Also aliased as: λ
latd() click to toggle source

Returns latitude degrees (unsigned integer).

# File lib/geo/coord.rb, line 355
def latd
  lat.abs.to_i
end
latdms(hemisphere: true) click to toggle source

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(hemisphere: false)  # => [50, 0, 15.9984]

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

g.latdms                     # => [50, 0, 15.9984, "S"]
g.latdms(hemisphere: false)  # => [-50, 0, 15.9984]
# File lib/geo/coord.rb, line 409
def latdms(hemisphere: true)
  hemisphere ? [latd, latm, lats, lath] : [latsign * latd, latm, lats]
end
lath() click to toggle source

Returns latitude hemisphere (upcase letter 'N' or 'S').

# File lib/geo/coord.rb, line 370
def lath
  lat.positive? ? 'N' : 'S'
end
latlng() click to toggle source

Returns a two-element array of latitude and longitude.

g.latlng   # => [50.004444, 36.231389]
# File lib/geo/coord.rb, line 471
def latlng
  [lat, lng]
end
latm() click to toggle source

Returns latitude minutes (unsigned integer).

# File lib/geo/coord.rb, line 360
def latm
  (lat.abs * 60).to_i % 60
end
lats() click to toggle source

Returns latitude seconds (unsigned float).

# File lib/geo/coord.rb, line 365
def lats
  (lat.abs * 3600) % 60
end
lngd() click to toggle source

Returns longitude degrees (unsigned integer).

# File lib/geo/coord.rb, line 375
def lngd
  lng.abs.to_i
end
lngdms(hemisphere: true) click to toggle source

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(hemisphere: false)  # => [36, 13, 53.0004]

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

g.lngdms                     # => [36, 13, 53.0004, "E"]
g.lngdms(hemisphere: false)  # => [-36, 13, 53.0004]
# File lib/geo/coord.rb, line 428
def lngdms(hemisphere: true)
  hemisphere ? [lngd, lngm, lngs, lngh] : [lngsign * lngd, lngm, lngs]
end
lngh() click to toggle source

Returns longitude hemisphere (upcase letter 'E' or 'W').

# File lib/geo/coord.rb, line 390
def lngh
  lng.positive? ? 'E' : 'W'
end
lnglat() click to toggle source

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

g.lnglat   # => [36.231389, 50.004444]
# File lib/geo/coord.rb, line 479
def lnglat
  [lng, lat]
end
lngm() click to toggle source

Returns longitude minutes (unsigned integer).

# File lib/geo/coord.rb, line 380
def lngm
  (lng.abs * 60).to_i % 60
end
lngs() click to toggle source

Returns longitude seconds (unsigned float).

# File lib/geo/coord.rb, line 385
def lngs
  (lng.abs * 3600) % 60
end
phi() click to toggle source

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

# File lib/geo/coord.rb, line 434
def phi
  deg2rad(lat)
end
Also aliased as: φ
strfcoord(formatstr) click to toggle source

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"
# File lib/geo/coord.rb, line 560
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) click to toggle source

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}
# File lib/geo/coord.rb, line 491
def to_h(lat: :lat, lng: :lng)
  {lat => self.lat, lng => self.lng}
end
to_s(dms: true) click to toggle source

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"
# File lib/geo/coord.rb, line 462
def to_s(dms: true)
  format = dms ? %{%latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh} : '%lat,%lng'
  strfcoord(format)
end
λ()
Alias for: la
φ()
Alias for: phi

Private Instance Methods

_init(lat, lng) click to toggle source
# File lib/geo/coord.rb, line 616
def _init(lat, lng)
  lat = BigDecimal(lat.to_f, 10)
  lng = BigDecimal(lng.to_f, 10)

  raise ArgumentError, LAT_RANGE_ERROR % lat unless (-90..90).cover?(lat)
  raise ArgumentError, LNG_RANGE_ERROR % lng unless (-180..180).cover?(lng)

  @lat = lat
  @lng = lng
end
_init_dms(latd: 0, latm: 0, lats: 0, lath: nil, lngd: 0, lngm: 0, lngs: 0, lngh: nil) click to toggle source
# File lib/geo/coord.rb, line 632
def _init_dms(latd: 0, latm: 0, lats: 0, lath: nil, lngd: 0, lngm: 0, lngs: 0, lngh: nil) # rubocop:disable Metrics/AbcSize
  lat = (latd.to_d + latm.to_d / 60 + lats.to_d / 3600) * guess_sign(lath, LATH)
  lng = (lngd.to_d + lngm.to_d / 60 + lngs.to_d / 3600) * guess_sign(lngh, LNGH)

  _init(lat, lng)
end
deg2rad(d) click to toggle source
# File lib/geo/coord.rb, line 692
def deg2rad(d)
  d * Math::PI / 180
end
full_hash() click to toggle source
# File lib/geo/coord.rb, line 670
def full_hash
  {
    latd: latd,
    latds: latds,
    latm: latm,
    lats: lats,
    lath: lath,
    lat: lat,

    lngd: lngd,
    lngds: lngds,
    lngm: lngm,
    lngs: lngs,
    lngh: lngh,
    lng: lng
  }
end
guard_seconds(pattern, result) click to toggle source
# File lib/geo/coord.rb, line 646
def guard_seconds(pattern, result)
  m = pattern.match(/<(lat|lng)s>/)
  return result unless m && result.start_with?('60')

  carry = "#{m[1]}m".to_sym
  [pattern % {lats: 0, lngs: 0}, carry]
end
guess_sign(h, hemishperes) click to toggle source
# File lib/geo/coord.rb, line 639
def guess_sign(h, hemishperes)
  return 1 unless h

  hemishperes[h] or
    raise ArgumentError, "Unidentified hemisphere: #{h}"
end
latds() click to toggle source
# File lib/geo/coord.rb, line 662
def latds
  lat.to_i
end
latsign() click to toggle source
# File lib/geo/coord.rb, line 654
def latsign
  lat <=> 0
end
lngds() click to toggle source
# File lib/geo/coord.rb, line 666
def lngds
  lng.to_i
end
lngsign() click to toggle source
# File lib/geo/coord.rb, line 658
def lngsign
  lng <=> 0
end
rad2deg(r) click to toggle source
# File lib/geo/coord.rb, line 688
def rad2deg(r)
  (r / Math::PI * 180 + 360) % 360
end