vector_geometry 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ require "geometry/geometry"
2
+ require "geometry/spheroid/base"
3
+ require "geometry/spheroid/sphere"
4
+ require "geometry/vector/vector"
5
+ require "geometry/vector/geo_vector"
6
+ require "geometry/vector/earth_vector"
@@ -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