geo2d 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/VERSION +1 -1
  2. data/geo2d.gemspec +1 -1
  3. data/lib/geo2d.rb +127 -103
  4. data/test/test_geo2d.rb +51 -0
  5. metadata +1 -1
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
data/geo2d.gemspec CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{geo2d}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Javier Goizueta"]
data/lib/geo2d.rb CHANGED
@@ -1,38 +1,38 @@
1
1
  # Planar geometry of points and line-strings
2
2
  module Geo2D
3
-
3
+
4
4
  # Planar vectors; used also to represent points of the plane
5
5
  class Vector
6
-
6
+
7
7
  def initialize(x=0, y=0)
8
8
  @x = x.to_f
9
9
  @y = y.to_f
10
10
  end
11
-
11
+
12
12
  attr_accessor :x, :y
13
-
13
+
14
14
  def modulus
15
15
  Math.hypot(self.x, self.y)
16
16
  end
17
-
17
+
18
18
  def length
19
19
  modulus
20
20
  end
21
-
21
+
22
22
  def argument
23
23
  Math.atan2(self.y, self.x)
24
24
  end
25
-
25
+
26
26
  def +(other)
27
27
  other = Geo2D.Vector(other)
28
28
  Vector.new(self.x+other.x, self.y+other.y)
29
29
  end
30
-
30
+
31
31
  def -(other)
32
32
  other = Geo2D.Vector(other)
33
33
  Vector.new(self.x-other.x, self.y-other.y)
34
34
  end
35
-
35
+
36
36
  def *(scalar_or_vector)
37
37
  if Numeric===scalar_or_vector
38
38
  # scalar product
@@ -43,70 +43,74 @@ module Geo2D
43
43
  self.x*other.x + self.y*other.y
44
44
  end
45
45
  end
46
-
46
+
47
47
  def /(scalar)
48
48
  # self * 1.0/scalar
49
49
  Vector.new(self.x/scalar, self.y/scalar)
50
50
  end
51
-
51
+
52
52
  # z coordinate of cross product
53
53
  def cross_z(other)
54
54
  self.x*other.y - other.x*self.y
55
55
  end
56
-
56
+
57
57
  def dot(other)
58
58
  self.x*other.x + self.y*other.y
59
59
  end
60
-
60
+
61
61
  def ==(other)
62
62
  self.x == other.x && self.y == other.y
63
63
  end
64
-
64
+
65
65
  def to_a
66
66
  [self.x, self.y]
67
67
  end
68
-
68
+
69
69
  def to_s
70
70
  "(#{self.x}, #{self.y})"
71
71
  end
72
-
73
-
72
+
73
+
74
74
  def split
75
75
  to_a
76
76
  end
77
-
77
+
78
78
  # unitary vector in the direction of self
79
79
  def unitary
80
80
  self / self.modulus
81
81
  end
82
-
82
+
83
83
  # vector rotated 90 degrees counter-clockwise
84
84
  def ortho
85
85
  Vector.new(-self.y, self.x)
86
86
  end
87
-
87
+
88
88
  # angle between two vectors
89
89
  def angle_to(other)
90
90
  Math.atan2(cross_z(other), dot(other))
91
91
  end
92
-
92
+
93
93
  def aligned_with?(other)
94
94
  cross_z == 0
95
95
  end
96
-
96
+
97
97
  # multiply by matrix [[a11, a12], [a21, a22]]
98
98
  def transform(*t)
99
99
  a11, a12, a21, a22 = t.flatten
100
100
  x, y = self.x, self.y
101
101
  Vector.new(a11*x + a12*y, a21*x + a22*y)
102
102
  end
103
-
103
+
104
104
  # Apply arbitrary transformation (passed as a Proc or as a block)
105
105
  def apply(prc, &blk)
106
106
  prc ||= blk
107
107
  prc[self]
108
108
  end
109
-
109
+
110
+ def bounds
111
+ [x,y,x,y]
112
+ end
113
+
110
114
  def coerce(scalar)
