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.1.0'.freeze
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, **opts) ⇒ 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(nohemisphere = false) ⇒ 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(nohemisphere = false) ⇒ 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, **opts) ⇒ 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>
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
#lat ⇒ Object (readonly) Also known as: latitude
Latitude, degrees, signed float.
97 98 99 |
# File 'lib/geo/coord.rb', line 97 def lat @lat end |
#lng ⇒ Object (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:
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
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.
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
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”
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
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
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
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 |
#inspect ⇒ Object
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 |
#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.
438 439 440 |
# File 'lib/geo/coord.rb', line 438 def la deg2rad(lng) end |
#latd ⇒ Object
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 |
#lath ⇒ Object
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 |
#latlng ⇒ Object
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 |
#latm ⇒ Object
Returns latitude minutes (unsigned integer).
355 356 357 |
# File 'lib/geo/coord.rb', line 355 def latm (lat.abs * 60).to_i % 60 end |
#lats ⇒ Object
Returns latitude seconds (unsigned float).
360 361 362 |
# File 'lib/geo/coord.rb', line 360 def lats (lat.abs * 3600) % 60 end |
#lngd ⇒ Object
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 |
#lngh ⇒ Object
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 |
#lnglat ⇒ Object
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 |
#lngm ⇒ Object
Returns longitude minutes (unsigned integer).
375 376 377 |
# File 'lib/geo/coord.rb', line 375 def lngm (lng.abs * 60).to_i % 60 end |
#lngs ⇒ Object
Returns longitude seconds (unsigned float).
380 381 382 |
# File 'lib/geo/coord.rb', line 380 def lngs (lng.abs * 3600) % 60 end |
#phi ⇒ Object 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 |