# Google Polygon Encoding algorithm # Author. John D. Coryat 10/2007 USNaviguide.com and Marcelo Montagna maps.forum.nu # Adapted from: http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/ ## # If you want to display complicated path data on a Google Map, # the most efficient way to do so is with an encoded polyline or polygon. ## package USNaviguide_Google_Encode ; require 5.003 ; use USNaviguide_Douglas_Peucker ; use POSIX ; use strict ; BEGIN { use Exporter ; use vars qw ( $VERSION @ISA @EXPORT) ; $VERSION = 1.0 ; @ISA = qw ( Exporter ) ; @EXPORT = qw ( Google_Encode ) ; } # Call as: (, ) = &Google_Encode(, ); # Points Array Format: # ([lat1,lng1],[lat2,lng2],...[latn,lngn]) # sub Google_Encode { my $pointsRef = shift; my $tolerance = shift ; my @points = @{$pointsRef}; my $encodedPoints = '' ; my $encodedLevels = '' ; # Check for tolerance size... if ( !defined($tolerance) or !$tolerance ) { $tolerance = 1 ; # Default Value: 1 meter } # Run D-P on the points, eliminate redundancies... @points = &Douglas_Peucker( \@points, $tolerance ) ; # Encode Points... $encodedPoints = &createEncodings(\@points); # Encode Levels... $encodedLevels = &encodeLevels(\@points, $tolerance); # Escape backslashes $encodedPoints =~ s!\\!\\\\!g; return ($encodedLevels, $encodedPoints); } sub encodeLevels { my $pointsRef = shift ; my $tolerance = shift ; my @points = @{$pointsRef}; my @point = ( ) ; my %pnthash = ( ) ; my @pntlev = ( ) ; my $numLevels = 18 ; my $zoomFactor = 2 ; my $en_levels = '' ; my $lat = 0 ; my $lng = 0 ; my $i = 0 ; my $j = 0 ; my $k = 0 ; my $x = '' ; my $encodelev = &encodeNumber(1) ; # Build up a point hash to be used to reference original points to their location... # Mark all points at lowest possible level to start... for($i=0; $i < scalar(@points); $i++) { $pointsRef = $points[$i]; @point = @{$pointsRef}; $lat = $point[0]; $lng = $point[1]; $pnthash{"$lat,$lng"} = $i ; $pntlev[$i] = $encodelev ; } # Iterate through the levels and calculate with an increasing tolerance... # Each time through, mark all points left with current level... for($i = 1; $i < $numLevels; $i++) { @points = &Douglas_Peucker( \@points, $tolerance * ($zoomFactor ** $i) ) ; $encodelev = &encodeNumber($i) ; # Mark Points Still present... for($j=0; $j < scalar(@points); $j++) { $pointsRef = $points[$j]; @point = @{$pointsRef}; $lat = $point[0]; $lng = $point[1]; $k = $pnthash{"$lat,$lng"} ; $pntlev[$k] = $encodelev ; } # Stop when all points are calculated and only 3 are left (line)... if ( scalar(@points) < 4 ) { last ; } } # Force first and last point to be highest level... $encodelev = &encodeNumber($numLevels - 1) ; $pntlev[0] = $encodelev ; $pntlev[$#pntlev] = $encodelev ; # Build up encoded Level string... foreach $x ( @pntlev ) { $en_levels .= $x ; } return $en_levels; } # ############## Numeric subroutines below ############################# # Documentation from Google http://www.google.com/apis/maps/documentation/polylinealgorithm.html # # 1. Take the initial signed value: # -179.9832104 # 2. Take the decimal value and multiply it by 1e5, flooring the result: # -17998321 sub createEncodings { my $pointsRef = shift ; my @points = @{$pointsRef}; my $encoded_points = '' ; my $pointRef = '' ; my @point = ( ) ; my $plat = 0 ; my $plng = 0 ; my $lat = 0 ; my $lng = 0 ; my $late5 = 0 ; my $lnge5 = 0 ; my $dlat = 0 ; my $dlng = 0 ; my $i = 0 ; for($i=0; $i < scalar(@points); $i++) { $pointRef = $points[$i]; @point = @{$pointRef}; $lat = $point[0]; $lng = $point[1]; $late5 = floor($lat * 1e5); $lnge5 = floor($lng * 1e5); $dlat = $late5 - $plat; $dlng = $lnge5 - $plng; $plat = $late5; $plng = $lnge5; $encoded_points .= &encodeSignedNumber($dlat) . &encodeSignedNumber($dlng); } return $encoded_points; } # 3. Convert the decimal value to binary. Note that a negative value must be inverted # and provide padded values toward the byte boundary: # 00000001 00010010 10100001 11110001 # 11111110 11101101 10100001 00001110 # 11111110 11101101 01011110 00001111 # 4. Shift the binary value: # 11111110 11101101 01011110 00001111 0 # 5. If the original decimal value is negative, invert this encoding: # 00000001 00010010 10100001 11110000 1 # 6. Break the binary value out into 5-bit chunks (starting from the right hand side): # 00001 00010 01010 10000 11111 00001 # 7. Place the 5-bit chunks into reverse order: # 00001 11111 10000 01010 00010 00001 # 8. OR each value with 0x20 if another bit chunk follows: # 100001 111111 110000 101010 100010 000001 # 9. Convert each value to decimal: # 33 63 48 42 34 1 # 10. Add 63 to each value: # 96 126 111 105 97 64 # 11. Convert each value to its ASCII equivalent: # `~oia@ sub encodeSignedNumber { use integer; my $num = shift; my $sgn_num = $num << 1; if ($num < 0) { $sgn_num = ~($sgn_num); } return &encodeNumber($sgn_num); } sub encodeNumber { use integer; my $encodeString = '' ; my $num = shift; my $nextValue = 0 ; my $finalValue = 0 ; while($num >= 0x20) { $nextValue = (0x20 | ($num & 0x1f)) + 63; $encodeString .= chr($nextValue); $num >>= 5; } $finalValue = $num + 63; $encodeString .= chr($finalValue); return $encodeString; } 1; __END__ =head1 SYNOPSIS # Test Program: # Google Polygon Encoding Test Program # Author. John D. Coryat 10/2007 USNaviguide.com use strict; my $infile = $ARGV[0] ; my $outfile = $ARGV[1] ; my $tolerance = $ARGV[2] ; my @Ipoints = ( ) ; my $output = '' ; my $encodedPoints = '' ; my $encodedLevels = '' ; if(!$infile or !$outfile) { print "Usage: Google_Encode.pl \n"; print "Data format: (lat,lng)\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 ; print "Input: " . $#Ipoints . " Tolerance in Meters: $tolerance\n" ; # The output is a JSON string containing an array of polylines. ($encodedLevels, $encodedPoints) = &Google_Encode(\@Ipoints, $tolerance); $output = '' ; $output .= 'new GPolygon.fromEncoded({'; $output .= ' polylines: ['; $output .= ' {points: "'.$encodedPoints.'",'; $output .= ' levels: "'.$encodedLevels.'",'; $output .= ' color: "#0000ff",'; $output .= ' opacity: 0.7,'; $output .= ' weight: 3,'; $output .= ' numLevels: 18,'; $output .= ' zoomFactor: 2},'; $output .= '],'; $output .= ' fill: true,'; $output .= ' color: "#0000ff",'; $output .= ' opacity: 0.4,'; $output .= ' outline: true'; $output .= '});'; open OUT, ">$outfile" ; print OUT $output; close OUT ; =cut