111
115
  if scalar.kind_of?(Numeric)
112
116
  [self, scalar]
@@ -114,16 +118,24 @@ module Geo2D
114
118
  raise ArgumentError, "Vector: cannot coerce #{scalar.class}"
115
119
  end
116
120
  end
117
-
121
+
118
122
  end
119
-
123
+
124
+ def distance_to(other)
125
+ if other.kind_of?(Vector)
126
+ (other-self).modulus
127
+ else
128
+ other.distance_to?(self)
129
+ end
130
+ end
131
+
120
132
  module_function
121
-
133
+
122
134
  # Vector constructor
123
135
  def Vector(*args)
124
136
  case args.size
125
137
  when 2
126
- x, y = args
138
+ x, y = args
127
139
  when 1
128
140
  arg = args.first
129
141
  if arg.is_a?(Vector)
@@ -138,54 +150,54 @@ module Geo2D
138
150
  if arg.respond_to?(:x) && arg.respond_to?(:y)
139
151
  x, y = arg.x, arg.y
140
152
  else
141
- raise ArgumentError,"Invalid point definition"
153
+ raise ArgumentError,"Invalid point definition"
142
154
  end
143
155
  end
144
156
  else
145
- raise ArgumentError,"Invalid number of parameters for a point"
157
+ raise ArgumentError,"Invalid number of parameters for a point"
146
158
  end
147
159
  Vector.new(x,y)
148
160
  end
149
-
161
+
150
162
  # Line segment between two points (defined by Vectors)
151
163
  class LineSegment
152
-
164
+
153
165
  def initialize(p1, p2)
154
166
  @start = p1
155
167
  @end = p2
156
168
  raise ArgumentError,"Degenerate LineSegment" if p1==p2
157
169
  end
158
-
170
+
159
171
  attr_reader :start, :end
160
-
172
+
161
173
  def points
162
174
  [@start, @end]
163
175
  end
164
-
176
+
165
177
  def n_points
166
178
  2
167
179
  end
168
-
180
+
169
181
  def vector
170
182
  @vector ||= (@end-@start)
171
183
  end
172
-
184
+
173
185
  def length
174
186
  @length ||= vector.modulus
175
187
  end
176
-
188
+
177
189
  def angle
178
190
  vector.argument
179
191
  end
180
-
192
+
181
193
  def angle_at(parallel_distance)
182
194
  angle
183
195
  end
184
-
196
+
185
197
  def aligned_with?(point)
186
198
  vector.aligned_width?(point-@start)
187
199
  end
188
-
200
+
189
201
  def contains?(point)
190
202
  if self.aligned_with?(point)
191
203
  l,d = self.locate_point(point)
@@ -194,23 +206,23 @@ module Geo2D
194
206
  false
195
207
  end
196
208
  end
197
-
209
+
198
210
  def direction
199
211
  @u ||= vector.unitary
200
212
  end
201
-
213
+
202
214
  # Returns the position in the segment (distance from the start node along the line) of the nearest line point
203
215
  # to the point (point projected on the line) and the perpendicular separation of the point from the line (the
204
216
  # distance from the point to the line).
205
217
  # If the last parameter is true, the resulting point is forced to lie in the segment (so the distance along
206
218
  # the line is between 0 and the segment's length) and the second result is the distance from the point to the
207
219
  # 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)
220
+ def locate_point(point, corrected=false)
221
+ point = Geo2D.Vector(point)
210
222
  v = point - @start
211
223
  l = v.dot(direction)
212
224
  d = direction.cross_z(v) # == (v-l*direction).length == v.length*Math.sin(v.angle_to(direction))
213
-
225
+
214
226
  if corrected
215
227
  if l<0
216
228
  l = 0
@@ -220,10 +232,10 @@ module Geo2D
220
232
  d = (point-@end).length
221
233
  end
222
234
  end
223
-
235
+
224
236
  [l, d]
225
237
  end
226
-
238
+
227
239
  # Computes the position of a point in the line given the distance along the line from the starting node.
