geo2d 0.1.0 → 0.1.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.
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