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 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