228
240
  # If a second parameter is passed it indicates the separation of the computed point in the direction
229
241
  # perpendicular to the line; the point is on the left side of the line if the separation is > 0.
@@ -237,27 +249,27 @@ module Geo2D
237
249
  def distance_to(point)
238
250
  locate_point(point, true).last
239
251
  end
240
-
252
+
241
253
  # Distance from the line that contains the segment to the point
242
254
  def line_distance_to(point)
243
255
  locate_point(point, false).last
244
256
  end
245
-
257
+
246
258
  def length_to(point)
247
259
  locate_point(point, true).first
248
260
  end
249
-
261
+
250
262
  # multiply by matrix [[a11, a12], [a21, a22]]
251
263
  def transform(*t)
252
264
  LineSegment.new(@start.transform(*t), @end = @end.transform(*t))
253
265
  end
254
-
266
+
255
267
  # Apply arbitrary transformation (passed as a Proc or as a block)
256
268
  def apply(prc, &blk)
257
269
  prc ||= blk
258
270
  LineSegment.new(prc[@start], prc[@end])
259
271
  end
260
-
272
+
261
273
  # Returns the side of the line that contains the segment in which the point lies:
262
274
  # * +1 the point is to the left of the line (as seen from the orientation of the segment)
263
275
  # * -1 is in the right side
@@ -266,38 +278,44 @@ module Geo2D
266
278
  v = vector.cross_z(point-@start)
267
279
  v < 0 ? -1 : (v > 0 ? +1 : 0)
268
280
  end
269
-
281
+
282
+ def bounds
283
+ xmn, xmx = [@start.x, @end.x].sort
284
+ ymn, ymx = [@start.y, @end.y].sort
285
+ [xmn, ymn, xmx, ymx]
286
+ end
287
+
270
288
  end
271
-
289
+
272
290
  class LineString
273
-
291
+
274
292
  def initialize(*vertices)
275
293
  @vertices = vertices
276
-
294
+
277
295
  to_remove = []
278
296
  prev = nil
279
297
  @vertices.each_with_index do |v, i|
280
298
  to_remove << i if prev && prev==v
281
299
  prev = v
282
- end
300
+ end
283
301
  to_remove.each do |i|
284
302
  @vertices.delete_at i
285
303
  end
286
-
304
+
287
305
  end
288
-
306
+
289
307
  def start
290
308
  @vertices.first
291
309
  end
292
-
310
+
293
311
  def end
294
312
  @vertices.last
295
313
  end
296
-
314
+
297
315
  def length
298
316
  @length ||= total_length
299
317
  end
300
-
318
+
301
319
  def n_points
302
320
  @vertices.size
303
321
  end
@@ -309,58 +327,58 @@ module Geo2D
309
327
  yield v
310
328
  end
311
329
  end
312
-
330
+
313
331
  def n_segments
314
332
  [n_points - 1,0].max
315
333
  end
316
-
334
+
317
335
  def segments
318
336
  (0...n_segments).to_a.map{|i| segment(i)}
319
337
  end
320
-
338
+
321
339
  def each_segment
322
340
  (0...n_segments).each do |i|
323
341
  yield segment(i)
324
342
  end
325
343
  end
326
-
344
+
327
345
  def segment(i)
328
346
  raise ArgumentError, "Invalid segment index #{i}" unless i>=0 && i<n_segments
329
347
  LineSegment.new(@vertices[i],@vertices[i+1])
330
348
  end
331
-
349
+
332
350
  def distance_to(point)
333
351
  locate_point(point, true).last
334
352
  end
335
-
353
+
336
354
  def length_to(point)
337
355
  locate_point(point, true).first
338
356
  end
339
-
340
- # return parallalel distance and separation;
357
+
358
+ # return parallalel distance and separation;
341
359
  # if corrected, then parallalel distance is in [0,length] (the point is inside the line)
342
360
  # parallel distance in [0,length] , separation
343
- def locate_point(point, corrected=false)
361
+ def locate_point(point, corrected=false)
344
362
  best = nil
