latitude 0.1.0 → 0.2.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.
- data/README.md +29 -2
- data/lib/latitude.rb +20 -6
- data/lib/latitude/coordinate.rb +108 -0
- data/lib/latitude/version.rb +1 -1
- data/lib/latitude/vincenty.rb +2 -2
- data/spec/coordinate_spec.rb +170 -0
- metadata +5 -2
data/README.md
CHANGED
@@ -27,8 +27,35 @@ Or install it yourself as:
|
|
27
27
|
|
28
28
|
Note that coordinates are positive for N/E and negative for S/W.
|
29
29
|
|
30
|
-
|
31
|
-
|
30
|
+
### Using Coordinate objects
|
31
|
+
|
32
|
+
A `Coordinate` takes in a `:latitude` and `:longitude` in either decimal
|
33
|
+
or DMS format. For example,
|
34
|
+
|
35
|
+
```
|
36
|
+
coordinate = Coordinate.new(:latitude => "10 S",
|
37
|
+
:longitude => "9° 30′ E")
|
38
|
+
coordinate.latitdue #=> -10.0
|
39
|
+
coordinate.longitude #=> 9.5
|
40
|
+
```
|
41
|
+
|
42
|
+
`Coordinate`s have multiple helper methods:
|
43
|
+
|
44
|
+
`#great_circle_distance_to(final_coordinate)` calculates the great
|
45
|
+
circle distance between coordinates.
|
46
|
+
|
47
|
+
`#initial_bearing_to(final_coordinate)` calculates the initial bearing
|
48
|
+
if traveling along a great circle route to another coordinate.
|
49
|
+
|
50
|
+
`#final_bearing_to(initial_coordinate)` calculates the final bearing if
|
51
|
+
traveling along a great circle route from another coordinate.
|
52
|
+
|
53
|
+
### Without Coordinate objects
|
54
|
+
|
55
|
+
```
|
56
|
+
Latitude.great_circle_distance(start_latitude, start_longitude, end_latitude,
|
57
|
+
end_longitude)
|
58
|
+
```
|
32
59
|
|
33
60
|
Calculates the great circle distance in kilometers between two
|
34
61
|
coordinates.
|
data/lib/latitude.rb
CHANGED
@@ -1,24 +1,38 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
# encoding: utf-8
|
1
3
|
require "latitude/version"
|
2
4
|
require "latitude/vincenty"
|
5
|
+
require "latitude/coordinate"
|
3
6
|
|
4
7
|
module Latitude
|
5
8
|
extend self
|
6
9
|
def great_circle_distance(start_latitude, start_longitude, end_latitude, end_longitude)
|
10
|
+
start_coordinate = Coordinate.new(:latitude => start_latitude,
|
11
|
+
:longitude => start_longitude)
|
12
|
+
end_coordinate = Coordinate.new(:latitude => end_latitude,
|
13
|
+
:longitude => end_longitude)
|
7
14
|
# in kilometers
|
8
|
-
m_distance =
|
9
|
-
end_latitude, end_longitude)
|
15
|
+
m_distance = start_coordinate.great_circle_distance_to(end_coordinate)
|
10
16
|
|
11
17
|
return m_distance / 1000.0
|
12
18
|
end
|
13
19
|
|
14
20
|
def initial_bearing(start_latitude, start_longitude, end_latitude, end_longitude)
|
15
|
-
|
16
|
-
|
21
|
+
start_coordinate = Coordinate.new(:latitude => start_latitude,
|
22
|
+
:longitude => start_longitude)
|
23
|
+
end_coordinate = Coordinate.new(:latitude => end_latitude,
|
24
|
+
:longitude => end_longitude)
|
25
|
+
|
26
|
+
start_coordinate.initial_bearing_to(end_coordinate)
|
17
27
|
end
|
18
28
|
|
19
29
|
def final_bearing(start_latitude, start_longitude, end_latitude, end_longitude)
|
20
|
-
|
21
|
-
|
30
|
+
start_coordinate = Coordinate.new(:latitude => start_latitude,
|
31
|
+
:longitude => start_longitude)
|
32
|
+
end_coordinate = Coordinate.new(:latitude => end_latitude,
|
33
|
+
:longitude => end_longitude)
|
34
|
+
|
35
|
+
end_coordinate.final_bearing_from(start_coordinate)
|
22
36
|
end
|
23
37
|
|
24
38
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
class Coordinate
|
5
|
+
COORDINATE_CHARS = "0-9.\'\"°′″"
|
6
|
+
attr_reader :latitude, :longitude
|
7
|
+
|
8
|
+
def initialize(params = {})
|
9
|
+
self.latitude = params[:latitude]
|
10
|
+
self.longitude = params[:longitude]
|
11
|
+
end
|
12
|
+
|
13
|
+
def latitude=(val)
|
14
|
+
@latitude = convert_degree_input_to_decimal(val, ["N","S"], ["S"])
|
15
|
+
end
|
16
|
+
|
17
|
+
def longitude=(val)
|
18
|
+
@longitude = convert_degree_input_to_decimal(val, ["E","W"], ["W"])
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid?
|
22
|
+
latitude && longitude && (latitude.abs <= 90) && (longitude.abs <= 180)
|
23
|
+
end
|
24
|
+
|
25
|
+
def great_circle_distance_to(final_coordinate)
|
26
|
+
Vincenty.great_circle_distance(latitude, longitude,
|
27
|
+
final_coordinate.latitude,
|
28
|
+
final_coordinate.longitude)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initial_bearing_to(final_coordinate)
|
32
|
+
Vincenty.initial_bearing(latitude, longitude,
|
33
|
+
final_coordinate.latitude,
|
34
|
+
final_coordinate.longitude)
|
35
|
+
end
|
36
|
+
|
37
|
+
def final_bearing_from(start_coordinate)
|
38
|
+
Vincenty.final_bearing(start_coordinate.latitude,
|
39
|
+
start_coordinate.longitude,
|
40
|
+
latitude, longitude)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def convert_degree_input_to_decimal(input, valid_directions, negative_directions)
|
45
|
+
return if input.nil? || (input.is_a?(String) && input.empty?)
|
46
|
+
input.strip! if input.is_a? String
|
47
|
+
|
48
|
+
return Float(input) if is_number?(input)
|
49
|
+
|
50
|
+
raise ArgumentError unless input.gsub(valid_char_regexp(valid_directions), "").empty?
|
51
|
+
raise ArgumentError unless valid_end_char_regexp(valid_directions).match(input)
|
52
|
+
|
53
|
+
if valid_directions.map(&:upcase).include? input[-1].upcase
|
54
|
+
direction = input[-1].upcase
|
55
|
+
convert_to_negative = negative_directions.map(&:upcase).include? direction
|
56
|
+
input = input[0...-1].strip
|
57
|
+
else
|
58
|
+
convert_to_negative = false
|
59
|
+
end
|
60
|
+
|
61
|
+
return (convert_to_negative ? -1 : 1) * Float(input) if is_number?(input)
|
62
|
+
|
63
|
+
degrees = minutes = seconds = 0
|
64
|
+
# first, get the degrees
|
65
|
+
if degree_location = input.index(/°/)
|
66
|
+
degree_input = input[0...degree_location].strip
|
67
|
+
input = input[degree_location+1..-1].strip
|
68
|
+
degrees = Float(degree_input)
|
69
|
+
end
|
70
|
+
|
71
|
+
if minute_location = input.index(/[′\']/)
|
72
|
+
minute_input = input[0...minute_location].strip
|
73
|
+
input = input[minute_location+1..-1].strip
|
74
|
+
minutes = Float(minute_input)
|
75
|
+
end
|
76
|
+
|
77
|
+
if second_location = input.index(/[″\"]/)
|
78
|
+
second_input = input[0...second_location].strip
|
79
|
+
input = input[second_location+1..-1].strip
|
80
|
+
seconds = Float(second_input)
|
81
|
+
end
|
82
|
+
|
83
|
+
decimal_input = degrees + minutes/60 + seconds/3600
|
84
|
+
|
85
|
+
return (convert_to_negative ? -1 : 1) * decimal_input
|
86
|
+
end
|
87
|
+
|
88
|
+
def is_number?(val)
|
89
|
+
begin
|
90
|
+
Float(val)
|
91
|
+
return true
|
92
|
+
rescue
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def basic_char_regexp
|
98
|
+
Regexp.new("[" + COORDINATE_CHARS + "]")
|
99
|
+
end
|
100
|
+
|
101
|
+
def valid_char_regexp(additional_chars = [])
|
102
|
+
Regexp.new("[" + COORDINATE_CHARS + additional_chars.join + ", ]")
|
103
|
+
end
|
104
|
+
|
105
|
+
def valid_end_char_regexp(additional_chars = [])
|
106
|
+
Regexp.new("[" + COORDINATE_CHARS + additional_chars.join + "$]")
|
107
|
+
end
|
108
|
+
end
|
data/lib/latitude/version.rb
CHANGED
data/lib/latitude/vincenty.rb
CHANGED
@@ -44,8 +44,8 @@ private
|
|
44
44
|
|
45
45
|
def iterative_solver(phi_1, lambda_1, phi_2, lambda_2, a, b, f)
|
46
46
|
l = lambda_2 - lambda_1
|
47
|
-
sin_u1, cos_u1
|
48
|
-
sin_u2, cos_u2
|
47
|
+
sin_u1, cos_u1 = get_trig_trio(phi_1, f)
|
48
|
+
sin_u2, cos_u2 = get_trig_trio(phi_2, f)
|
49
49
|
|
50
50
|
lam = l
|
51
51
|
iterations = 0
|
@@ -0,0 +1,170 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Coordinate do
|
6
|
+
let(:coordinate) { Coordinate.new(:latitude => 0, :longitude => 0) }
|
7
|
+
let(:blank_coordinate) { Coordinate.new }
|
8
|
+
|
9
|
+
describe "initialization" do
|
10
|
+
it "should initialize with provided parameters" do
|
11
|
+
new_coordinate = Coordinate.new(:latitude => 10,
|
12
|
+
:longitude => 20)
|
13
|
+
expect(new_coordinate.latitude).to eq(10)
|
14
|
+
expect(new_coordinate.longitude).to eq(20)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "coordinate parsing" do
|
19
|
+
it "should accept a simple string for latitudes and longitudes" do
|
20
|
+
coordinate.latitude = "-10"
|
21
|
+
coordinate.longitude = "10"
|
22
|
+
expect(coordinate.latitude).to eq(-10)
|
23
|
+
expect(coordinate.longitude).to eq(10)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should accept a decimal string for latitudes and longitudes" do
|
27
|
+
coordinate.latitude = "-10.5"
|
28
|
+
coordinate.longitude = "10.5"
|
29
|
+
expect(coordinate.latitude).to eq(-10.5)
|
30
|
+
expect(coordinate.longitude).to eq(10.5)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "with cardinal directions" do
|
34
|
+
it "should save the latitude if given N as cardinal direction" do
|
35
|
+
coordinate.latitude = "10.5 N"
|
36
|
+
expect(coordinate.latitude).to eq(10.5)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should make the latitude negative if given S as a cardinal direction" do
|
40
|
+
coordinate.latitude = "10.5 S"
|
41
|
+
expect(coordinate.latitude).to eq(-10.5)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should handle spaces around things" do
|
45
|
+
coordinate.latitude = " 10.5 N "
|
46
|
+
expect(coordinate.latitude).to eq(10.5)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should throw an exception if given E or W as a direction for latitude" do
|
50
|
+
expect { coordinate.latitude = "10 E" }.to raise_error(ArgumentError)
|
51
|
+
expect { coordinate.latitude = "10 W" }.to raise_error(ArgumentError)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should save the longitude if given E as a cardinal direction" do
|
55
|
+
coordinate.longitude = "10.5 E"
|
56
|
+
expect(coordinate.longitude).to eq(10.5)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should make the longitude negative if given W as a cardinal direction" do
|
60
|
+
coordinate.longitude = "10.5 W"
|
61
|
+
expect(coordinate.longitude).to eq(-10.5)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should throw an exception if given N or S as a direction for longitude" do
|
65
|
+
expect { coordinate.longitude = "10 N" }.to raise_error(ArgumentError)
|
66
|
+
expect { coordinate.longitude = "10 S" }.to raise_error(ArgumentError)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "parsing degrees minutes and seconds" do
|
71
|
+
it "should take simple degrees input and set as decimal" do
|
72
|
+
coordinate.latitude = "50°"
|
73
|
+
expect(coordinate.latitude).to eq(50)
|
74
|
+
|
75
|
+
coordinate.longitude = "10.5 °"
|
76
|
+
expect(coordinate.longitude).to eq(10.5)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should take degrees and minutes" do
|
80
|
+
coordinate.latitude = "50° 0'"
|
81
|
+
expect(coordinate.latitude).to eq(50)
|
82
|
+
|
83
|
+
coordinate.latitude = "50° 0′"
|
84
|
+
expect(coordinate.latitude).to eq(50)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should convert minutes to 1/60ths of degrees" do
|
88
|
+
coordinate.latitude = "50° 30'"
|
89
|
+
expect(coordinate.latitude).to eq(50.5)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should take degrees, minutes, and seconds" do
|
93
|
+
coordinate.latitude = "50° 0' 0\""
|
94
|
+
expect(coordinate.latitude).to eq(50)
|
95
|
+
|
96
|
+
coordinate.latitude = "50° 0' 0″"
|
97
|
+
expect(coordinate.latitude).to eq(50)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should take degrees and seconds" do
|
101
|
+
coordinate.latitude = "50° 0\""
|
102
|
+
expect(coordinate.latitude).to eq(50)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should convert seconds to 1/60ths of minutes" do
|
106
|
+
coordinate.latitude = "50° 0' 36\""
|
107
|
+
expect(coordinate.latitude).to eq(50.01)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "validation" do
|
113
|
+
it "should be valid if given valid lat and long" do
|
114
|
+
expect(coordinate).to be_valid
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should be invalid unless latitude provided" do
|
118
|
+
blank_coordinate.longitude = 0
|
119
|
+
expect(blank_coordinate).to_not be_valid
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should be invalid unless longitude provided" do
|
123
|
+
blank_coordinate.latitude = 0
|
124
|
+
expect(blank_coordinate).to_not be_valid
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should be invalid unless latitude is between -90 and 90" do
|
128
|
+
coordinate.latitude = -100
|
129
|
+
expect(blank_coordinate).to_not be_valid
|
130
|
+
|
131
|
+
coordinate.latitude = 100
|
132
|
+
expect(blank_coordinate).to_not be_valid
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should be invalid unless longitude is between -180 and 180" do
|
136
|
+
coordinate.longitude = -190
|
137
|
+
expect(blank_coordinate).to_not be_valid
|
138
|
+
|
139
|
+
coordinate.longitude = 190
|
140
|
+
expect(blank_coordinate).to_not be_valid
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "#great_circle_distance_to" do
|
145
|
+
it "should call the vincenty formulae to calculate the great circle distances" do
|
146
|
+
expect(Vincenty).to receive(:great_circle_distance).with(10, 20, 30, 40)
|
147
|
+
start_coordinate = Coordinate.new(:latitude => 10, :longitude => 20)
|
148
|
+
end_coordinate = Coordinate.new(:latitude => 30, :longitude => 40)
|
149
|
+
start_coordinate.great_circle_distance_to(end_coordinate)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "#initial_bearing_to" do
|
154
|
+
it "should call the vincenty formulae to calculate the initial bearing" do
|
155
|
+
expect(Vincenty).to receive(:initial_bearing).with(10, 20, 30, 40)
|
156
|
+
start_coordinate = Coordinate.new(:latitude => 10, :longitude => 20)
|
157
|
+
end_coordinate = Coordinate.new(:latitude => 30, :longitude => 40)
|
158
|
+
start_coordinate.initial_bearing_to(end_coordinate)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe "#final_bearing_from" do
|
163
|
+
it "should call the vincenty formulae to calculate the final bearing" do
|
164
|
+
expect(Vincenty).to receive(:final_bearing).with(10, 20, 30, 40)
|
165
|
+
start_coordinate = Coordinate.new(:latitude => 10, :longitude => 20)
|
166
|
+
end_coordinate = Coordinate.new(:latitude => 30, :longitude => 40)
|
167
|
+
end_coordinate.final_bearing_from(start_coordinate)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: latitude
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-06-
|
12
|
+
date: 2014-06-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -77,8 +77,10 @@ files:
|
|
77
77
|
- Rakefile
|
78
78
|
- latitude.gemspec
|
79
79
|
- lib/latitude.rb
|
80
|
+
- lib/latitude/coordinate.rb
|
80
81
|
- lib/latitude/version.rb
|
81
82
|
- lib/latitude/vincenty.rb
|
83
|
+
- spec/coordinate_spec.rb
|
82
84
|
- spec/latitude_spec.rb
|
83
85
|
- spec/spec_helper.rb
|
84
86
|
homepage: https://github.com/umtrey/latitude-gem
|
@@ -107,5 +109,6 @@ signing_key:
|
|
107
109
|
specification_version: 3
|
108
110
|
summary: Calculates distances between two geographic coordinates.
|
109
111
|
test_files:
|
112
|
+
- spec/coordinate_spec.rb
|
110
113
|
- spec/latitude_spec.rb
|
111
114
|
- spec/spec_helper.rb
|