## Copyright 2009 Thomas Fischer <fischer@unix-ag.uni-kl.de>

##  This Perl script 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 3 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, see <http://www.gnu.org/licenses/>.

use Math::Trig;
use Image::Magick;
use LWP::UserAgent;
use Getopt::Std;
use strict;
use warnings;

## output filename, can be set with  -o FILENAME
my $outputfilename = "map.png";

## zoom level of map, can be set with  -z N
## where N is 1..16 or "auto"
my $zoom = "auto";
my $useautozoom = $zoom eq "auto";

## sparse map flag (draw only tiles touched by tracks)
## can be set with  -s
my $sparse = 0;

## quiet flag, can be set with  -q
my $quiet = 0;

## maximum number of tiles used in autozoom mode
## can be set with  -a N
## setting parameter -a forces autozoom
my $maxnumautotiles = 32;

## additional border tiles
my $additionalborder = 0;

## radius for waypoint circles
my $waypointcircleradius = "auto";

## colors used for drawing tracks
## used in round-robin fashion
my @drawingcolors = (
    '#00dd00', '#0099ff', '#ff9900', '#99ff00', '#9900ff', '#ff0099',
    '#00ff99', '#dd0000', '#0000dd', '#cccc00', '#cc00cc', '#00cccc'
);

## drawing style of lower layer for tracks
## for colors see  @drawingcolors
my %drawingstylelowerlayer = ( linewidth => 4 );

## drawing style of upper layer for tracks
my %drawingstyleupperlayer = ( stroke => '#ffffff', linewidth => 1 );

## text style for copyright notice
my %copyrightnoticestyle = (
    fill       => 'graya(30%, 0.5)',
    pointsize  => 9,
    background => 'graya(90%, 0.5)',
    offset     => 2
);

## text style for scale
my %scalestyle = (
    fill       => 'graya(0%, 1.0)',
    pointsize  => 12,
    background => 'graya(100%, 0.7)',
    offset     => 8
);

## how to post-process the background image (all tiles)
## before drawing tracks on this background
#my %backgroundpostprocess = ( saturation => 10.0, brightness => 70.0 );
my %backgroundpostprocess = ( saturation => 0.0, brightness => 110.0 );

## caching directory, where to put the tiles followed by a file prefix
## default is current directory, prefix is "tile"
my $tilesprefix = "tile";

## tile URLs
my $baseurl        = "http://tah.openstreetmap.org/Tiles/tile";
my $tilesourcename = "osmrender";

my $minxtile   = undef;
my $maxxtile   = undef;
my $minytile   = undef;
my $maxytile   = undef;
my $numxtiles  = undef;
my $numytiles  = undef;
my $pxwidth    = undef;
my $pxheight   = undef;
my $minlat     = undef;
my $minlong    = undef;
my $deltalat   = undef;
my $deltalong  = undef;
my $maxlat     = undef;
my $maxlong    = undef;
my @trkseglist = ();
my @wptlist    = ();
my $image      = undef;
my %usedtiles  = ();

$|             = 1;            # disable buffering of print/STDOUT
$main::VERSION = "20090727";
$Getopt::Std::STANDARD_HELP_VERSION = 1;

my $ua = LWP::UserAgent->new(
    agent      => "gpx2png",
    keep_alive => 1,
    env_proxy  => 1,
);

