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