geo_coord 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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