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