Edit: eric256 has made the following code obsolete for my purpose by whipping up a faster and more efficient flood fill algorithm in a reply below. I'll be leaving this up for anyone who might actually need a QuadTree thought.

I'm currently working on a 3D application, which deals mostly with an integer-based 3d grid. This grid is seperated into two-dimensional 16x16 blocks. Over the course of development it became apparent that one point it would be advantageous to be able to quickly sub-divide these blocks into a list of non-overlapping rectangles, based on an on/off property of each unit within these blocks.

As such it was suggested to me to create a QuadTree. This worked out pretty well for me and implementation went quickly and successfully. However, as i'm only one person, I'm worried that i might have done something stupid or overlooked something and would be happy if some of you monks could spend a few minutes to look it over and comment on things i could've done differently or maybe even better. (I'm also kinda wondering if it'd be worth it to submit this on CPAN.)

The code of the Module is as follows:
package Lifevis::QuadTree; use 5.010; use strict; use warnings; use Carp qw/ carp /; use enum qw(BITMASK: NW SW SE NE); use constant ALL => NW | SW | SE | NE; my %pos_bitmask = ( nw => NW, sw => SW, se => SE, ne => NE, ); my @adjacent_pairs = ( [ 'nw', 'ne' ], [ 'sw', 'se' ], [ 'nw', 'sw' ], + [ 'ne', 'se' ], ); ############################### # # sub new() - constructor # # my $test = Lifevis::QuadTree->new( # -xmin => 0, # -xmax => 15, # -ymin => 0, # -ymax => 15, # -parent => undef, # -pos => undef); # # Note that it treats the coordinates as those of an integer-based gri +d, # with the height and width being equal and a power of 2. # ############################### sub new { # create self as new object my $class = shift; my $self = {}; bless( $self, $class ); # initialize base attributes $self->{CHILDREN} = {}; $self->{FILLSTATE} = 0; # set coordinates, parent, position, based on input, bail if input + not complete my %args = @_; for my $arg (qw/ xmin ymin xmax ymax parent pos /) { if ( !exists $args{"-$arg"} ) { carp "- must specify $arg"; return; } $self->{ uc $arg } = $args{"-$arg"}; } # calculate middle of coordinate range if ( $self->{XMAX} != $self->{XMIN} ) { $self->{XMID} = $self->{XMIN} + int( ( $self->{XMAX} - $self-> +{XMIN} ) / 2 ); $self->{YMID} = $self->{YMIN} + int( ( $self->{YMAX} - $self-> +{YMIN} ) / 2 ); } return $self; } ############################### # # sub set( $x, $y ) # # This function drills down into the quad tree, creating nodes as need +ed, until it reaches # the node that exactly matches the cordinates given as input. Once th +at node is reached # it sets all fill bits on that node by calling the adjust_fillstate m +ethod. # ############################### sub fill { my ( $self, $x, $y ) = @_; # set working variables to make code below more readable my ( $xmin, $ymin, $xmax, $ymax, $xmid, $ymid ) = ( $self->{XMIN}, $self->{YMIN}, $self->{XMAX}, $self->{YMAX}, $s +elf->{XMID}, $self->{YMID} ); # initialize working variables my ( $pos, $child ); my ( @xcoords, @ycoords ); # find out in what quadrant of the current node the given coordina +tes are if ( $y <= $ymid ) { $pos = 'n'; } else { $pos = 's'; } if ( $x <= $xmid ) { $pos .= 'w'; } else { $pos .= 'e'; } # get pointer to child node that corresponds with the quadrant, cr +eate new child if necessary $child = $self->{CHILDREN}{$pos}; if ( !defined $child ) { # calculate the coordinate range fors the new child node based + on the quadrant if ( $pos =~ /n/ ) { @ycoords = ( $ymin, $ymid ); } else { @ycoords = ( 1 + $ymid, $ymax ); } if ( $pos =~ /w/ ) { @xcoords = ( $xmin, $xmid ); } else { @xcoords = ( 1 + $xmid, $xmax ); } # create new node and assign it to the current node as child f +or the given quadrant $child = Lifevis::QuadTree->new( -xmin => $xcoords[0], -xmax => $xcoords[1], -ymin => $ycoords[0], -ymax => $ycoords[1], -parent => $self, -pos => $pos_bitmask{$pos} ); $self->{CHILDREN}{$pos} = $child; } # set all fill bits on child node if it matches the given coordina +tes exactly, # otherwise call set on the child for the given quadrant to drill +deeper if ( defined $xcoords[0] && $xcoords[0] == $xcoords[1] ) { $child->adjust_fillstate(ALL); } else { $child->fill( $x, $y ); } return; } ############################### # # sub adjust_fillstate( $quadrant_bit ) # ############################### sub adjust_fillstate { my ( $self, $quadrant_bit ) = @_; # activate the given fill bit on the current node $self->{FILLSTATE} |= $quadrant_bit; # if all fill bits on the current node are set, delete children an +d # activate the fill bit of the quadrant of the current node on the + parent node if ( $self->{FILLSTATE} == ALL ) { undef $self->{CHILDREN}; $self->{PARENT}->adjust_fillstate( $self->{POS} ); } return; } ############################### # # sub adjust_fillstate( $quadrant_bit ) # # This function extracts all the non-overlapping rectangles in the tre +e, # returning their coordinates seperated by : in sets seperated by |. # ############################### sub get_rectangles { my ($self) = @_; # in case the whole tree gets filled, return the coordinates of th +e root node if ( $self->{FILLSTATE} == ALL ) { return "$self->{XMIN}:$self->{XMAX}:$self->{YMIN}:$self->{YMAX +}|"; } my $rectangles; # check if childs are adjacent and combine their coordinates as we +ll as remove them from the tree for my $i ( 0 .. 3 ) { # get pair of possible adjacent childs my $one = $adjacent_pairs[$i][0]; my $two = $adjacent_pairs[$i][1]; # check if fill bits of pair are set if ( ( $self->{FILLSTATE} & $pos_bitmask{$one} ) and ( $self-> +{FILLSTATE} & $pos_bitmask{$two} ) ) { # get pointers for pair, makes the next step more readable my $child1 = $self->{CHILDREN}{$one}; my $child2 = $self->{CHILDREN}{$two}; # combine their coords $rectangles .= "$child1->{XMIN}:$child2->{XMAX}:$child1->{ +YMIN}:$child2->{YMAX}|"; # delete them from the hash and unset their fill bit delete $self->{CHILDREN}{$one}; delete $self->{CHILDREN}{$two}; $self->{FILLSTATE} ^= $pos_bitmask{$one}; $self->{FILLSTATE} ^= $pos_bitmask{$two}; } } # deal with remaining childs for my $pos ( keys %{ $self->{CHILDREN} } ) { my $child = $self->{CHILDREN}{$pos}; # either get the coordinates of child if is completely set, or + get rectangles within child if ( $child->{FILLSTATE} == ALL ) { $rectangles .= "$child->{XMIN}:$child->{XMAX}:$child->{YMI +N}:$child->{YMAX}|"; } else { $rectangles .= $self->{CHILDREN}{$pos}->get_rectangles; } } # return string with coordinates of rectangles within this node return $rectangles; } 1;
and a usage example would be this:
use Lifevis::QuadTree; my $test = Lifevis::QuadTree->new( -xmin => 0, -xmax => 15, -ymin => 0, -ymax => 15, -parent => undef, -pos => undef ); for my $x (0..3) { for my $y (0..3) { next if( $x ==0 and $y == 0); $test->fill($x,$y); } } my $moo = $test->get_rectangles;
Thanks in advance to anyone who spent a bit of time looking over this. :)