## parse command line parameters, set internal variables
sub parseCmdLineParam {
    my %opts = ();
    getopts( 'hqso:z:a:b:t:r:', \%opts );

    if ( $opts{h} ) {
        HELP_MESSAGE();
        exit 0;
    }

    ## set sparse flag
    $sparse = 1 if $opts{s};

    ## set quiet flag
    $quiet = 1 if $opts{q};

    ## set output filename
    if ( defined( $opts{o} ) ) {
        $outputfilename = $opts{o};
    }

    ## set zoom level
    if ( defined( $opts{z} ) ) {
        my $param = $opts{z};
        if ( $param =~ /(\d+)/ && $1 >= 1 && $1 <= 16 ) {
            $zoom        = $1;
            $useautozoom = 0;
        }
        elsif ( $opts{z} eq "auto" ) {
            $zoom        = "auto";
            $useautozoom = 1;
        }
        else {
            die
              "zoom level set but invalid; must be number in 1..16 or \"auto\"";
        }
    }

    ## set maximum number of tiles for autozoom
    if ( defined( $opts{a} ) ) {
        my $param = $opts{a};
        if ( $param =~ /(\d+)/ && $1 >= 1 && $1 <= 512 ) {
            $maxnumautotiles = $1;
            $zoom            = "auto";
            $useautozoom     = 1;
        }
        else {
            die
"maximum number of tiles for autozoom set but invalid; must be number in 1..512";
        }
    }

    ## set additional border tiles
    if ( defined( $opts{b} ) ) {
        my $param = $opts{b};
        if ( $param =~ /(\d+)/ && $1 >= 0 && $1 <= 32 ) {
            $additionalborder = $1;
            print "Additional border tiles set to " . $additionalborder . "\n"
              if ( $quiet == 0 );
        }
        else {
            die "additional border tiles but invalid; must be number in 0..32";
        }
    }

    ## set radius for waypoint circles
    if ( defined( $opts{r} ) ) {
        my $param = $opts{r};
        if ( $param =~ /(\d+)/ && $1 >= 1 && $1 <= 512 ) {
            $waypointcircleradius = $1;
        }
        elsif ( $param eq "auto" ) {
            $waypointcircleradius = "auto";
        }
        else {
            die
"radius for waypoint circles set but invalid; must be number in 1..512 or \"auto\"";
        }
    }

    ## select source of images tiles
    if ( defined( $opts{t} ) ) {
        my $tilesource = $opts{t};
        if ( $tilesource eq "mapnik" ) {
            $tilesourcename = $tilesource;
            $baseurl        = "http://tile.openstreetmap.org";
        }
        elsif ( $tilesource eq "cyclemap" || $tilesource eq "cycle" ) {
            $tilesourcename = "cyclemap";
            $baseurl        = "http://andy.sandbox.cloudmade.com/tiles/cycle";
        }
        elsif ( $tilesource eq "maplint" ) {
            $tilesourcename = $tilesource;
            $baseurl        = "http://tah.openstreetmap.org/Tiles/maplint";
        }
        elsif ( $tilesource eq "noname" ) {
            $tilesourcename = $tilesource;
            $baseurl        = "http://matt.sandbox.cloudmade.com";
        }
        elsif ( $tilesource eq "opnvkarte" || $tilesource eq "oepnvkarte" ) {
            $tilesourcename = "opnvkarte";
            $baseurl        = "http://tile.xn--pnvkarte-m4a.de/tilegen";
        }
    }

    ## print all set flags (if not quiet)
    if ( $quiet == 0 ) {
        print "Sparse mode is " . ( $sparse == 1 ? "ON" : "OFF" ) . "\n";
        print "Using tile images from \"" . $tilesourcename . "\"\n";
        print "Zoom level is " . $zoom;
        if ( $useautozoom > 0 ) {
            print " with at most " . $maxnumautotiles . " tiles";
        }
        print "\n";
        print "Output file is " . $outputfilename . "\n";
    }
}

sub HELP_MESSAGE {
    print
"\nThis programs converts .gpx files (GPS tracks) into images (e.g. PNG images)\n";
    print
"by using images tiles from the OpenStreetMap project and drawing sequences\n";
    print "of lines corresponding to GPS points.\n\n";
    print "Copyright 2009 Thomas Fischer <fischer\@unix-ag.uni-kl.de>\n";
    print
"This code is release under the GNU Public Licence version 3 or any later version.\n\n";
    print "This script is called like\n";
    print "  perl gpx2png.pl [OPTIONS] [GPXFILES]\n\n";
    print
"Available options (all optional, default values will be used if not specified)\n";
    print
"  -o FILENAME   Output filename of the image. Default: $outputfilename\n";
    print "  -z N          Zoom level (number or \"auto\"). Default: $zoom\n";
    print
"  -a N          Autozoom: Do not use more than N tiles to draw tracks. Default: $maxnumautotiles\n";
    print
"  -b N          Additional map image tiles around the map. Default: $additionalborder\n";
    print
"  -r N          Radius for waypoint circles. Default: $waypointcircleradius\n";
    print
"  -t SOURCE     Select the source of image tiles. Possible values for SOURCE:\n";
    print "                   osmrender   (default)\n";
    print "                   mapnik\n";
    print "                   maplint\n";
    print "                   cyclemap\n";
    print "                   noname\n";
    print "                   opnvkarte\n";
    print
"  -s            Sparse mode: Include only tiles touched by GPS tracks. Default: off\n";
    print
      "  -q            Quiet mode: Do not print status output. Default: off\n";
    print "\nGPS tracks (format .gpx) are passed as a list of filenames,\n";
    print "or the .gpx files' content is piped into gpx2png.pl\n";
}