345
-
363
+
346
364
  total_l = 0
347
365
  (0...n_segments).each do |i|
348
-
366
+
349
367
  seg = segment(i)
350
368
  seg_l = seg.length
351
-
369
+
352
370
  l,d = seg.locate_point(point, false)
353
371
  max_i = n_segments-1
354
-
372
+
355
373
  if (l>0 || i==0) && (l<=seg_l || i==max_i)
356
374
  if best.nil? || d<best.last
357
375
  best = [total_l+l, d]
358
376
  end
359
- end
360
-
377
+ end
378
+
361
379
  total_l += seg_l
362
380
  end
363
-
381
+
364
382
  if best && corrected
365
383
  l, d = best
366
384
  if l<0
@@ -372,11 +390,11 @@ module Geo2D
372
390
  end
373
391
  best = [l, d]
374
392
  end
375
-
393
+
376
394
  best
377
-
395
+
378
396
  end
379
-
397
+
380
398
  def interpolate_point(parallel_distance, separation=0, sweep=nil)
381
399
  # separation>0 => left side of line in direction of travel
382
400
  i, l = segment_position_of(parallel_distance)
@@ -384,18 +402,18 @@ module Geo2D
384
402
  sweep = 0.0 unless sweep.kind_of?(Numeric)
385
403
  if i>0 && l<sweep
386
404
  a = 0.5*(segment(i-1).angle+segment(i).angle) + Math::PI/2
387
- @vertices[i] + separation*Vector(Math.cos(a), Math.sin(a))
405
+ @vertices[i] + separation*Geo2D.Vector(Math.cos(a), Math.sin(a))
388
406
  elsif i<(n_segments-1) && l>=(segment_length(i)-sweep)
389
407
  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))
408
+ @vertices[i+1] + separation*Geo2D.Vector(Math.cos(a), Math.sin(a))
391
409
  else
392
410
  segment(i).interpolate_point(l, separation)
393
- end
394
- else
411
+ end
412
+ else
395
413
  segment(i).interpolate_point(l, separation)
396
414
  end
397
415
  end
398
-
416
+
399
417
  def angle_at(parallel_distance, sweep=false)
400
418
  i,l = segment_position_of(parallel_distance)
401
419
  if sweep
@@ -406,34 +424,40 @@ module Geo2D
406
424
  0.5*(segment(i).angle+segment(i+1).angle)
407
425
  else
408
426
  segment(i).angle
409
- end
427
+ end
410
428
  else
411
429
  segment(i).angle
412
430
  end
413
431
  end
414
-
432
+
415
433
  # multiply by matrix [[a11, a12], [a21, a22]]
416
434
  def transform(*t)
417
435
  LineString.new(*@vertices.map{|v| v.transforme(*t)})
418
436
  end
419
-
437
+
420
438
  def apply(prc=nil, &blk)
421
439
  prc = prc || blk
422
440
  LineString.new(*@vertices.map{|v| prc[v]})
423
441
  end
424
-
442
+
425
443
  def contains?(point)
426
444
  self.locate_point(point, true).last == 0
427
445
  end
428
-
446
+
447
+ def bounds
448
+ xs = @vertices.map{|v| v.x}
449
+ ys = @vertices.map{|v| v.y}
450
+ [xs.min, ys.min, xs.max, ys.max]
451
+ end
452
+
429
453
  private
430
-
454
+
431
455
  def segment_length(i)
432
456
  raise ArgumentError, "Invalid segment index #{i}" unless i>=0 && i<n_segments
433
457
  @segment_lengths ||= [nil]*n_segments
434
458
  @segment_lengths[i] ||= (@vertices[i+1]-@vertices[i]).modulus
435
459
  end
436
-
460
+
437
461
  def total_length
438
462
  l = 0
439
463
  (0...n_segments).each do |i|
@@ -441,7 +465,7 @@ module Geo2D
441
465
  end
442
466
  l
443
467
  end
444
-
468
+
445
469
  # find segment and distance in segment corresponding to total parallel distance TODO: rename
