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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +60 -0
- data/Rakefile +1 -0
- data/lib/libgeo.rb +24 -0
- data/lib/libgeo/coordinate.rb +187 -0
- data/lib/libgeo/coordinate/class_methods.rb +243 -0
- data/lib/libgeo/coordinate/hemi_helpers.rb +80 -0
- data/lib/libgeo/coordinate/presenters.rb +67 -0
- data/lib/libgeo/formatter.rb +61 -0
- data/lib/libgeo/formatter/compiler.rb +128 -0
- data/lib/libgeo/formatter/evaluator.rb +53 -0
- data/lib/libgeo/latitude.rb +36 -0
- data/lib/libgeo/longitude.rb +48 -0
- data/lib/libgeo/version.rb +3 -0
- data/libgeo.gemspec +23 -0
- data/spec/shared_examples/coordinate_attrs.rb +74 -0
- data/spec/shared_examples/coordinate_inputs.rb +127 -0
- data/spec/shared_examples/coordinate_presenters.rb +16 -0
- data/spec/shared_examples/coordinate_validation.rb +89 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/unit/libgeo/coordinate_spec.rb +82 -0
- data/spec/unit/libgeo/formatter/compiler_spec.rb +76 -0
- data/spec/unit/libgeo/formatter/evaluator_spec.rb +69 -0
- data/spec/unit/libgeo/formatter_spec.rb +34 -0
- data/spec/unit/libgeo/latitude_spec.rb +378 -0
- data/spec/unit/libgeo/longitude_spec.rb +377 -0
- metadata +114 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Libgeo
|
4
|
+
class Coordinate
|
5
|
+
module HemiHelpers
|
6
|
+
|
7
|
+
##
|
8
|
+
# Make coordinate northen
|
9
|
+
#
|
10
|
+
def north!
|
11
|
+
return unless valid_hemisphere?(:N)
|
12
|
+
@hemisphere = :N
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Make coordinate southern
|
17
|
+
#
|
18
|
+
def south!
|
19
|
+
return unless valid_hemisphere?(:S)
|
20
|
+
@hemisphere = :S
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Make coordinate western
|
25
|
+
#
|
26
|
+
def west!
|
27
|
+
return unless valid_hemisphere?(:W)
|
28
|
+
@hemisphere = :W
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Make coordinate eastern
|
33
|
+
#
|
34
|
+
def east!
|
35
|
+
return unless valid_hemisphere?(:E)
|
36
|
+
@hemisphere = :E
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Check if coordinate in northen hemisphere
|
41
|
+
#
|
42
|
+
# Returns: {Boolean}
|
43
|
+
#
|
44
|
+
def north?
|
45
|
+
normalize_hemi
|
46
|
+
hemisphere == :N
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Check if coordinate in southern hemisphere
|
51
|
+
#
|
52
|
+
# Returns: {Boolean}
|
53
|
+
#
|
54
|
+
def south?
|
55
|
+
normalize_hemi
|
56
|
+
hemisphere == :S
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Check if coordinate in western hemisphere
|
61
|
+
#
|
62
|
+
# Returns: {Boolean}
|
63
|
+
#
|
64
|
+
def west?
|
65
|
+
normalize_hemi
|
66
|
+
hemisphere == :W
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Check if coordinate in eastern hemisphere
|
71
|
+
#
|
72
|
+
# Returns: {Boolean}
|
73
|
+
#
|
74
|
+
def east?
|
75
|
+
normalize_hemi
|
76
|
+
hemisphere == :E
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Libgeo
|
4
|
+
class Coordinate
|
5
|
+
module Presenters
|
6
|
+
|
7
|
+
##
|
8
|
+
# Represent coordinate as float
|
9
|
+
#
|
10
|
+
# Returns: {Float} decimal coordinate
|
11
|
+
#
|
12
|
+
def to_f
|
13
|
+
(negative_hemisphere? ? -1 : 1) * (degrees + minutes_only.to_d / 60).to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Represent coordinate in NMEA format
|
18
|
+
#
|
19
|
+
# Params:
|
20
|
+
# - formatter {Formatter} custom formatter (optional, default: '%2d%2M,%H')
|
21
|
+
#
|
22
|
+
# Returns: {String} formatted NMEA coordinate
|
23
|
+
#
|
24
|
+
def to_nmea(formatter=Format::NMEA_LAT)
|
25
|
+
to_s(formatter)
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Format coordinate as string
|
30
|
+
#
|
31
|
+
# By default it uses standart DMS format
|
32
|
+
#
|
33
|
+
# Params:
|
34
|
+
# - formatter {Formatter} custom formatter (optional, default: '%2d°%2m′%S″%H')
|
35
|
+
#
|
36
|
+
# Returns: {String} formatted string
|
37
|
+
#
|
38
|
+
def to_s(formatter=Format::DMS)
|
39
|
+
formatter.format(self)
|
40
|
+
end
|
41
|
+
alias :to_dms :to_s
|
42
|
+
|
43
|
+
##
|
44
|
+
# Represent coordinate as hash
|
45
|
+
#
|
46
|
+
# Returns: {Hash} with type, degrees, minutes, seconds and hemisphere
|
47
|
+
#
|
48
|
+
def to_hash
|
49
|
+
{
|
50
|
+
type: type,
|
51
|
+
degrees: degrees,
|
52
|
+
minutes: minutes,
|
53
|
+
seconds: seconds,
|
54
|
+
hemisphere: hemisphere
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Inspect string
|
60
|
+
#
|
61
|
+
def inspect
|
62
|
+
"#<#{self.class} hemisphere=#{hemisphere} degrees=#{degrees} minutes=#{minutes} seconds=#{seconds}>"
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'libgeo/formatter/compiler'
|
4
|
+
require 'libgeo/formatter/evaluator'
|
5
|
+
|
6
|
+
module Libgeo
|
7
|
+
|
8
|
+
##
|
9
|
+
# Class: coordinate formatter
|
10
|
+
#
|
11
|
+
# Compiles sprintf-like patterns and present
|
12
|
+
# given Coordinate as a DMS string
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# coord = Latitude.new(:N, 48, 4, 15.7)
|
17
|
+
# my_format = Formatter.new('%H%d°%2M′')
|
18
|
+
# my_format.format(coord) # => 'N48°04.26306′'
|
19
|
+
#
|
20
|
+
#
|
21
|
+
class Formatter
|
22
|
+
|
23
|
+
##
|
24
|
+
# Source pattern
|
25
|
+
#
|
26
|
+
attr_reader :pattern
|
27
|
+
|
28
|
+
##
|
29
|
+
# Create new string representing coordinate
|
30
|
+
#
|
31
|
+
# Params:
|
32
|
+
# - coordinate {Latitude|Longitude} source coordinate
|
33
|
+
# - evaluator {Class} private
|
34
|
+
#
|
35
|
+
# Returns: {String} formatted string
|
36
|
+
#
|
37
|
+
def format(coordinate, evaluator=Evaluator)
|
38
|
+
evaluator.new(coordinate).instance_eval(expression)
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Represent instance as string
|
43
|
+
#
|
44
|
+
def inspect
|
45
|
+
"#<Libgeo::Formatter pattern=(#{pattern})>"
|
46
|
+
end
|
47
|
+
alias :to_s :inspect
|
48
|
+
alias :pretty_inspect :inspect
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :expression
|
53
|
+
|
54
|
+
def initialize(pattern)
|
55
|
+
@pattern = pattern.freeze
|
56
|
+
@expression = Compiler.new(pattern).compile
|
57
|
+
freeze
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module Libgeo
|
6
|
+
class Formatter
|
7
|
+
|
8
|
+
##
|
9
|
+
# Class: template compiler
|
10
|
+
#
|
11
|
+
# Tooks the sprintf-like pattern string
|
12
|
+
# and compiles it to the ruby code
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# Compiler.new('%d:%2m').compile
|
17
|
+
#
|
18
|
+
class Compiler
|
19
|
+
|
20
|
+
##
|
21
|
+
# Constant: set of keywords
|
22
|
+
#
|
23
|
+
# Defines keywords and corresponding ruby methods
|
24
|
+
# where `c` is a given coordinate object
|
25
|
+
#
|
26
|
+
KEYWORDS = {
|
27
|
+
'd' => 'c.deg',
|
28
|
+
'm' => 'c.mins',
|
29
|
+
'M' => 'c.minutes_only',
|
30
|
+
's' => 'c.secs.to_i',
|
31
|
+
'S' => 'c.secs',
|
32
|
+
'h' => 'c.hemi.to_s.downcase',
|
33
|
+
'H' => 'c.hemi'
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
TOKEN_REG = /%\d?\w/.freeze
|
37
|
+
STRING_REG = /./.freeze
|
38
|
+
|
39
|
+
##
|
40
|
+
# Resulted ruby expression
|
41
|
+
#
|
42
|
+
attr_reader :result
|
43
|
+
|
44
|
+
##
|
45
|
+
# Compile the template
|
46
|
+
#
|
47
|
+
# Returns: {String} ruby expression
|
48
|
+
#
|
49
|
+
def compile
|
50
|
+
return result if result
|
51
|
+
|
52
|
+
scan until scanner.eos?
|
53
|
+
|
54
|
+
finalize_string
|
55
|
+
|
56
|
+
@result = parts.join(' << ').freeze
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :scanner, :parts
|
62
|
+
|
63
|
+
##
|
64
|
+
# Constructor:
|
65
|
+
#
|
66
|
+
# Params:
|
67
|
+
# - pattern {String} pattern string
|
68
|
+
#
|
69
|
+
def initialize(pattern)
|
70
|
+
@scanner = StringScanner.new(pattern)
|
71
|
+
@parts = []
|
72
|
+
@curr_string = ''
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Private: scan for subsrings and process them
|
77
|
+
#
|
78
|
+
def scan
|
79
|
+
if key = scanner.scan(TOKEN_REG)
|
80
|
+
finalize_string
|
81
|
+
process_token(key)
|
82
|
+
else
|
83
|
+
@curr_string << scanner.scan(STRING_REG)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Private: process % tokens
|
89
|
+
#
|
90
|
+
# When % token given - we split it to the parts: key and pad.
|
91
|
+
# For example `%2m` will have key `m` and pad `2`
|
92
|
+
# Then we add corresponding ruby code to buffer
|
93
|
+
# and wrap it with `pad()` function if pad given.
|
94
|
+
# For `%2m` we will receive `pad(c.mins.to_s, 2)` instruction
|
95
|
+
#
|
96
|
+
# Params:
|
97
|
+
# - key {String} matched % substring
|
98
|
+
#
|
99
|
+
def process_token(token)
|
100
|
+
key = token[-1]
|
101
|
+
pad = token[1..-2].to_i
|
102
|
+
|
103
|
+
instr = KEYWORDS[key] + '.to_s'
|
104
|
+
instr = "pad(#{instr}, #{pad})" if pad > 0
|
105
|
+
|
106
|
+
parts << instr
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Private: finalize plain string
|
111
|
+
#
|
112
|
+
# To avoid redundant string operations in the final
|
113
|
+
# expression, we store each matched substring in the temporary
|
114
|
+
# instance variable. When the next % token given - we
|
115
|
+
# concatenate the content of this variable and clean it.
|
116
|
+
# As a result, we have string 'foo' instead of 'f' << 'o' << 'o'
|
117
|
+
#
|
118
|
+
def finalize_string
|
119
|
+
unless @curr_string.empty?
|
120
|
+
parts << %('#{@curr_string}')
|
121
|
+
@curr_string = ''
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Libgeo
|
4
|
+
class Formatter
|
5
|
+
|
6
|
+
##
|
7
|
+
# Class: sandbox for evaluating compiled expressions
|
8
|
+
#
|
9
|
+
class Evaluator < BasicObject
|
10
|
+
|
11
|
+
##
|
12
|
+
# Coordinate
|
13
|
+
#
|
14
|
+
attr_reader :c
|
15
|
+
|
16
|
+
##
|
17
|
+
# Add leading zeroes if needed
|
18
|
+
#
|
19
|
+
# Adds leading zeroes in case if int part of
|
20
|
+
# number doesn't have needed length
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
#
|
24
|
+
# pad('3.14', 2) # => '03.14'
|
25
|
+
# pad('13.14', 2) # => '13.14'
|
26
|
+
# pad(42, 3) # => '042'
|
27
|
+
#
|
28
|
+
# Params:
|
29
|
+
# - val {String|Numeric} number to process
|
30
|
+
# - num {Fixnum} length of int part
|
31
|
+
#
|
32
|
+
# Returns: {String} number with or without zeroes added
|
33
|
+
#
|
34
|
+
def pad(val, i)
|
35
|
+
val = val.to_s.split('.')
|
36
|
+
val[0] = add_zeroes(val[0], i)
|
37
|
+
val.join('.')
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def initialize(coordinate)
|
43
|
+
@c = coordinate
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_zeroes(val, i)
|
47
|
+
num = i - val.length
|
48
|
+
num = num > 0 ? num : 0
|
49
|
+
('0' * num) + val
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Libgeo
|
4
|
+
class Latitude < Coordinate
|
5
|
+
|
6
|
+
##
|
7
|
+
# Constant: Valid hemisphere values
|
8
|
+
#
|
9
|
+
HEMISPHERES = Set.new([:N, :S]).freeze
|
10
|
+
|
11
|
+
POSITIVE_HEMISPHERE = :N
|
12
|
+
NEGATIVE_HEMISPHERE = :S
|
13
|
+
|
14
|
+
MAX_DEGREES = 90
|
15
|
+
|
16
|
+
##
|
17
|
+
# Coordinate type
|
18
|
+
#
|
19
|
+
def type
|
20
|
+
:latitude
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
CORRECTIONS = {
|
26
|
+
N: Set.new(['N', 'north', :north]).freeze,
|
27
|
+
S: Set.new(['S', 'south', :south]).freeze
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
DIRECTIONS = {
|
31
|
+
:> => :N,
|
32
|
+
:< => :S
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Libgeo
|
4
|
+
class Longitude < Coordinate
|
5
|
+
|
6
|
+
##
|
7
|
+
# Constant: Valid hemisphere values
|
8
|
+
#
|
9
|
+
HEMISPHERES = Set.new([:W, :E]).freeze
|
10
|
+
|
11
|
+
POSITIVE_HEMISPHERE = :E
|
12
|
+
NEGATIVE_HEMISPHERE = :W
|
13
|
+
|
14
|
+
MAX_DEGREES = 180
|
15
|
+
|
16
|
+
##
|
17
|
+
# Coordinate type
|
18
|
+
#
|
19
|
+
def type
|
20
|
+
:longitude
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Represent coordinate in NMEA format
|
25
|
+
#
|
26
|
+
# Params:
|
27
|
+
# - formatter {Formatter} custom formatter (optional, default: '%2d%3M,%H')
|
28
|
+
#
|
29
|
+
# Returns: {String} formatted NMEA coordinate
|
30
|
+
#
|
31
|
+
def to_nmea(formatter=Format::NMEA_LON)
|
32
|
+
to_s(formatter)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
CORRECTIONS = {
|
38
|
+
W: Set.new(['W', 'west', :west]).freeze,
|
39
|
+
E: Set.new(['E', 'east', :east]).freeze
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
DIRECTIONS = {
|
43
|
+
:> => :E,
|
44
|
+
:< => :W
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|