Return to Index   blog
Douglas-Peucker Polygon/Line Simplification in Perl Download Zipped
# Douglas - Peucker algorithm 
# Author. John D. Coryat 01/2007 USNaviguide.com
# Adapted from: http://mapserver.gis.umn.edu/community/scripts/thin.pl
##
# Douglas-Peucker polyline simplification algorithm. First draws single line
# from start to end. Then finds largest deviation from this straight line, and if
# greater than tolerance, includes that point, splitting the original line into
# two new lines. Repeats recursively for each new line created.
##
package USNaviguide_Douglas_Peucker ;
require 5.003 ;
use strict ;

BEGIN {
 use Exporter ;
 use vars qw ( $VERSION @ISA @EXPORT) ;
 $VERSION	= 1.0 ;
 @ISA		= qw ( Exporter ) ;
 @EXPORT	= qw ( 
 Douglas_Peucker
 perp_distance
 haversine_distance_meters
 angle3points
 ) ;
}

# Call as: @Opoints = &Douglas_Peucker( <reference to input array of points>, <tolerance>) ;
# Returns: Array of points
# Points Array Format:
# ([lat1,lng1],[lat2,lng2],...[latn,lngn])
#

sub Douglas_Peucker
{
my $href	= shift ;
my $tolerance	= shift ;
my @Ipoints	= @$href ;
my @Opoints	= ( ) ;
my @stack	= ( ) ;
my $fIndex	= 0 ;
my $fPoint	= '' ;
my $aIndex	= 0 ;
my $anchor	= '' ;
my $max		= 0 ;
my $maxIndex	= 0 ;
my $point	= '' ;
my $dist	= 0 ;
my $polygon	= 0 ;					# Line Type

$anchor = $Ipoints[0] ; 				# save first point

push( @Opoints, $anchor ) ;

$aIndex = 0 ;						# Anchor Index

# Check for a polygon: At least 4 points and the first point == last point...

if ( $#Ipoints >= 4 and $Ipoints[0] == $Ipoints[$#Ipoints] )
{
 $fIndex = $#Ipoints - 1 ;				# Start from the next to last point
 $polygon = 1 ;						# It's a polygon

} else
{
 $fIndex = $#Ipoints ;					# It's a path (open polygon)
}

push( @stack, $fIndex ) ;

# Douglas - Peucker algorithm...

while(@stack)
{
 $fIndex = $stack[$#stack] ;
 $fPoint = $Ipoints[$fIndex] ;
 $max = $tolerance ;		 			# comparison values
 $maxIndex = 0 ;

 # Process middle points...

 for (($aIndex+1) .. ($fIndex-1))
 {
  $point = $Ipoints[$_] ;
  $dist = &perp_distance($anchor, $fPoint, $point);

  if( $dist >= $max )
  {
   $max = $dist ;
   $maxIndex = $_;
  }
 }

 if( $maxIndex > 0 )
 {
  push( @stack, $maxIndex ) ;
 } else
 {
  push( @Opoints, $fPoint ) ;
  $anchor = $Ipoints[(pop @stack)] ;
  $aIndex = $fIndex ;
 }
}

if ( $polygon )						# Check for Polygon
{
 push( @Opoints, $Ipoints[$#Ipoints] ) ;		# Add the last point

 # Check for collapsed polygons, use original data in that case...

 if( $#Opoints < 4 )
 {
  @Opoints = @Ipoints ;
 }
}

return ( @Opoints ) ;

}

# Calculate Perpendicular Distance in meters between a line (two points) and a point...
# my $dist = &perp_distance( <line point 1>, <line point 2>, <point> ) ;

sub perp_distance					# Perpendicular distance in meters
{
 my $lp1	= shift ;
 my $lp2	= shift ;
 my $p		= shift ;
 my $dist	= &haversine_distance_meters( $lp1, $p ) ;
 my $angle	= &angle3points( $lp1, $lp2, $p ) ; 

 return ( sprintf("%0.6f", abs($dist * sin($angle)) ) ) ;
}

# Calculate Distance in meters between two points...

sub haversine_distance_meters
{
 my $p1	= shift ;
 my $p2	= shift ;

 my $O = 3.141592654/180 ;
 my $b = $$p1[0] * $O ;
 my $c = $$p2[0] * $O ;
 my $d = $b - $c ;
 my $e = ($$p1[1] * $O) - ($$p2[1] * $O) ;
 my $f = 2 * &asin( sqrt( (sin($d/2) ** 2) + cos($b) * cos($c) * (sin($e/2) ** 2)));

 return sprintf("%0.4f",$f * 6378137) ; 		# Return meters

 sub asin
 {
  atan2($_[0], sqrt(1 - $_[0] * $_[0])) ;
 }
}

# Calculate Angle in Radians between three points...

sub angle3points					# Angle between three points in radians
{
 my $p1	= shift ;
 my $p2	= shift ;
 my $p3 = shift ;
 my $m1 = &slope( $p2, $p1 ) ;
 my $m2 = &slope( $p3, $p1 ) ;
 
 return ($m2 - $m1) ;

 sub slope						# Slope in radians
 {
  my $p1	= shift ;
  my $p2	= shift ;
  return( sprintf("%0.6f",atan2( (@$p2[1] - @$p1[1]),( @$p2[0] - @$p1[0] ))) ) ;
 }
}

1;

__END__

=head1 SYNOPSIS

# Test Program:

# Douglas - Peucker Test Program
# Author. John D. Coryat 01/2007 USNaviguide.com

use strict;

my $infile	= $ARGV[0] ;
my $outfile	= $ARGV[1] ;
my $tolerance	= $ARGV[2] ;
my @Ipoints	= ( ) ;
my @Opoints	= ( ) ;
my $data	= '' ;

if(!$infile or !$outfile or !$tolerance)
{
  print "Usage: douglas-peucker.pl <input file name> <output file name> <tolerance in meters>\n";
  print "Data format: (lat,lng)\n" ;
  exit ;
}

if ( $tolerance <= 0 )
{
 print "Tolerance (meters) must be greater than zero.\n" ;
 exit
}

if ( !(-s $infile) )
{
 print "Input File ($infile) not found.\n" ;
 exit
}

if (-s $outfile)
{
 print "Output File ($outfile) exists.\n" ;
 exit
}

open IN, $infile ;

while ( $data =  )
{
 if ( $data	=~ /\((.*),(.*)\)/ )
 {
  push( @Ipoints, [$1,$2] ) ;
 }
}
close IN ;

@Opoints = &Douglas_Peucker( \@Ipoints, $tolerance ) ;

open OUT, ">$outfile" ;

foreach $data (@Opoints)
{ 
 print OUT "($$data[0],$$data[1])\n" ;
}

close OUT ;

print "Input: " . $#Ipoints . " Output: " . $#Opoints . " Tolerance: $tolerance\n" ;

=cut

      
Copyright © 1997-2009 USNaviguide.com. All rights reserved.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.