geo_coord 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7e6b043870b18d1cb3c9ccbf3b7b6270a63e19fc
4
- data.tar.gz: c9938260cf2f45f3c255e099891ddcf2ac1747d4
2
+ SHA256:
3
+ metadata.gz: 36feeb545afb9bf13f5d330302ba226b9c9fc9ce28aa1b223ad0c796ca1e2e37
4
+ data.tar.gz: 57ea4296740735b6c8606738b38498225130620a43056832ae7ee3a96c8b3b2a
5
5
  SHA512:
6
- metadata.gz: 482499de585615150a74734bf23c72936bc7bd10c7b14447f7c251f7d7c79c4fa573b232d25e5d7de6468b2da0e2a11890ff36f7466ef8edb4385cffe44b6ebc
7
- data.tar.gz: 6d7f5c2a3c52ed6315922edfe8ca7cd2b682e8c51b4a6c9dde7d46b7ec0af77168f3d95f9abc2623aac8a20d16759b128f9065b5506c0402501ddb4b7681efd0
6
+ metadata.gz: 45c9aa30f9d38d1ea2d96cd89fefe0f09ff77a1d18d515402ba6ccb2d37b92a5b8d3ef1903c619cc8c655f4787750db1dae1f457330ac58d2c84ec086ed8962b
7
+ data.tar.gz: 77ffed86527f6752f4885c0e22173aa84214c8074a7793fc0026ce8d7c78ee43bc3c6d6e1b62a43d77de03619dcc097dde576531c176d149e115b0bf235532f3
@@ -0,0 +1,12 @@
1
+ # Geo::Coord changelog
2
+
3
+ ## 0.1.0 - Feb 3, 2018
4
+
5
+ * Switch to `BigDecimal` for internal values storage;
6
+ * More friendly `#inspect` & `#to_s` format;
7
+ * Rename `#to_a` to `#latlng` & `#lnglat`;
8
+ * Fix `#lats` formula bug.
9
+
10
+ ## 0.0.1 - Jun 06, 2016
11
+
12
+ Initial release as a gem.
data/README.md CHANGED
@@ -40,74 +40,29 @@ I still have a small hope it would be part of stdlib once, that's why I
40
40
  preserve the style of specs (outdated rspec, but compatible with mspec used
41
41
  for standard library) and docs (yard in RDoc-compatibility mode).
42
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
43
  ## Installation
91
44
 
92
45
  Now when it is a gem, just do your usual `gem install geo_coord` or add
93
- `gem "geo_coord"` to your Gemfile.
46
+ `gem "geo_coord", require: "geo/coord"` to your Gemfile.
94
47
 
95
48
  ## Usage
96
49
 
97
50
  ### Creation
98
51
 
99
52
  ```ruby
53
+ require 'geo/coord'
54
+
100
55
  # From lat/lng pair:
101
56
  g = Geo::Coord.new(50.004444, 36.231389)
102
- # => #<Geo::Coord 50.004444,36.231389>
57
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
103
58
 
104
59
  # Or using keyword arguments form:
105
60
  g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
106
- # => #<Geo::Coord 50.004444,36.231389>
61
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
107
62
 
108
63
  # Keyword arguments also allow creation of Coord from components:
109
64
  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>
65
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
111
66
  ```
112
67
 
113
68
  For parsing API responses you'd like to use `from_h`,
@@ -116,7 +71,7 @@ and knows synonyms (lng/lon/longitude):
116
71
 
117
72
  ```ruby
118
73
  g = Geo::Coord.from_h('LAT' => 50.004444, 'LON' => 36.231389)
119
- # => #<Geo::Coord 50.004444,36.231389>
74
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
120
75
  ```
121
76
 
122
77
  For math, you'd probably like to be able to initialize
@@ -124,7 +79,7 @@ Coord with radians rather than degrees:
124
79
 
125
80
  ```ruby
126
81
  g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
127
- # => #<Geo::Coord 50.004444,36.231389>
82
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
128
83
  ```
129
84
 
130
85
  There's also family of string parsing methods, with different
