Math-Polygon-Tree

 view release on metacpan or  search on metacpan

lib/Math/Polygon/Tree.pm  view on Meta::CPAN

package Math::Polygon::Tree;
$Math::Polygon::Tree::VERSION = '0.08';
# ABSTRACT: fast check if point is inside polygon

# $Id$


use 5.010;
use strict;
use warnings;
use utf8;
use Carp;

use base qw{ Exporter };

use List::Util qw{ reduce first sum min max };
use List::MoreUtils qw{ all any };
use POSIX qw/ floor ceil /;

# todo: remove gpc and use simple bbox clip
our $CLIPPER_CLASS;
BEGIN {
    my @clippers = qw/
        Math::Geometry::Planar::GPC::PolygonXS
        Math::Geometry::Planar::GPC::Polygon
    /;

    for my $class ( @clippers ) {
        eval "require $class"  or next;
        $CLIPPER_CLASS = $class;
        last;
    }

    croak "No clipper class available" if !$CLIPPER_CLASS;
}
    


our @EXPORT_OK = qw{
    polygon_bbox
    polygon_centroid
    polygon_contains_point
};

our %EXPORT_TAGS = ( all => \@EXPORT_OK );


# tree options
our $MAX_LEAF_POINTS = 16;
our $SLICE_FIELD = 0.0001;
our $SLICE_NUM_COEF = 2;
our $SLICE_NUM_SPEED_COEF = 1;

# floating-point comparsion accuracy
our $POLYGON_BORDER_WIDTH = 1e-9;



sub new {
    my ($class, @in_contours) = @_;
    my %opt = ref $in_contours[-1] eq 'HASH' ? %{pop @in_contours} : ();
    my $self = bless {}, $class;

lib/Math/Polygon/Tree.pm  view on Meta::CPAN

                        # upper edge
                        if ( $c->[$i]->[1] == $y1 && $c->[$j]->[1] == $y1 ) {
                            my $y = max grep {$_ < $y1} map {$_->[1]} @$c;
                            push @{ $self->{filled_bboxes} }, [$x0, $y, $x1, $y1];
                        }
                    }
                }
            }
        }

        return $self;
    }


    # calc number of pieces (need to tune!)
    my ($xmin, $ymin, $xmax, $ymax) = @{$self->{bbox}};
    my $xy_ratio = ($xmax-$xmin) / ($ymax-$ymin);
    my $nparts = $SLICE_NUM_COEF * log( exp(1) * ($nrpoints/$MAX_LEAF_POINTS)**$SLICE_NUM_SPEED_COEF );

    my $x_parts = $self->{x_parts} = ceil( sqrt($nparts * $xy_ratio) );
    my $y_parts = $self->{y_parts} = ceil( sqrt($nparts / $xy_ratio) );
    my $x_size  = $self->{x_size}  = ($xmax-$xmin) / $x_parts;
    my $y_size  = $self->{y_size}  = ($ymax-$ymin) / $y_parts;


    # slice
    my $subparts = $self->{subparts} = [];
    
    my $gpc_poly = $CLIPPER_CLASS->new_gpc();
    $gpc_poly->add_polygon( $_, 0 )  for @contours;
    
    for my $j ( 0 .. $y_parts-1 ) {
        for my $i ( 0 .. $x_parts ) {

            my $x0 = $xmin + ($i  -$SLICE_FIELD)*$x_size;
            my $y0 = $ymin + ($j  -$SLICE_FIELD)*$y_size;
            my $x1 = $xmin + ($i+1+$SLICE_FIELD)*$x_size;
            my $y1 = $ymin + ($j+1+$SLICE_FIELD)*$y_size;

            my $gpc_slice = $CLIPPER_CLASS->new_gpc();
            $gpc_slice->add_polygon([ [$x0,$y0],  [$x0,$y1], [$x1,$y1], [$x1,$y0], [$x0,$y0] ], 0);

            my @slice_parts = $gpc_poly->clip_to($gpc_slice, 'INTERSECT')->get_polygons();

            # empty part
            if ( !@slice_parts ) {
                $subparts->[$i]->[$j] = 0;
                next;
            }

            # filled part
            if (
                @slice_parts == 1 && @{$slice_parts[0]} == 4
                && all { ($_->[0]==$x0 || $_->[0]==$x1) && ($_->[1]==$y0 || $_->[1]==$y1) } @{$slice_parts[0]}
            ) {
                $subparts->[$i]->[$j] = 1;
                next;
            }

            # complex subpart
            $subparts->[$i]->[$j] = Math::Polygon::Tree->new( @slice_parts, (%opt ? \%opt : ()) );
        }
    }

    return $self;
}




