geo2d 0.1.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/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/geo2d.gemspec +54 -0
- data/lib/geo2d.rb +494 -0
- data/test/helper.rb +10 -0
- data/test/test_geo2d.rb +51 -0
- metadata +75 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Javier Goizueta
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= Geo2D
|
2
|
+
|
3
|
+
Basic planar geometry functions, dealing with vectors, points, lines and line-strings.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2009 Javier Goizueta. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "geo2d"
|
8
|
+
gem.summary = %Q{Planar Geometry functions}
|
9
|
+
gem.description = %Q{Geo2D provides basic Planar Geometry functions for line-strings (poly-lines.)}
|
10
|
+
gem.email = "jgoizueta@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/jgoizueta/Geo2D"
|
12
|
+
gem.authors = ["Javier Goizueta"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "Geo2D #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/geo2d.gemspec
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{geo2d}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Javier Goizueta"]
|
12
|
+
s.date = %q{2009-11-20}
|
13
|
+
s.description = %q{Geo2D provides basic Planar Geometry functions for line-strings (poly-lines.)}
|
14
|
+
s.email = %q{jgoizueta@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"geo2d.gemspec",
|
27
|
+
"lib/geo2d.rb",
|
28
|
+
"test/helper.rb",
|
29
|
+
"test/test_geo2d.rb"
|
30
|
+
]
|
31
|
+
s.homepage = %q{http://github.com/jgoizueta/Geo2D}
|
32
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
s.rubygems_version = %q{1.3.5}
|
35
|
+
s.summary = %q{Planar Geometry functions}
|
36
|
+
s.test_files = [
|
37
|
+
"test/helper.rb",
|
38
|
+
"test/test_geo2d.rb"
|
39
|
+
]
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 3
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
46
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
47
|
+
else
|
48
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
49
|
+
end
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
data/lib/geo2d.rb
ADDED
@@ -0,0 +1,494 @@
|
|
1
|
+
# Planar geometry of points and line-strings
|
2
|
+
module Geo2D
|
3
|
+
|
4
|
+
# Planar vectors; used also to represent points of the plane
|
5
|
+
class Vector
|
6
|
+
|
7
|
+
def initialize(x=0, y=0)
|
8
|
+
@x = x.to_f
|
9
|
+
@y = y.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :x, :y
|
13
|
+
|
14
|
+
def modulus
|
15
|
+
Math.hypot(self.x, self.y)
|
16
|
+
end
|
17
|
+
|
18
|
+
def length
|
19
|
+
modulus
|
20
|
+
end
|
21
|
+
|
22
|
+
def argument
|
23
|
+
Math.atan2(self.y, self.x)
|
24
|
+
end
|
25
|
+
|
26
|
+
def +(other)
|
27
|
+
other = Geo2D.Vector(other)
|
28
|
+
Vector.new(self.x+other.x, self.y+other.y)
|
29
|
+
end
|
30
|
+
|
31
|
+
def -(other)
|
32
|
+
other = Geo2D.Vector(other)
|
33
|
+
Vector.new(self.x-other.x, self.y-other.y)
|
34
|
+
end
|
35
|
+
|
36
|
+
def *(scalar_or_vector)
|
37
|
+
if Numeric===scalar_or_vector
|
38
|
+
# scalar product
|
39
|
+
Vector.new(scalar_or_vector*self.x, scalar_or_vector*self.y)
|
40
|
+
else
|
41
|
+
# dot product
|
42
|
+
other = Geo2D.Vector(scalar_or_vector)
|
43
|
+
self.x*other.x + self.y*other.y
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def /(scalar)
|
48
|
+
# self * 1.0/scalar
|
49
|
+
Vector.new(self.x/scalar, self.y/scalar)
|
50
|
+
end
|
51
|
+
|
52
|
+
# z coordinate of cross product
|
53
|
+
def cross_z(other)
|
54
|
+
self.x*other.y - other.x*self.y
|
55
|
+
end
|
56
|
+
|
57
|
+
def dot(other)
|
58
|
+
self.x*other.x + self.y*other.y
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
self.x == other.x && self.y == other.y
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_a
|
66
|
+
[self.x, self.y]
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
"(#{self.x}, #{self.y})"
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def split
|
75
|
+
to_a
|
76
|
+
end
|
77
|
+
|
78
|
+
# unitary vector in the direction of self
|
79
|
+
def unitary
|
80
|
+
self / self.modulus
|
81
|
+
end
|
82
|
+
|
83
|
+
# vector rotated 90 degrees counter-clockwise
|
84
|
+
def ortho
|
85
|
+
Vector.new(-self.y, self.x)
|
86
|
+
end
|
87
|
+
|
88
|
+
# angle between two vectors
|
89
|
+
def angle_to(other)
|
90
|
+
Math.atan2(cross_z(other), dot(other))
|
91
|
+
end
|
92
|
+
|
93
|
+
def aligned_with?(other)
|
94
|
+
cross_z == 0
|
95
|
+
end
|
96
|
+
|
97
|
+
# multiply by matrix [[a11, a12], [a21, a22]]
|
98
|
+
def transform(*t)
|
99
|
+
a11, a12, a21, a22 = t.flatten
|
100
|
+
x, y = self.x, self.y
|
101
|
+
Vector.new(a11*x + a12*y, a21*x + a22*y)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Apply arbitrary transformation (passed as a Proc or as a block)
|
105
|
+
def apply(prc, &blk)
|
106
|
+
prc ||= blk
|
107
|
+
prc[self]
|
108
|
+
end
|
109
|
+
|
110
|
+
def coerce(scalar)
|
111
|
+
if scalar.kind_of?(Numeric)
|
112
|
+
[self, scalar]
|
113
|
+
else
|
114
|
+
raise ArgumentError, "Vector: cannot coerce #{scalar.class}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
module_function
|
121
|
+
|
122
|
+
# Vector constructor
|
123
|
+
def Vector(*args)
|
124
|
+
case args.size
|
125
|
+
when 2
|
126
|
+
x, y = args
|
127
|
+
when 1
|
128
|
+
arg = args.first
|
129
|
+
if arg.is_a?(Vector)
|
130
|
+
return arg
|
131
|
+
elsif arg.kind_of?(Array) && arg.size==2
|
132
|
+
x, y = arg
|
133
|
+
elsif arg.kind_of?(Hash)
|
134
|
+
if arg.has_key?(:x) && arg.has_key?(:y)
|
135
|
+
x, y = arg[:x], arg[:y]
|
136
|
+
end
|
137
|
+
else
|
138
|
+
if arg.respond_to?(:x) && arg.respond_to?(:y)
|
139
|
+
x, y = arg.x, arg.y
|
140
|
+
else
|
141
|
+
raise ArgumentError,"Invalid point definition"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
else
|
145
|
+
raise ArgumentError,"Invalid number of parameters for a point"
|
146
|
+
end
|
147
|
+
Vector.new(x,y)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Line segment between two points (defined by Vectors)
|
151
|
+
class LineSegment
|
152
|
+
|
153
|
+
def initialize(p1, p2)
|
154
|
+
@start = p1
|
155
|
+
@end = p2
|
156
|
+
raise ArgumentError,"Degenerate LineSegment" if p1==p2
|
157
|
+
end
|
158
|
+
|
159
|
+
attr_reader :start, :end
|
160
|
+
|
161
|
+
def points
|
162
|
+
[@start, @end]
|
163
|
+
end
|
164
|
+
|
165
|
+
def n_points
|
166
|
+
2
|
167
|
+
end
|
168
|
+
|
169
|
+
def vector
|
170
|
+
@vector ||= (@end-@start)
|
171
|
+
end
|
172
|
+
|
173
|
+
def length
|
174
|
+
@length ||= vector.modulus
|
175
|
+
end
|
176
|
+
|
177
|
+
def angle
|
178
|
+
vector.argument
|
179
|
+
end
|
180
|
+
|
181
|
+
def angle_at(parallel_distance)
|
182
|
+
angle
|
183
|
+
end
|
184
|
+
|
185
|
+
def aligned_with?(point)
|
186
|
+
vector.aligned_width?(point-@start)
|
187
|
+
end
|
188
|
+
|
189
|
+
def contains?(point)
|
190
|
+
if self.aligned_with?(point)
|
191
|
+
l,d = self.locate_point(point)
|
192
|
+
l>=0 && l<=self.length # => d==0
|
193
|
+
else
|
194
|
+
false
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def direction
|
199
|
+
@u ||= vector.unitary
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns the position in the segment (distance from the start node along the line) of the nearest line point
|
203
|
+
# to the point (point projected on the line) and the perpendicular separation of the point from the line (the
|
204
|
+
# distance from the point to the line).
|
205
|
+
# If the last parameter is true, the resulting point is forced to lie in the segment (so the distance along
|
206
|
+
# the line is between 0 and the segment's length) and the second result is the distance from the point to the
|
207
|
+
# segment (i.e. to the closest end of the segment if the projected point lies out of the segmen)
|
208
|
+
def locate_point(point, corrected=false)
|
209
|
+
point = Vector(point)
|
210
|
+
v = point - @start
|
211
|
+
l = v.dot(direction)
|
212
|
+
d = direction.cross_z(v) # == (v-l*direction).length == v.length*Math.sin(v.angle_to(direction))
|
213
|
+
|
214
|
+
if corrected
|
215
|
+
if l<0
|
216
|
+
l = 0
|
217
|
+
d = (point-@start).length
|
218
|
+
elsif l>total_l
|
219
|
+
l = self.length
|
220
|
+
d = (point-@end).length
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
[l, d]
|
225
|
+
end
|
226
|
+
|
227
|
+
# Computes the position of a point in the line given the distance along the line from the starting node.
|
228
|
+
# If a second parameter is passed it indicates the separation of the computed point in the direction
|
229
|
+
# perpendicular to the line; the point is on the left side of the line if the separation is > 0.
|
230
|
+
def interpolate_point(parallel_distance, separation=0)
|
231
|
+
p = @start + self.direction*parallel_distance
|
232
|
+
p += direction.ortho*separation unless separation==0
|
233
|
+
p
|
234
|
+
end
|
235
|
+
|
236
|
+
# Distance from the segment to a point
|
237
|
+
def distance_to(point)
|
238
|
+
locate_point(point, true).last
|
239
|
+
end
|
240
|
+
|
241
|
+
# Distance from the line that contains the segment to the point
|
242
|
+
def line_distance_to(point)
|
243
|
+
locate_point(point, false).last
|
244
|
+
end
|
245
|
+
|
246
|
+
def length_to(point)
|
247
|
+
locate_point(point, true).first
|
248
|
+
end
|
249
|
+
|
250
|
+
# multiply by matrix [[a11, a12], [a21, a22]]
|
251
|
+
def transform(*t)
|
252
|
+
LineSegment.new(@start.transform(*t), @end = @end.transform(*t))
|
253
|
+
end
|
254
|
+
|
255
|
+
# Apply arbitrary transformation (passed as a Proc or as a block)
|
256
|
+
def apply(prc, &blk)
|
257
|
+
prc ||= blk
|
258
|
+
LineSegment.new(prc[@start], prc[@end])
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns the side of the line that contains the segment in which the point lies:
|
262
|
+
# * +1 the point is to the left of the line (as seen from the orientation of the segment)
|
263
|
+
# * -1 is in the right side
|
264
|
+
# * 0 the point is on the line
|
265
|
+
def side_of(point)
|
266
|
+
v = vector.cross_z(point-@start)
|
267
|
+
v < 0 ? -1 : (v > 0 ? +1 : 0)
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
class LineString
|
273
|
+
|
274
|
+
def initialize(*vertices)
|
275
|
+
@vertices = vertices
|
276
|
+
|
277
|
+
to_remove = []
|
278
|
+
prev = nil
|
279
|
+
@vertices.each_with_index do |v, i|
|
280
|
+
to_remove << i if prev && prev==v
|
281
|
+
prev = v
|
282
|
+
end
|
283
|
+
to_remove.each do |i|
|
284
|
+
@vertices.delete_at i
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
def start
|
290
|
+
@vertices.first
|
291
|
+
end
|
292
|
+
|
293
|
+
def end
|
294
|
+
@vertices.last
|
295
|
+
end
|
296
|
+
|
297
|
+
def length
|
298
|
+
@length ||= total_length
|
299
|
+
end
|
300
|
+
|
301
|
+
def n_points
|
302
|
+
@vertices.size
|
303
|
+
end
|
304
|
+
def points
|
305
|
+
@vertices
|
306
|
+
end
|
307
|
+
def each_point
|
308
|
+
@vertices.each do |v|
|
309
|
+
yield v
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def n_segments
|
314
|
+
[n_points - 1,0].max
|
315
|
+
end
|
316
|
+
|
317
|
+
def segments
|
318
|
+
(0...n_segments).to_a.map{|i| segment(i)}
|
319
|
+
end
|
320
|
+
|
321
|
+
def each_segment
|
322
|
+
(0...n_segments).each do |i|
|
323
|
+
yield segment(i)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def segment(i)
|
328
|
+
raise ArgumentError, "Invalid segment index #{i}" unless i>=0 && i<n_segments
|
329
|
+
LineSegment.new(@vertices[i],@vertices[i+1])
|
330
|
+
end
|
331
|
+
|
332
|
+
def distance_to(point)
|
333
|
+
locate_point(point, true).last
|
334
|
+
end
|
335
|
+
|
336
|
+
def length_to(point)
|
337
|
+
locate_point(point, true).first
|
338
|
+
end
|
339
|
+
|
340
|
+
# return parallalel distance and separation;
|
341
|
+
# if corrected, then parallalel distance is in [0,length] (the point is inside the line)
|
342
|
+
# parallel distance in [0,length] , separation
|
343
|
+
def locate_point(point, corrected=false)
|
344
|
+
best = nil
|
345
|
+
|
346
|
+
total_l = 0
|
347
|
+
(0...n_segments).each do |i|
|
348
|
+
|
349
|
+
seg = segment(i)
|
350
|
+
seg_l = seg.length
|
351
|
+
|
352
|
+
l,d = seg.locate_point(point, false)
|
353
|
+
max_i = n_segments-1
|
354
|
+
|
355
|
+
if (l>0 || i==0) && (l<=seg_l || i==max_i)
|
356
|
+
if best.nil? || d<best.last
|
357
|
+
best = [total_l+l, d]
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
total_l += seg_l
|
362
|
+
end
|
363
|
+
|
364
|
+
if best && corrected
|
365
|
+
l, d = best
|
366
|
+
if l<0
|
367
|
+
l = 0
|
368
|
+
d = (point-points.first).length
|
369
|
+
elsif l>total_l
|
370
|
+
l = total_l
|
371
|
+
d = (point-points.last).length
|
372
|
+
end
|
373
|
+
best = [l, d]
|
374
|
+
end
|
375
|
+
|
376
|
+
best
|
377
|
+
|
378
|
+
end
|
379
|
+
|
380
|
+
def interpolate_point(parallel_distance, separation=0, sweep=nil)
|
381
|
+
# separation>0 => left side of line in direction of travel
|
382
|
+
i, l = segment_position_of(parallel_distance)
|
383
|
+
if sweep && separation!=0
|
384
|
+
sweep = 0.0 unless sweep.kind_of?(Numeric)
|
385
|
+
if i>0 && l<sweep
|
386
|
+
a = 0.5*(segment(i-1).angle+segment(i).angle) + Math::PI/2
|
387
|
+
@vertices[i] + separation*Vector(Math.cos(a), Math.sin(a))
|
388
|
+
elsif i<(n_segments-1) && l>=(segment_length(i)-sweep)
|
389
|
+
a = 0.5*(segment(i).angle+segment(i+1).angle) + Math::PI/2
|
390
|
+
@vertices[i+1] + separation*Vector(Math.cos(a), Math.sin(a))
|
391
|
+
else
|
392
|
+
segment(i).interpolate_point(l, separation)
|
393
|
+
end
|
394
|
+
else
|
395
|
+
segment(i).interpolate_point(l, separation)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def angle_at(parallel_distance, sweep=false)
|
400
|
+
i,l = segment_position_of(parallel_distance)
|
401
|
+
if sweep
|
402
|
+
sweep = 0.0 unless sweep.kind_of?(Numeric)
|
403
|
+
if i>0 && l<sweep
|
404
|
+
0.5*(segment(i-1).angle+segment(i).angle)
|
405
|
+
elsif i<(n_segments-1) && l>=(segment_length(i)-sweep)
|
406
|
+
0.5*(segment(i).angle+segment(i+1).angle)
|
407
|
+
else
|
408
|
+
segment(i).angle
|
409
|
+
end
|
410
|
+
else
|
411
|
+
segment(i).angle
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# multiply by matrix [[a11, a12], [a21, a22]]
|
416
|
+
def transform(*t)
|
417
|
+
LineString.new(*@vertices.map{|v| v.transforme(*t)})
|
418
|
+
end
|
419
|
+
|
420
|
+
def apply(prc=nil, &blk)
|
421
|
+
prc = prc || blk
|
422
|
+
LineString.new(*@vertices.map{|v| prc[v]})
|
423
|
+
end
|
424
|
+
|
425
|
+
def contains?(point)
|
426
|
+
self.locate_point(point, true).last == 0
|
427
|
+
end
|
428
|
+
|
429
|
+
private
|
430
|
+
|
431
|
+
def segment_length(i)
|
432
|
+
raise ArgumentError, "Invalid segment index #{i}" unless i>=0 && i<n_segments
|
433
|
+
@segment_lengths ||= [nil]*n_segments
|
434
|
+
@segment_lengths[i] ||= (@vertices[i+1]-@vertices[i]).modulus
|
435
|
+
end
|
436
|
+
|
437
|
+
def total_length
|
438
|
+
l = 0
|
439
|
+
(0...n_segments).each do |i|
|
440
|
+
l += segment_length(i)
|
441
|
+
end
|
442
|
+
l
|
443
|
+
end
|
444
|
+
|
445
|
+
# find segment and distance in segment corresponding to total parallel distance TODO: rename
|
446
|
+
def segment_position_of(l)
|
447
|
+
i = 0
|
448
|
+
max_i = n_segments-1
|
449
|
+
while l>(s=segment_length(i)) && i<max_i
|
450
|
+
l -= s
|
451
|
+
i += 1
|
452
|
+
end
|
453
|
+
return i, l
|
454
|
+
end
|
455
|
+
|
456
|
+
# compute parallel distance of position in segment TODO: rename
|
457
|
+
def distance_along_line_of(segment_i, distance_in_segment)
|
458
|
+
l = 0
|
459
|
+
(0...segment_i).each do |i|
|
460
|
+
l += segment_length(i)
|
461
|
+
end
|
462
|
+
l + distance_in_segment
|
463
|
+
end
|
464
|
+
|
465
|
+
end
|
466
|
+
|
467
|
+
def Point(*args)
|
468
|
+
Vector(*args)
|
469
|
+
end
|
470
|
+
|
471
|
+
# Segment constructor
|
472
|
+
def LineSegment(start_point, end_point)
|
473
|
+
LineSegment.new(Vector(start_point), Vector(end_point))
|
474
|
+
end
|
475
|
+
|
476
|
+
# Line-string constructor
|
477
|
+
def Line(*args)
|
478
|
+
#if args.size<3
|
479
|
+
# LineSegment.new(*args.map{|arg| Vector(arg)})
|
480
|
+
#else
|
481
|
+
LineString.new(*args.map{|arg| Vector(arg)})
|
482
|
+
#end
|
483
|
+
end
|
484
|
+
|
485
|
+
# Rotation transformation; given the center of rotation (a point, i.e. a Vector) and the angle
|
486
|
+
# this returns a procedure that can be used to apply the rotation to points.
|
487
|
+
def rotation(center, angle)
|
488
|
+
center = Vector(center)
|
489
|
+
sn = Math.sin(angle)
|
490
|
+
cs = Math.cos(angle)
|
491
|
+
lambda{|p| center + (p-center).transform(cs, sn, -sn, cs)}
|
492
|
+
end
|
493
|
+
|
494
|
+
end
|
data/test/helper.rb
ADDED
data/test/test_geo2d.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestGeo2d < Test::Unit::TestCase
|
4
|
+
include Geo2D
|
5
|
+
context "A Vector" do
|
6
|
+
setup do
|
7
|
+
@vector = Vector.new(10,20)
|
8
|
+
end
|
9
|
+
should "be constructible by components" do
|
10
|
+
assert_equal @vector, Vector(10,20)
|
11
|
+
end
|
12
|
+
should "be constructible by an array" do
|
13
|
+
assert_equal @vector, Vector([10,20])
|
14
|
+
end
|
15
|
+
should "be constructible by a vector" do
|
16
|
+
assert_equal @vector, Vector(@vector)
|
17
|
+
end
|
18
|
+
should "be constructible by a hash" do
|
19
|
+
assert_equal @vector, Vector(:x=>10, :y=>20)
|
20
|
+
end
|
21
|
+
should "be not constructible by any number of components other than 2" do
|
22
|
+
assert_raise(ArgumentError){Vector(10,20,30)}
|
23
|
+
assert_raise(ArgumentError){Vector(10)}
|
24
|
+
assert_raise(ArgumentError){Vector([10,20,30])}
|
25
|
+
assert_raise(ArgumentError){Vector([10])}
|
26
|
+
end
|
27
|
+
should "have a modulus" do
|
28
|
+
assert_equal Math.hypot(10,20), @vector.modulus
|
29
|
+
end
|
30
|
+
should "have an argument" do
|
31
|
+
assert_equal Math.atan2(20,10), @vector.argument
|
32
|
+
end
|
33
|
+
should "have a conmutative scalar product" do
|
34
|
+
assert_equal Vector.new(20,40), 2*@vector
|
35
|
+
assert_equal Vector.new(20,40), @vector*2
|
36
|
+
end
|
37
|
+
should "have a conmutative dot product" do
|
38
|
+
assert_equal 70, Vector(10,20)*Vector(3,2)
|
39
|
+
assert_equal 70, Vector(3,2)*Vector(10,20)
|
40
|
+
end
|
41
|
+
should "have equality and inequality operators" do
|
42
|
+
assert @vector==Vector(10,20)
|
43
|
+
assert @vector!=Vector(20,10)
|
44
|
+
end
|
45
|
+
should "be splittable" do
|
46
|
+
assert [10,20] == @vector.split
|
47
|
+
assert [10,20] == @vector.to_a
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geo2d
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Javier Goizueta
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-20 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: Geo2D provides basic Planar Geometry functions for line-strings (poly-lines.)
|
26
|
+
email: jgoizueta@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
files:
|
35
|
+
- .document
|
36
|
+
- .gitignore
|
37
|
+
- LICENSE
|
38
|
+
- README.rdoc
|
39
|
+
- Rakefile
|
40
|
+
- VERSION
|
41
|
+
- geo2d.gemspec
|
42
|
+
- lib/geo2d.rb
|
43
|
+
- test/helper.rb
|
44
|
+
- test/test_geo2d.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/jgoizueta/Geo2D
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Planar Geometry functions
|
73
|
+
test_files:
|
74
|
+
- test/helper.rb
|
75
|
+
- test/test_geo2d.rb
|