libgeo 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 795971339fd7c0de9304b188ec33f51057cd4adb
4
+ data.tar.gz: 27003885f254776998b25dd67b6aa41d84c24270
5
+ SHA512:
6
+ metadata.gz: ab071931a81588e30804c7f008d77eb8bb49c9abeaae7b2e155a99a3e76aa6e12a97eedfa10bfc35a83582f45e8426ff7041267594bd400442c276b96aea0027
7
+ data.tar.gz: 9367c5f271715e96b56de6033d41b18ed918e0f874baeaa23cce61cbca2fb113d803e675d61c4135b385bf2b73f8299450fc8a72a76371632b6781501eb91126
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,13 @@
1
+ bundler_args: --without dev
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - rbx-2.2
7
+ - jruby-19mode
8
+
9
+ branches:
10
+ only:
11
+ - master
12
+
13
+ script: bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in libgeo.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec', '~> 3.0.0.beta'
7
+
8
+ group :dev do
9
+ gem 'simplecov'
10
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Andrey Savchenko
2
+
3
+ MIT License
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.
@@ -0,0 +1,60 @@
1
+ # Libgeo
2
+
3
+ Collection of geographical primitives
4
+
5
+ - [![Build Status](https://travis-ci.org/Ptico/libgeo.png?branch=master)](https://travis-ci.org/Ptico/libgeo)
6
+ - [![Code Climate](https://codeclimate.com/github/Ptico/libgeo.png)](https://codeclimate.com/github/Ptico/libgeo)
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'libgeo'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install libgeo
21
+
22
+ ## Usage
23
+
24
+ ### Latitude/Longitude
25
+
26
+ ```ruby
27
+ lng = Longitude.decimal(39.342679)
28
+
29
+ lng.hemisphere # => :E
30
+ lng.degrees # => 39
31
+ lng.minutes # => 20
32
+ lng.seconds # => 33.6444
33
+
34
+ lng.western? # => false
35
+ lng.eastern? # => true
36
+
37
+ lng.to_s # => '39°20′33.6444″E'
38
+ lng.to_nmea # => '03920.56074,E'
39
+
40
+ lng.western!
41
+ lng.hemisphere # => :W
42
+
43
+ ltt = Latitude.nmea('03920.56074,N')
44
+
45
+ ltt.to_s # => '39°20′33.6444″N'
46
+
47
+ ltt_dms = Latitude.dms('39°20′33.6444″N')
48
+
49
+ ltt_dms.to_s # => '39°20′33.6444″N'
50
+ ltt_dms.to_nmea # => '3920.56074,N'
51
+
52
+ ```
53
+
54
+ ## Contributing
55
+
56
+ 1. Fork it ( http://github.com/Ptico/libgeo/fork )
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ require 'set'
4
+
5
+ require 'libgeo/version'
6
+ require 'libgeo/formatter'
7
+
8
+ module Libgeo
9
+
10
+ NORTH = :N
11
+ SOUTH = :S
12
+ WEST = :W
13
+ EAST = :E
14
+
15
+ module Format
16
+ DMS = Formatter.new('%2d°%2m′%S″%H')
17
+ NMEA_LAT = Formatter.new('%2d%2M,%H')
18
+ NMEA_LON = Formatter.new('%3d%2M,%H')
19
+ end
20
+ end
21
+
22
+ require 'libgeo/coordinate'
23
+ require 'libgeo/latitude'
24
+ require 'libgeo/longitude'
@@ -0,0 +1,187 @@
1
+ # encoding: utf-8
2
+
3
+ require 'bigdecimal'
4
+ require 'bigdecimal/util'
5
+ require 'set'
6
+
7
+ require 'libgeo/coordinate/class_methods'
8
+ require 'libgeo/coordinate/presenters'
9
+ require 'libgeo/coordinate/hemi_helpers'
10
+
11
+ module Libgeo
12
+ ##
13
+ # Class: basic coordinate
14
+ #
15
+ # Provides basic functionality for Latitude and Longitude.
16
+ # In some cases can be used as standalone class.
17
+ #
18
+ class Coordinate
19
+ extend ClassMethods
20
+ include Presenters
21
+ include HemiHelpers
22
+
23
+ HEMISPHERES = Set.new([:N, :E, :S, :W]).freeze
24
+ NEGATIVE_HEMISPHERES = Set.new([:S, :W]).freeze
25
+
26
+ attr_accessor :degrees, :minutes, :seconds, :hemisphere
27
+
28
+ ##
29
+ # Shortcuts
30
+ alias :hemi :hemisphere
31
+ alias :deg :degrees
32
+ alias :mins :minutes
33
+ alias :secs :seconds
34
+
35
+ ##
36
+ # Coordinate type
37
+ #
38
+ def type
39
+ nil
40
+ end
41
+
42
+ ##
43
+ # Check coordinates equality
44
+ #
45
+ # Returns: {Boolean} true if coordinates are same
46
+ #
47
+ def eql?(other)
48
+ degrees.eql?(other.degrees) &&
49
+ minutes.eql?(other.minutes) &&
50
+ seconds.eql?(other.seconds) &&
51
+ hemisphere.eql?(other.hemisphere)
52
+ end
53
+ alias_method :==, :eql?
54
+
55
+ ##
56
+ # Decimal minutes with seconds included
57
+ #
58
+ # Returns: {Float} minutes
59
+ #
60
+ def minutes_only
61
+ (minutes + seconds.to_d / 60).to_f
62
+ end
63
+
64
+ ##
65
+ # Hemisphere validator
66
+ #
67
+ # Validates if given value can be assigned to Coordinate
68
+ # or if current hemi is valid
69
+ #
70
+ # Params:
71
+ # - value hemisphere value to check (optional)
72
+ #
73
+ # Returns: {Boolean}
74
+ #
75
+ def valid_hemisphere?(value=nil)
76
+ self.class::HEMISPHERES.include?(value || hemisphere)
77
+ end
78
+
79
+ ##
80
+ # Check if current hemisphere negative
81
+ #
82
+ # Returns: {Boolean}
83
+ #
84
+ def negative_hemisphere?
85
+ if valid_hemisphere?
86
+ NEGATIVE_HEMISPHERES.include?(hemi)
87
+ elsif direction
88
+ :< == direction
89
+ else
90
+ false
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Freeze all the things!
96
+ #
97
+ def freeze
98
+ normalize_data
99
+
100
+ [@degrees, @minutes, @seconds, @hemisphere, @direction].each { |ivar| ivar.freeze }
101
+
102
+ super
103
+ end
104
+
105
+ private
106
+
107
+ attr_reader :direction
108
+
109
+ ##
110
+ # Constructor:
111
+ #
112
+ # Params:
113
+ # - hemi_or_dir {Symbol|String} hemisphere value or direction
114
+ # - degrees {Fixnum} degrees part
115
+ # - minutes {Fixnum} minutes part
116
+ # - seconds {Float} seconds part
117
+ #
118
+ def initialize(hemi_or_dir, degrees, minutes, seconds)
119
+ @degrees = degrees
120
+ @minutes = minutes
121
+ @seconds = seconds
122
+
123
+ if [:<, :>].include?(hemi_or_dir)
124
+ @direction = hemi_or_dir
125
+ else
126
+ @hemisphere = hemi_or_dir
127
+ end
128
+
129
+ normalize_data
130
+ end
131
+
132
+ ##
133
+ # Private: make sure that all attrs has right types
134
+ #
135
+ def normalize_data
136
+ normalize_hemi
137
+ normalize_values
138
+ end
139
+
140
+ ##
141
+ # Private: detect and assign hemisphere value
142
+ #
143
+ # Check if hemisphere can be properly detected
144
+ # and assign it
145
+ #
146
+ def normalize_hemi
147
+ if hemisphere
148
+ normalize_hemi_wording
149
+ elsif direction
150
+ get_hemi_from_direction
151
+ end
152
+ end
153
+
154
+ ##
155
+ # Private: normalize hemisphere value
156
+ #
157
+ # If hemisphere have incorrect value - lookup for
158
+ # available correction from dictionary
159
+ #
160
+ def normalize_hemi_wording
161
+ unless valid_hemisphere?
162
+ self.class::CORRECTIONS.each_pair do |v, c|
163
+ @hemisphere = v if c.include?(hemisphere)
164
+ end
165
+ end
166
+ end
167
+
168
+ ##
169
+ # Private: get hemisphere value from direction
170
+ #
171
+ # Mostly for plain Coordinate instance (not Latitude or Longitude)
172
+ #
173
+ def get_hemi_from_direction
174
+ @hemisphere = self.class::DIRECTIONS[direction]
175
+ end
176
+
177
+ ##
178
+ # Private: normalize attribute types
179
+ #
180
+ def normalize_values
181
+ @degrees = degrees.to_i
182
+ @minutes = minutes.to_i
183
+ @seconds = seconds.to_f
184
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,243 @@
1
+ # encoding: utf-8
2
+
3
+ module Libgeo
4
+ class Coordinate
5
+ module ClassMethods
6
+
7
+ DMS_SEPARATORS = /['°′"`\ \,]+/.freeze
8
+ NMEA_SEPARATORS = /(,\ |,|\ )/.freeze
9
+
10
+ ##
11
+ # Factory: make a coordinate from decimal value
12
+ #
13
+ # Examples:
14
+ #
15
+ # Longitude.decimal(39.342679) # => #<Longitude hemisphere=E degrees=39 minutes=20 ...
16
+ #
17
+ # Params:
18
+ # - value {Float} decimal coordinate
19
+ #
20
+ # Returns: {Latitude|Longitude|Coordinate} instance
21
+ #
22
+ def decimal(value)
23
+ minutes = value.abs.to_d.modulo(1) * 60 # extract minutes from decimal degrees
24
+ create(dir_from(value), value.abs.to_i, *min_with_sec(minutes))
25
+ end
26
+
27
+ ##
28
+ # Factory: make a coordinate from nmea input
29
+ #
30
+ # Examples:
31
+ #
32
+ # Longitude.nmea('03920.56074,E') # => #<Longitude hemisphere=E degrees=39 minutes=20 ...
33
+ #
34
+ # Params:
35
+ # - input {String} nmea coordinate
36
+ #
37
+ # Returns: {Latitude|Longitude|Coordinate} instance
38
+ #
39
+ def nmea(input)
40
+ value, hemi = prepare_nmea(input)
41
+
42
+ value = value.to_f
43
+
44
+ direction = dir_from_values(value, hemi)
45
+
46
+ degrees = value.to_i.abs / 100
47
+
48
+ create(direction, degrees, *min_with_sec_nmea(value.abs), hemi)
49
+ end
50
+
51
+ ##
52
+ # Factory: make a coordinate from dms value
53
+ #
54
+ # Examples:
55
+ #
56
+ # Longitude.dms("58°39′13.5 S") # => #<Longitude hemisphere=S degrees=58 minutes=39 ...
57
+ #
58
+ # Params:
59
+ # - inputs {String} dms coordinate
60
+ #
61
+ # Returns: {Latitude|Longitude|Coordinate} instance
62
+ #
63
+ def dms(input)
64
+ string_values = input.split(DMS_SEPARATORS)
65
+
66
+ degrees = string_values[0].to_i # get degrees and minutes
67
+ minutes = (string_values[1] || 0).to_i
68
+ seconds = (string_values[2] || 0).to_f
69
+ hemi = string_values[3]
70
+
71
+ direction = dir_from_values(degrees, hemi)
72
+
73
+ create(direction, degrees.abs, minutes, seconds, hemi)
74
+ end
75
+
76
+ ##
77
+ # Factory: make a coordinate from degrees and full minutes
78
+ #
79
+ # Params:
80
+ # - degrees {Fixnum} degrees part
81
+ # - minutes {Float} full minutes, with seconds
82
+ #
83
+ # Examples:
84
+ #
85
+ # Latitude.degrees_minutes(-39, 20.56074) # => #<Latitude hemisphere=S degrees=39 minutes=20 ...
86
+ #
87
+ # Returns: {Latitude|Longitude|Coordinate} instance
88
+ #
89
+ def degrees_minutes(degrees, minutes)
90
+ create(dir_from(degrees), degrees.abs.to_i, *min_with_sec(minutes))
91
+ end
92
+
93
+ private
94
+
95
+ ##
96
+ # Private: extract splited values from nmea notation
97
+ #
98
+ # Params:
99
+ # - input {String} input string in nmea notation
100
+ #
101
+ # Examples:
102
+ #
103
+ # input # => "03922.54, E"
104
+ # prepare_nmea(input) # => ["03922.54", "E"]
105
+ #
106
+ # input2 # => "+03922.54"
107
+ # prepare_nmea(input) # => ["+03922.54"]
108
+ #
109
+ # Returns: [{String}, {String}] numbers and hemisphere
110
+ #
111
+ def prepare_nmea(input)
112
+ splited_values = input.split(NMEA_SEPARATORS)
113
+
114
+ splited_values.delete_if { |str| str =~ NMEA_SEPARATORS }
115
+ end
116
+
117
+ ##
118
+ # Private: extract integer minutes and float seconds from float minutes
119
+ #
120
+ # Params:
121
+ # - minutes {Float|Decimal} minutes
122
+ #
123
+ # Returns: [{Ineteger}, {Float}] rounded minutes and exracted seconds
124
+ #
125
+ def min_with_sec(minutes_f)
126
+ seconds = minutes_f.to_d.modulo(1) * 60
127
+
128
+ [minutes_f.to_i, seconds.to_f]
129
+ end
130
+
131
+ ##
132
+ # Private: extract minutes and seconds from nmea coord
133
+ #
134
+ # Params:
135
+ # - value {Float} nmea coordinate
136
+ #
137
+ # Returns: [{Ineteger}, {Float}] extracted minutes and seconds
138
+ #
139
+ def min_with_sec_nmea(value)
140
+ minutes = value.to_i % 100
141
+ seconds = (value.modulo(1) * 60).round(4)
142
+
143
+ [minutes, seconds]
144
+ end
145
+
146
+ ##
147
+ # Private: detect direction based on neg/pos
148
+ #
149
+ # Params:
150
+ # - degrees {Integer} degrees
151
+ #
152
+ # Examples:
153
+ #
154
+ # dir_from(-5) # => :<
155
+ # dir_from(10) # => :>
156
+ #
157
+ # Returns: {Symbol} symbol of direction
158
+ #
159
+ def dir_from(degrees)
160
+ degrees < 0 ? :< : :>
161
+ end
162
+
163
+ ##
164
+ # Private: detect direction from degrees of hemisphere
165
+ #
166
+ # Params:
167
+ # - numbers {Ineteger|Float|Decimal} numerical value of coordinate (degrees)
168
+ # - hemi {String|Symbol|nil} hemisphere if available
169
+ #
170
+ # Examples:
171
+ #
172
+ # dir_from_values(43, 'S') # => :<
173
+ # dir_from_values(43, nil) # => :>
174
+ #
175
+ # Returns: {Symbol} symbol of direction
176
+ #
177
+ def dir_from_values(numbers, hemi)
178
+ if hemi
179
+ (NEGATIVE_HEMISPHERES.include? hemi.to_sym) ? :< : :>
180
+ else
181
+ dir_from(numbers.to_i)
182
+ end
183
+ end
184
+
185
+ ##
186
+ # Private: fabric. Make a coordinate from given values
187
+ #
188
+ # Params:
189
+ # - direction {Symbol} symbol of hemisphere direction
190
+ # - degrees {Integer}
191
+ # - minutes {Integer}
192
+ # - second {Integer|Float}
193
+ # - hemi {Symbol|String} (optional) hemisphere
194
+ #
195
+ # Raises:
196
+ # - {ArgumentError} on wrong params values
197
+ #
198
+ # Returns: {Longitude|Latotude} created coordinate
199
+
200
+ def create(direction, degrees, minutes, seconds, hemi=nil)
201
+ validate_values(degrees, minutes, seconds, hemi)
202
+
203
+ self.new(direction, degrees, minutes, seconds)
204
+ end
205
+
206
+ ##
207
+ # Private: validates given values
208
+ #
209
+ # Params:
210
+ # - degrees {Ineteger}
211
+ # - minutes {Integer}
212
+ # - seconds {Ineteger|Float}
213
+ # - hemi {Symbol|String|nil} hemisphere
214
+ #
215
+ # Raises:
216
+ # - {ArgumentError} on wrong values
217
+ #
218
+ def validate_values(degrees, minutes, seconds, hemi)
219
+ if degrees > self::MAX_DEGREES || minutes > 60 || seconds > 60
220
+ raise ArgumentError.new('values out of range')
221
+ end
222
+
223
+ validate_hemisphere(hemi.to_sym) if hemi
224
+ end
225
+
226
+
227
+ ##
228
+ # Hemisphere validator
229
+ #
230
+ # Validates given hemisphere
231
+ #
232
+ # Params:
233
+ # - value {Symbol} - hemisphere
234
+ #
235
+ # Raises:
236
+ # - {ArgumentError} on wrong hemisphere
237
+ #
238
+ def validate_hemisphere(value)
239
+ raise ArgumentError.new('wrong hemisphere') unless HEMISPHERES.include?(value)
240
+ end
241
+ end
242
+ end
243
+ end