@@ -133,21 +88,21 @@ applicability:
133
88
  ```ruby
134
89
  # Tries to parse (lat, lng) pair:
135
90
  g = Geo::Coord.parse_ll('50.004444, 36.231389')
136
- # => #<Geo::Coord 50.004444,36.231389>
91
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
137
92
 
138
93
  # Tries to parse degrees/minutes/seconds:
139
94
  g = Geo::Coord.parse_dms('50° 0′ 16″ N, 36° 13′ 53″ E')
140
- # => #<Geo::Coord 50.004444,36.231389>
95
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
141
96
 
142
97
  # Tries to do best guess:
143
98
  g = Geo::Coord.parse('50.004444, 36.231389')
144
- # => #<Geo::Coord 50.004444,36.231389>
99
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
145
100
  g = Geo::Coord.parse('50° 0′ 16″ N, 36° 13′ 53″ E')
146
- # => #<Geo::Coord 50.004444,36.231389>
101
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
147
102
 
148
103
  # Allows user to provide pattern:
149
104
  g = Geo::Coord.strpcoord('50.004444, 36.231389', '%lat, %lng')
150
- # => #<Geo::Coord 50.004444,36.231389>
105
+ # => #<Geo::Coord 50°0'16"N 36°13'53"E>
151
106
  ```
152
107
 
