geo2d 0.1.1 → 0.1.2

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