vector_geometry 0.0.1
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/COPYING +20 -0
- data/Gemfile +9 -0
- data/README.md +153 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/geometry/geometry.rb +47 -0
- data/lib/geometry/spheroid/base.rb +106 -0
- data/lib/geometry/spheroid/sphere.rb +29 -0
- data/lib/geometry/vector/earth_vector.rb +7 -0
- data/lib/geometry/vector/geo_vector.rb +234 -0
- data/lib/geometry/vector/vector.rb +223 -0
- data/lib/vector_geometry.rb +6 -0
- data/spec/exercises_spec.rb +210 -0
- data/spec/geo_vector_spec.rb +38 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/vector_spec.rb +274 -0
- data/vector_geometry.gemspec +61 -0
- metadata +114 -0
@@ -0,0 +1,223 @@
|
|
1
|
+
|
2
|
+
module Geometry
|
3
|
+
|
4
|
+
class Vector
|
5
|
+
|
6
|
+
# Return an x/y vector based on a magnitude and a heading.
|
7
|
+
def self.from_polar(magnitude, angle, options = {})
|
8
|
+
angle = Geometry.deg_to_rad(angle) if options[:unit] = :deg
|
9
|
+
|
10
|
+
y = Math.sin(angle) * magnitude
|
11
|
+
x = Math.cos(angle) * magnitude
|
12
|
+
|
13
|
+
self.new(x,y)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :x, :y, :z
|
17
|
+
|
18
|
+
def initialize(x, y, z = 0)
|
19
|
+
@x = x.to_f
|
20
|
+
@y = y.to_f
|
21
|
+
@z = z.to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(other)
|
25
|
+
self.class.new(@x + other.x, @y + other.y, @z + other.z)
|
26
|
+
end
|
27
|
+
alias :+ :add
|
28
|
+
|
29
|
+
def subtract(other)
|
30
|
+
self.class.new(@x - other.x, @y - other.y, @z - other.z)
|
31
|
+
end
|
32
|
+
alias :- :subtract
|
33
|
+
|
34
|
+
def multiply(n)
|
35
|
+
scale(n.to_f)
|
36
|
+
end
|
37
|
+
alias :* :multiply
|
38
|
+
|
39
|
+
def divide(n)
|
40
|
+
scale(1.0/n.to_f)
|
41
|
+
end
|
42
|
+
alias :/ :divide
|
43
|
+
|
44
|
+
# Return the magnitude of self.
|
45
|
+
def magnitude
|
46
|
+
Math.sqrt(@x ** 2 + @y ** 2 + @z ** 2)
|
47
|
+
end
|
48
|
+
alias :r :magnitude
|
49
|
+
|
50
|
+
def scale(scalar)
|
51
|
+
self.class.new(@x * scalar, @y * scalar, @z * scalar)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Normalize self, that is, return the unit vector with the same direction as self.
|
55
|
+
def normalize
|
56
|
+
divide(magnitude)
|
57
|
+
end
|
58
|
+
|
59
|
+
def heading
|
60
|
+
Math.atan2(@y,@x)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the dot product of self and the passed in vector.
|
64
|
+
def dot(other)
|
65
|
+
return (@x * other.x) + (@y * other.y) + (@z * other.z)
|
66
|
+
end
|
67
|
+
alias :scalar_product :dot
|
68
|
+
|
69
|
+
# Return the cross product of self and the passed in vector.
|
70
|
+
def cross(other)
|
71
|
+
new_x = (@y * other.z) - (@z * other.y)
|
72
|
+
new_y = (@z * other.x) - (@x * other.z)
|
73
|
+
new_z = (@x * other.y) - (@y * other.x)
|
74
|
+
|
75
|
+
self.class.new(new_x, new_y, new_z)
|
76
|
+
end
|
77
|
+
alias :vector_product :cross
|
78
|
+
|
79
|
+
|
80
|
+
# Return the magnitude of the cross product of self and the passed in vector.
|
81
|
+
def cross_length(other)
|
82
|
+
# cross(other).magnitude
|
83
|
+
|
84
|
+
# It is more efficient to not create a new Vector object since we are only returning
|
85
|
+
# a scalar value
|
86
|
+
new_x = (@y * other.z) - (@z * other.y)
|
87
|
+
new_y = (@z * other.x) - (@x * other.z)
|
88
|
+
new_z = (@x * other.y) - (@y * other.x)
|
89
|
+
|
90
|
+
Math.sqrt(new_x ** 2 + new_y ** 2 + new_z ** 2)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return the unit vector of the cross product of self and the passed in vector.
|
94
|
+
def cross_normal(other)
|
95
|
+
cross(other).normalize
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return the angle (in radians) between self and the passed in vector.
|
99
|
+
def angle(other)
|
100
|
+
|
101
|
+
# Two options here:
|
102
|
+
#
|
103
|
+
# 1. Math.atan2(other.cross_length(self), dot(other))
|
104
|
+
#
|
105
|
+
# This is stable but slower (x 1.5)
|
106
|
+
#
|
107
|
+
# 2. Math.acos(dot(other) / (r * other.r))
|
108
|
+
#
|
109
|
+
# This is faster but unstable around 0 and pi where the gradient of acos approaches
|
110
|
+
# infinity. An alternative way to view this is that the gradient of cos approaches
|
111
|
+
# zero and small differences in angle can be indistinguishable at some number of
|
112
|
+
# decimal places.
|
113
|
+
#
|
114
|
+
|
115
|
+
# Math.acos(dot(other) / (r * other.r))
|
116
|
+
Math.atan2(other.cross_length(self), dot(other))
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return the cartesian distance between self and the passed vector
|
120
|
+
def distance(other)
|
121
|
+
(other - self).magnitude.abs
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns true of the passed in vector is perpendicular to self.
|
125
|
+
def orthogonal?(other)
|
126
|
+
dot(other) == 0
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns true if the passed in vector is parallel to self.
|
130
|
+
def parallel?(other)
|
131
|
+
cross(other).magnitude == 0
|
132
|
+
end
|
133
|
+
|
134
|
+
def ==(other)
|
135
|
+
@x == other.x && @y == other.y && @z == other.z
|
136
|
+
end
|
137
|
+
|
138
|
+
# Calculate the distance of self from the infinite line passing through the two passed in points.
|
139
|
+
def distance_from_line(point_a,point_b)
|
140
|
+
|
141
|
+
# Define the line as the vector between two points
|
142
|
+
line_vector = point_b - point_a
|
143
|
+
|
144
|
+
# Define a second vector representing the distance between self and the line start
|
145
|
+
point_vector = self - point_a
|
146
|
+
|
147
|
+
# The magnitude of the cross product is equal to the area of the parallelogram described
|
148
|
+
# by the two vectors. Dividing by the line length gives the perpendicular distance.
|
149
|
+
(line_vector.cross(point_vector).magnitude / line_vector.magnitude).abs
|
150
|
+
end
|
151
|
+
|
152
|
+
# Calculate the distance of self from the line segment starting and ending with the two passed in points.
|
153
|
+
def distance_from_line_segment(point_a,point_b)
|
154
|
+
|
155
|
+
# Define the line as the vector between two points
|
156
|
+
line_vector = point_b - point_a
|
157
|
+
|
158
|
+
# Define a second vector representing the distance between self and the line start
|
159
|
+
point_vector = self - point_a
|
160
|
+
|
161
|
+
# Determine if self falls within the perpendicular 'shadow' of the line by calculating
|
162
|
+
# the projection of the point vector onto the line.
|
163
|
+
#
|
164
|
+
# The dot product divided by the magnitude of the line gives the absolute projection
|
165
|
+
# of the point vector onto the line.
|
166
|
+
#
|
167
|
+
# Dividing again by the line magnitude gives the relative projection along the line,
|
168
|
+
# i.e. the ratio of the projection to the line. Values between 0-1 indicate that the
|
169
|
+
# point falls within the perpendicular shadow.
|
170
|
+
#
|
171
|
+
projection_ratio = line_vector.dot(point_vector) / line_vector.magnitude ** 2
|
172
|
+
|
173
|
+
if projection_ratio >= 1
|
174
|
+
# The point is beyond point b, calculate distance to point b
|
175
|
+
distance = (point_b - self).magnitude
|
176
|
+
elsif projection_ratio <= 0
|
177
|
+
# The point is beyond point a, calculate distance to point a
|
178
|
+
distance = (point_a - self).magnitude
|
179
|
+
else
|
180
|
+
# The point is in the shadow of the line, return the perpendicular distance
|
181
|
+
distance = line_vector.cross(point_vector).magnitude / line_vector.magnitude
|
182
|
+
end
|
183
|
+
|
184
|
+
return distance.abs
|
185
|
+
end
|
186
|
+
|
187
|
+
def distance_from_polyline(polyline)
|
188
|
+
|
189
|
+
# memoize the last processed point as both array and vector objects
|
190
|
+
last_array = polyline.first
|
191
|
+
last_vector = self.class.new(last_array[0], last_array[1], last_array[2]) # this should support 3 arguments surely?!
|
192
|
+
|
193
|
+
minimum_distance = 999999999999
|
194
|
+
|
195
|
+
polyline[1..-1].each do |vertex|
|
196
|
+
|
197
|
+
next if vertex == last_array
|
198
|
+
|
199
|
+
start_vector = last_vector
|
200
|
+
end_vector = self.class.new(vertex[0],vertex[1], vertex[2]) # this should support 3 arguments surely?!
|
201
|
+
|
202
|
+
this_segment_distance = distance_from_line_segment(start_vector, end_vector)
|
203
|
+
|
204
|
+
if(this_segment_distance < minimum_distance)
|
205
|
+
minimum_distance = this_segment_distance
|
206
|
+
end
|
207
|
+
|
208
|
+
last_array = vertex
|
209
|
+
last_vector = end_vector
|
210
|
+
end
|
211
|
+
|
212
|
+
return minimum_distance
|
213
|
+
end
|
214
|
+
|
215
|
+
def inspect
|
216
|
+
puts "[#{@x}, #{@y}, #{@z}]"
|
217
|
+
end
|
218
|
+
|
219
|
+
protected
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
# require 'geometry'
|
3
|
+
|
4
|
+
include Geometry
|
5
|
+
|
6
|
+
describe Vector do
|
7
|
+
|
8
|
+
context "30 degrees and magnitude 2" do
|
9
|
+
|
10
|
+
before do
|
11
|
+
@angle = 30 # degrees
|
12
|
+
@magnitude = 2
|
13
|
+
|
14
|
+
@vector = Vector.from_polar(@magnitude, @angle, :unit => :deg)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should calculate the x component" do
|
18
|
+
|
19
|
+
@vector.x.should be_within(0.00001).of(Math.sqrt(3))
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
it "should calculate the y component" do
|
24
|
+
|
25
|
+
@vector.y.should be_within(0.00001).of(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
context "39 degrees and magnitude 55" do
|
31
|
+
|
32
|
+
before do
|
33
|
+
@angle = 39 # degrees
|
34
|
+
@magnitude = 55
|
35
|
+
|
36
|
+
@vector = Vector.from_polar(@magnitude, @angle, :unit => :deg)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should calculate the x component" do
|
40
|
+
|
41
|
+
@vector.x.should be_within(0.01).of(42.74)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
it "should calculate the y component" do
|
46
|
+
|
47
|
+
@vector.y.should be_within(0.01).of(34.61)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
context "44 degrees and magnitude 28" do
|
53
|
+
|
54
|
+
before do
|
55
|
+
@angle = 28 # degrees
|
56
|
+
@magnitude = 44
|
57
|
+
|
58
|
+
@vector = Vector.from_polar(@magnitude, @angle, :unit => :deg)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should calculate the x component" do
|
62
|
+
|
63
|
+
@vector.x.should be_within(0.01).of(38.84)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
it "should calculate the y component" do
|
68
|
+
|
69
|
+
@vector.y.should be_within(0.01).of(20.65)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
context "2,2 plus 2,1" do
|
75
|
+
|
76
|
+
|
77
|
+
it "should calculate the resultant vector" do
|
78
|
+
|
79
|
+
vector_1 = Vector.new(2,2)
|
80
|
+
vector_2 = Vector.new(2,1)
|
81
|
+
|
82
|
+
resultant = vector_1 + vector_2
|
83
|
+
|
84
|
+
resultant.x.should eql 4.0
|
85
|
+
resultant.y.should eql 3.0
|
86
|
+
resultant.magnitude.should eql 5.0
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
context "40,0 plus 0,50" do
|
92
|
+
|
93
|
+
|
94
|
+
it "should calculate the resultant vector" do
|
95
|
+
|
96
|
+
vector_1 = Vector.new(40,0)
|
97
|
+
vector_2 = Vector.new(0,50)
|
98
|
+
|
99
|
+
resultant = vector_1 + vector_2
|
100
|
+
|
101
|
+
resultant.x.should eql 40.0
|
102
|
+
resultant.y.should eql 50.0
|
103
|
+
resultant.magnitude.should be_within(0.1).of(64.0)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
context "4,2 plus 2,5" do
|
109
|
+
|
110
|
+
|
111
|
+
it "should calculate the resultant vector" do
|
112
|
+
|
113
|
+
vector_1 = Vector.new(4,2)
|
114
|
+
vector_2 = Vector.new(2,5)
|
115
|
+
|
116
|
+
resultant = vector_1 + vector_2
|
117
|
+
|
118
|
+
resultant.x.should eql 6.0
|
119
|
+
resultant.y.should eql 7.0
|
120
|
+
resultant.magnitude.should be_within(0.02).of(9.2)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
context "2,3,4 and 5,6,7" do
|
126
|
+
|
127
|
+
it "should calucate the cross product" do
|
128
|
+
vector_1 = Vector.new(2,3,4)
|
129
|
+
vector_2 = Vector.new(5,6,7)
|
130
|
+
|
131
|
+
resultant = vector_1.cross(vector_2)
|
132
|
+
|
133
|
+
resultant.x.should eql -3.0
|
134
|
+
resultant.y.should eql 6.0
|
135
|
+
resultant.z.should eql -3.0
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
context "great circle" do
|
141
|
+
|
142
|
+
before do
|
143
|
+
@point_1 = [51.454007, -0.263672]
|
144
|
+
@point_2 = [55.862982, -4.251709]
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should calculate a great circle distance using the haversine formula" do
|
148
|
+
hv_distance = Geometry.haversine_distance(@point_1,@point_2,Geometry::Spheroid::Earth.mean_radius,:unit => :deg)
|
149
|
+
|
150
|
+
hv_distance.should be_within(0.0001).of(556.0280983558)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should calculate a great circle distance using the angle between two cartesian vectors" do
|
154
|
+
vector_1 = EarthVector.from_geographic(@point_1[0],@point_1[1])
|
155
|
+
vector_2 = EarthVector.from_geographic(@point_2[0],@point_2[1])
|
156
|
+
|
157
|
+
angle = vector_1.angle(vector_2)
|
158
|
+
vec_distance = angle * Geometry::Spheroid::Earth.mean_radius
|
159
|
+
|
160
|
+
vec_distance.should be_within(0.0000001).of(557.4229505894594)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should calculate distance between point using Pythagoras' theorum" do
|
164
|
+
|
165
|
+
# Scale longitude change according to latitude
|
166
|
+
# Perhaps taking the mean latitude between the two points would be an improvement
|
167
|
+
deg_ew = (@point_2[1] - @point_1[1]) * Math.cos(@point_1[0])
|
168
|
+
deg_ns = (@point_1[0] - @point_2[0])
|
169
|
+
|
170
|
+
# Calculate angle change using Pythagoras
|
171
|
+
deg_change = Math.sqrt(deg_ew ** 2 + deg_ns ** 2)
|
172
|
+
|
173
|
+
# Convert to radians and calculate distance
|
174
|
+
py_distance = Geometry.deg_to_rad(deg_change) * Geometry::Spheroid::Earth.mean_radius
|
175
|
+
|
176
|
+
py_distance.should be_within(0.000001).of(517.4118204446571)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "haversine and geocentric vector approaches should be equal" do
|
180
|
+
# Haversine
|
181
|
+
hv_distance = Geometry.haversine_distance(@point_1,@point_2,Geometry::Spheroid::Earth.mean_radius,:unit => :deg)
|
182
|
+
|
183
|
+
# Vector
|
184
|
+
vector_1 = EarthVector.from_geographic(@point_1[0],@point_1[1], :geocentric => true)
|
185
|
+
vector_2 = EarthVector.from_geographic(@point_2[0],@point_2[1], :geocentric => true)
|
186
|
+
vec_distance = vector_1.angle(vector_2) * Geometry::Spheroid::Earth.mean_radius
|
187
|
+
|
188
|
+
hv_distance.should be_within(0.0000001).of(vec_distance)
|
189
|
+
end
|
190
|
+
|
191
|
+
it "Pythagoras should underestimate great circle" do
|
192
|
+
# Since the pythagoras approach assumes a flat surface
|
193
|
+
|
194
|
+
# Haversine
|
195
|
+
hv_distance = Geometry.haversine_distance(@point_1,@point_2,Geometry::Spheroid::Earth.mean_radius,:unit => :deg)
|
196
|
+
|
197
|
+
# Pythagoras
|
198
|
+
deg_ew = (@point_2[1] - @point_1[1]) * Math.cos(@point_1[0])
|
199
|
+
deg_ns = (@point_1[0] - @point_2[0])
|
200
|
+
deg_change = Math.sqrt(deg_ew ** 2 + deg_ns ** 2)
|
201
|
+
py_distance = Geometry.deg_to_rad(deg_change) * Geometry::Spheroid::Earth.mean_radius
|
202
|
+
|
203
|
+
py_distance.should be < hv_distance
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should convert degrees to radians"
|
209
|
+
|
210
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Geometry::GeoVector do
|
4
|
+
|
5
|
+
it "should calculate the great circle intersection" do
|
6
|
+
point_1 = Geometry::GeoVector.from_geographic( 0.0, 0.0)
|
7
|
+
point_2 = Geometry::GeoVector.from_geographic( 0.0, 10.0)
|
8
|
+
point_3 = Geometry::GeoVector.from_geographic( 20.0, 10.0)
|
9
|
+
point_4 = Geometry::GeoVector.from_geographic(-20.0, 10.0)
|
10
|
+
|
11
|
+
intersection = Geometry::GeoVector.from_great_circle_intersection(point_1,point_2,point_3,point_4)
|
12
|
+
|
13
|
+
intersection.should be_a Geometry::GeoVector
|
14
|
+
|
15
|
+
coords = intersection.to_geographic(:unit=>:deg)
|
16
|
+
|
17
|
+
coords.first.should be_within(0.001).of(0.0)
|
18
|
+
coords.last.should be_within(0.001).of(-170.0)
|
19
|
+
|
20
|
+
antipode_coords = intersection.antipode.to_geographic(:unit=>:deg)
|
21
|
+
|
22
|
+
antipode_coords.first.should be_within(0.001).of(0.0)
|
23
|
+
antipode_coords.last.should be_within(0.001).of(10.0)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should calculate a great circle distance" do
|
27
|
+
point_1 = Geometry::EarthVector.from_geographic(0.0, 0.0)
|
28
|
+
point_2 = Geometry::EarthVector.from_geographic(0.0, 10.0)
|
29
|
+
|
30
|
+
geo_vector_estimate = point_1.great_circle_distance(point_2)
|
31
|
+
|
32
|
+
haversine_estimate = Geometry.haversine_distance([0.0, 0.0], [0.0, 10.0], Geometry::Spheroid::Earth.mean_radius, :unit => :deg)
|
33
|
+
|
34
|
+
haversine_estimate.should be_within(0.001).of(geo_vector_estimate)
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|