sub read_poly_file {
    my ($file) = @_;

    my $need_to_open = !ref $file || ref $file eq 'SCALAR';
    my $fh = $need_to_open
        ? do { open my $in, '<', $file  or croak "Couldn't open $file: $@"; $in }
        : $file;

    my @contours;
    my $pid;
    my @cur_points;
    while ( my $line = readline $fh ) {
        # new contour
        if ( $line =~ /^([\-\!]?) (\d+)/x ) {
            $pid = $1 ? -$2 : $2;
            next;
        }

        # point
        if ( $line =~ /^\s+([0-9.Ee+-]+)\s+([0-9.Ee+-]+)/ ) {
            push @cur_points, [ $1+0, $2+0 ];
            next;
        }

        # !!! inner contour - skipping
        if ( $line =~ /^END/  &&  $pid < 0 ) {
            @cur_points = ();
            next;
        }

        # outer contour
        if ( $line =~ /^END/  &&  @cur_points ) {
            push @contours, [ @cur_points ];
            @cur_points = ();
            next;
        }
    }

    close $fh  if $need_to_open;
    return @contours;
}



sub contains {
    my ($self, $point) = @_;

    croak "Point should be a reference"  if ref $point ne 'ARRAY';

    # check bbox
    my ($px, $py) = @$point;

lib/Math/Polygon/Tree.pm  view on Meta::CPAN


    if ( $sq == 0 ) {
        my $bbox = polygon_bbox($contour);
        return [ ($bbox->[0]+$bbox->[2])/2, ($bbox->[1]+$bbox->[3])/2 ];
    }

    return [$sx/$sq, $sy/$sq];
}



sub polygon_contains_point {
    my ($point, @poly) = @_;
    my $contour = ref $poly[0]->[0] ? $poly[0] : \@poly;

    my ($x, $y) = @$point;
    my ($px, $py) = @{ $contour->[0] };
    my ($nx, $ny);

    my $inside = 0;

    for my $i ( 1 .. scalar @$contour ) { 
        ($nx, $ny) =  @{ $contour->[ $i % scalar @$contour ] };

        return -1  if abs($y-$py) < $POLYGON_BORDER_WIDTH && abs($x-$px) < $POLYGON_BORDER_WIDTH;

        return -1
            if  abs($y-$py) < $POLYGON_BORDER_WIDTH  &&  abs($py-$ny) < $POLYGON_BORDER_WIDTH
                && ( $x >= $px  ||  $x >= $nx )
                && ( $x <= $px  ||  $x <= $nx );

        next    if  abs($py-$ny) < $POLYGON_BORDER_WIDTH;
        next    if  $y < $py  &&  $y < $ny;
        next    if  $y > $py  &&  $y > $ny;
        next    if  $x > $px  &&  $x > $nx;

        my $xx = ($y-$py)*($nx-$px)/($ny-$py)+$px;
        return -1   if  abs($x-$xx) < $POLYGON_BORDER_WIDTH;

        next    if  $y <= $py  &&  $y <= $ny;

        $inside = 1 - $inside
            if  abs($px-$nx)<$POLYGON_BORDER_WIDTH  ||  $x < $xx;
    }
    continue { ($px, $py) = ($nx, $ny); }

    return $inside;
}


