geo2d 0.1.1 → 0.1.2

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 +2 -2
  3. data/lib/geo2d.rb +57 -66
  4. data/test/test_geo2d.rb +4 -4
  5. metadata +2 -2
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
data/geo2d.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{geo2d}
8
- s.version = "0.1.1"
8
+ s.version = "0.1.2"
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"]
12
- s.date = %q{2009-11-20}
12
+ s.date = %q{2009-11-22}
13
13
  s.description = %q{Geo2D provides basic Planar Geometry functions for line-strings (poly-lines.)}
14
14
  s.email = %q{jgoizueta@gmail.com}
15
15
  s.extra_rdoc_files = [
data/lib/geo2d.rb CHANGED
@@ -100,6 +100,11 @@ module Geo2D
100
100
  x, y = self.x, self.y
101
101
  Vector.new(a11*x + a12*y, a21*x + a22*y)
102
102
  end
103
+
104
+ # vector rotation (center at origin); for a general rotation use Geo2D.rotation
105
+ def rotate(angle)
106
+ transform(*Geo2D.rotation_transform(angle))
107
+ end
103
108
 
104
109
  # Apply arbitrary transformation (passed as a Proc or as a block)
105
110
  def apply(prc, &blk)
@@ -200,7 +205,7 @@ module Geo2D
200
205
 
201
206
  def contains?(point)
202
207
  if self.aligned_with?(point)
203
- l,d = self.locate_point(point)
208
+ l,d,r = self.locate_point(point)
204
209
  l>=0 && l<=self.length # => d==0
205
210
  else
206
211
  false
@@ -213,46 +218,58 @@ module Geo2D
213
218
 
214
219
  # Returns the position in the segment (distance from the start node along the line) of the nearest line point
215
220
  # to the point (point projected on the line) and the perpendicular separation of the point from the line (the
216
- # distance from the point to the line).
221
+ # distance from the point to the line, but signed: negative when the point is on the right side of the line.
217
222
  # If the last parameter is true, the resulting point is forced to lie in the segment (so the distance along
218
223
  # the line is between 0 and the segment's length) and the second result is the distance from the point to the
219
- # segment (i.e. to the closest end of the segment if the projected point lies out of the segmen)
224
+ # segment (i.e. to the closest end of the segment if the projected point lies out of the segment)
225
+ # The third returned value is a rotation that must be applied to the perpendicular vector; it is nonzero
226
+ # only when the last parameter is true and the point is closer to a segment end than to any other segment point.
220
227
  def locate_point(point, corrected=false)
221
228
  point = Geo2D.Vector(point)
222
229
  v = point - @start
223
230
  l = v.dot(direction)
224
231
  d = direction.cross_z(v) # == (v-l*direction).length == v.length*Math.sin(v.angle_to(direction))
232
+ rotation = 0
225
233
 
226
234
  if corrected
235
+ # rotation = atan2(-(l.modulo(length))*d.sign, d.abs)
227
236
  if l<0
237
+ rotation = Math.atan2(d < 0 ? -l : l, d.abs)
228
238
  l = 0
229
- d = (point-@start).length
230
- elsif l>total_l
239
+ d = d/Math.cos(rotation) # d.sign*(point-@start).length
240
+ elsif l>self.length
241
+ l -= self.length
242
+ rotation = Math.atan2(d < 0 ? -l : l, d.abs)
231
243
  l = self.length
232
- d = (point-@end).length
244
+ d = d/Math.cos(rotation) # d = d.sign*(point-@end).length
233
245
  end
234
246
  end
235
247
 
236
- [l, d]
248
+ [l, d, rotation]
237
249
  end
238
250
 
239
251
  # Computes the position of a point in the line given the distance along the line from the starting node.
240
252
  # If a second parameter is passed it indicates the separation of the computed point in the direction
241
253
  # perpendicular to the line; the point is on the left side of the line if the separation is > 0.
242
- def interpolate_point(parallel_distance, separation=0)
254
+ # The third parameter is a rotation applied to the vector from the line to the point.
255
+ def interpolate_point(parallel_distance, separation=0, rotation=0)
243
256
  p = @start + self.direction*parallel_distance
244
- p += direction.ortho*separation unless separation==0
257
+ unless separation==0
258
+ d = direction.ortho*separation
259
+ d = d.rotate(rotation) unless rotation==0
260
+ p += d
261
+ end
245
262
  p
246
263
  end
247
264
 
248
265
  # Distance from the segment to a point
249
266
  def distance_to(point)
250
- locate_point(point, true).last
267
+ locate_point(point, true)[1].abs
251
268
  end
252
269
 
253
270
  # Distance from the line that contains the segment to the point
254
271
  def line_distance_to(point)
255
- locate_point(point, false).last
272
+ locate_point(point, false)[1].abs
256
273
  end
257
274
 
258
275
  def length_to(point)
@@ -348,17 +365,21 @@ module Geo2D
348
365
  end
349
366
 
350
367
  def distance_to(point)
351
- locate_point(point, true).last
368
+ locate_point(point, true)[1].abs
352
369
  end
353
370
 
354
371
  def length_to(point)
355
372
  locate_point(point, true).first
356
373
  end
357
374
 
358
- # return parallalel distance and separation;
359
- # if corrected, then parallalel distance is in [0,length] (the point is inside the line)
360
- # parallel distance in [0,length] , separation
361
- def locate_point(point, corrected=false)
375
+ # Return the position of a point in relation to the line: parallalel distance along the line,
376
+ # separation and rotation of the separation (from the perpendicular.)
377
+ # Parallalel distance is in [0,length].
378
+ # If we have l,d,r = line.locate_point(point), then the closest line point to point is:
379
+ # line.interpolate_point(l) and line.interpolate_point(l,d,r) is point; d.abs is the distance from line to point;
380
+ # point is on the left of line if d>0; on the right if d<0 and on the line if d==0.
381
+ # If we want to align a text with the line at point, we would use the angle line.angle_at(l,r)
382
+ def locate_point(point)
362
383
  best = nil
363
384
 
364
385
  total_l = 0
@@ -367,67 +388,33 @@ module Geo2D
367
388
  seg = segment(i)
368
389
  seg_l = seg.length
369
390
 
370
- l,d = seg.locate_point(point, false)
371
- max_i = n_segments-1
391
+ l,d,rotation = seg.locate_point(point, true)
372
392
 
373
- if (l>0 || i==0) && (l<=seg_l || i==max_i)
374
- if best.nil? || d<best.last
375
- best = [total_l+l, d]
376
- end
393
+ if best.nil? || d.abs<best[1].abs
394
+ best = [total_l+l, d, rotation]
377
395
  end
378
396
 
379
397
  total_l += seg_l
380
398
  end
381
399
 
382
- if best && corrected
383
- l, d = best
384
- if l<0
385
- l = 0
386
- d = (point-points.first).length
387
- elsif l>total_l
388
- l = total_l
389
- d = (point-points.last).length
390
- end
391
- best = [l, d]
392
- end
393
-
394
400
  best
395
401
 
396
402
  end
397
403
 
398
- def interpolate_point(parallel_distance, separation=0, sweep=nil)
404
+ # Compute a point position given the relative position in the line as returned by locate_point().
405
+ def interpolate_point(parallel_distance, separation=0, rotation=0)
399
406
  # separation>0 => left side of line in direction of travel
400
407
  i, l = segment_position_of(parallel_distance)
401
- if sweep && separation!=0
402
- sweep = 0.0 unless sweep.kind_of?(Numeric)
403
- if i>0 && l<sweep
404
- a = 0.5*(segment(i-1).angle+segment(i).angle) + Math::PI/2
405
- @vertices[i] + separation*Geo2D.Vector(Math.cos(a), Math.sin(a))
406
- elsif i<(n_segments-1) && l>=(segment_length(i)-sweep)
407
- a = 0.5*(segment(i).angle+segment(i+1).angle) + Math::PI/2
408
- @vertices[i+1] + separation*Geo2D.Vector(Math.cos(a), Math.sin(a))
409
- else
410
- segment(i).interpolate_point(l, separation)
411
- end
412
- else
413
- segment(i).interpolate_point(l, separation)
414
- end
408
+ segment(i).interpolate_point(l, separation, rotation)
415
409
  end
416
410
 
417
- def angle_at(parallel_distance, sweep=false)
411
+ # Angle of the line at a point (in radians, from the X axis, counter-clockwise.)
412
+ # The rotation parameter is added to this angle, if the rotation obtained from locate_point is used,
413
+ # points external to the line have a natural orientation parallel to the line (than can be use to rotate
414
+ # texts or symbols to be aligned with the line.)
415
+ def angle_at(parallel_distance, rotation=0)
418
416
  i,l = segment_position_of(parallel_distance)
419
- if sweep
420
- sweep = 0.0 unless sweep.kind_of?(Numeric)
421
- if i>0 && l<sweep
422
- 0.5*(segment(i-1).angle+segment(i).angle)
423
- elsif i<(n_segments-1) && l>=(segment_length(i)-sweep)
424
- 0.5*(segment(i).angle+segment(i+1).angle)
425
- else
426
- segment(i).angle
427
- end
428
- else
429
- segment(i).angle
430
- end
417
+ segment(i).angle + rotation
431
418
  end
432
419
 
433
420
  # multiply by matrix [[a11, a12], [a21, a22]]
@@ -441,7 +428,7 @@ module Geo2D
441
428
  end
442
429
 
443
430
  def contains?(point)
444
- self.locate_point(point, true).last == 0
431
+ self.locate_point(point)[1] == 0
445
432
  end
446
433
 
447
434
  def bounds
@@ -506,13 +493,17 @@ module Geo2D
506
493
  #end
507
494
  end
508
495
 
496
+ def rotation_transform(angle)
497
+ sn = Math.sin(angle)
498
+ cs = Math.cos(angle)
499
+ [cs, sn, -sn, cs]
500
+ end
501
+
509
502
  # Rotation transformation; given the center of rotation (a point, i.e. a Vector) and the angle
510
503
  # this returns a procedure that can be used to apply the rotation to points.
511
504
  def rotation(center, angle)
512
505
  center = Vector(center)
513
- sn = Math.sin(angle)
514
- cs = Math.cos(angle)
515
- lambda{|p| center + (p-center).transform(cs, sn, -sn, cs)}
506
+ lambda{|p| center + (p-center).rotate(angle)}
516
507
  end
517
508
 
518
509
  end
data/test/test_geo2d.rb CHANGED
@@ -63,8 +63,8 @@ class TestGeo2d < Test::Unit::TestCase
63
63
  should "reference the points in the line consistently" do
64
64
  seg_max = @seg.points.map{|p| p.length}.max
65
65
  @pnts.each do |pnt|
66
- l,d = @seg.locate_point(pnt)
67
- pnt2 = @seg.interpolate_point(l,d)
66
+ l,d,r = @seg.locate_point(pnt)
67
+ pnt2 = @seg.interpolate_point(l,d,r)
68
68
  tolerance = [pnt.modulus, seg_max].max * Float::EPSILON * 2
69
69
  assert (pnt2-pnt).length < tolerance, "Point #{pnt} yields #{pnt2} [#{(pnt2-pnt).length} / #{tolerance}]"
70
70
  end
@@ -88,8 +88,8 @@ class TestGeo2d < Test::Unit::TestCase
88
88
  should "reference the points in the line consistently" do
89
89
  seg_max = @seg.points.map{|p| p.length}.max
90
90
  @pnts.each do |pnt|
91
- l,d = @seg.locate_point(pnt)
92
- pnt2 = @seg.interpolate_point(l,d)
91
+ l,d,r = @seg.locate_point(pnt)
92
+ pnt2 = @seg.interpolate_point(l,d,r)
93
93
  tolerance = [pnt.modulus, seg_max].max * Float::EPSILON * 2
94
94
  assert (pnt2-pnt).length < tolerance, "Point #{pnt} yields #{pnt2} [#{(pnt2-pnt).length} / #{tolerance}]"
95
95
  end
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.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Goizueta
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-20 00:00:00 +01:00
12
+ date: 2009-11-22 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency