geo_coord 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7e6b043870b18d1cb3c9ccbf3b7b6270a63e19fc
4
+ data.tar.gz: c9938260cf2f45f3c255e099891ddcf2ac1747d4
5
+ SHA512:
6
+ metadata.gz: 482499de585615150a74734bf23c72936bc7bd10c7b14447f7c251f7d7c79c4fa573b232d25e5d7de6468b2da0e2a11890ff36f7466ef8edb4385cffe44b6ebc
7
+ data.tar.gz: 6d7f5c2a3c52ed6315922edfe8ca7cd2b682e8c51b4a6c9dde7d46b7ec0af77168f3d95f9abc2623aac8a20d16759b128f9065b5506c0402501ddb4b7681efd0
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Victor 'Zverok' Shepelev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # Geo::Coord—simple geo coordinates class for Ruby
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/geo_coord.svg)](http://badge.fury.io/rb/geo_coord)
4
+ [![Dependency Status](https://gemnasium.com/zverok/geo_coord.svg)](https://gemnasium.com/zverok/geo_coord)
5
+ [![Build Status](https://travis-ci.org/zverok/geo_coord.svg?branch=master)](https://travis-ci.org/zverok/geo_coord)
6
+ [![Coverage Status](https://coveralls.io/repos/zverok/geo_coord/badge.svg?branch=master)](https://coveralls.io/r/zverok/geo_coord?branch=master)
7
+
8
+ `Geo::Coord` is a basic class representing `[latitude, longitude]` pair
9
+ and incapsulating related concepts and calculations.
10
+
11
+ ## Features
12
+
13
+ * Simple storage for geographical latitude & longitude pair;
14
+ * Easily converted from/to many different representations (arrays, hashes,
15
+ degrees/minutes/seconds, radians, strings with different formatting);
16
+ * Geodesy math (distances, directions, endpoints) via precise Vincenty
17
+ formula.
18
+
19
+ ## Reasons
20
+
21
+ Geo coordinates are, in fact, one of basic types in XXI century programming.
22
+
23
+ This gem is a (desperate) attempt to provide such a "basic" type ready
24
+ to be dropped into any of Ruby code, to unify all different `LatLng` or
25
+ `Point` or `Location` classes in existing geography and geo-aware gems
26
+ for easy data exchange and natural usage.
27
+
28
+ As an independent gem, this attempt is doomed by design, but why not
29
+ to try?..
30
+
31
+ Initially, I've done this work as a proposal for inclusion in Ruby's
32
+ standard library, but it was not met very well.
33
+ So, now I'm releasing it as a gem to be available at least for my own
34
+ other projects.
35
+
36
+ You can read my initial proposal [here](https://github.com/zverok/geo_coord/blob/master/StdlibProposal.md)
37
+ and discussion in Ruby tracker [there](https://bugs.ruby-lang.org/issues/12361).
38
+
39
+ I still have a small hope it would be part of stdlib once, that's why I
40
+ preserve the style of specs (outdated rspec, but compatible with mspec used
41
+ for standard library) and docs (yard in RDoc-compatibility mode).
42
+
43
+ ## Design decisions
44
+
45
+ While designing `Geo` library, my reference point was standard `Time`
46
+ class (and, to lesser extent, `Date`/`DateTime`). It has these
47
+ responsibilities:
48
+ * stores data in simple internal form;
49
+ * helps to parse and format data to and from strings;
50
+ * provides easy access to logical components of data;
51
+ * allows most simple and unambiguous calculations.
52
+
53
+ **Namespace name**: The gem takes pretty short and generic top-level
54
+ namespace name `Geo`, but creates only one class inside it: `Geo::Coord`.
55
+
56
+ **Main type name**: as far as I can see, there's no good singular name
57
+ for `(lat, lng)` pair concept. In different libraries, there can be seen
58
+ names like `LatLng`, or `Location`, or `Point`; and in natural language
59
+ just "coordinates" used frequently. I propose the name `Coord`, which
60
+ is pretty short, easy to remember, demonstrates intentions (and looks
61
+ like singular, so you can have "one coord object" and "array of coords",
62
+ which is not 100% linguistically correct, yet convenient). Alternative
63
+ `Point` name seems to be too ambiguous, being used in many contexts.
64
+
65
+ `Geo::Coord` object is **immutable**, there's no semantical sense in
66
+ `location.latitude = ...` or something like this.
67
+
68
+ **Units**: `Geo` calculations (just like `Time` calculations) provide
69
+ no units options, just returning numbers measured in "default" units:
70
+ metres for distances (as they are SI unit) and degrees for azimuth.
71
+ Latitude and longitude are stored in degrees, but radians values accessors
72
+ are provided (being widely used in geodesy math).
73
+
74
+ All coordinates and calculations are thought to be in
75
+ [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System#A_new_World_Geodetic_System:_WGS_84)
76
+ coordinates reference system, being current standard for maps and GPS.
77
+
78
+ There's introduced **a concept of globe** used internally for calculations.
79
+ Only generic (sphere) and Earth globes are implemented, but for 2016 I
80
+ feel like the current design of basic types should take in consideration
81
+ possibility of writing Ruby scripts for Mars maps analysis. Only one
82
+ geodesy formula is implemented (Vincenty, generally considered one of
83
+ the most precise), as for standard library class it considered
84
+ unnecessary to provide a user with geodesy formulae options.
85
+
86
+ No **map projection** math was added into the current gem, but it
87
+ may be a good direction for further work. No **elevation** data considered
88
+ either.
89
+
90
+ ## Installation
91
+
92
+ Now when it is a gem, just do your usual `gem install geo_coord` or add
93
+ `gem "geo_coord"` to your Gemfile.
94
+
95
+ ## Usage
96
+
97
+ ### Creation
98
+
99
+ ```ruby
100
+ # From lat/lng pair:
101
+ g = Geo::Coord.new(50.004444, 36.231389)
102
+ # => #<Geo::Coord 50.004444,36.231389>
103
+
104
+ # Or using keyword arguments form:
105
+ g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
106
+ # => #<Geo::Coord 50.004444,36.231389>
107
+
108
+ # Keyword arguments also allow creation of Coord from components:
109
+ g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
110
+ # => #<Geo::Coord 50.004444,36.231389>
111
+ ```
112
+
113
+ For parsing API responses you'd like to use `from_h`,
114
+ which accepts String and Symbol keys, any letter case,
115
+ and knows synonyms (lng/lon/longitude):
116
+
117
+ ```ruby
118
+ g = Geo::Coord.from_h('LAT' => 50.004444, 'LON' => 36.231389)
119
+ # => #<Geo::Coord 50.004444,36.231389>
120
+ ```
121
+
122
+ For math, you'd probably like to be able to initialize
123
+ Coord with radians rather than degrees:
124
+
125
+ ```ruby
126
+ g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
127
+ # => #<Geo::Coord 50.004444,36.231389>
128
+ ```
129
+
130
+ There's also family of string parsing methods, with different
131
+ applicability:
132
+
133
+ ```ruby
134
+ # Tries to parse (lat, lng) pair:
135
+ g = Geo::Coord.parse_ll('50.004444, 36.231389')
136
+ # => #<Geo::Coord 50.004444,36.231389>
137
+
138
+ # Tries to parse degrees/minutes/seconds:
139
+ g = Geo::Coord.parse_dms('50° 0′ 16″ N, 36° 13′ 53″ E')
140
+ # => #<Geo::Coord 50.004444,36.231389>
141
+
142
+ # Tries to do best guess:
143
+ g = Geo::Coord.parse('50.004444, 36.231389')
144
+ # => #<Geo::Coord 50.004444,36.231389>
145
+ g = Geo::Coord.parse('50° 0′ 16″ N, 36° 13′ 53″ E')
146
+ # => #<Geo::Coord 50.004444,36.231389>
147
+
148
+ # Allows user to provide pattern:
149
+ g = Geo::Coord.strpcoord('50.004444, 36.231389', '%lat, %lng')
150
+ # => #<Geo::Coord 50.004444,36.231389>
151
+ ```
152
+
153
+ [Pattern language description](http://www.rubydoc.info/gems/geo_coord/Geo/Coord#strpcoord-class_method)
154
+
155
+ ### Examining the object
156
+
157
+ Having Coord object, you can get its properties:
158
+
159
+ ```ruby
160
+ g = Geo::Coord.new(50.004444, 36.231389)
161
+ g.lat # => 50.004444
162
+ g.latd # => 50 -- latitude degrees
163
+ g.lath # => N -- latitude hemisphere
164
+ g.lngh # => E -- longitude hemishpere
165
+ g.phi # => 0.8727421884291233 -- longitude in radians
166
+ g.latdms # => [50, 0, 15.998400000011316, "N"]
167
+ # ...and so on
168
+ ```
169
+
170
+ ### Formatting and converting
171
+
172
+ ```ruby
173
+ g.to_s # => "50.004444,36.231389"
174
+ g.strfcoord('%latd°%latm′%lats″%lath %lngd°%lngm′%lngs″%lngh')
175
+ # => "50°0′16″N 36°13′53″E"
176
+
177
+ g.to_h(lat: 'LAT', lng: 'LON') # => {'LAT'=>50.004444, 'LON'=>36.231389}
178
+ ```
179
+
180
+ ### Geodesy math
181
+
182
+ ```ruby
183
+ kharkiv = Geo::Coord.new(50.004444, 36.231389)
184
+ kyiv = Geo::Coord.new(50.45, 30.523333)
185
+
186
+ kharkiv.distance(kyiv) # => 410211.22377421556
187
+ kharkiv.azimuth(kyiv) # => 279.12614358262067
188
+ kharkiv.endpoint(410_211, 280) # => #<Geo::Coord 50.505975,30.531283>
189
+ ```
190
+
191
+ [Full API Docs](http://www.rubydoc.info/gems/geo_coord)
192
+
193
+ ## Author
194
+
195
+ [Victor Shepelev](https://zverok.github.io)
196
+
197
+ ## License
198
+
199
+ [MIT](https://github.com/zverok/geo_coord/blob/master/LICENSE.txt).
data/StdlibProposal.md ADDED
@@ -0,0 +1,175 @@
1
+ ## Proposal
2
+
3
+ Add `Geo::Coord` class to Ruby standard library, representing
4
+ `[latitude, longitude]` pair + convenience methods. Add `Geo` standard
5
+ library with additional calculations and convenience methods.
6
+
7
+ ## Rationale
8
+
9
+ In modern applications, working with geographical coordinates is frequent.
10
+ We propose to think of such coordinates (namely, `latitude, longitude` pair)
11
+ as of "basic" type that should be supported by standard library - the same
12
+ way as we support `Time`/`Date`/`DateTime` instead of having it defined
13
+ by user/gems.
14
+
15
+ This type is too "small" to be defined by separate gem, so, all of existing
16
+ geo gems (GeoKit, RGeo, GeoRuby, Graticule etc.) define their own
17
+ `LatLng`, or `Location`, or `Point`, whatever.
18
+
19
+ On other hand, API design for this "small" type is vague enough for all
20
+ those similar types to be incompatible and break small habits and conventions
21
+ when you change from one geo library to another, or try to use several
22
+ simultaneously.
23
+
24
+ Additionaly, many gems somehow working with geo coordinates (for weather,
25
+ or timezone, or another tasks) generally prefer not to introduce type, and
26
+ just work with `[lat, lng]` array, which is not very convenient, faithfully.
27
+
28
+ So, having "geo coordinates" functionality in standard library seems
29
+ reasonable and valuable.
30
+
31
+ ## Existing/reference solutions
32
+
33
+ Ruby:
34
+
35
+ * [GeoKit::LatLng](http://www.rubydoc.info/github/geokit/geokit/master/Geokit/LatLng);
36
+ * [RGeo::Feature::Point](http://www.rubydoc.info/gems/rgeo/RGeo/Feature/Point)
37
+ (with several "private" implementation classes); RGeo implements full
38
+ [OGC Simple Features](https://en.wikipedia.org/wiki/Simple_Features) specification,
39
+ so, its points have `z` and `m` coordinates, projection and much more;
40
+ * [Graticule::Location](http://www.rubydoc.info/github/collectiveidea/graticule/Graticule/Location)
41
+ (not strictly a `[lat,lng]` wrapper);
42
+ * [Rosamary::Node](http://www.rubydoc.info/gems/rosemary/0.4.4/Rosemary/Node)
43
+ (follows naming convention of underlying OpenStreetMap API);
44
+
45
+ Other sources:
46
+ * Python: [geopy.Point](http://geopy.readthedocs.org/en/latest/#geopy.point.Point);
47
+ * [ElasticSearch](https://www.elastic.co/blog/geo-location-and-search)
48
+ uses hash with "lat" and "lon" keys;
49
+ * Google Maps [Geocoding API](https://developers.google.com/maps/documentation/geocoding/intro#GeocodingResponses)
50
+ uses hash with "lat" and "lng" keys;
51
+ * PostGIS: [pretty complicated](http://postgis.net/docs/manual-2.2/using_postgis_dbmanagement.html#RefObject)
52
+ has _geometrical_ (projected) and _geographical_ (lat, lng) points and
53
+ stuff.
54
+
55
+ ## Design decisions
56
+
57
+ While designing `Geo` library, our reference point was standard `Time`
58
+ class (and, to lesser extent, `Date`/`DateTime`). It has this
59
+ responsibilities:
60
+ * stores data in simple internal form;
61
+ * helps to parse and format data to and from strings;
62
+ * provides easy access to logical components of data;
63
+ * allows most simple and unambiguous calculations.
64
+
65
+ **Main type name**: as far as we can see, there's no good singular name
66
+ for `(lat, lng)` pair concept. As mentioned above, there can be seen
67
+ names like `LatLng`, or `Location`, or `Point`; and in natural language
68
+ just "coordinates" used frequently. We propose the name `Coord`, which
69
+ is pretty short, easy to remember, demonstrates intentions (and looks
70
+ like singular, so you can have "one coord object" and "array of coords",
71
+ which is not 100% linguistically correct, yet convenient). Alternative
72
+ `Point` name seems to be too ambigous, being used in many contexts.
73
+
74
+ `Geo::Coord` object is **immutable**, there's no semantical sense in
75
+ `location.latitude = ...` or something like this.
76
+
77
+ **Units**: `Geo` calculations (just like `Time` calculations) provide
78
+ no units options, just returning numbers measured in "default" units:
79
+ metres for distances (as they are SI unit) and degrees for azimuth.
80
+ Latitude and longitude are stored in degrees, but radians values accessors
81
+ are provided (being widely used in geodesy math).
82
+
83
+ All coordinates and calculations are thought to be in
84
+ [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System#A_new_World_Geodetic_System:_WGS_84)
85
+ coordinates reference system, being current standard for maps and GPS.
86
+
87
+ There's introduced **concept of globe** used internally for calculations.
88
+ Only generic (sphere) and Earth globes are implemented, but for 2016 we
89
+ feel like current design of basic types should take in consideration
90
+ possibility of writing Ruby scripts for Mars maps analysis. Only one
91
+ geodesy formula is implemented (Vincenty, generally considered one of
92
+ the most precise), as for standard library class it considered
93
+ unnecessary to provide user with geodesy formulae options.
94
+
95
+ No **map projection** math was added into current proposal, but it
96
+ may be a good direction for further work. No **elevation** data considered
97
+ either.
98
+
99
+ ## Proposal details
100
+
101
+ ### `Geo::Coord` class
102
+
103
+ Represents `[latitude, longitude]` pair. Latitude is -90 to +90 (degrees).
104
+ Longitude is -180 to +180.
105
+
106
+ Class methods:
107
+ * `new(lat, lng)` creates instance from two Numerics (in degrees);
108
+ * `new(lat:, lng:)` keyword arguments form of above;
109
+ * `new(latd:, latm:, lats:, lath:, lngd: lngm:, lngs: lngh:)` creates
110
+ instance from coordinates in (deg, min, sec, hemisphere) form; hemispheres
111
+ are "N"/"S" for latitude and "W"/E" for longitude; any component except
112
+ for degrees can be omitted; if hemisphere is omitted, it is decided by
113
+ degrees sign (lat: positive is "N", lng: positive is "E");
114
+ * `from_h(hash)` creates instance from hash with `"lat"` or `"latitude"`
115
+ key and `"lon"` or `"lng"` or `"longitude"` key (case-independent);
116
+ * `from_radians(phi, la)` creates instance from radian values;
117
+ * `strpcoord` parses string into coordinates by provided pattern (see
118
+ below for pattern description);
119
+ * `parse_ll` parses coordinates string in `"float, float"` form;
120
+ * `parse_dms` parses coordinates string in `d m s h, d m s h` format
121
+ (considering several widely used symbols for degrees, minutes and seconds);
122
+ * `parse` tries to parse string into coordinates from various formats.
123
+
124
+ Instance methods:
125
+ * `lat` and `lng`, returning `Float`s, signed;
126
+ * `latitude` and `longitude` as an aliases; `lon` as an additional
127
+ aliases for longitude;
128
+ * `latd`, `latm`, `lats`, `lath`: degree, minute, second, hemisphere;
129
+ `latd` and `latm` are `Fixnum`, `lats` is `Float`, `lath` is "N"/"S"; all
130
+ numbers are unsigned;
131
+ * `lngd`, `lngm`, `lngs`, `lngh`: the same for longitude (hemisphere is
132
+ "W"/"E");
133
+ * `latdms(nohemisphere = false)` returns `[latd, latm, lats, lath]` with
134
+ `nohemisphere` param equal to `false`, and `[±latd, latm, lats]` with
135
+ `true`; same with `lngdms` for longitude;
136
+ * `phi` and `φ` is latitude in radians (helpful for math), `la` or `λ`
137
+ is longitude in radians (not `lambda` to not confuse with Kernel method);
138
+ * `to_s` returning string like "50.004444,36.231389" (good for map
139
+ URLs construction, for example);
140
+ * `to_h(lat: :lat, lng: :lng)` converts coord to hash (with
141
+ desired key names);
142
+ * `to_a` converts coord to simple `[lat, lng]` pair;
143
+ * `strfcoord(formatstr)` for complex coordinate formatting (see below
144
+ for format string description);
145
+ * `distance(other)` calculates distance to another point (in metres);
146
+ * `azimuth(other)` calculates direction to target (in degrees);
147
+ * `endpoint(direction, azimuth)` calculates final point of the line of
148
+ `distance` metres going in `azimuth` direction from current point.
149
+
150
+ #### `strpcoord`/`strfcoord`
151
+
152
+ Example:
153
+ ```ruby
154
+ kharkiv.strfcoord('%latdu°%latm′%lats″ %lath, %lngdu°%lngm′%lngs″ %lngh')
155
+ # => "50°0′16″ N, 36°13′53″ E"
156
+ ```
157
+
158
+ Directives:
159
+ * `%lat` - full latitude, float; can be formatted with more control like
160
+ `%.4lat` (four digits after point) or `%+lat` (explicit plus sign for
161
+ positive latitudes);
162
+ * `%latd` - latitude degrees, unsigned, integer
163
+ * `%latds` - latitude degrees, signed
164
+ * `%latm` - latitude minutes, unsigned, integer
165
+ * `%lats` - latitude seconds, unsigned, integer, but can be formatted as
166
+ float: `%.2lats`
167
+ * `%lath` - latitude hemisphere, one letter ("N"/"S")
168
+ * `%lng`, `%lngd`, `%lngds`, `%lngs`, `%lngh`, `%lngH` - same for longitude
169
+ * `%%` literal `%` sign
170
+
171
+ ### Current implementation
172
+
173
+ Proposed implementation can be found at https://github.com/zverok/geo_coord.
174
+ It was created with thoughts of standard library, so, all docs are in
175
+ RDoc format, and tests/specs are in mspec-compatible rspec flavour.
data/geo_coord.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ require './lib/geo/coord/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'geo_coord'
5
+ s.version = Geo::Coord::VERSION
6
+ s.authors = ['Victor Shepelev']
7
+ s.email = 'zverok.offline@gmail.com'
8
+ s.homepage = 'https://github.com/zverok/geo_coord'
9
+
10
+ s.summary = 'Geo::Coord class'
11
+ s.description = <<-EOF
12
+ EOF
13
+ s.licenses = ['MIT']
14
+
15
+ s.files = `git ls-files`.split($RS).reject do |file|
16
+ file =~ /^(?:
17
+ spec\/.*
18
+ |Gemfile
19
+ |Rakefile
20
+ |\.rspec
21
+ |\.gitignore
22
+ |\.rubocop.yml
23
+ |\.travis.yml
24
+ )$/x
25
+ end
26
+ s.require_paths = ["lib"]
27
+
28
+ s.required_ruby_version = '>= 2.1.0'
29
+
30
+ s.add_development_dependency 'rubocop', '>= 0.40'
31
+ s.add_development_dependency 'rspec', '= 2.14'
32
+ s.add_development_dependency 'mspec'
33
+ s.add_development_dependency 'simplecov', '~> 0.9'
34
+ s.add_development_dependency 'rake'
35
+ s.add_development_dependency 'rubygems-tasks'
36
+ s.add_development_dependency 'yard'
37
+ s.add_development_dependency 'coveralls'
38
+ end
@@ -0,0 +1,165 @@
1
+ require 'singleton'
2
+
3
+ module Geo
4
+ # @private
5
+ module Coord::Globes # :nodoc:all
6
+ # Notes on this module
7
+ #
8
+ # **Credits:**
9
+ #
10
+ # Most of the initial code/algo, as well as tests were initially borrowed from
11
+ # [Graticule](https://github.com/collectiveidea/graticule).
12
+ #
13
+ # Algo descriptions borrowed from
14
+ #
15
+ # * http://www.movable-type.co.uk/scripts/latlong.html (simple)
16
+ # * http://www.movable-type.co.uk/scripts/latlong-vincenty.html (Vincenty)
17
+ #
18
+ # **On naming and code style:**
19
+ #
20
+ # Two main methods (distance/azimuth between two points and endpoint by
21
+ # startpoint and distance/azimuth) are named `inverse` & `direct` due
22
+ # to solving "two main geodetic problems": https://en.wikipedia.org/wiki/Geodesy#Geodetic_problems
23
+ #
24
+ # Code for them is pretty "un-Ruby-ish", trying to preserve original
25
+ # formulae as much as possible (including use of Greek names and
26
+ # inconsistent naming of some things: "simple" solution of direct problem
27
+ # names distance `d`, while Vincenty formula uses `s`).
28
+ #
29
+ class Generic
30
+ include Singleton
31
+ include Math
32
+
33
+ def inverse(phi1, la1, phi2, la2)
34
+ # See http://www.movable-type.co.uk/scripts/latlong.html
35
+ delta_phi = phi2 - phi1
36
+ delta_la = la2 - la1
37
+ a = sin(delta_phi/2)**2 + cos(phi1)*cos(phi2) * sin(delta_la/2)**2
38
+ c = 2 * atan2(sqrt(a), sqrt(1-a))
39
+ d = r * c
40
+
41
+ y = sin(delta_la) * cos(phi1)
42
+ x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(delta_la)
43
+
44
+ a = atan2(y, x)
45
+
46
+ [d, a]
47
+ end
48
+
49
+ def direct(phi1, la1, d, alpha1)
50
+ phi2 = asin( sin(phi1)*cos(d/r) +
51
+ cos(phi1)*sin(d/r)*cos(alpha1) )
52
+ la2 = la1 + atan2(sin(alpha1)*sin(d/r)*cos(phi1), cos(d/r)-sin(phi1)*sin(phi2))
53
+ [phi2, la2]
54
+ end
55
+
56
+ private
57
+
58
+ def r
59
+ self.class::RADIUS
60
+ end
61
+ end
62
+
63
+ class Earth < Generic
64
+ # All in SI units (metres)
65
+ RADIUS = 6378135
66
+ MAJOR_AXIS = 6378137
67
+ MINOR_AXIS = 6356752.3142
68
+ F = (MAJOR_AXIS - MINOR_AXIS) / MAJOR_AXIS
69
+
70
+ VINCENTY_MAX_ITERATIONS = 20
71
+ VINCENTY_TOLERANCE = 1e-12
72
+
73
+ def inverse(phi1, la1, phi2, la2)
74
+ # Vincenty formula
75
+ # See http://www.movable-type.co.uk/scripts/latlong-vincenty.html
76
+ l = la2 - la1
77
+ u1 = atan((1-F) * tan(phi1))
78
+ u2 = atan((1-F) * tan(phi2))
79
+ sin_u1 = sin(u1); cos_u1 = cos(u1)
80
+ sin_u2 = sin(u2); cos_u2 = cos(u2)
81
+
82
+ la = l # first approximation
83
+ la_, cosSqalpha, sin_sigma, cos_sigma, sigma, cos2sigmaM, sinla, cosla = nil
84
+
85
+ VINCENTY_MAX_ITERATIONS.times do
86
+ sinla = sin(la)
87
+ cosla = cos(la)
88
+
89
+ sin_sigma = sqrt((cos_u2*sinla) * (cos_u2*sinla) +
90
+ (cos_u1*sin_u2-sin_u1*cos_u2*cosla) * (cos_u1*sin_u2-sin_u1*cos_u2*cosla))
91
+
92
+ return [0, 0] if sin_sigma == 0 # co-incident points
93
+
94
+ cos_sigma = sin_u1*sin_u2 + cos_u1*cos_u2*cosla
95
+ sigma = atan2(sin_sigma, cos_sigma)
96
+ sinalpha = cos_u1 * cos_u2 * sinla / sin_sigma
97
+ cosSqalpha = 1 - sinalpha*sinalpha
98
+ cos2sigmaM = cos_sigma - 2*sin_u1*sin_u2/cosSqalpha
99
+ cos2sigmaM = 0 if cos2sigmaM.nan? # equatorial line: cosSqalpha=0 (§6)
100
+
101
+ c = F/16*cosSqalpha*(4+F*(4-3*cosSqalpha))
102
+ la_ = la
103
+ la = l + (1-c) * F * sinalpha *
104
+ (sigma + c*sin_sigma*(cos2sigmaM+c*cos_sigma*(-1+2*cos2sigmaM*cos2sigmaM)))
105
+
106
+ break if la_ && (la - la_).abs < VINCENTY_TOLERANCE
107
+ end
108
+
109
+ # Formula failed to converge (happens on antipodal points)
110
+ # We'll call Haversine formula instead.
111
+ return super if (la - la_).abs > VINCENTY_TOLERANCE
112
+
113
+ uSq = cosSqalpha * (MAJOR_AXIS**2 - MINOR_AXIS**2) / (MINOR_AXIS**2)
114
+ a = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
115
+ b = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
116
+ delta_sigma = b*sin_sigma*(cos2sigmaM+b/4*(cos_sigma*(-1+2*cos2sigmaM*cos2sigmaM)-
117
+ b/6*cos2sigmaM*(-3+4*sin_sigma*sin_sigma)*(-3+4*cos2sigmaM*cos2sigmaM)))
118
+
119
+ s = MINOR_AXIS * a * (sigma-delta_sigma)
120
+ alpha1 = atan2(cos_u2*sinla, cos_u1*sin_u2 - sin_u1*cos_u2*cosla)
121
+
122
+ [s, alpha1]
123
+ end
124
+
125
+ def direct(phi1, la1, s, alpha1)
126
+ sinalpha1 = sin(alpha1)
127
+ cosalpha1 = cos(alpha1)
128
+
129
+ tanU1 = (1-F) * tan(phi1)
130
+ cosU1 = 1 / sqrt(1 + tanU1**2)
131
+ sinU1 = tanU1 * cosU1
132
+ sigma1 = atan2(tanU1, cosalpha1)
133
+ sinalpha = cosU1 * sinalpha1
134
+ cosSqalpha = 1 - sinalpha**2
135
+ uSq = cosSqalpha * (MAJOR_AXIS**2 - MINOR_AXIS**2) / (MINOR_AXIS**2);
136
+ a = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
137
+ b = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
138
+
139
+ sigma = s / (MINOR_AXIS*a)
140
+ sigma_ = nil
141
+
142
+ begin
143
+ cos2sigmaM = cos(2*sigma1 + sigma);
144
+ sinsigma = sin(sigma);
145
+ cossigma = cos(sigma);
146
+ delta_sigma = b*sinsigma*(cos2sigmaM+b/4*(cossigma*(-1+2*cos2sigmaM**2)-
147
+ b/6*cos2sigmaM*(-3+4*sinsigma**2)*(-3+4*cos2sigmaM**2)))
148
+ sigma_ = sigma
149
+ sigma = s / (MINOR_AXIS*a) + delta_sigma
150
+ end while (sigma-sigma_).abs > 1e-12
151
+
152
+ tmp = sinU1*sinsigma - cosU1*cossigma*cosalpha1
153
+ phi2 = atan2(sinU1*cossigma + cosU1*sinsigma*cosalpha1, (1-F)*sqrt(sinalpha**2 + tmp**2))
154
+ la = atan2(sinsigma*sinalpha1, cosU1*cossigma - sinU1*sinsigma*cosalpha1)
155
+ c = F/16*cosSqalpha*(4+F*(4-3*cosSqalpha))
156
+ l = la - (1-c) * F * sinalpha *
157
+ (sigma + c*sinsigma*(cos2sigmaM+c*cossigma*(-1+2*cos2sigmaM*cos2sigmaM)))
158
+
159
+ la2 = (la1+l+3*PI) % (2*PI) - PI # normalise to -PI...+PI
160
+
161
+ [phi2, la2]
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,5 @@
1
+ module Geo
2
+ class Coord
3
+ VERSION = '0.0.1'.freeze
4
+ end
5
+ end