1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Math::Polygon::Tree - fast check if point is inside polygon

=head1 VERSION

version 0.08

=head1 SYNOPSIS

    use Math::Polygon::Tree;

    my $poly  = [ [0,0], [0,2], [2,2], ... ];
    my $bound = Math::Polygon::Tree->new( $poly );

    if ( $bound->contains( [1,1] ) )  { ... }

=head1 DESCRIPTION

Math::Polygon::Tree creates a tree of polygon parts for fast check if object is inside this polygon.
This method is effective if polygon has hundreds or more segments.

=head1 METHODS

=head2 new

Takes contours and creates a tree structure.
All polygons are outers, inners are not implemented.

Contour is an arrayref of points:

    my $poly1 = [ [0,0], [0,2], [2,2], ... ];   
    ...
    my $bound = Math::Polygon::Tree->new( $poly1, $poly2, ..., \%opt );

or a .poly file

    my $bound1 = Math::Polygon::Tree->new( \*STDIN );
    my $bound2 = Math::Polygon::Tree->new( 'boundary.poly' );

Options:

    prepare_rough

=head2 contains

    my $is_inside = $bound->contains( [1,1] );
    if ( $is_inside ) { ... }

Checks if point is inside bound polygon.

Returns 1 if point is inside polygon, -1 if it lays on polygon boundary, or 0 otherwise.

=head2 contains_points

    # list of points
    if ( $bound->contains_points( [1,1], [2,2] ... ) )  { ... }

    # arrayref of points
    if ( $bound->contains_points( [[1,1], [2,2] ...] ) )  { ... }

Checks if all points are inside or outside polygon.

Returns 1 if all points are inside polygon, 0 if all outside, or B<undef> otherwise.

=head2 contains_bbox_rough

    my $bbox = [ 1, 1, 2, 2 ];
    if ( $bound->contains_bbox_rough( $bbox, \%opt ) )  { ... }

Rough check if box is inside bound polygon.

Returns 1 if box is inside polygon, 0 if box is outside polygon or B<undef> if it 'doubts'.

Options:

    inaccurate - allow false positive results

=head2 contains_polygon_rough

Checks if polygon is inside bound polygon.

Returns 1 if inside, 0 if outside or B<undef> if 'doubts'. 

    if ( $bound->contains_polygon_rough( [ [1,1], [1,2], [2,2], ... ] ) )  { ... }

=head2 bbox

    my $bbox = $bound->bbox();
    my ($xmin, $ymin, $xmax, $ymax) = @$bbox;

Returns polygon's bounding box.

=head1 FUNCTIONS

=head2 read_poly_file

    my @contours = read_poly_file( \*STDIN );
    my @contours = read_poly_file( 'bound.poly' )

Reads content of .poly-file. See http://wiki.openstreetmap.org/wiki/.poly

=head2 polygon_bbox

    my $bbox = polygon_bbox( [[1,1], [1,2], [2,2], ... ] );
    my ($xmin, $ymin, $xmax, $ymax) = @$bbox;

Returns polygon's bounding box.

=head2 bbox_union

    my $united_bbox = bbox_union($bbox1, $bbox2);

Returns united bbox for two bboxes/points.

=head2 polygon_centroid

    my $center_point = polygon_centroid( [ [1,1], [1,2], [2,2], ... ] );

Returns polygon's weightened center.

Math::Polygon 1.02+ has the same function, but it is very inaccurate.

=head2 polygon_contains_point

    my $is_inside = polygon_contains_point($point, $polygon);

Function that tests if polygon contains point (modified one from Math::Polygon::Calc).

Returns -1 if point lays on polygon's boundary

=head1 AUTHOR

liosha <liosha@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by liosha.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut



( run in 0.568 second using v1.01-cache-2.11-cpan-df04353d9ac )