## map latitude/longitude and zoom level to tile number (x/y)
sub getTileNumber {
    my ( $lat, $lon, $zoom ) = @_;

    my $xtile = int( ( $lon + 180 ) / 360 * ( 1 << $zoom ) );
    my $l     = log( tan( $lat * pi / 180 ) + sec( $lat * pi / 180 ) );
    my $ytile = int( ( 1 - $l / pi ) / 2 * ( 1 << $zoom ) );

    return ( ( $xtile, $ytile ) );
}

sub Project {
    my ( $X, $Y, $Zoom ) = @_;
    my $Unit  = 1 / ( 2**$Zoom );
    my $relY1 = $Y * $Unit;
    my $relY2 = $relY1 + $Unit;

# note: $LimitY = ProjectF(degrees(atan(sinh(pi)))) = log(sinh(pi)+cosh(pi)) = pi
# note: degrees(atan(sinh(pi))) = 85.051128..
#my $LimitY = ProjectF(85.0511);

    # so stay simple and more accurate
    my $LimitY = pi;
    my $RangeY = 2 * $LimitY;
    $relY1 = $LimitY - $RangeY * $relY1;
    $relY2 = $LimitY - $RangeY * $relY2;
    my $Lat1 = ProjectMercToLat($relY1);
    my $Lat2 = ProjectMercToLat($relY2);
    $Unit = 360 / ( 2**$Zoom );
    my $Long1 = -180 + $X * $Unit;
    return ( ( $Lat2, $Long1, $Lat1, $Long1 + $Unit ) );    # S,W,N,E
}

sub ProjectMercToLat($) {
    my $MercY = shift();
    return ( 180 / pi * atan( sinh($MercY) ) );
}

sub ProjectF {
    my $Lat = shift;
    $Lat = deg2rad($Lat);
    my $Y = log( tan($Lat) + ( 1 / cos($Lat) ) );
    return ($Y);
}

## create URL to fetch tile depending on x/y numbering and zoom level
sub getURL {
    my ( $x, $y, $zoom ) = @_;
    return "$baseurl/$zoom/$x/$y.png";
}

## create file name to store/cache a tile depending on x/y numbering and zoom level
sub getFilename {
    my ( $x, $y, $zoom ) = @_;
    return
      sprintf( $tilesprefix . "-" . $tilesourcename . "-z%03d-x%05d-y%05d.png",
        $zoom, $x, $y );
}

## download a remove file given an URL and store it in a given local filename
sub downloadFile {
    my ( $url, $localfilename ) = @_;

    my $res = $ua->simple_request( HTTP::Request->new( GET => $url ) );
    if ( $res->code == 200 ) {
        open( FILE, ">$localfilename" )
          || die "Can't open $localfilename: $!\n";
        binmode FILE;
        print FILE $res->content;
        close FILE;
    }
    else {
        print "\n";
        die "Cannot download file $url";
    }
}