446
470
  def segment_position_of(l)
447
471
  i = 0
@@ -452,27 +476,27 @@ module Geo2D
452
476
  end
453
477
  return i, l
454
478
  end
455
-
479
+
456
480
  # compute parallel distance of position in segment TODO: rename
457
- def distance_along_line_of(segment_i, distance_in_segment)
481
+ def distance_along_line_of(segment_i, distance_in_segment)
458
482
  l = 0
459
483
  (0...segment_i).each do |i|
460
484
  l += segment_length(i)
461
485
  end
462
486
  l + distance_in_segment
463
487
  end
464
-
488
+
465
489
  end
466
-
490
+
467
491
  def Point(*args)
468
492
  Vector(*args)
469
493
  end
470
-
494
+
471
495
  # Segment constructor
472
496
  def LineSegment(start_point, end_point)
473
497
  LineSegment.new(Vector(start_point), Vector(end_point))
474
498
  end
475
-
499
+
476
500
  # Line-string constructor
477
501
  def Line(*args)
478
502
  #if args.size<3
@@ -481,7 +505,7 @@ module Geo2D
481
505
  LineString.new(*args.map{|arg| Vector(arg)})
482
506
  #end
483
507
  end
484
-
508
+
485
509
  # Rotation transformation; given the center of rotation (a point, i.e. a Vector) and the angle
486
510
  # this returns a procedure that can be used to apply the rotation to points.
487
511
  def rotation(center, angle)
@@ -490,5 +514,5 @@ module Geo2D
490
514
  cs = Math.cos(angle)
491
515
  lambda{|p| center + (p-center).transform(cs, sn, -sn, cs)}
492
516
  end
493
-
517
+
494
518
  end
data/test/test_geo2d.rb CHANGED
@@ -47,5 +47,56 @@ class TestGeo2d < Test::Unit::TestCase
47
47
  assert [10,20] == @vector.to_a
48
48
  end
49
49
  end
50
+
51
+ context "Given a line segment" do
52
+
53
+ setup do
54
+ @seg = LineSegment([10,20],[78,57])
55
+ end
56
+
57
+ context "And some points" do
58
+
59
+ setup do
60
+ @pnts = [[30,40],[10,20],[78,57],[0,0],[-1,10],[20,30],[0.5*(10+20),0.5*(78+57)]].map{|x,y| Point(x,y)}
61
+ end
62
+
63
+ should "reference the points in the line consistently" do
64
+ seg_max = @seg.points.map{|p| p.length}.max
65
+ @pnts.each do |pnt|
66
+ l,d = @seg.locate_point(pnt)
67
+ pnt2 = @seg.interpolate_point(l,d)
68
+ tolerance = [pnt.modulus, seg_max].max * Float::EPSILON * 2
69
+ assert (pnt2-pnt).length < tolerance, "Point #{pnt} yields #{pnt2} [#{(pnt2-pnt).length} / #{tolerance}]"
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+
76
+ context "Given a line string" do
77
+
78
+ setup do
79
+ @seg = Line([10,20],[78,57],[30,50],[110,60],[100,30])
80
+ end
81
+
82
+ context "And some points" do
83
+
84
+ setup do
85
+ @pnts = [[30,40],[10,20],[78,57],[0,0],[-1,10],[20,30],[0.5*(10+20),0.5*(78+57)]].map{|x,y| Point(x,y)}
86
+ end
87
+
88
+ should "reference the points in the line consistently" do
89
+ seg_max = @seg.points.map{|p| p.length}.max
90
+ @pnts.each do |pnt|
91
+ l,d = @seg.locate_point(pnt)
92
+ pnt2 = @seg.interpolate_point(l,d)
93
+ tolerance = [pnt.modulus, seg_max].max * Float::EPSILON * 2
94
+ assert (pnt2-pnt).length < tolerance, "Point #{pnt} yields #{pnt2} [#{(pnt2-pnt).length} / #{tolerance}]"
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+
50
101
  end
51
102
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geo2d
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Goizueta