File Coverage

blib/lib/Geo/SpaceManager.pm
Criterion Covered Total %
statement 149 205 72.6
branch 81 136 59.5
condition 39 50 78.0
subroutine 10 11 90.9
pod 6 6 100.0
total 285 408 69.8


line stmt bran cond sub pod time code
1             # Geo::SpaceManger
2             #
3             # This package implements the Geo::SpaceManager class. This class
4             # may be used to place two-dimensional rectangles without overlap
5             # into a two-dimensional space. It does this by maintaining a dataset
6             # of free space still available in the space and returning the
7             # closest available location in which an additional rectangle
8             # of a specified size may be added.
9              
10             package Geo::SpaceManager;
11              
12 6     6   215846 use strict;
  6         15  
  6         238  
13 6     6   33 use warnings;
  6         10  
  6         236  
14 6     6   32 use Carp qw(carp croak);
  6         18  
  6         22675  
15              
16             =pod
17              
18             =head1 NAME
19              
20             Geo::SpaceManager - Place rectangles without overlap
21              
22             =cut
23              
24             # public global variables
25             our $VERSION = '0.93';
26             our $DEBUG = 0;
27              
28             # private global variables
29             my @opposite = ( 2, 3, 0, 1 ); # opposite sides of rectangle
30              
31             =head1 SYNOPSIS
32              
33             use Geo::SpaceManager;
34              
35             $sm = Geo::SpaceManager->new([0,0,100,100]);
36             my $r1 = [10,10,40,30];
37             my $r2 = [20,20,60,40];
38             my $r3 = [50,10,80,70];
39             my $r4 = [20,50,90,90];
40             my $p1 = $sm->nearest($r1); # returns [10,10,40,30];
41             $sm->add($p1);
42             my $p2 = $sm->nearest($r2); # returns [20,30,60,50];
43             $sm->add($p2);
44             my $p3 = $sm->nearest($r3); # returns [60,10,90,70]
45             $sm->add($p3);
46             my $p4 = $sm->nearest($r4); # returns undef
47              
48             =head1 DESCRIPTION
49              
50             Geo::SpaceManager keeps track of the free space available in a two-dimensional
51             space as upright (non-rotated) rectangles are added. The module can find the
52             nearest available location where a rectangle may be placed or indicate that
53             the rectangle cannot be placed in any of the remaining free space.
54              
55             Rectangles are specified by references to four-element arrays giving the
56             boundaries of the rectangle:
57              
58             [ left, bottom, right, top ]
59              
60             Reflected boundary values may be used by swapping left <-> right and
61             top <-> bottom when specifying rectangles, but the return value of
62             nearest() will return a value as shown above.
63              
64             =head1 CONSTRUCTOR
65              
66             =head2 new
67              
68             The new() constructor should be called with the rectangle representing
69             the entire free space to be managed. A second, optional argument turns
70             debugging on if it has a true value.
71              
72             my $sm = Geo::SpaceManager->new( [0, 0, 100, 100] );
73             my $sm_debug = Geo::SpaceManager->new( [0, 0, 100, 100], 1 );
74            
75             =cut
76              
77             sub new
78             {
79 7     7 1 890 my( $class, $top, $dbg ) = @_;
80 7 50       29 croak("No space to manage in call to new") unless $top;
81 7 100       27 $DEBUG = $dbg if defined $dbg;
82 7         16 my $self = {};
83 7         20 bless $self, $class;
84 7         18 $top = [ @$top ];
85 7         32 $self->_normalize($top);
86 7         44 $self->{top} = $top;
87 7         22 $self->{free} = [ $top ];
88 7         24 $self->{minimum} = [ 0, 0 ];
89 7 100       286 print "Creating manager for space [@$top]\n" if $DEBUG;
90 7         24 return $self;
91             }
92              
93             =head1 METHODS
94              
95             =head2 set_minimum_size
96              
97             Set the minimum size for rectangles to be added.
98              
99             The following all set the minimum size of rectangles to (10,20):
100              
101             $sm->set_minimum_size([10,20]);
102             $sm->set_minimum_size([0,0,10,20]);
103             $sm->set_minimum_size([10,30,20,50]);
104             $sm->set_minimum_size([20,50,10,30]);
105              
106             Setting a minimum size means that SpaceManager can be more efficient
107             in space and time by discarding free-space areas if they are too small
108             to contain any more rectangles of the minimum size.
109              
110             You should set the minimum size before adding any rectangles and not
111             change it afterwards with another call to set_minimum_size.
112              
113             =cut
114              
115             sub set_minimum_size
116             {
117 1     1 1 6 my( $self, $rec ) = @_;
118 1         3 my @r;
119 1 50       4 unless( defined $rec ) {
120 0         0 carp("No minimum height and width passed to set_minimum_size");
121 0         0 return;
122             }
123 1   100     15 $r[$_] = ($$rec[$_] || 0 ) for (0..3);
124 1         5 $self->{minimum} = [ abs( $r[2] - $r[0] ), abs( $r[3] - $r[1] ) ];
125 1         3 return 1;
126             }
127              
128             =head2 add
129              
130             Add a rectangle to the current free space.
131              
132             $sm->add( [10,20,50,40] );
133              
134             The free space will be reduced by the rectangle. The method returns 1
135             if successful and undef on failure. The only failures will be if the
136             rectangle argument is missing or if it lies entirely outside of
137             the space.
138              
139             =cut
140              
141             sub add
142             {
143 309     309 1 500214 my( $self, $rec ) = @_;
144            
145 309 50 33     2137 unless( $rec and (@$rec == 4) ) {
146 0         0 carp("Invalid rectangle passed to add");
147 0         0 return;
148             }
149              
150 309 50       743 print "\nAdding [@$rec] to ", scalar @{$self->{free}},
  0         0  
151             " free rectangles\n" if $DEBUG;
152            
153 309         1209 my( $left, $bottom, $right, $top ) = $self->_normalized($rec);
154 309         462 my( @new_set, @reduced_set );
155            
156             # check if provided rectangle lies inside space
157 309         1448 for my $i ( 0 .. 1 ) {
158 618 50 33     4789 if( ($rec->[$i] > $self->{top}->[$opposite[$i]] ) ||
159             ($rec->[$i+2] < $self->{top}->[$opposite[$i+2]]) ) {
160 0         0 carp(sprintf "Rectangle [%s] is outside of space [%s]",
161 0         0 join(',',@$rec), join(',',@{$self->{top}} ) );
162 0         0 return;
163             }
164             }
165              
166             # check to see which current free-space rectangles are intersected by new one
167 309         6350 foreach my $r ( @{$self->{free}} ) {
  309         754  
168 2961         3426 my $reduce = 0;
169 2961 50       5674 print " Check [@$r] for reduction\n" if $DEBUG;
170            
171             # see if rectangles intersect
172 2961 50       5760 next if( $left >= $$r[2] );
173 2961 100       6467 next if( $right <= $$r[0] );
174 1814 100       4144 next if( $bottom >= $$r[3] );
175 1208 100       3118 next if( $top <= $$r[1] );
176            
177             # new rectangle intersects a free-space rectangle, which must be reduced
178             # determine which edges of the current free-space rectangle are
179             # intersected by the new one and form a new free-space rectangle
180             # by reducing the old one to the part that is not intersected.
181            
182             # see if new rectangle completly surrounds free rectangle
183 392 100 100     3076 if( $left <= $$r[0] &&
      100        
      100        
184             $bottom <= $$r[1] &&
185             $right >= $$r[2] &&
186             $top >= $$r[3] ) {
187              
188             # new rectangle covers this free rectangle completely -- remove it
189 4         14 $reduce = 1;
190 4 50       14 print " covered by new rectangle\n" if $DEBUG;
191              
192             }else{
193              
194             # left-reduced?
195 388 50       852 print " check if right edge $right in ($$r[0],$$r[2]):\n" if $DEBUG;
196 388 100       1285 if( $right < $$r[2] ) {
197 354         1146 my $newr = [ $right, $$r[1], $$r[2], $$r[3] ];
198 354 50       908 print " reduce [@$r] at $right to give [@$newr]\n" if $DEBUG;
199 354         479 push( @new_set, $newr);
200 354         467 $reduce = 1;
201             }
202            
203             # top-reduced?
204 388 50       4285 print " check if bottom edge $bottom in ($$r[1],$$r[3]):\n" if $DEBUG;
205 388 100       846 if( $bottom > $$r[1] ) {
206 22         50 my $newr = [ $$r[0], $$r[1], $$r[2], $bottom ];
207 22 50       49 print " reduce [@$r] at $bottom to give [@$newr]\n" if $DEBUG;
208 22         26 push( @new_set, $newr);
209 22         27 $reduce = 1;
210             }
211            
212             # right-reduced?
213 388 50       707 print " check if left edge $left in ($$r[0],$$r[2]):\n" if $DEBUG;
214 388 100       765 if( $left > $$r[0] ) {
215 13         33 my $newr = [ $$r[0], $$r[1], $left, $$r[3] ];
216 13 50       586 print " reduce [@$r] at $left to give [@$newr]\n" if $DEBUG;
217 13         21 push( @new_set, $newr);
218 13         17 $reduce = 1;
219             }
220            
221             # bottom-reduced?
222 388 50       943 print " check if top edge $top in ($$r[1],$$r[3]):\n" if $DEBUG;
223 388 100       1237 if( $top < $$r[3] ) {
224 352         930 my $newr = [ $$r[0], $top, $$r[2], $$r[3] ];
225 352 50       701 print " reduce [@$r] at $top to give [@$newr]\n" if $DEBUG;
226 352         434 push( @new_set, $newr);
227 352         1068 $reduce = 1;
228             }
229             }
230            
231             # put the existing rectangle on a list of rectangles to be removed
232             # if it was reduced or covered by the new one
233 392 50       806 if( $reduce ) {
234 392         835 push( @reduced_set, $r );
235             }
236             }
237            
238 309 50       1722 if( $DEBUG ) {
239 0         0 print scalar @new_set, " new rectangles, ", scalar @reduced_set,
240             " rectangles to be removed\n";
241             }
242              
243             # determine which of the new free-space rectangles to keep:
244            
245             # 1. only keep the reduced free-space rectangles if they are greater
246             # than the minimum size (which can be zero)
247             # 2. don't keep a rectangle if it is identical to one already in the list
248             # (i.e., has a lower index)
249             # 3. don't keep a rectangle if it is entirely within another
250            
251 309         1080 my @mod_new_set;
252 309 50       589 print " Test new candidates for inclusion:\n" if $DEBUG;
253 309         1655 ADD: for my $i1 ( 0 .. $#new_set ) {
254 741         1437 my $r1 = $new_set[$i1];
255 741 50       1319 print " check [@$r1]\n" if $DEBUG;
256            
257             # skip if less than minimum size
258             next if(
259 741         2543 ( $$r1[2] - $$r1[0] ) < ${$self->{minimum}}[0] or
  723         3251  
260 741 100 100     1005 ( $$r1[3] - $$r1[1] ) < ${$self->{minimum}}[1]
261             );
262            
263             # compare to other candidate rectangles
264 705         1713 for my $i2 ( 0 .. $#new_set ) {
265            
266             # don't compare with itself
267 2441 100       6804 next if $i1 eq $i2;
268 1744         2056 my $r2 = $new_set[$i2];
269 1744 50       3404 print " compare with [@$r2]\n" if $DEBUG;
270              
271             # see if identical to another one
272 1744 100 100     32340 if(
      66        
      66        
273             ($$r1[0] == $$r2[0]) &&
274             ($$r1[1] == $$r2[1]) &&
275             ($$r1[2] == $$r2[2]) &&
276             ($$r1[3] == $$r2[3]) ) {
277            
278             # keep the last one if they are identical
279 584 100       1749 next ADD if $i1 < $i2;
280              
281             }else{
282             # skip this one if it is entirely within another one
283             next ADD if(
284 1160 100 100     5329 ($$r1[0] >= $$r2[0]) &&
      100        
      100        
285             ($$r1[1] >= $$r2[1]) &&
286             ($$r1[2] <= $$r2[2]) &&
287             ($$r1[3] <= $$r2[3]) );
288             }
289             }
290 398         731 push( @mod_new_set, $r1 );
291 398 50       1003 print " keeping [@$r1]\n" if $DEBUG;
292             }
293              
294 309 50       849 if( $DEBUG ) {
295 0         0 print " keeping ", scalar @mod_new_set, " new rectangles\n";
296 0         0 print " deleting ", scalar @reduced_set, " current rectangles\n";
297 0         0 print " New:\n";
298 0         0 foreach my $r ( @mod_new_set ) {
299 0         0 print " [@$r]\n";
300             }
301 0         0 print " Delete:\n";
302 0         0 foreach my $r ( @reduced_set ) {
303 0         0 print " [@$r]\n";
304             }
305             }
306              
307             # form the new set of free-space rectangles
308            
309             # delete rectangles that have been reduces
310 309         448 my @new_free;
311 309         358 DEL: foreach my $r ( @{$self->{free}} ) {
  309         664  
312 2961         3835 foreach my $s ( @reduced_set ) {
313 3643 100       13738 next DEL if( $r eq $s );
314             }
315 2569         4117 push( @new_free, $r );
316 2569 50       6119 print " keeping [@$r]\n" if $DEBUG;
317             }
318            
319             # add reduced parts of old rectangles that have made it through
320             # the selection process
321 309         777 push( @new_free, @mod_new_set );
322              
323             # save the new set
324 309         1478 $self->{free} = \@new_free;
325              
326 309 50       834 if( $DEBUG ) {
327 0         0 print "New Free Set (", scalar @new_free, "):\n";
328 0         0 foreach my $rec ( @new_free ) {
329 0         0 my $area = ( $$rec[2] - $$rec[0] )*( $$rec[3] - $$rec[1] );
330 0         0 print " [@$rec] $area\n";
331             }
332 0         0 print "\n";
333             }
334 309         2445 return 1;
335             }
336              
337             =head2 nearest
338              
339             Find the nearest location in which to place the specified rectangle.
340              
341             $r = $sm->nearest([10,30,30,50]);
342              
343             The method will return a reference to an array of four scalars specifying
344             a rectangle of the same size as the supplied one that fits wholly into
345             an available free space if space can be found. The rectangle will be
346             a copy of the provided one if it fits as is. The method will return
347             undef if there is no free space that can contain the supplied rectangle.
348              
349             =cut
350              
351             sub nearest
352             {
353 510     510 1 713528 my( $self, $rec ) = @_;
354 510 50       1509 print "find nearest free location to contain [@$rec]\n" if $DEBUG;
355 510         740 my( $best, $best_dist );
356 510         1145 $self->_normalize($rec);
357 510         1685 my $w = $$rec[2] - $$rec[0];
358 510         951 my $h = $$rec[3] - $$rec[1];
359 510 50       1163 print " width=$w, height=$h\n" if $DEBUG;
360              
361             # search every available free-space rectangle to find best fit
362 510         2095 foreach my $r ( @{$self->{free}} ) {
  510         1111  
363 737 50       1337 print " check [@$r]\n" if $DEBUG;
364 737 50 66     8208 return [ @$rec ] if(
      66        
      33        
365             ($$rec[0] >= $$r[0]) &&
366             ($$rec[1] >= $$r[1]) &&
367             ($$rec[2] <= $$r[2]) &&
368             ($$rec[3] <= $$r[3]) );
369            
370             # see if rectangle would fit
371 228 50       367 printf " check size against (%.2f,%.2f)\n", ($$r[2]-$$r[0]),
372             ($$r[3]-$$r[1]) if $DEBUG;
373 228 50       433 next if( $w > ( $$r[2] - $$r[0] ));
374 228 50       573 next if( $h > ( $$r[3] - $$r[1] ));
375            
376             # see how far rectangle would have to be moved to be placed inside
377            
378 228         329 my @dif = map { $$rec[$_] - $$r[$_] } ( 0..3 );
  912         1689  
379 228         307 my @absdif = map { abs($_) } @dif;
  912         1166  
380 228         281 my $ydif = 0;
381 228 50       430 if( ($dif[1] * $dif[3]) > 0 ) {
382 0 0       0 $ydif = ( ($absdif[1] > $absdif[3]) ? $absdif[3] : $absdif[1] );
383             }
384 228         215 my $xdif = 0;
385 228 50       430 if( ($dif[0] * $dif[2]) > 0 ) {
386 228 50       402 $xdif = ( ($absdif[0] > $absdif[2]) ? $absdif[2] : $absdif[0] );
387             }
388 228         280 my $dist = $xdif * $xdif + $ydif * $ydif;
389 228 50       373 print " dif=[@dif], del=[$xdif,$ydif], dist=$dist\n" if $DEBUG;
390 228 100 66     988 if( ! $best or ($dist < $best_dist) ) {
391 47         57 $best = $r;
392 47         52 $best_dist = $dist;
393 47 50       130 print " best so far\n" if $DEBUG;
394             }
395             }
396            
397             # quit if doesn't fit
398 1 50       8 return undef unless $best;
399            
400 0 0       0 print " nearest free space is [@$best]\n" if $DEBUG;
401 0         0 my $r = [ @$rec ];
402            
403             # translate rectangle to nearest edge of nearest rectangle
404 0 0       0 if( $$r[0] < $$best[0] ) {
    0          
405 0         0 $$r[0] = $$best[0];
406 0         0 $$r[2] = $$r[0] + $w;
407             }elsif( $$r[2] > $$best[2] ) {
408 0         0 $$r[2] = $$best[2];
409 0         0 $$r[0] = $$r[2] - $w;
410             }
411 0 0       0 if( $$r[1] < $$best[1] ) {
    0          
412 0         0 $$r[1] = $$best[1];
413 0         0 $$r[3] = $$r[1] + $h;
414             }elsif( $$r[3] > $$best[3] ) {
415 0         0 $$r[3] = $$best[3];
416 0         0 $$r[1] = $$r[3] - $h;
417             }
418            
419 0         0 return $r;
420             }
421              
422             =head2 distance
423              
424             Return the distance between two (x,y) points or two rectangles.
425              
426             $dist = $sm->distance( $rect1, $rect2 );
427             $dist = $sm->distance( [0,0], [3,4] ); # returns 5
428              
429             Calculate the distance between the two arguments, which should be
430             references to arrays with at least two elements. Only the first two
431             elements will be used, so you may pass refereces to two arrays with
432             four elements that represent rectangles. This method may be used to
433             find how far away the nearest available location is from a desired
434             rectangle placement location.
435              
436             $s = $sm->nearest($r);
437             $d = $sm->distance($r,$s);
438             print "nearest available location is $d units away\n";
439            
440             =cut
441              
442             sub distance
443             {
444 2049     2049 1 4619 my( $self, $r1, $r2 ) = @_;
445 2049 50       5471 unless ( $r2 ) {
446 0         0 carp("Please pass two arguments to distance method");
447 0         0 return;
448             }
449 2049         3310 my $x_delta = ( $r1->[0] - $r2->[0] );
450 2049         4694 my $y_delta = ( $r1->[1] - $r2->[1] );
451 2049         13171 return sqrt( ($x_delta * $x_delta) + ($y_delta * $y_delta) );
452             }
453              
454             =head2 dump
455              
456             $sm->dump()
457              
458             Print out the current set of free-space rectangles to standard output.
459             The area of each rectangle is also printed.
460              
461             =cut
462              
463             sub dump
464             {
465 0     0 1 0 my $self = shift;
466 0         0 print "Current Free Set ", scalar @{$self->{free}}, " Rectangles:\n";
  0         0  
467 0         0 foreach my $rec (
  0         0  
468             sort {
469 0 0       0 if( ${$a}[0] == ${$b}[0] ) {
  0         0  
  0         0  
470 0         0 return ${$a}[1] <=> ${$b}[1];
  0         0  
  0         0  
471             }else{
472 0         0 return ${$a}[0] <=> ${$b}[0];
  0         0  
  0         0  
473             }
474             } @{$self->{free}} ) {
475 0         0 my $s = "[ @$rec ]";
476 0         0 my $area = ( $$rec[2] - $$rec[0] )*( $$rec[3] - $$rec[1] );
477 0         0 printf " %8d %s\n", $area, $s;
478             }
479 0         0 print "\n";
480             }
481              
482             ################################################################################
483              
484             # internal functions
485              
486             # _normalize
487             #
488             # Exchange left<->right and top<->bottom if not in order
489             #
490             sub _normalize
491             {
492 517     517   824 my( $self, $rec ) = @_;
493 517 50       1038 return unless $rec;
494 517         1303 for my $i ( 0 .. 1 ) {
495 1034 100       4155 if( $$rec[$i] > $$rec[$opposite[$i]] ) {
496 400         1558 ( $$rec[$i], $$rec[$opposite[$i]] ) =
497             ( $$rec[$opposite[$i]], $$rec[$i] );
498             }
499             }
500             }
501              
502             # _normalized
503             #
504             # Return list of boundaries of normalized rectangle
505             #
506             sub _normalized
507             {
508 309     309   584 my( $self, $rec ) = @_;
509 309 50       868 return unless $rec;
510 309         685 my( $left, $bottom, $right, $top ) = @$rec;
511 309 100       729 if( $left > $right ) {
512 45         79 ( $left, $right ) = ( $right, $left );
513             }
514 309 100       659 if( $bottom > $top ) {
515 45         71 ( $bottom, $top ) = ( $top, $bottom );
516             }
517 309         944 return ( $left, $bottom, $right, $top );
518             }
519              
520             =head1 ACKNOWLEDGMENTS
521              
522             The algorithm used is based on that described by Bernard and Jacquenet
523             in "Free space modeling for placing rectangles without overlap"
524             which appeared in the Journal of Universal Computer Science, vol. 3,
525             no. 6, 1997. See http://www.jucs.org/jucs_3_6/free_space_modeling_for
526              
527             The term "space manager" was used by Bell and Feiner in their paper
528             "Dynamic Space Management for User Interfaces",
529             Proc. UIST '00, San Diego, CA, November 5-8 2000. pp. 239-248.
530              
531             =head1 LIMITATIONS
532              
533             The algorithm used is first-come-first-served and makes no attempt at
534             optimization such as minimum displacements. The first rectangle placed
535             will occupy its desired location, while others may have to be moved,
536             farther and farther as more are placed.
537              
538             There is no method for removing rectangles and restoring the space
539             they occupied. Doing so is not trivial and remains the goal of a
540             later update to this module, if there turns out to be a demand for
541             such a feature. See the Bell and Feiner paper cited above for details
542             on how this could be done.
543              
544             This module does in theory handle the placement of overlapping
545             rectangles. That is you can place a rectangle that overlaps a
546             rectangle that was already added and was, therefore, not
547             returned by a call to the nearest method. The module should
548             reduce the free space correctly in this case. However, this
549             feature has not been thoroughly tested and there may still be
550             bugs. It is safest to add only rectangles that have been returned
551             from the nearest method.
552              
553             =head1 BUGS
554              
555             None known at this time.
556              
557             =head1 SUPPORT
558              
559             Please e-mail the author if any bugs are found.
560              
561             =head1 AUTHOR
562              
563             Jim Gibson
564             CPAN ID: JGIBSON
565            
566             Jim@Gibson.org
567             jim.gibson.org
568              
569             =head1 COPYRIGHT
570              
571             This program is free software; you can redistribute
572             it and/or modify it under the same terms as Perl itself.
573              
574             The full text of the license can be found in the
575             LICENSE file included with this module.
576              
577             =head1 SEE ALSO
578              
579             perl(1).
580              
581             =cut
582              
583             1;