## read GPX data from a file handle and store line segment points in @trkseglist and waypoints in @wptlist
sub readGPXfromFile {
    my $handle             = $_[0];
    my @internaltrkseglist = ();

    while (<$handle>) {
        if (/trkpt lat=["']([-0-9.]+)["'] lon=["']([-0-9.]+)["']/) {
            push @internaltrkseglist, [ ( $1, $2 ) ];
        }
        elsif (/\/trkseg/) {
            print "Segment with " . @internaltrkseglist . " points\n"
              if ( $quiet == 0 );
            if ( @internaltrkseglist > 0 ) {
                push @trkseglist, [@internaltrkseglist];
                @internaltrkseglist = ();
            }
        }
        elsif (/^<wpt lat=["']([-0-9.]+)["'] lon=["']([-0-9.]+)["']/) {
            push @wptlist, [ ( $1, $2 ) ];
        }
    }
}

sub determineTiles {
    $minxtile = 1000000;
    $maxxtile = 0;
    $minytile = 1000000;
    $maxytile = 0;

    $zoom = 16 if ( $useautozoom > 0 );

    for my $trkseg (@trkseglist) {
        foreach my $trkpt ( @{$trkseg} ) {
            my ( $lat, $long ) = @{$trkpt};
            ( my $xtile, my $ytile ) = getTileNumber( $lat, $long, $zoom );
            if ( $xtile > $maxxtile ) { $maxxtile = $xtile; }
            if ( $ytile > $maxytile ) { $maxytile = $ytile; }
            if ( $xtile < $minxtile ) { $minxtile = $xtile; }
            if ( $ytile < $minytile ) { $minytile = $ytile; }
            $usedtiles{ $xtile . "|" . $ytile } = 1;
        }
    }
    for my $wpt (@wptlist) {
        my ( $lat, $long ) = @{$wpt};
        ( my $xtile, my $ytile ) = getTileNumber( $lat, $long, $zoom );
        if ( $xtile > $maxxtile ) { $maxxtile = $xtile; }
        if ( $ytile > $maxytile ) { $maxytile = $ytile; }
        if ( $xtile < $minxtile ) { $minxtile = $xtile; }
        if ( $ytile < $minytile ) { $minytile = $ytile; }
        $usedtiles{ $xtile . "|" . $ytile } = 1;
    }

    die
"Invalid input data, no coordinates given minxtile=$minxtile  maxxtile=$maxxtile  minytile=$minytile  maxytile=$maxytile"
      if ( $minxtile > $maxxtile || $minytile > $maxytile );

    # consider additional border
    $maxxtile += $additionalborder;
    $maxytile += $additionalborder;
    $minxtile -= $additionalborder;
    $minytile -= $additionalborder;

    $numxtiles = $maxxtile - $minxtile + 1;
    $numytiles = $maxytile - $minytile + 1;
    $pxwidth   = $numxtiles * 256;
    $pxheight  = $numytiles * 256;

    ( undef, $minlat, $maxlong, undef ) =
      Project( $minxtile, $minytile, $zoom );
    ( $minlong, undef, undef, $maxlat ) =
      Project( $maxxtile, $maxytile, $zoom );

    $deltalat  = $maxlat - $minlat;
    $deltalong = $maxlong - $minlong;

    if ( $useautozoom > 0 ) {
        my $div = 0;
        while ( ( $numxtiles + 1 ) * ( $numytiles + 1 ) > $maxnumautotiles ) {
            ++$div;
            $numxtiles >>= 1;
            $numytiles >>= 1;
        }
        $pxwidth  = $numxtiles * 256;
        $pxheight = $numytiles * 256;

        $useautozoom = 0;
        if ( $div > 0 ) {
            $zoom -= $div;
            %usedtiles = ();
            print "Autozoom is setting zoom to " . $zoom . "\n"
              if ( $quiet == 0 );
            determineTiles();
        }
        else {
            print "Autozoom is setting zoom to " . $zoom . "\n";
        }
    }
}

## read all GPX data from files (ARGV) or STDIN and perform some analysis
sub readAllGPX {
    @trkseglist = ();
    @wptlist    = ();

    if ( @ARGV == 0 ) {
        print "Reading .gpx files from STDIN\n" if ( $quiet == 0 );
        readGPXfromFile( \*STDIN );
    }
    else {
        for my $filename (@ARGV) {
            print "Reading file $filename\n" if ( $quiet == 0 );
            open( FILE, '<', $filename ) or next;
            readGPXfromFile( \*FILE );
            close(FILE);
        }
    }

    determineTiles();
}

## download all tile images required to draw the given GPX data
sub downloadTiles {
    if ( $sparse == 0 ) {
        print "Using " . ( $numxtiles * $numytiles ) . " tiles\n"
          if ( $quiet == 0 );
    }
    else {
        print "Using " .
          keys(%usedtiles)
          . " tiles (out of "
          . ( $numxtiles * $numytiles )
          . " possible)\n"
          if ( $quiet == 0 );
    }

    for my $y ( $minytile .. $maxytile ) {
        for my $x ( $minxtile .. $maxxtile ) {
            my $url = getURL( $x, $y, $zoom );
            my $filename = getFilename( $x, $y, $zoom );

            if ( ( $sparse == 0 || defined( $usedtiles{ $x . "|" . $y } ) )
                && !-e "$filename" )
            {
                printf "Downloading tile (%6d|%6d)", $x, $y if ( $quiet == 0 );
                downloadFile( $url, $filename );
                print "\n" if ( $quiet == 0 );
            }
        }
    }
}

## create a background image by combining all tile images
## perform some post-processing on final background image
sub initializeBackgroundImage {
    my $size = ( $numxtiles * 256 ) . 'x' . ( $numytiles * 256 );
    print "Building background image of size " . $size if ( $quiet == 0 );

    $image = Image::Magick->new( size => $size );
    die "\nCannot create image" unless defined($image);
    my $w = $image->ReadImage('NULL:white');
    die "\n$w" if "$w";

    for my $y ( $minytile .. $maxytile ) {
        for my $x ( $minxtile .. $maxxtile ) {
            my $filename = getFilename( $x, $y, $zoom );
            if ( ( $sparse == 0 || defined( $usedtiles{ $x . "|" . $y } ) )
                && -e $filename )
            {
                my $tileimage = Image::Magick->new;
                $w = $tileimage->Read($filename);
                die "\n$w" if "$w";

                $image->Composite(
                    image   => $tileimage,
                    compose => 'Over',
                    x       => ( $x - $minxtile ) * 256,
                    y       => ( $y - $minytile ) * 256
                );
            }
        }
    }

    $w = $image->Modulate(%backgroundpostprocess);
    die "\n$w" if "$w";
    print "\n" if ( $quiet == 0 );
}

## determine pixel position for a coordinate relative to the tileimage
## where it is drawn on
sub getPixelPosForCoordinates {
    my ( $lon, $lat, $zoom ) = @_;
    my ( $xtile, $ytile ) = getTileNumber( $lat, $lon, $zoom );

    my $xoffset = ( $xtile - $minxtile ) * 256;
    my $yoffset = ( $ytile - $minytile ) * 256;

    my ( $south, $west, $north, $east ) = Project( $xtile, $ytile, $zoom );

    my $x = ( $lon - $west ) * 256 /  ( $east - $west ) + $xoffset;
    my $y = ( $lat - $north ) * 256 / ( $south - $north ) + $yoffset;

    return ( $x, $y );
}

## draw given trek segment as a sequence of lines with a given drawing style
sub drawTrekSegment {
    my @trkseg       = @{ $_[0] };
    my %drawingStyle = %{ $_[1] };

    my $lastx = undef;
    my $lasty = undef;

    foreach my $trkpt (@trkseg) {
        my ( $long, $lat ) = @{$trkpt};

        my ( $x, $y ) = getPixelPosForCoordinates( $lat, $long, $zoom );

        if ( defined($lastx) && defined($lasty) ) {
            $drawingStyle{points} = "$x,$y $lastx,$lasty";
            my $w = $image->Draw(%drawingStyle);
            die "\n$w" if "$w";
        }

        $lastx = $x;
        $lasty = $y;
    }

    print "." if ( $quiet == 0 );
}

## draw given waypoint as a circle with a given drawing style
sub drawWaypoint {
    my ( $long, $lat ) = @{ $_[0] };
    my %drawingStyle = %{ $_[1] };

    if ( $waypointcircleradius eq "auto" ) {
        $waypointcircleradius = int( ( $numxtiles + $numytiles ) / 3 );
        print "Waypoint circle radius set to " . $waypointcircleradius . "\n"
          if ( $quiet == 0 );
    }

    my ( $x, $y ) = getPixelPosForCoordinates( $lat, $long, $zoom );
    my $x1 = $x - $waypointcircleradius;
    my $y1 = $y - $waypointcircleradius;
    my $x2 = $x + $waypointcircleradius;
    my $y2 = $y + $waypointcircleradius;

    $drawingStyle{points} = "$x1,$y1 $x2,$y2";
    my $w = $image->Draw(%drawingStyle);
    die "\n$w" if "$w";

    print "." if ( $quiet == 0 );
}

## draw all GPX tracks first with on a "lower" layer and then on an "upper" layer
sub drawAllTracks {
    print "Drawing tracks " if ( $quiet == 0 );

    my %drawingStyle = (%drawingstylelowerlayer);
    $drawingStyle{primitive} = 'line';
    my $colorcounter = 0;
    for my $trkseg (@trkseglist) {
        $drawingStyle{stroke} =
          $drawingcolors[ ( ++$colorcounter ) % @drawingcolors ];
        drawTrekSegment( \@{$trkseg}, \%drawingStyle );
    }

    %drawingStyle = (%drawingstyleupperlayer);
    $drawingStyle{primitive} = 'line';
    for my $trkseg (@trkseglist) {
        drawTrekSegment( \@{$trkseg}, \%drawingStyle );
    }
    print "\n" if ( $quiet == 0 );
}

## draw all GPX waypoints first with on a "lower" layer and then on an "upper" layer
sub drawAllWaypoints {
    return unless @wptlist > 0;

    print "Drawing waypoints " if ( $quiet == 0 );

    my %drawingStyle = (%drawingstylelowerlayer);
    $drawingStyle{primitive} = 'circle';
    $drawingStyle{fill}      = 'none';
    my $colorcounter = 0;
    for my $wpt (@wptlist) {
        $drawingStyle{stroke} =
          $drawingcolors[ ( ++$colorcounter ) % @drawingcolors ];
        drawWaypoint( \@{$wpt}, \%drawingStyle );
    }

    %drawingStyle            = (%drawingstyleupperlayer);
    $drawingStyle{primitive} = 'circle';
    $drawingStyle{fill}      = 'none';
    for my $wpt (@wptlist) {
        drawWaypoint( \@{$wpt}, \%drawingStyle );
    }
    print "\n" if ( $quiet == 0 );
}

## add a copyright statement according to http://wiki.openstreetmap.org/wiki/Legal_FAQ
sub addCopyright {
    my $offset    = $copyrightnoticestyle{offset};
    my %textparam = %copyrightnoticestyle;
    $textparam{x}       = $offset;
    $textparam{y}       = $offset;
    $textparam{gravity} = 'SouthEast';

    $textparam{text} =
      'Tile images © OpenStreetMap (and) contributors, CC-BY-SA';
    ( undef, undef, undef, undef, my $width, my $height ) =
      $image->QueryFontMetrics(%textparam);
    $image->Draw(
        fill      => $copyrightnoticestyle{background},
        primitive => 'rectangle',
        points    => ""
          . ( $pxwidth - $width - 2 * $offset ) . ","
          . ( $pxheight - $height - 2 * $offset )
          . " $pxwidth,$pxheight"
    );
    $image->Annotate(%textparam);

    $textparam{text} =
      "Generated with gpx2png $main::VERSION, © Th. Fischer, GPL 3";
    $textparam{y} = $height + 3 * $offset + 1;
    ( undef, undef, undef, undef, $width, $height ) =
      $image->QueryFontMetrics(%textparam);
    $image->Draw(
        fill      => $copyrightnoticestyle{background},
        primitive => 'rectangle',
        points    => ""
          . ( $pxwidth - $width - 2 * $offset ) . ","
          . ( $pxheight - $textparam{y} - $height - $offset )
          . " $pxwidth,"
          . ( $pxheight - $textparam{y} + $offset )
    );
    $image->Annotate(%textparam);
}

## add a legend
sub drawScale {
    my @lenghtScaleLine =
      ( 0, 64, 51, 51, 51, 41, 41, 82, 82, 65, 65, 65, 52, 52, 52, 42, 42 );
    my @textScaleLine = (
        0,        '5000 km', '2000 km', '1000 km', '500 km', '200 km',
        '100 km', '100 km',  '50 km',   '20 km',   '10 km',  '5 km',
        '2 km',   '1000 m',  '500 m',   '200 m',   '100 m'
    );
    my $borderdist = $scalestyle{offset};
    my $lenght     = $borderdist + $lenghtScaleLine[$zoom];
    my $y          = $pxheight - $borderdist;
    $image->Draw(
        fill      => "black",
        stroke    => "black",
        primitive => "rectangle",
        points    => "$borderdist,$y $lenght," . ( $y - 3 )
    );
    $image->Draw(
        fill      => "white",
        stroke    => "white",
        primitive => "rectangle",
        points    => "$borderdist," . ( $y - 1 ) . " $lenght," . ( $y - 2 )
    );

    $y -= 7;
    $scalestyle{text} = $textScaleLine[$zoom];
    $scalestyle{x}    = $borderdist + 2;
    $scalestyle{y}    = $y - 2;
    ( undef, undef, undef, undef, my $width, my $height ) =
      $image->QueryFontMetrics(%scalestyle);
    $image->Draw(
        fill      => $scalestyle{background},
        primitive => 'rectangle',
        points    => ""
          . ( $width + $borderdist + 4 ) . ","
          . ( $y - $height )
          . " $borderdist,"
          . $y
    );
    $image->Annotate(%scalestyle);
}

## save final image (background and tracks) to output filename
sub saveImage {
    print "Saving to file " . $outputfilename if ( $quiet == 0 );
    my $w = $image->Write($outputfilename);
    die "$w"   if "$w";
    print "\n" if ( $quiet == 0 );
}

parseCmdLineParam();
readAllGPX();
downloadTiles();
initializeBackgroundImage();
drawAllTracks();
drawAllWaypoints();
drawScale();
addCopyright();
saveImage();
