latitude 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
- `Latitude.great_circle_distance(start_latitude, start_longitude, end_latitude,
31
- end_longitude)`
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.
@@ -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 = Vincenty.great_circle_distance(start_latitude, start_longitude,
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
- Vincenty.initial_bearing(start_latitude, start_longitude,
16
- end_latitude, end_longitude)
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
- Vincenty.final_bearing(start_latitude, start_longitude,
21
- end_latitude, end_longitude)
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
@@ -1,3 +1,3 @@
1
1
  module Latitude
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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, tan_u1 = get_trig_trio(phi_1, f)
48
- sin_u2, cos_u2, tan_u2 = get_trig_trio(phi_2, f)
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.1.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-15 00:00:00.000000000 Z
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