Class: Geo::Coord
- Inherits:
-
Object
- Object
- Geo::Coord
- 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.2.0'
Instance Attribute Summary collapse
-
#lat ⇒ Object
(also: #latitude)
readonly
Latitude, degrees, signed float.
-
#lng ⇒ Object
(also: #longitude, #lon)
readonly
Longitude, degrees, signed float.
Class Method Summary collapse
-
.from_h(hash) ⇒ Object
Creates Coord from hash, containing latitude and longitude.
-
.from_rad(phi, la) ⇒ Object
Creates Coord from φ and λ (latitude and longitude in radians).
-
.parse(str) ⇒ Object
Tries its best to parse Coord from string containing it (in any known form).
-
.parse_dms(str) ⇒ Object
Parses Coord from string containing latitude and longitude in degrees-minutes-seconds-hemisphere format.
-
.parse_ll(str) ⇒ Object
Parses Coord from string containing float latitude and longitude.
-
.strpcoord(str, pattern) ⇒ Object
Parses
str
into Coord with providedpattern
.
Instance Method Summary collapse
-
#==(other) ⇒ Object
Compares with
other
. -
#azimuth(other) ⇒ Object
Calculates azimuth (direction) to
other
in degrees. -
#distance(other) ⇒ Object
Calculates distance to
other
in SI units (meters). -
#endpoint(distance, azimuth) ⇒ Object
Given distance in meters and azimuth in degrees, calculates other point on globe being on that direction/azimuth from current.
-
#initialize(lat = nil, lng = nil, **kwargs) ⇒ Coord
constructor
Creates Coord object.
-
#inspect ⇒ Object
Returns a string represent coordinates object.
-
#la ⇒ Object
(also: #λ)
Latitude in radians.
-
#latd ⇒ Object
Returns latitude degrees (unsigned integer).
-
#latdms(hemisphere: true) ⇒ Object
Returns latitude components: degrees, minutes, seconds and optionally a hemisphere:.
-
#lath ⇒ Object
Returns latitude hemisphere (upcase letter ‘N’ or ‘S’).
-
#latlng ⇒ Object
Returns a two-element array of latitude and longitude.
-
#latm ⇒ Object
Returns latitude minutes (unsigned integer).
-
#lats ⇒ Object
Returns latitude seconds (unsigned float).
-
#lngd ⇒ Object
Returns longitude degrees (unsigned integer).
-
#lngdms(hemisphere: true) ⇒ Object
Returns longitude components: degrees, minutes, seconds and optionally a hemisphere:.
-
#lngh ⇒ Object
Returns longitude hemisphere (upcase letter ‘E’ or ‘W’).
-
#lnglat ⇒ Object
Returns a two-element array of longitude and latitude (reverse order to
latlng
). -
#lngm ⇒ Object
Returns longitude minutes (unsigned integer).
-
#lngs ⇒ Object
Returns longitude seconds (unsigned float).
-
#phi ⇒ Object
(also: #φ)
Latitude in radians.
-
#strfcoord(formatstr) ⇒ Object
Formats coordinates according to directives in
formatstr
. -
#to_h(lat: :lat, lng: :lng) ⇒ Object
Returns hash of latitude and longitude.
-
#to_s(dms: true) ⇒ Object
Returns a string representing coordinates.
Constructor Details
#initialize(lat = nil, lng = nil, **kwargs) ⇒ Coord
Creates Coord object.
There are three forms of usage:
-
Coord.new(lat, lng)
withlat
andlng
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>
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# 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 |
Instance Attribute Details
#lat ⇒ Object (readonly) Also known as: latitude
Latitude, degrees, signed float.
100 101 102 |
# File 'lib/geo/coord.rb', line 100 def lat @lat end |
#lng ⇒ Object (readonly) Also known as: longitude, lon
Longitude, degrees, signed float.
103 104 105 |
# File 'lib/geo/coord.rb', line 103 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:
127 128 129 130 131 132 133 134 135 |
# 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) ⇒ Object
142 143 144 |
# File 'lib/geo/coord.rb', line 142 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.
234 235 236 237 238 |
# 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) ⇒ 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.
213 214 215 216 217 218 219 220 221 |
# 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) ⇒ Object
195 196 197 198 199 200 |
# 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) ⇒ 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”
279 280 281 282 283 284 285 286 287 |
# 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 |
Instance Method Details
#==(other) ⇒ Object
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.
350 351 352 |
# File 'lib/geo/coord.rb', line 350 def ==(other) other.is_a?(self.class) && other.lat == lat && other.lng == lng end |
#azimuth(other) ⇒ Object
594 595 596 |
# File 'lib/geo/coord.rb', line 594 def azimuth(other) rad2deg(@globe.inverse(phi, la, other.phi, other.la).last) end |
#distance(other) ⇒ Object
582 583 584 |
# File 'lib/geo/coord.rb', line 582 def distance(other) @globe.inverse(phi, la, other.phi, other.la).first end |
#endpoint(distance, azimuth) ⇒ Object
606 607 608 609 |
# 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 ⇒ Object
Returns a string represent coordinates object.
g.inspect # => "#<Geo::Coord 50.004444,36.231389>"
453 454 455 |
# File 'lib/geo/coord.rb', line 453 def inspect strfcoord(%{#<#{self.class.name} %latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh>}) end |
#la ⇒ Object 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.
443 444 445 |
# File 'lib/geo/coord.rb', line 443 def la deg2rad(lng) end |
#latd ⇒ Object
Returns latitude degrees (unsigned integer).
355 356 357 |
# File 'lib/geo/coord.rb', line 355 def latd lat.abs.to_i end |
#latdms(hemisphere: true) ⇒ 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(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]
409 410 411 |
# File 'lib/geo/coord.rb', line 409 def latdms(hemisphere: true) hemisphere ? [latd, latm, lats, lath] : [latsign * latd, latm, lats] end |
#lath ⇒ Object
Returns latitude hemisphere (upcase letter ‘N’ or ‘S’).
370 371 372 |
# File 'lib/geo/coord.rb', line 370 def lath lat.positive? ? 'N' : 'S' end |
#latlng ⇒ Object
Returns a two-element array of latitude and longitude.
g.latlng # => [50.004444, 36.231389]
471 472 473 |
# File 'lib/geo/coord.rb', line 471 def latlng [lat, lng] end |
#latm ⇒ Object
Returns latitude minutes (unsigned integer).
360 361 362 |
# File 'lib/geo/coord.rb', line 360 def latm (lat.abs * 60).to_i % 60 end |
#lats ⇒ Object
Returns latitude seconds (unsigned float).
365 366 367 |
# File 'lib/geo/coord.rb', line 365 def lats (lat.abs * 3600) % 60 end |
#lngd ⇒ Object
Returns longitude degrees (unsigned integer).
375 376 377 |
# File 'lib/geo/coord.rb', line 375 def lngd lng.abs.to_i end |
#lngdms(hemisphere: true) ⇒ 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(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]
428 429 430 |
# File 'lib/geo/coord.rb', line 428 def lngdms(hemisphere: true) hemisphere ? [lngd, lngm, lngs, lngh] : [lngsign * lngd, lngm, lngs] end |
#lngh ⇒ Object
Returns longitude hemisphere (upcase letter ‘E’ or ‘W’).
390 391 392 |
# File 'lib/geo/coord.rb', line 390 def lngh lng.positive? ? 'E' : 'W' end |
#lnglat ⇒ Object
Returns a two-element array of longitude and latitude (reverse order to latlng
).
g.lnglat # => [36.231389, 50.004444]
479 480 481 |
# File 'lib/geo/coord.rb', line 479 def lnglat [lng, lat] end |
#lngm ⇒ Object
Returns longitude minutes (unsigned integer).
380 381 382 |
# File 'lib/geo/coord.rb', line 380 def lngm (lng.abs * 60).to_i % 60 end |
#lngs ⇒ Object
Returns longitude seconds (unsigned float).
385 386 387 |
# File 'lib/geo/coord.rb', line 385 def lngs (lng.abs * 3600) % 60 end |
#phi ⇒ Object Also known as: φ
Latitude in radians. Geodesy formulae almost alwayse use greek Phi for it.
434 435 436 |
# File 'lib/geo/coord.rb', line 434 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"
560 561 562 563 564 565 566 567 568 569 570 571 572 |
# 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) ⇒ 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}
491 492 493 |
# 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) ⇒ 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"
462 463 464 465 |
# 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 |