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
strinto Coord with providedpattern.
Instance Method Summary collapse
-
#==(other) ⇒ Object
Compares with
other. -
#azimuth(other) ⇒ Object
Calculates azimuth (direction) to
otherin degrees. -
#distance(other) ⇒ Object
Calculates distance to
otherin 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)withlatandlngbeing 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:
-
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>
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
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>
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
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.
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
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
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
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
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
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>
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 |