geo_coord 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +199 -0
- data/StdlibProposal.md +175 -0
- data/geo_coord.gemspec +38 -0
- data/lib/geo/coord/globes.rb +165 -0
- data/lib/geo/coord/version.rb +5 -0
- data/lib/geo/coord.rb +667 -0
- metadata +163 -0
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
|