153
108
  [Pattern language description](http://www.rubydoc.info/gems/geo_coord/Geo/Coord#strpcoord-class_method)
@@ -170,7 +125,8 @@ g.latdms # => [50, 0, 15.998400000011316, "N"]
170
125
  ### Formatting and converting
171
126
 
172
127
  ```ruby
173
- g.to_s # => "50.004444,36.231389"
128
+ g.to_s # => "50°0'16\"N 36°13'53\"E"
129
+ g.to_s(dms: false) # => "50.004444,36.231389"
174
130
  g.strfcoord('%latd°%latm′%lats″%lath %lngd°%lngm′%lngs″%lngh')
175
131
  # => "50°0′16″N 36°13′53″E"
176
132
 
@@ -185,11 +141,72 @@ kyiv = Geo::Coord.new(50.45, 30.523333)
185
141
 
186
142
  kharkiv.distance(kyiv) # => 410211.22377421556
187
143
  kharkiv.azimuth(kyiv) # => 279.12614358262067
188
- kharkiv.endpoint(410_211, 280) # => #<Geo::Coord 50.505975,30.531283>
144
+ kharkiv.endpoint(410_211, 280) # => #<Geo::Coord 50°30'22"N 30°31'53"E>
189
145
  ```
190
146
 
191
147
  [Full API Docs](http://www.rubydoc.info/gems/geo_coord)
192
148
 
149
+ ## Design decisions
150
+
151
+ While designing `Geo` library, my reference point was standard `Time`
152
+ class (and, to lesser extent, `Date`/`DateTime`). It has these
153
+ responsibilities:
154
+
155
+ * stores data in simple internal form;
156
+ * helps to parse and format data to and from strings;
157
+ * provides easy access to logical components of data;
158
+ * allows most simple and unambiguous calculations.
159
+
160
+ **Namespace name**: The gem takes pretty short and generic top-level
161
+ namespace name `Geo`, but creates only one class inside it: `Geo::Coord`.
162
+
163
+ **Main type name**: as far as I can see, there's no good singular name
164
+ for `(lat, lng)` pair concept. In different libraries, there can be seen
165
+ names like `LatLng`, or `Location`, or `Point`; and in natural language
166
+ just "coordinates" used frequently. I propose the name `Coord`, which
167
+ is pretty short, easy to remember, demonstrates intentions (and looks
168
+ like singular, so you can have "one coord object" and "array of coords",
169
+ which is not 100% linguistically correct, yet convenient). Alternative
170
+ `Point` name seems to be too ambiguous, being used in many contexts.
171
+
172
+ `Geo::Coord` object is **immutable**, there's no semantical sense in
173
+ `location.latitude = ...` or something like this.
174
+
175
+ **Units**: `Geo` calculations (just like `Time` calculations) provide
176
+ no units options, just returning numbers measured in "default" units:
177
+ metres for distances (as they are SI unit) and degrees for azimuth.
178
+ Latitude and longitude are stored in degrees, but radians values accessors
179
+ are provided (being widely used in geodesy math).
180
+
181
+ **Internal storage**: Since ver 0.0.2, latitude and longitude stored
182
+ internally as an instances of `BigDecimal`. While having some memory
183
+ and performance downsides, this datatype provides _correctness_ of
184
+ conversions between floating point & deg-min-sec representations:
185
+
186
+ ```ruby
187
+ # 33.3 should be 33°18'00"
188
+ # Float:
189
+ 33.3 * 60 % 60 # => 17.999999999999773 minutes
190
+ # BigDecimal
191
+ BigDecimal(33.3, 10) * 60 % 60 # => 0.18e2
192
+ ```
193
+
194
+ All coordinates and calculations are thought to be in
195
+ [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System#A_new_World_Geodetic_System:_WGS_84)
196
+ coordinates reference system, being current standard for maps and GPS.
197
+
198
+ There's introduced **a concept of globe** used internally for calculations.
199
+ Only generic (sphere) and Earth globes are implemented, but for 2010th I
200
+ feel like the current design of basic types should take in consideration
201
+ possibility of writing Ruby scripts for Mars maps analysis. Only one
202
+ geodesy formula is implemented (Vincenty, generally considered one of
203
+ the most precise), as for standard library class it considered
204
+ unnecessary to provide a user with geodesy formulae options.
205
+
206
+ No **map projection** math was added into the current gem, but it
207
+ may be a good direction for further work. No **elevation** data considered
208
+ either.
209
+
193
210
  ## Author
194
211
 
195
212
  [Victor Shepelev](https://zverok.github.io)
@@ -8,22 +8,17 @@ Gem::Specification.new do |s|
8
8
  s.homepage = 'https://github.com/zverok/geo_coord'
9
9
 
10
10
  s.summary = 'Geo::Coord class'
11
- s.description = <<-EOF
12
- EOF
11
+ s.description = <<-DESC
12
+ DESC
13
13
  s.licenses = ['MIT']
14
14
 
15
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
16
+ file =~ %r{^(?: spec\/.* |Gemfile |Rakefile
17
+ |\.rspec |\.gitignore |\.rubocop.yml
23
18
  |\.travis.yml
24
- )$/x
19
+ )$}x
25
20
  end
26
- s.require_paths = ["lib"]
21
+ s.require_paths = ['lib']
27
22
 
28
23
  s.required_ruby_version = '>= 2.1.0'
29
24
 
@@ -35,4 +30,5 @@ Gem::Specification.new do |s|
35
30
  s.add_development_dependency 'rubygems-tasks'
36
31
  s.add_development_dependency 'yard'
37
32
  s.add_development_dependency 'coveralls'
33
+ s.add_development_dependency 'dokaz'
38
34
  end
@@ -1,3 +1,5 @@
1
+ require 'bigdecimal'
2
+
1
3
  # Geo::Coord is Ruby's library for handling [lat, lng] pairs of
2
4
  # geographical coordinates. It provides most of basic functionality
3
5
  # you may expect (storing and representing coordinate pair), as well
@@ -19,48 +21,48 @@ module Geo
19
21
  #
20
22
  # # From lat/lng pair:
21
23
  # g = Geo::Coord.new(50.004444, 36.231389)
22
- # # => #<Geo::Coord 50.004444,36.231389>
24
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
23
25
  #
24
26
  # # Or using keyword arguments form:
25
27
  # g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
26
- # # => #<Geo::Coord 50.004444,36.231389>
28
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
27
29
  #
28
30
  # # Keyword arguments also allow creation of Coord from components:
29
31
  # g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
30
- # # => #<Geo::Coord 50.004444,36.231389>
32
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
31
33
  #
32
34
  # For parsing API responses you'd like to use +from_h+,
33
35
  # which accepts String and Symbol keys, any letter case,
34
36
  # and knows synonyms (lng/lon/longitude):
35
37
  #
36
38
  # g = Geo::Coord.from_h('LAT' => 50.004444, 'LON' => 36.231389)
37
- # # => #<Geo::Coord 50.004444,36.231389>
39
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
38
40
  #
39
41
  # For math, you'd probably like to be able to initialize
40
42
  # Coord with radians rather than degrees:
41
43
  #
42
44
  # g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
43
- # # => #<Geo::Coord 50.004444,36.231389>
45
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
44
46
  #
45
47
  # There's also family of parsing methods, with different applicability:
46
48
  #
47
49
  # # Tries to parse (lat, lng) pair:
48
50
  # g = Geo::Coord.parse_ll('50.004444, 36.231389')
49
- # # => #<Geo::Coord 50.004444,36.231389>
51
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
50
52
  #
51
53
  # # Tries to parse degrees/minutes/seconds:
52
54
  # g = Geo::Coord.parse_dms('50° 0′ 16″ N, 36° 13′ 53″ E')
53
- # # => #<Geo::Coord 50.004444,36.231389>
55
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
54
56
  #
55
57
  # # Tries to do best guess:
56
58
  # g = Geo::Coord.parse('50.004444, 36.231389')
57
- # # => #<Geo::Coord 50.004444,36.231389>
59
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
58
60
  # g = Geo::Coord.parse('50° 0′ 16″ N, 36° 13′ 53″ E')
59
- # # => #<Geo::Coord 50.004444,36.231389>
61
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
60
62
  #
61
- # # Allows user to provide pattern (see below for pattern language):
63
+ # # Allows user to provide pattern:
62
64
  # g = Geo::Coord.strpcoord('50.004444, 36.231389', '%lat, %lng')
63
- # # => #<Geo::Coord 50.004444,36.231389>
65
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
64
66
  #
65
67
  # Having Coord object, you can get its properties:
66
68
  #
@@ -117,7 +119,7 @@ module Geo
117
119
  # and longitude ("lng", "lon", "long", "longitude").
118
120
  #
119
121
  # g = Geo::Coord.from_h('LAT' => 50.004444, longitude: 36.231389)
120
- # # => #<Geo::Coord 50.004444,36.231389>
122
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
121
123
  #
122
124
  def from_h(hash)
123
125
  h = hash.map { |k, v| [k.to_s.downcase.to_sym, v] }.to_h
@@ -132,7 +134,7 @@ module Geo
132
134
  # Creates Coord from φ and λ (latitude and longitude in radians).
133
135
  #
134
136
  # g = Geo::Coord.from_rad(0.8727421884291233, 0.6323570306208558)
135
- # # => #<Geo::Coord 50.004444,36.231389>
137
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
136
138
  #
137
139
  def from_rad(phi, la)
138
140
  new(phi * 180 / Math::PI, la * 180 / Math::PI)
@@ -150,7 +152,7 @@ module Geo
150
152
  # @private
151
153
  DEG_PATTERN = '[ °d]'.freeze # :nodoc:
152
154
  # @private
153
- MIN_PATTERN = "['m]".freeze # :nodoc:
155
+ MIN_PATTERN = "['′’m]".freeze # :nodoc:
154
156
  # @private
155
157
  SEC_PATTERN = '["″s]'.freeze # :nodoc:
156
158
 
@@ -158,24 +160,31 @@ module Geo
158
160
  LL_PATTERN = /^(#{FLOAT_PATTERN})\s*[,; ]\s*(#{FLOAT_PATTERN})$/ # :nodoc:
159
161
 
160
162
  # @private
161
- DMS_PATTERN = # :nodoc:
162
- /^\s*
163
- (?<latd>#{INT_PATTERN})#{DEG_PATTERN}\s*
164
- ((?<latm>#{UINT_PATTERN})#{MIN_PATTERN}\s*
165
- ((?<lats>#{UFLOAT_PATTERN})#{SEC_PATTERN}\s*)?)?
166
- (?<lath>[NS])?
167
- \s*[,; ]\s*
168
- (?<lngd>#{INT_PATTERN})#{DEG_PATTERN}\s*
169
- ((?<lngm>#{UINT_PATTERN})#{MIN_PATTERN}\s*
170
- ((?<lngs>#{UFLOAT_PATTERN})#{SEC_PATTERN}\s*)?)?
171
- (?<lngh>[EW])?
172
- \s*$/x
163
+ DMS_LATD_P = "(?<latd>#{INT_PATTERN})#{DEG_PATTERN}".freeze # :nodoc:
164
+ # @private
165
+ DMS_LATM_P = "(?<latm>#{UINT_PATTERN})#{MIN_PATTERN}".freeze # :nodoc:
166
+ # @private
167
+ DMS_LATS_P = "(?<lats>#{UFLOAT_PATTERN})#{SEC_PATTERN}".freeze # :nodoc:
168
+ # @private
169
+ DMS_LAT_P = "#{DMS_LATD_P}\\s*#{DMS_LATM_P}\\s*#{DMS_LATS_P}\\s*(?<lath>[NS])".freeze # :nodoc:
170
+
171
+ # @private
172
+ DMS_LNGD_P = "(?<lngd>#{INT_PATTERN})#{DEG_PATTERN}".freeze # :nodoc:
173
+ # @private
174
+ DMS_LNGM_P = "(?<lngm>#{UINT_PATTERN})#{MIN_PATTERN}".freeze # :nodoc:
175
+ # @private
176
+ DMS_LNGS_P = "(?<lngs>#{UFLOAT_PATTERN})#{SEC_PATTERN}".freeze # :nodoc:
177
+ # @private
178
+ DMS_LNG_P = "#{DMS_LNGD_P}\\s*#{DMS_LNGM_P}\\s*#{DMS_LNGS_P}\\s*(?<lngh>[EW])".freeze # :nodoc:
179
+
180
+ # @private
181
+ DMS_PATTERN = /^\s*#{DMS_LAT_P}\s*[,; ]\s*#{DMS_LNG_P}\s*$/x # :nodoc:
173
182
 
174
183
  # Parses Coord from string containing float latitude and longitude.
175
184
  # Understands several types of separators/spaces between values.
176
185
  #
177
186
  # Geo::Coord.parse_ll('-50.004444 +36.231389')
178
- # # => #<Geo::Coord -50.004444,36.231389>
187
+ # # => #<Geo::Coord 50°0'16"S 36°13'53"E>
179
188
  #
180
189
  # If parse_ll is not wise enough to understand your data, consider
181
190
  # using ::strpcoord.
@@ -193,7 +202,7 @@ module Geo
193
202
  # explicit hemisphere and no-hemisphere (signed degrees) formats.
194
203
  #
195
204
  # Geo::Coord.parse_dms('50°0′16″N 36°13′53″E')
196
- # # => #<Geo::Coord 50.004444,36.231389>
205
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
197
206
  #
198
207
  # If parse_dms is not wise enough to understand your data, consider
199
208
  # using ::strpcoord.
@@ -212,9 +221,9 @@ module Geo
212
221
  # known form).
213
222
  #
214
223
  # Geo::Coord.parse('-50.004444 +36.231389')
215
- # # => #<Geo::Coord -50.004444,36.231389>
224
+ # # => #<Geo::Coord 50°0'16"S 36°13'53"E>
216
225
  # Geo::Coord.parse('50°0′16″N 36°13′53″E')
217
- # # => #<Geo::Coord 50.004444,36.231389>
226
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
218
227
  #
219
228
  # If you know exact form in which coordinates are
220
229
  # provided, it may be wider to consider parse_ll, parse_dms or
@@ -268,12 +277,9 @@ module Geo
268
277
  pattern = PARSE_PATTERNS.inject(pattern) do |memo, (pfrom, pto)|
269
278
  memo.gsub(pfrom, pto)
270
279
  end
271
- if (m = Regexp.new('^' + pattern).match(str))
272
- h = m.names.map { |n| [n.to_sym, _extract_match(m, n)] }.to_h
273
- new(h)
274
- else
275
- raise ArgumentError, "Coordinates str #{str} can't be parsed by pattern #{pattern}"
276
- end
280
+ match = Regexp.new('^' + pattern).match(str)
281
+ raise ArgumentError, "Coordinates str #{str} can't be parsed by pattern #{pattern}" unless match
282
+ new(match.names.map { |n| [n.to_sym, _extract_match(match, n)] }.to_h)
277
283
  end
278
284
 
279
285
  private
@@ -299,19 +305,19 @@ module Geo
299
305
  # key +lat+ and partial longitude keys +lngd+, +lngm+ and so on.
300
306
  #
301
307
  # g = Geo::Coord.new(50.004444, 36.231389)
302
- # # => #<Geo::Coord 50.004444,36.231389>
308
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
303
309
  #
304
310
  # # Or using keyword arguments form:
305
311
  # g = Geo::Coord.new(lat: 50.004444, lng: 36.231389)
306
- # # => #<Geo::Coord 50.004444,36.231389>
312
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
307
313
  #
308
314
  # # Keyword arguments also allow creation of Coord from components:
309
315
  # g = Geo::Coord.new(latd: 50, latm: 0, lats: 16, lath: 'N', lngd: 36, lngm: 13, lngs: 53, lngh: 'E')
310
- # # => #<Geo::Coord 50.004444,36.231389>
316
+ # # => #<Geo::Coord 50°0'16"N 36°13'53"E>
311
317
  #
312
318
  # # Providing defaults:
313
319
  # g = Geo::Coord.new(lat: 50.004444)
314
- # # => #<Geo::Coord 50.004444,0.000000>
320
+ # # => #<Geo::Coord 50°0'16"N 0°0'0"W>
315
321
  #
316
322
  def initialize(lat = nil, lng = nil, **opts)
317
323
  @globe = Globes::Earth.instance
@@ -352,7 +358,7 @@ module Geo
352
358
 
353
359
  # Returns latitude seconds (unsigned float).
354
360
  def lats
355
- (lat.abs * 3600) % 3600
361
+ (lat.abs * 3600) % 60
356
362
  end
357
363
 
358
364
  # Returns latitude hemisphere (upcase letter 'N' or 'S').
@@ -386,14 +392,14 @@ module Geo
386
392
  # # Nothern hemisphere:
387
393
  # g = Geo::Coord.new(50.004444, 36.231389)
388
394
  #
389
- # g.latdms # => [50, 0, 15.998400000011316, "N"]
390
- # g.latdms(true) # => [50, 0, 15.998400000011316]
395
+ # g.latdms # => [50, 0, 15.9984, "N"]
396
+ # g.latdms(true) # => [50, 0, 15.9984]
391
397
  #
392
398
  # # Southern hemisphere:
393
399
  # g = Geo::Coord.new(-50.004444, 36.231389)
394
400
  #
395
- # g.latdms # => [50, 0, 15.998400000011316, "S"]
396
- # g.latdms(true) # => [-50, 0, 15.998400000011316]
401
+ # g.latdms # => [50, 0, 15.9984, "S"]
402
+ # g.latdms(true) # => [-50, 0, 15.9984]
397
403
  #
398
404
  def latdms(nohemisphere = false)
399
405
  nohemisphere ? [latsign * latd, latm, lats] : [latd, latm, lats, lath]
@@ -405,14 +411,14 @@ module Geo
405
411
  # # Eastern hemisphere:
406
412
  # g = Geo::Coord.new(50.004444, 36.231389)
407
413
  #
408
- # g.lngdms # => [36, 13, 53.00040000000445, "E"]
409
- # g.lngdms(true) # => [36, 13, 53.00040000000445]
414
+ # g.lngdms # => [36, 13, 53.0004, "E"]
415
+ # g.lngdms(true) # => [36, 13, 53.0004]
410
416
  #
411
417
  # # Western hemisphere:
412
418
  # g = Geo::Coord.new(50.004444, 36.231389)
413
419
  #
414
- # g.lngdms # => [36, 13, 53.00040000000445, "E"]
415
- # g.lngdms(true) # => [-36, 13, 53.00040000000445]
420
+ # g.lngdms # => [36, 13, 53.0004, "E"]
421
+ # g.lngdms(true) # => [-36, 13, 53.0004]
416
422
  #
417
423
  def lngdms(nohemisphere = false)
418
424
  nohemisphere ? [lngsign * lngd, lngm, lngs] : [lngd, lngm, lngs, lngh]
@@ -440,25 +446,35 @@ module Geo
440
446
  # g.inspect # => "#<Geo::Coord 50.004444,36.231389>"
441
447
  #
442
448
  def inspect
443
- '#<%s %s>' % [self.class.name, to_s]
449
+ strfcoord(%{#<#{self.class.name} %latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh>})
444
450
  end
445
451
 
446
452
  # Returns a string representing coordinates.
447
453
  #
448
- # g.to_s # => "50.004444,36.231389"
454
+ # g.to_s # => "50°0'16\"N 36°13'53\"E"
455
+ # g.to_s(dms: false) # => "50.004444,36.231389"
449
456
  #
450
- def to_s
451
- '%f,%f' % [lat, lng]
457
+ def to_s(dms: true)
458
+ format = dms ? %{%latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh} : '%lat,%lng'
459
+ strfcoord(format)
452
460
  end
453
461
 
454
462
  # Returns a two-element array of latitude and longitude.
455
463
  #
456
- # g.to_a # => [50.004444, 36.231389]
464
+ # g.latlng # => [50.004444, 36.231389]
457
465
  #
458
- def to_a
466
+ def latlng
459
467
  [lat, lng]
460
468
  end
461
469
 
470
+ # Returns a two-element array of longitude and latitude (reverse order to +latlng+).
471
+ #
472
+ # g.lnglat # => [36.231389, 50.004444]
473
+ #
474
+ def lnglat
475
+ [lng, lat]
476
+ end
477
+
462
478
  # Returns hash of latitude and longitude. You can provide your keys
463
479
  # if you want:
464
480
  #
@@ -480,17 +496,17 @@ module Geo
480
496
 
481
497
  # @private
482
498
  DIRECTIVES = { # :nodoc:
499
+ /%(#{FLOATUFLAGS})?lats/ => proc { |m| "%<lats>#{m[1] || '.0'}f" },
500
+ '%latm' => '%<latm>i',
483
501
  /%(#{INTFLAGS})?latds/ => proc { |m| "%<latds>#{m[1]}i" },
484
502
  '%latd' => '%<latd>i',
485
- '%latm' => '%<latm>i',
486
- /%(#{FLOATUFLAGS})?lats/ => proc { |m| "%<lats>#{m[1] || '.0'}f" },
487
503
  '%lath' => '%<lath>s',
488
504
  /%(#{FLOATFLAGS})?lat/ => proc { |m| "%<lat>#{m[1]}f" },
489
505
 
506
+ /%(#{FLOATUFLAGS})?lngs/ => proc { |m| "%<lngs>#{m[1] || '.0'}f" },
507
+ '%lngm' => '%<lngm>i',
490
508
  /%(#{INTFLAGS})?lngds/ => proc { |m| "%<lngds>#{m[1]}i" },
491
509
  '%lngd' => '%<lngd>i',
492
- '%lngm' => '%<lngm>i',
493
- /%(#{FLOATUFLAGS})?lngs/ => proc { |m| "%<lngs>#{m[1] || '.0'}f" },
494
510
  '%lngh' => '%<lngh>s',
495
511
  /%(#{FLOATFLAGS})?lng/ => proc { |m| "%<lng>#{m[1]}f" }
496
512
  }.freeze
@@ -529,13 +545,23 @@ module Geo
529
545
  # g.strfcoord("%latd°%latm'%lath -- %lngd°%lngm'%lngh")
530
546
  # # => "50°0'N -- 36°13'E"
531
547
  #
548
+ # +strfcoord+ handles seconds rounding implicitly:
549
+ #
550
+ # pos = Geo::Coord.new(0.033333, 91.333333)
551
+ # pos.lats # => 0.599988e2
552
+ # pos.strfcoord('%latd %latm %.05lats') # => "0 1 59.99880"
553
+ # pos.strfcoord('%latd %latm %lats') # => "0 2 0"
554
+ #
532
555
  def strfcoord(formatstr)
533
556
  h = full_hash
534
557
 
535
558
  DIRECTIVES.reduce(formatstr) do |memo, (from, to)|
536
559
  memo.gsub(from) do
537
560
  to = to.call(Regexp.last_match) if to.is_a?(Proc)
538
- to % h
561
+ res = to % h
562
+ res, carrymin = guard_seconds(to, res)
563
+ h[carrymin] += 1 if carrymin
564
+ res
539
565
  end
540
566
  end
541
567
  end
@@ -570,7 +596,7 @@ module Geo
570
596
  #
571
597
  # kharkiv = Geo::Coord.new(50.004444, 36.231389)
572
598
  # kharkiv.endpoint(410_211, 280)
573
- # # => #<Geo::Coord 50.505975,30.531283>
599
+ # # => #<Geo::Coord 50°30'22"N 30°31'53"E>
574
600
  #
575
601
  def endpoint(distance, azimuth)
576
602
  phi2, la2 = @globe.direct(phi, la, distance, deg2rad(azimuth))
@@ -579,17 +605,15 @@ module Geo
579
605
 
580
606
  private
581
607
 
582
- def _init(lat, lng)
583
- lat = lat.to_f
584
- lng = lng.to_f
608
+ LAT_RANGE_ERROR = 'Expected latitude to be between -90 and 90, %p received'.freeze
609
+ LNG_RANGE_ERROR = 'Expected longitude to be between -180 and 180, %p received'.freeze
585
610
 
586
- unless (-90..90).cover?(lat)
587
- raise ArgumentError, "Expected latitude to be between -90 and 90, #{lat} received"
588
- end
611
+ def _init(lat, lng)
612
+ lat = BigDecimal(lat.to_f, 10)
613
+ lng = BigDecimal(lng.to_f, 10)
589
614
 
590
- unless (-180..180).cover?(lng)
591
- raise ArgumentError, "Expected longitude to be between -180 and 180, #{lng} received"
592
- end
615
+ raise ArgumentError, LAT_RANGE_ERROR % lat unless (-90..90).cover?(lat)
616
+ raise ArgumentError, LNG_RANGE_ERROR % lng unless (-180..180).cover?(lng)
593
617
 
594
618
  @lat = lat
595
619
  @lng = lng
@@ -620,6 +644,13 @@ module Geo
620
644
  raise ArgumentError, "Unidentified hemisphere: #{h}"
621
645
  end
622
646
 
647
+ def guard_seconds(pattern, result)
648
+ m = pattern.match(/<(lat|lng)s>/)
649
+ return result unless m && result.start_with?('60')
650
+ carry = "#{m[1]}m".to_sym
651
+ [pattern % {lats: 0, lngs: 0}, carry]
652
+ end
653
+
623
654
  def latsign
624
655
  lat <=> 0
625
656
  end
@@ -1,5 +1,5 @@
1
1
  module Geo
2
2
  class Coord
3
- VERSION = '0.0.1'.freeze
3
+ VERSION = '0.1.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geo_coord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Shepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-06 00:00:00.000000000 Z
11
+ date: 2018-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: dokaz
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  description: ''
126
140
  email: zverok.offline@gmail.com
127
141
  executables: []
@@ -129,6 +143,7 @@ extensions: []
129
143
  extra_rdoc_files: []
130
144
  files:
131
145
  - ".yardopts"
146
+ - CHANGELOG.md
132
147
  - LICENSE.txt
133
148
  - README.md
134
149
  - StdlibProposal.md
@@ -156,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
171
  version: '0'
157
172
  requirements: []
158
173
  rubyforge_project:
159
- rubygems_version: 2.4.8
174
+ rubygems_version: 2.7.4
160
175
  signing_key:
161
176
  specification_version: 4
162
177
  summary: Geo::Coord class