Replies are listed 'Best First'.
Re: RFC: Implementation of a QuadTree and worries of a lone programmer
by GrandFather (Saint) on Nov 07, 2008 at 21:25 UTC

    I haven't looked in detail at the code, but I'd put the block comments into pod blocks. In the short term they are easier to edit and in the longer term there are nice tools for pulling the pod out into various documentation formats. Consider:

    =head3 adjust_fillstate my $rects = adjust_fillstate( $quadrant_bit ); This function extracts all the non-overlapping rectangles in the tree, returning their coordinates separated by : in sets separated by |. =cut

    which renders in HTML as:


    adjust_fillstate

       my $rects = adjust_fillstate( $quadrant_bit );
    

    This function extracts all the non-overlapping rectangles in the tree, returning their coordinates separated by : in sets separated by |.


    Perl reduces RSI - it saves typing
      I didn't know about this and it's really nicer than masses of #s. Thanks for the hint.
Re: Implementation of a QuadTree and worries of a lone programmer
by zentara (Cardinal) on Nov 07, 2008 at 18:10 UTC
    Just some BS'ing...brainstorming or bullsh*ting...take your pick. :-)

    I'm not sure what the Quad tree is used for, and whether you really need it, but as far as overlooking something, I would wonder if it is the fastest way to get your lists. Might not it be faster to make AoA's (arrays of arrays), and loop thru them with Inline-C, finding all $states==1, then extracting the $x,$y,$z.

    I'm just mentioning it, because if your grid gets larger, and you are doing 3d stuff, you will need speed. Perl is relatively slow doing recursive math calculations.


    I'm not really a human, but I play one on earth Remember How Lucky You Are
      What it'll be used for is this: I want to make use of OpenGL occlusion routines to determine which parts of the world can be skipped in drawing. For that i need to create a rough shape around the parts that would be drawn, keeping it cheap enough to actually make a difference. That shape is created by taking all units that would contain something and drawing shapes around them.

      I could go the naive way and draw one cube per unit and call it a day. However that would be rather slow and i'd prefer to draw a rectangular shape that encompasses as many units as possible.

      To give an example on practice, with a map such as this:
      0 4 8 12 15 00111000000000000 1111000000000000 1111000000000000 1111000000000000 40000000000000000 0000000000000000 0000000000000000 0000000000000000 80000000000000000 0000000000000000 0000000000000000 0000000000000000 120000000000000000 0000000000000000 0000000000000000 150000000000000000
      , the result would be: 0:3:2:3|2:3:0:1|0:1:1:1|1:1:0:0|. This would be 4 shapes as opposed to the 15 shapes needed if i were drawing this naively.

      I'm not sure at all what you mean in your example in the first place. But my guts tell me that you kinda missed the point. Also, i have no idea about how to use inline-c.

      Edit: I should also note, the grid won't get bigger. At least not in my application. 16x16 is the limit on the data i have. I merely made it work for general cases in case others might find it useful and because i found it easier to work that way.

        I don't understand your notation there "0:3:2:3|2:3:0:1|0:1:1:1|1:1:0:0|", but can't you draw the area with 1's as two objects one large tall one and one small tall one? (1,0)-(3,3) and (0,1)-(0,3) or of course you could split it the other way (0,1)->(3,3) and (1,0)->(3,0).


        ___________
        Eric Hodges