source: subversion/applications/rendering/tilesAtHome/lib/Tileset.pm @ 12207

Last change on this file since 12207 was 12207, checked in by Dirk Stoecker, 11 years ago

fixed typo in lowzoom

File size: 60.2 KB
Line 
1package Tileset;
2
3=pod
4
5=head1 Tileset package
6
7=head2 Copyright and Authors
8
9Copyright 2006-2008, Dirk-Lueder Kreie, Sebastian Spaeth,
10Matthias Julius and others
11
12This program is free software; you can redistribute it and/or
13modify it under the terms of the GNU General Public License
14as published by the Free Software Foundation; either version 2
15of the License, or (at your option) any later version.
16
17=head2 Description of functions
18
19=cut
20
21use warnings;
22use strict;
23use File::Temp qw/ tempfile tempdir /;
24use Error qw(:try);
25use TahConf;
26use Server;
27use tahlib;
28use tahproject;
29use File::Copy;
30use File::Path;
31use GD 2 qw(:DEFAULT :cmp);
32
33#-----------------------------------------------------------------------------
34# creates a new Tileset instance and returns it
35# parameter is a request object with x,y,z, and layer atributes set
36# $self->{WorkingDir} is a temporary directory that is only used by this job and
37# which is deleted when the Tileset instance is not in use anymore.
38#-----------------------------------------------------------------------------
39sub new
40{
41    my $class = shift;
42    my $Config = TahConf->getConfig();
43    my $req = shift;    #Request object
44
45    my $self = {
46        req => $req,
47        Config => $Config,
48        JobTime => undef,     # API fetching time for the job as timestamp
49        bbox => undef,        # bbox of required tileset
50        marg_bbox => undef,   # bbox of required tileset including margins
51        childThread => 0,     # marks whether we are a parent or child thread
52        };
53
54    my $delTmpDir = 1-$Config->get('Debug');
55
56    $self->{JobDir} = tempdir( 
57         sprintf("%d_%d_%d_XXXXX",$self->{req}->ZXY),
58         DIR      => $Config->get('WorkingDirectory'), 
59         CLEANUP  => $delTmpDir,
60         );
61
62    # create true color images by default
63    GD::Image->trueColor(1);
64
65    # create blank comparison images
66    my $EmptyLandImage = new GD::Image(256,256);
67    my $MapLandBackground = $EmptyLandImage->colorAllocate(248,248,248);
68    $EmptyLandImage->fill(127,127,$MapLandBackground);
69
70    my $EmptySeaImage = new GD::Image(256,256);
71    my $MapSeaBackground = $EmptySeaImage->colorAllocate(181,214,241);
72    $EmptySeaImage->fill(127,127,$MapSeaBackground);
73
74    # Some broken versions of Inkscape occasionally produce totally black
75    # output. We detect this case and throw an error when that happens.
76    my $BlackTileImage = new GD::Image(256,256);
77    my $BlackTileBackground = $BlackTileImage->colorAllocate(0,0,0);
78    $BlackTileImage->fill(127,127,$BlackTileBackground);
79
80    $self->{EmptyLandImage} = $EmptyLandImage;
81    $self->{EmptySeaImage} = $EmptySeaImage;
82    $self->{BlackTileImage} = $BlackTileImage;
83
84    # Inkscape auto-backup/reset setup
85    # Takes a backup copy of ~/.inkscape/preferences.xml if
86    # AutoResetInkscapePrefs is turned on in config and we are using Inkscape
87    # as rasterizer.
88    # This backup copy is restored if Inkscape crashes and mentions
89    # preferences.xml on STDERR
90    # FIXME: this must check the integrity of the preference file first, otherwise we backup a broken config, which will later on be restored, leading to a failure loop
91    if( $Config->get("AutoResetInkscapePrefs") == 1 &&
92        $SVG::Rasterize::object->engine()->isa('SVG::Rasterize::Engine::Inkscape') ){
93
94        $self->{inkscape_autobackup}{cfgfile} = glob('~/.inkscape/');
95        if($self->{inkscape_autobackup}{cfgfile})
96        {
97            $self->{inkscape_autobackup}{cfgfile} .= "preferences.xml";
98            $self->{inkscape_autobackup}{backupfile} = "$self->{inkscape_autobackup}{cfgfile}.bak"
99                if defined($self->{inkscape_autobackup}{cfgfile});
100
101            if( -f $self->{inkscape_autobackup}{cfgfile} ){
102                if ( -s $self->{inkscape_autobackup}{cfgfile} == 0 ) {
103                    #emty config file found! i delete it
104                    unlink($self->{inkscape_autobackup}{cfgfile});
105                }
106                else {
107                    copy($self->{inkscape_autobackup}{cfgfile}, $self->{inkscape_autobackup}{backupfile})
108                        or do {
109                            warn "Error doing backup of $self->{inkscape_autobackup}{cfgfile} to $self->{inkscape_autobackup}{backupfile}: $!\n";
110                            delete($self->{inkscape_autobackup});
111                    };
112                }
113            } else {
114                delete($self->{inkscape_autobackup});
115            }
116        }
117    }
118
119    bless $self, $class;
120    return $self;
121}
122
123#-----------------------------------------------------------------------------
124# Tileset destructor. Call cleanup in case we did not clean up properly earlier.
125#-----------------------------------------------------------------------------
126sub DESTROY
127{
128    my $self = shift;
129    # Don't clean up in child threads
130    return if ($self->{childThread});
131
132    # only cleanup if we are the parent thread
133    $self->cleanup();
134}
135
136#-----------------------------------------------------------------------------
137# generate does everything that is needed to end up with a finished tileset
138# that just needs compressing and uploading. It outputs status messages, and
139# hands back the job to the server in case of critical errors.
140#-----------------------------------------------------------------------------
141sub generate
142{
143    my $self = shift;
144    my $req =  $self->{req};
145    my $Config = $self->{Config};
146
147    $::currentSubTask = "";
148    ::keepLog($$,"GenerateTileset","start","x=".$req->X.',y='.$req->Y.',z='.$req->Z." for layers ".$req->layers_str);
149
150    $self->{bbox}= bbox->new(ProjectXY($req->ZXY));
151
152    ::statusMessage(sprintf("Tileset (%d,%d,%d) around %.2f,%.2f", $req->ZXY, $self->{bbox}->center), 1, 0);
153
154    if($req->Z >= 12)
155    {
156        #------------------------------------------------------
157        # Download data (returns full path to data.osm or 0)
158        #------------------------------------------------------
159
160        my $beforeDownload = time();
161        my $FullDataFile = $self->downloadData($req->layers);
162        ::statusMessage("Download in ".(time() - $beforeDownload)." sec",1,10); 
163
164        #------------------------------------------------------
165        # Handle all layers, one after the other
166        #------------------------------------------------------
167
168        foreach my $layer ($req->layers)
169        {
170            # TileDirectory is the name of the directory for finished tiles
171            my $TileDirectory = sprintf("%s_%d_%d_%d.dir", $Config->get($layer."_Prefix"), $req->ZXY);
172
173            # JobDirectory is the directory where all final .png files are stored.
174            # It is not used for temporary files.
175            my $JobDirectory = File::Spec->join($self->{JobDir}, $TileDirectory);
176            mkdir $JobDirectory;
177
178            $self->generateNormalLayer($layer);
179        }
180    }
181    else
182    {
183        my %layers = map {$_ => 1} $req->layers;
184        my %alllayers = %layers;
185        if($layers{tile}) # make sure we have working directories
186        {
187            $alllayers{captionless} = 1;
188            $alllayers{caption} = 1;
189        }
190
191        foreach my $layer (keys %alllayers)
192        {
193            my $TileDirectory = sprintf("%s_%d_%d_%d.dir", $Config->get($layer."_Prefix"), $req->ZXY);
194            my $JobDirectory = File::Spec->join($self->{JobDir}, $TileDirectory);
195            mkdir $JobDirectory;
196        }
197
198        #------------------------------------------------------
199        # Download data (returns full path to data.osm or 0)
200        #------------------------------------------------------
201
202        if($layers{caption})
203        {
204            my $beforeDownload = time();
205            my $FullDataFile = $self->downloadData("caption");
206            ::statusMessage("Download in ".(time() - $beforeDownload)." sec",1,10); 
207            $self->generateNormalLayer("caption");
208        }
209
210        eval
211        {
212            require Image::Magick;
213            if(($Image::Magick::VERSION cmp "6.4.5") < 0)
214            {
215              die "At least Version 6.4.5 of ImageMagick required to get usable results.";
216            }
217            Image::Magick->import();
218            require LWP::Simple;
219            require File::Compare;
220            require OceanTiles;
221            $self->{OceanTiles} = new OceanTiles();
222            $self->{EmptyLandImageIM} = new Image::Magick(size=>'256x256');
223            $self->{EmptyLandImageIM}->Read("xc:rgb(248,248,248)") and die;
224            $self->{EmptySeaImageIM} = new Image::Magick(size=>'256x256');
225            $self->{EmptySeaImageIM}->Read("xc:rgb(181,214,241)") and die;
226        };
227        my $maxlayer = ($req->Z < 6) ? 6 : 12;
228        if(!$@)
229        {
230            $::progress=0;
231            $::progressPercent=0;
232
233            my $numlayers = 0;
234            ++$numlayers if $alllayers{captionless};
235            ++$numlayers if $alllayers{maplint};
236
237            $self->{NumTiles} = 0;
238            # up-to maxlayer, as this needs to be downloaded as well
239            my $t = 1;
240            for(my $i = $req->Z; $i <= $maxlayer; ++$i)
241            {
242                $self->{NumTiles} += $numlayers*$t;
243
244                # no caption or tile download for $maxlayer level!
245                $self->{NumTiles} += 2*$t if $i < $maxlayer && $alllayers{tile};
246
247                $t *= 4;
248            }
249
250            my $forkpid = 0;
251            if($Config->get("Fork"))
252            {
253                $forkpid = fork();
254                if($forkpid == 0)
255                {
256                    $self->{childThread}=1;
257                    my $num = 2**($maxlayer-$req->Z);
258                    $self->{NumTiles} = $num*$num;
259                    my $startx = $req->X*$num;
260                    my $starty = $req->Y*$num;
261                    for(my $i = 0; $i < $num; ++$i)
262                    {
263                        for(my $j = 0; $j < $num; ++$j)
264                        {
265                            $self->getFile("captionless", $maxlayer, $startx+$i,
266                            $starty+$j);
267                        }
268                    }
269                    exit(1);
270                }
271            }
272
273            if($layers{tile})
274            {
275                # also produces captionless
276                $self->lowZoom($req->ZXY, $maxlayer, "tile", "captionless", "caption");
277            }
278            elsif($layers{captionless})
279            {
280                $self->lowZoom($req->ZXY, $maxlayer, "captionless", "captionless");
281            }
282            if($layers{maplint})
283            {
284                $self->lowZoom($req->ZXY, $maxlayer, "maplint", "maplint");
285            }
286            waitpid($forkpid, 0) if $forkpid;
287        }
288        else
289        {
290            ::statusMessage("Tile stiching not supported without Image::Magick.", 1, 1);
291        }
292        # now copy/cleanup the results
293        foreach my $layer ($req->layers)
294        {
295            next if $layer eq "caption";
296            my $TileDirectory = sprintf("%s_%d_%d_%d.dir", $Config->get($layer."_Prefix"), $req->ZXY);
297            my $JobDirectory = File::Spec->join($self->{JobDir}, $TileDirectory);
298            my $hasdata = 0;
299            my $file;
300            opendir(DIR, $JobDirectory);
301            while(defined($file = readdir(DIR)))
302            {
303                if($file =~ /^[a-z]+_$maxlayer/)
304                {
305                    if($Config->get("Debug"))
306                    {
307                        rename File::Spec->join($JobDirectory, $file),
308                        File::Spec->join($self->{JobDir}, $file);
309                    }
310                    else
311                    {
312                        unlink(File::Spec->join($JobDirectory, $file));
313                    }
314                }
315                else
316                {
317                    ++$hasdata;
318                }
319            }
320            if($hasdata)
321            {
322                if ($Config->get("CreateTilesetFile") and !$Config->get("LocalSlippymap")) {
323                    $self->createTilesetFile($layer);
324                }
325                else {
326                    $self->createZipFile($layer);
327                }
328            }
329        }
330    }
331
332    $::currentSubTask = "";
333    ::keepLog($$,"GenerateTileset","stop",'x='.$req->X.',y='.$req->Y.',z='.$req->Z." for layers ".$req->layers_str);
334
335    # Cleaning up of tmpdirs etc. are called in the destructor DESTROY
336}
337
338sub generateNormalLayer
339{
340    my ($self,$layer) = @_;
341
342    #reset progress for each layer
343    $::progress=0;
344    $::progressPercent=0;
345    $::currentSubTask = $layer;
346
347    #------------------------------------------------------
348    # Go through preprocessing steps for the current layer
349    # This puts preprocessed files like data-maplint-closeareas.osm in $self->{JobDir}
350    # and returns the file name of the resulting data file.
351    #------------------------------------------------------
352
353    my $layerDataFile = $self->runPreprocessors($layer);
354
355    #------------------------------------------------------
356    # Preprocessing finished, start rendering to SVG to PNG
357    # $layerDataFile is just the filename
358    #------------------------------------------------------
359
360    if ($self->{Config}->get("Fork"))
361    {   # Forking to render zoom levels in parallel
362        $self->forkedRender($layer, $layerDataFile);
363    }
364    else
365    {   # Non-forking render
366        $self->nonForkedRender($layer, $layerDataFile);
367    }
368}
369
370sub lowZoomFileName
371{
372    my ($self, $Layer, $Z, $X, $Y) = @_;
373
374    my $prefixd = sprintf "%s_%d_%d_%d",$self->{Config}->get("${Layer}_Prefix"),
375    $self->{req}->ZXY;
376    my $prefix = sprintf "%s_%d_%d_%d",$self->{Config}->get("${Layer}_Prefix"),
377    $Z,$X,$Y;
378    return (File::Spec->join($self->{JobDir}, "$prefixd.dir", "$prefix.png"),
379      "$Layer ($Z,$X,$Y)");
380}
381
382sub getFile {
383    my ($self, $Layer, $Z, $X, $Y) = @_;
384    my ($pfile, $file) = $self->lowZoomFileName($Layer, $Z, $X, $Y);
385    ++$::progress;
386    $::progressPercent = $::progress / $self->{NumTiles} * 100;
387    ::statusMessage("Loading $Layer($Z,$X,$Y)", 0, 10);
388    for(my $i = 0; $i < 3 && !-f $pfile; ++$i)
389    {
390        eval
391        {
392            if($self->{Config}->get('Debug'))
393            {
394                ::statusMessage("Download file $file",1,6);
395            }
396            LWP::Simple::mirror(sprintf("http://tah.openstreetmap.org/Tiles/%s/%d/%d/%d.png",
397            $Layer,$Z,$X,$Y),$pfile);
398        };
399        unlink $pfile if($@);
400    }
401    throw TilesetError "The image $file download failed", "lowzoom" if !-f $pfile;
402}
403
404# Recursively create (including any downloads necessary) a tile
405sub lowZoom {
406    my ($self, $Z, $X, $Y, $MaxZ, $OutputLayer, $BaseLayer, $CaptionLayer) = @_;
407
408    $::currentSubTask = $OutputLayer;
409
410    # Get tiles
411    if($Z >= $MaxZ)
412    {
413        $self->getFile($BaseLayer,$Z,$X,$Y);
414    }
415    else
416    {
417        # Recursively get/create the 4 subtiles
418        $self->lowZoom($Z+1,$X*2,$Y*2,$MaxZ,$OutputLayer,$BaseLayer,$CaptionLayer);
419        $self->lowZoom($Z+1,$X*2+1,$Y*2,$MaxZ,$OutputLayer,$BaseLayer,$CaptionLayer);
420        $self->lowZoom($Z+1,$X*2,$Y*2+1,$MaxZ,$OutputLayer,$BaseLayer,$CaptionLayer);
421        $self->lowZoom($Z+1,$X*2+1,$Y*2+1,$MaxZ,$OutputLayer,$BaseLayer,$CaptionLayer);
422
423        $self->getFile($CaptionLayer,$Z,$X,$Y) if $CaptionLayer;
424
425        # Create the tile from those subtiles
426        $self->supertile($X,$Y,$Z,$OutputLayer,$BaseLayer,$CaptionLayer);
427    }
428}
429
430# Open a PNG file, and return it as a Magick image (or 0 if not found)
431sub readLocalImage
432{
433    my ($self,$Layer,$Z,$X,$Y) = @_;
434    my ($pfile, $file) = $self->lowZoomFileName($Layer, $Z, $X, $Y);
435
436    my $imImage;
437    throw TilesetError "The image $file is missing", "lowzoom" if !-f $pfile;
438    my $Image = GD::Image->newFromPng($pfile);
439    throw TilesetError "The image $file failed to load", "lowzoom" if !$Image;
440
441    # Detect empty tiles here:
442    if (File::Compare::compare($pfile, "emptyland.png") == 0)
443    {
444        return 0 if($Layer eq "caption");
445        $imImage = $self->{EmptyLandImageIM};
446    }
447    elsif (File::Compare::compare($pfile, "emptysea.png") == 0)
448    {
449        return 0 if($Layer eq "caption");
450        $imImage = $self->{EmptySeaImageIM};
451    }
452    elsif (not ($Image->compare($self->{EmptyLandImage}) & GD_CMP_IMAGE))
453    {
454        return 0 if($Layer eq "caption");
455        $imImage = $self->{EmptyLandImageIM};
456    }
457    elsif (not ($Image->compare($self->{EmptySeaImage}) & GD_CMP_IMAGE))
458    {
459        return 0 if($Layer eq "caption");
460        $imImage = $self->{EmptySeaImageIM};
461    }
462    elsif (not ($Image->compare($self->{BlackTileImage}) & GD_CMP_IMAGE))
463    {
464        return 0 if($Layer eq "caption");
465        if($Z == 12 && $self->{OceanTiles})
466        {
467            my $state = $self->{OceanTiles}->getState($X, $Y);
468            if($state eq "sea")
469            {
470                ::statusMessage("Tile state mismatch for $file: mixed/black != sea", 1, 3);
471                $imImage = $self->{EmptySeaImageIM};
472            }
473            elsif($state eq "land")
474            {
475                ::statusMessage("Tile state mismatch for $file: mixed/black != land", 1, 3);
476                $imImage = $self->{EmptyLandImageIM};
477            }
478        }
479
480        # make tile a dark blue, so someone fixes this error
481        if(!$imImage)
482        {
483            ::statusMessage("Tile state mismatch for $file: mixed/black found", 1, 3);
484
485            $imImage = new Image::Magick(size=>'256x256');
486            $imImage->Read("xc:rgb(0,0,255)");
487        }
488    }
489    # try to work around an ImageMagick bug with transparency in >= 6.4.3
490    elsif($Layer eq "caption" && open(FILE,">",$pfile))
491    {
492        $Image->trueColorToPalette();
493        print FILE $Image->png;
494        close FILE;
495    }
496    if($imImage && $Z == 12 && $Layer ne "caption" && $self->{OceanTiles})
497    {
498        my $state = $self->{OceanTiles}->getState($X, $Y);
499        if($imImage == $self->{EmptySeaImageIM})
500        {
501            if($state ne "sea")
502            {
503                ::statusMessage("Tile state mismatch for $file: sea != $state", 1, 3);
504                $imImage = $self->{EmptyLandImageIM} if($state eq "land");
505            }
506        }
507        else
508        {
509            if($state ne "land")
510            {
511                ::statusMessage("Tile state mismatch for $file: land != $state", 1, 3);
512                $imImage = $self->{EmptySeaImageIM} if($state eq "sea");
513            }
514        }
515    }
516    if(!$imImage)
517    {
518        $imImage = new Image::Magick;
519        if (my $err = $imImage->Read($pfile))
520        {
521            throw TilesetError "The image $file failed to load: $err", "lowzoom";
522        }
523    }
524
525    return($imImage);
526}
527
528sub supertile {
529    my ($self,$X,$Y,$Z,$OutputLayer,$BaseLayer,$CaptionLayer) = @_;
530    my $Config = $self->{Config};
531
532    my ($pfile, $file) = $self->lowZoomFileName($BaseLayer, $Z, $X, $Y);
533    my $Image;
534    if(!-f $pfile)
535    {
536        ++$::progress;
537        $::progressPercent = $::progress / $self->{NumTiles} * 100;
538        # Load the subimages
539        my $AA = $self->readLocalImage($BaseLayer,$Z+1,$X*2,$Y*2);
540        my $BA = $self->readLocalImage($BaseLayer,$Z+1,$X*2+1,$Y*2);
541        my $AB = $self->readLocalImage($BaseLayer,$Z+1,$X*2,$Y*2+1);
542        my $BB = $self->readLocalImage($BaseLayer,$Z+1,$X*2+1,$Y*2+1);
543
544        if($AA == $self->{EmptySeaImageIM}
545        && $AB == $self->{EmptySeaImageIM}
546        && $BA == $self->{EmptySeaImageIM}
547        && $BB == $self->{EmptySeaImageIM})
548        {
549            ::statusMessage("Writing sea $file", 0, 6);
550            copy("emptysea.png", $pfile);
551        }
552        elsif($AA == $self->{EmptyLandImageIM}
553        && $AB == $self->{EmptyLandImageIM}
554        && $BA == $self->{EmptyLandImageIM}
555        && $BB == $self->{EmptyLandImageIM})
556        {
557            ::statusMessage("Writing land $file", 0, 6);
558            copy("emptyland.png", $pfile);
559        }
560        else
561        {
562            $Image = Image::Magick->new(size=>'512x512');
563            # Create the supertile
564            $Image->ReadImage('xc:white');
565
566            # Copy the subimages into the 4 quadrants
567            foreach my $x (0, 1)
568            {
569                foreach my $y (0, 1)
570                {
571                    next unless (($Z < 9) || (($x == 0) && ($y == 0)));
572                    $Image->Composite(image => $AA,
573                                    geometry => sprintf("512x512+%d+%d", $x, $y),
574                                    compose => "darken");
575
576                    $Image->Composite(image => $BA,
577                                    geometry => sprintf("512x512+%d+%d", $x + 256, $y),
578                                    compose => "darken");
579
580                    $Image->Composite(image => $AB,
581                                    geometry => sprintf("512x512+%d+%d", $x, $y + 256),
582                                    compose => "darken");
583
584                    $Image->Composite(image => $BB,
585                                    geometry => sprintf("512x512+%d+%d", $x + 256, $y + 256),
586                                    compose => "darken");
587                }
588            }
589
590            $Image->Scale(width => "256", height => "256");
591            $Image->Set(type=>"Palette");
592            $Image->Set(quality => 90); # compress image
593            $Image->Write($pfile);
594            $self->optimizePng($pfile, $Config->get("${BaseLayer}_Transparent"));
595            ::statusMessage("Writing $file", 0, 6);
596        }
597    }
598    if($CaptionLayer)
599    {
600        ++$::progress;
601        $::progressPercent = $::progress / $self->{NumTiles} * 100;
602        # CaptionFile can be empty --> nothing to do
603        my $CaptionFile = $self->readLocalImage($CaptionLayer,$Z,$X,$Y);
604
605        $Image = $self->readLocalImage($BaseLayer,$Z,$X,$Y) if !$Image;
606
607        # Overlay the captions onto the tiled image and then write it
608        $Image->Composite(image => $CaptionFile) if $CaptionFile;
609        ($pfile, $file) = $self->lowZoomFileName($OutputLayer, $Z, $X, $Y);
610        $Image->Write($pfile);
611        my $gdimage = GD::Image->newFromPng($pfile);
612        throw TilesetError "The image $file failed to load", "lowzoom" if !$gdimage;
613        if (not ($gdimage->compare($self->{EmptyLandImage}) & GD_CMP_IMAGE)) {
614            ::statusMessage("Writing land $file", 0, 6);
615            copy("emptyland.png", $pfile);
616        }
617        elsif (not ($gdimage->compare($self->{EmptySeaImage}) & GD_CMP_IMAGE)) {
618            ::statusMessage("Writing sea $file", 0, 6);
619            copy("emptysea.png", $pfile);
620        }
621        else
622        {
623            $self->optimizePng($pfile, $Config->get("${OutputLayer}_Transparent"));
624            ::statusMessage("Writing $file", 0, 6);
625        }
626    }
627}
628
629#------------------------------------------------------------------
630
631=pod
632
633=head3 downloadData
634
635Download the area for the tileset (whole or in stripes, as required)
636into $self->{JobDir}
637
638B<parameter>: none
639
640B<returns>: filename
641I<filename>: resulting data osm filename (without path).
642
643=cut
644#-------------------------------------------------------------------
645sub downloadData
646{
647    my ($self, @layers) = @_;
648    my $req = $self->{req};
649    my $Config = $self->{Config};
650
651    $::progress = 0;
652    $::progressPercent = 0;
653    $::currentSubTask = "Download";
654   
655    # Adjust requested area to avoid boundary conditions
656    my $N1 = $self->{bbox}->N + ($self->{bbox}->N-$self->{bbox}->S)*$Config->get("BorderNS");
657    my $S1 = $self->{bbox}->S - ($self->{bbox}->N-$self->{bbox}->S)*$Config->get("BorderNS");
658    my $E1 = $self->{bbox}->E + ($self->{bbox}->E-$self->{bbox}->W)*$Config->get("BorderWE");
659    my $W1 = $self->{bbox}->W - ($self->{bbox}->E-$self->{bbox}->W)*$Config->get("BorderWE");
660    $self->{marg_bbox} = bbox->new($N1,$E1,$S1,$W1);
661
662    # TODO: verify the current system cannot handle segments/ways crossing the
663    # 180/-180 deg meridian and implement proper handling of this case, until
664    # then use this workaround:
665
666    if($W1 <= -180) {
667      $W1 = -180; # api apparently can handle -180
668    }
669    if($E1 > 180) {
670      $E1 = 180;
671    }
672
673    my $bbox = sprintf("%f,%f,%f,%f", $W1, $S1, $E1, $N1);
674
675    my $DataFile = File::Spec->join($self->{JobDir}, "data.osm");
676   
677    my @predicates;
678    foreach my $layer (@layers) {
679        my %layer_config = $Config->varlist("^${layer}_", 1);
680        if (not $layer_config{"predicates"}) {
681            @predicates = ();
682            last;
683        }
684        my $predicates = $layer_config{"predicates"};
685        # strip spaces in predicates
686        $predicates =~ s/\s+//g;
687        push(@predicates, split(/,/, $predicates));
688    }
689
690    my @OSMServers = (@predicates) ? split(/,/, $Config->get("XAPIServers")) : split(/,/, $Config->get("APIServers"));
691
692    my $Server = Server->new();
693    my $res;
694    my $reason;
695
696    if ($req->priority() > 1) {
697        my $firstServer = shift(@OSMServers);
698        if($firstServer eq "API") {
699            my $secondServer = shift(@OSMServers);
700            unshift(@OSMServers, $firstServer);
701            unshift(@OSMServers, $secondServer);
702        } else {
703            unshift(@OSMServers, $firstServer);
704        }
705    }
706
707    my $filelist;
708    foreach my $OSMServer (@OSMServers) {
709        $self->{JobTime} = time();
710        my @URLS;
711        my @title;
712        if (@predicates) {
713            foreach my $predicate (@predicates) {
714                my $URL = $Config->get("XAPI_$OSMServer");
715                $URL =~ s/%p/${predicate}/g;                # substitute %p place holder with predicate
716                $URL =~ s/%v/$Config->get('OSMVersion')/ge; # substitute %v place holder with API version
717                push(@URLS, $URL);
718                push(@title, $predicate);
719            }
720        }
721        else {
722            my $URL = $Config->get("API_$OSMServer");
723            $URL =~ s/%v/$Config->get('OSMVersion')/ge; # substitute %v place holder with API version
724            push(@URLS, $URL);
725            push(@title, "map data");
726        }
727
728        $filelist = [];
729        my $i=0;
730        foreach my $URL (@URLS) {
731            ++$i;
732            my $partialFile = File::Spec->join($self->{JobDir}, "data-$i.osm");
733            my $title = pop @title;
734            ::statusMessage("Downloading $title for " . join(",",@layers) ." from ".$OSMServer, 0, 3);
735           
736            # download tile data in one piece *if* the tile is not too complex
737            if ($req->complexity() < 20_000_000) {
738                my $currentURL = $URL;
739                $currentURL =~ s/%b/${bbox}/g;
740                print "Downloading: $currentURL\n" if ($Config->get("Debug"));
741                try {
742                    $Server->downloadFile($currentURL, $partialFile, 0);
743                    push(@{$filelist}, $partialFile);
744                    $res = 1;
745                }
746                catch ServerError with { # just do nothing if there was an error during download
747                    my $err = shift();
748                    print "Download failed: " . $err->text() . "\n" if ($Config->get("Debug"));;
749                };
750            }
751
752            if ((! $res) and ($Config->get("FallBackToSlices"))) {
753                ::statusMessage("Trying smaller slices for $title from $OSMServer",1,0);
754                my $slice = (($E1 - $W1) / 10); # A slice is one tenth of the width
755                my $slicesdownloaded=0;
756                for (my $j = 1; $j <= 10; $j++) {
757                    my $bbox = sprintf("%f,%f,%f,%f", $W1 + ($slice * ($j - 1)), $S1, $W1 + ($slice * $j), $N1);
758                    my $currentURL = $URL;
759                    $currentURL =~ s/%b/${bbox}/g;    # substitute bounding box place holder
760                    $partialFile = File::Spec->join($self->{JobDir}, "data-$i-$j.osm");
761                    $res = 0;
762                    for (my $k = 1; $k <= 3; $k++) {  # try each slice 3 times
763                        ::statusMessage("Downloading $title (slice $j of 10) from $OSMServer", 0, 3);
764                        print "Downloading: $currentURL\n" if ($Config->get("Debug"));
765                        try {
766                            $Server->downloadFile($currentURL, $partialFile, 0);
767                            $res = 1;
768                            ++$slicesdownloaded;
769                        }
770                        catch ServerError with {
771                            my $err = shift();
772                            print "Download failed: " . $err->text() . "\n" if ($Config->get("Debug"));;
773                            my $message = ($k < 3) ? "Download of $title slice $j from $OSMServer failed, trying again" : "Download of $title slice $j from $OSMServer failed 3 times, giving up";
774                            ::statusMessage($message, 0, 3);
775                        };
776                        last if ($res); # don't try again if download was successful
777                    }
778                    last if (!$res); # don't download remaining slices if one fails
779                    push(@{$filelist}, $partialFile);
780                }
781                $res = ($slicesdownloaded == 10);
782            }
783            if (!$res) {
784                ::statusMessage("Download of $title from $OSMServer failed", 0, 3);
785                last; # don't download other URLs if this one failed
786            } 
787        } # foreach @URLS
788
789        last if ($res); # don't try another server if the download was successful
790    } # foreach @OSMServers
791
792    if ($res) {   # Download of data succeeded
793        ::statusMessage("Download of data complete", 1, 10);
794    }
795    else {
796        # we need to have an additional full line error message here, as the exception will
797        # ignore our partial displayed previous lines
798        ::statusMessage("All servers tried for data download", 1, 3);
799        my $OSMServers = join(',', @OSMServers);
800        throw TilesetError "Download of data failed from $OSMServers", "nodata ($OSMServers)";
801    }
802
803    ($res, $reason) = ::mergeOsmFiles($DataFile, $filelist);
804    if(!$res) {
805        throw TilesetError "Striped download failed with: " . $reason;
806    }
807
808    # Check for correct UTF8 (else inkscape will run amok later)
809    # FIXME: This doesn't seem to catch all string errors that inkscape trips over.
810    ::statusMessage("Checking for UTF-8 errors",0,3);
811    if (my $line = ::fileUTF8ErrCheck($DataFile))
812    {
813        ::statusMessage(sprintf("found incorrect UTF-8 chars in line %d. job (%d,%d,%d)",$line, $req->ZXY),1,0);
814        throw TilesetError "UTF8 test failed", "utf8";
815    }
816    ::resetFault("utf8"); #reset to zero if no UTF8 errors found.
817    return $DataFile;
818}
819
820
821#------------------------------------------------------
822# Go through preprocessing steps for the current layer
823# expects $self->{JobDir}/data.osm as input and produces
824# $self->{JobDir}/dataList-of-preprocessors.osm
825# parameter: (layername)
826# returns:   filename (without path)
827#-------------------------------------------------------------
828sub runPreprocessors
829{
830    my $self = shift;
831    my $layer= shift;
832    my $req = $self->{req};
833    my $Config = $self->{Config};
834
835    my @ppchain = ();
836    my $outputFile;
837
838    # config option may be empty, or a comma separated list of preprocessors
839    foreach my $preprocessor(split /,/, $Config->get($layer."_Preprocessor"))
840    {
841        my $inputFile = File::Spec->join($self->{JobDir},
842                                         sprintf("data%s.osm", join("-", @ppchain)));
843        push(@ppchain, $preprocessor);
844        $outputFile = File::Spec->join($self->{JobDir},
845                                          sprintf("data%s.osm", join("-", @ppchain)));
846
847        if (-f $outputFile)
848        {
849            # no action; files for this preprocessing step seem to have been created
850                # by another layer already!
851        }
852        elsif ($preprocessor eq "maplint")
853        {
854            # Pre-process the data file using maplint
855            my $Cmd = sprintf("\"%s\" tr %s %s > \"%s\"",
856                    $Config->get("XmlStarlet"),
857                    "maplint/lib/run-tests.xsl",
858                    "$inputFile",
859                    "tmp.$$");
860            ::statusMessage("Running maplint",0,3);
861            ::runCommand($Cmd,$$);
862            $Cmd = sprintf("\"%s\" tr %s %s > \"%s\"",
863                        $Config->get("XmlStarlet"),
864                        "maplint/lib/convert-to-tags2.xsl",
865                        "tmp.$$",
866                        "$outputFile");
867            ::statusMessage("Creating tags from maplint",0,3);
868            ::runCommand($Cmd,$$);
869            unlink("tmp.$$");
870        }
871        elsif ($preprocessor eq "close-areas")
872        {
873            my $Cmd = sprintf("perl close-areas.pl %d %d %d < %s > %s",
874                        $req->X,
875                        $req->Y,
876                        $req->Z,
877                        "$inputFile",
878                        "$outputFile");
879            if($Config->get('Debug'))
880            {
881                ::statusMessage("Running close-areas ($Cmd)",0,3);
882            }
883            else
884            {
885                ::statusMessage("Running close-areas",0,3);
886            }
887            ::runCommand($Cmd,$$);
888        }
889        elsif ($preprocessor eq "area-center")
890        {
891           if ($Config->get("JavaAvailable"))
892           {
893               if ($Config->get("JavaVersion") >= 1.6)
894               {
895                   # use preprocessor only for XSLT for now. Using different algorithm for area center might provide inconsistent results"
896                   # on tile boundaries. But XSLT is currently in minority and use different algorithm than orp anyway, so no difference.
897                   my $Cmd = sprintf("java -cp %s com.bretth.osmosis.core.Osmosis -q -p org.tah.areaCenter.AreaCenterPlugin --read-xml %s --area-center --write-xml %s",
898                               join($Config->get("JavaSeparator"), "java/osmosis/osmosis.jar", "java/area-center.jar"),
899                               $inputFile,
900                               $outputFile);
901                   ::statusMessage("Running area-center",0,3);
902                   if (!::runCommand($Cmd,$$))
903                   {
904                       ::statusMessage("Area-center failed, ignoring",0,3);
905                       copy($inputFile,$outputFile);
906                   }
907               } else 
908               {
909                   ::statusMessage("Java version at least 1.6 is required for area-center preprocessor",0,3);
910                   copy($inputFile,$outputFile);
911               }
912           }
913           else
914           {
915              copy($inputFile,$outputFile);
916           }
917        }
918        elsif ($preprocessor eq "noop")
919        {
920            copy($inputFile,$outputFile);
921        }
922        else
923        {
924            throw TilesetError "Invalid preprocessing step '$preprocessor'", $preprocessor;
925        }
926    }
927
928    # everything went fine. Get final filename and return it.
929    my ($Volume, $path, $OSMfile) = File::Spec->splitpath($outputFile);
930    return $OSMfile;
931}
932
933#-------------------------------------------------------------------
934# renders the tiles, using threads
935# paramter: ($layer, $layerDataFile)
936#-------------------------------------------------------------------
937sub forkedRender
938{
939    my $self = shift;
940    my ($layer, $layerDataFile) = @_;
941    my $req = $self->{req};
942    my $Config = $self->{Config};
943    my $minzoom = $req->Z;
944    my $maxzoom = $Config->get($layer."_MaxZoom");
945
946    my $numThreads = 2 * $Config->get("Fork");
947    my @pids;
948
949    for (my $thread = 0; $thread < $numThreads; $thread ++) 
950    {
951        # spawn $numThreads threads
952        my $pid = fork();
953        if (not defined $pid) 
954        {   # exit if asked to fork but unable to
955            throw TilesetError "GenerateTileset: could not fork, exiting", "fatal";
956        }
957        elsif ($pid == 0) 
958        {   # we are the child process
959            $self->{childThread}=1;
960            for (my $zoom = ($minzoom + $thread) ; $zoom <= $maxzoom; $zoom += $numThreads) 
961            {
962                try {
963                    $self->Render($layer, $zoom, $layerDataFile)
964                }
965                otherwise {
966                    # an error occurred while rendering.
967                    # Thread exits and returns (255+)0 here
968                    exit(0);
969                }
970            }
971            # Rendering went fine, have thread return (255+)1
972            exit(1);
973        } else
974        {   # we are the parent thread, record child pid
975            push(@pids, $pid);
976        }
977    }
978
979    # now wait that all child render processes exited and check their return value
980    # retvalue >> 8 is the real ret value. wait returns -1 if there are no child processes
981    my $success = 1;
982    foreach my $pid(@pids)
983    {
984        waitpid($pid,0);
985        $success &= ($? >> 8);
986    }
987
988    ::statusMessage("exit forked renderer returning $success",0,6);
989    if (not $success) {
990        throw TilesetError "at least one render thread returned an error", "renderer";
991    }
992
993    if ($Config->get("CreateTilesetFile") and !$Config->get("LocalSlippymap")) {
994        $self->createTilesetFile($layer);
995    }
996    else {
997        $self->createZipFile($layer);
998    }
999}
1000
1001
1002#-------------------------------------------------------------------
1003# renders the tiles, not using threads
1004# paramter: ($layer, $layerDataFile)
1005#-------------------------------------------------------------------
1006sub nonForkedRender
1007{
1008    my $self = shift;
1009    my ($layer, $layerDataFile) = @_;
1010    my $req = $self->{req};
1011    my $Config = $self->{Config};
1012    my $minzoom = $req->Z;
1013    my $maxzoom = $Config->get($layer."_MaxZoom");
1014
1015    for (my $zoom = $req->Z ; $zoom <= $maxzoom; $zoom++) {
1016        $self->Render($layer, $zoom, $layerDataFile)
1017    }
1018
1019    if ($Config->get("CreateTilesetFile") and !$Config->get("LocalSlippymap")) {
1020        $self->createTilesetFile($layer);
1021    }
1022    else {
1023        $self->createZipFile($layer);
1024    }
1025}
1026
1027
1028#-------------------------------------------------------------------
1029# renders the tiles for one zoom level
1030# paramter: ($layer, $zoom, $layerDataFile)
1031#-------------------------------------------------------------------
1032sub Render
1033{
1034    my $self = shift;
1035    my ($layer, $zoom, $layerDataFile) = @_;
1036    my $Config = $self->{Config};
1037    my $req = $self->{req};
1038
1039    $::progress = 0;
1040    $::progressPercent = 0;
1041    $::currentSubTask = "$layer-z$zoom";
1042
1043    my $stripes = 1;
1044    if ($Config->get("RenderStripes")) {
1045        my $level = $zoom - $req->Z;
1046        if ($level >= $Config->get("RenderStripes")) {
1047            $stripes = 4 ** ($level - $Config->get("RenderStripes") + 1);
1048            if ($stripes > 2 ** $level) {
1049                $stripes = 2 ** $level;
1050            }
1051        }
1052    }
1053   
1054    $self->GenerateSVG($layer, $zoom, $layerDataFile);
1055
1056    $self->RenderSVG($layer, $zoom, $stripes);
1057
1058    $self->SplitTiles($layer, $zoom, $stripes);
1059}
1060
1061
1062#-----------------------------------------------------------------------------
1063# Generate SVG for one zoom level
1064#   $layer - layer to be processed
1065#   $zoom - which zoom currently is processsed
1066#   $layerDataFile - name of the OSM data file (which is in the JobDir)
1067#-----------------------------------------------------------------------------
1068sub GenerateSVG 
1069{
1070    my $self = shift;
1071    my ($layer, $zoom, $layerDataFile) = @_;
1072    my $Config = TahConf->getConfig();
1073    ::statusMessage("Generating SVG file", 0, 6);
1074 
1075    # Render the file (returns 0 on failure)
1076    if (! ::xml2svg(
1077            File::Spec->join($self->{JobDir}, $layerDataFile),
1078            $self->{bbox},
1079            $Config->get($layer . "_Rules." . $zoom),
1080            File::Spec->join($self->{JobDir}, "$layer-z$zoom.svg"),
1081            $zoom))
1082    {
1083        throw TilesetError "Render failure", "renderer";
1084    }
1085
1086    ::statusMessage("SVG done", 1, 10);
1087}
1088
1089
1090#-----------------------------------------------------------------------------
1091# Render SVG for one zoom level
1092#   $layer - layer to be processed
1093#   $zoom
1094#-----------------------------------------------------------------------------
1095sub RenderSVG
1096{
1097    my $self = shift;
1098    my ($layer, $zoom, $stripes) = @_;
1099    my $Config = $self->{Config};
1100    my $Req = $self->{req};
1101   
1102    # File locations
1103    my $svg_file = File::Spec->join($self->{JobDir},"$layer-z$zoom.svg");
1104
1105    my $tile_size = 256; # Tiles are 256 pixels square
1106    # png_width/png_height is the width/height dimension of resulting PNG file
1107    my $png_width = $tile_size * (2 ** ($zoom - $Req->Z));
1108    my $png_height = $png_width / $stripes;
1109
1110    # SVG excerpt in SVG units
1111    my ($height, $width, $valid) = ::getSize(File::Spec->join($self->{JobDir}, "$layer-z$zoom.svg"));
1112    my $stripe_height = $height / $stripes;
1113
1114    for (my $stripe = 0; $stripe <= $stripes - 1; $stripe++) {
1115        my $png_file = File::Spec->join($self->{JobDir},"$layer-z$zoom-s$stripe.png");
1116
1117        # Create an object describing what area of the svg we want
1118        my $box = SVG::Rasterize::CoordinateBox->new
1119            ({
1120                space => { top => 0, bottom => $height, left => 0, right => $width },
1121                box => { left => 0, right => $width, top => ($stripe_height * $stripe), bottom => ($stripe_height * ($stripe+1)) }
1122             });
1123       
1124        # Make a variable that points to the renderer to save lots of typing...
1125        my $rasterize = $SVG::Rasterize::object;
1126        my $engine = $rasterize->engine();
1127
1128        my %rasterize_params = (
1129            infile => $svg_file,
1130            outfile => $png_file,
1131            width => $png_width,
1132            height => $png_height,
1133            area => $box
1134            );
1135        if( ref($engine) =~ /batik/i && $Config->get('BatikJVMSize') ){
1136            $rasterize_params{heapsize} = $Config->get('BatikJVMSize');
1137        }
1138
1139        ::statusMessage("Rendering",0,3);
1140
1141        my $error = 0;
1142        try {
1143            $rasterize->convert(%rasterize_params);
1144        } catch SVG::Rasterize::Engine::Error::Prerequisite with {
1145            my $e = shift;
1146
1147            ::statusMessage("Rasterizing failed because of unsatisfied prerequisite: $e",1,0);
1148
1149            throw TilesetError("Exception in RenderSVG: $e");
1150        } catch SVG::Rasterize::Engine::Error::NoOutput with {
1151            my $e = shift;
1152
1153            ::statusMessage("Rasterizing failed to create output: $e",1,0);
1154            print "Rasterize command: \"".join('", "', @{$e->{cmd}})."\"\n" if $e->{cmd};
1155            print "Rasterize engine STDOUT:".$e->{stdout}."\n" if $e->{stdout};
1156            print "Rasterize engine STDERR:".$e->{stderr}."\n" if $e->{stderr};
1157
1158            $Req->is_unrenderable(1);
1159            throw TilesetError("Exception in RenderSVG: $e");
1160        } catch SVG::Rasterize::Engine::Inkscape::Error::Runtime with {
1161            my $e = shift;
1162            $_[1] = 1; # Set second parameter scalar to 1 so the next catch block is used
1163
1164            my $corrupt = 0;
1165            if( $e->{stderr} =~ /preferences.xml/ ){
1166                $corrupt = 1;
1167                warn "* Inkscape preference file corrupt. Delete Inkscape's preferences.xml to continue\n";
1168                if( defined($self->{inkscape_autobackup}) ){
1169                    my $cfg = $self->{inkscape_autobackup}{cfgfile};
1170                    my $bak = $self->{inkscape_autobackup}{backupfile};
1171                    warn "   AutoResetInkscapePrefs set, trying to reset $cfg\n";
1172                    unlink $cfg if( -f $cfg ); 
1173                    # FIXME: check backup is correct before putting back, or check preferences OK before backup.
1174                    $corrupt = 0; #if( rename($bak, $cfg) ); # how do we deal with a defect backup?
1175                }
1176            }
1177
1178            if( $corrupt ){
1179                ## this error is fatal because it needs human intervention before processing can continue
1180                throw TilesetError("Inkscape preference file corrupt. Delete to continue", 'fatal');
1181                addFault("fatal",1);
1182            }
1183
1184        } catch SVG::Rasterize::Engine::Error::Runtime with {
1185            my $e = shift;
1186
1187            ::statusMessage("Rasterizing failed with runtime exception: $e",1,0);
1188            print "Rasterize command: \"".join('", "', @{$e->{cmd}})."\"\n" if $e->{cmd};
1189            print "Rasterize engine STDOUT:".$e->{stdout}."\n" if $e->{stdout};
1190            print "Rasterize engine STDERR:".$e->{stderr}."\n" if $e->{stderr};
1191
1192            $Req->is_unrenderable(1);
1193            throw TilesetError("Exception in RenderSVG: $e");
1194        };
1195    }
1196}
1197
1198#-----------------------------------------------------------------------------
1199# Split PNG for one zoom level into tiles
1200#   $layer - layer to be processed
1201#   $zoom
1202#   $stripes - number of stripes the layer has been rendered
1203#-----------------------------------------------------------------------------
1204sub SplitTiles
1205{
1206    my $self = shift;
1207    my ($layer, $zoom, $stripes) = @_;
1208    my $Config = $self->{Config};
1209    my $Req = $self->{req};
1210
1211    my $minzoom = $Req->Z;
1212    my $size = 2 ** ($zoom - $minzoom);
1213    my $minx = $Req->X * $size;
1214    my $miny = $Req->Y * $size;
1215    my $number_tiles = $size * $size;
1216    my $stripe_height = $size / $stripes;
1217
1218    # Size of tiles
1219    my $pixels = 256;
1220
1221    $::progress = 0;
1222    $::progressPercent = 0;
1223
1224    # Use one subimage for everything, and keep copying data into it
1225    my $SubImage = new GD::Image($pixels, $pixels, 1);#$Config->get($layer."_Transparent") ? 1 : 0);
1226
1227    my $i = 0;
1228    my ($x, $y);
1229
1230    for (my $stripe = 0; $stripe <= $stripes - 1; $stripe++) {
1231        ::statusMessage("Splitting stripe $stripe",0,3);
1232
1233        my $png_file = File::Spec->join($self->{JobDir},"$layer-z$zoom-s$stripe.png");
1234        my $Image = GD::Image->newFromPng($png_file);
1235
1236        if( not defined $Image ) {
1237            throw TilesetError "SplitTiles: Missing File $png_file encountered", "fatal";
1238        }
1239
1240        for (my $iy = 0; $iy <= $stripe_height - 1; $iy++) {
1241            for (my $ix = 0; $ix <= $size - 1; $ix++) {
1242                $x = $minx + $ix;
1243                $y = $miny + $iy + $stripe * $stripe_height;
1244                $i++;
1245                $::progress = $i;
1246                $::progressPercent = $i / $number_tiles * 100;
1247                ::statusMessage("Writing tile $x $y", 0, 10); 
1248                # Get a tiles'worth of data from the main image
1249                $SubImage->copy($Image,
1250                                0,                   # Dest X offset
1251                                0,                   # Dest Y offset
1252                                $ix * $pixels,       # Source X offset
1253                                $iy * $pixels,       # Source Y offset
1254                                $pixels,             # Copy width
1255                                $pixels);            # Copy height
1256
1257                # Decide what the tile should be called
1258                my $tile_file;
1259                if ($Config->get("LocalSlippymap")) {
1260                    my $tile_dir = File::Spec->join($Config->get("LocalSlippymap"), $Config->get("${layer}_Prefix"), $zoom, $x);
1261                    File::Path::mkpath($tile_dir);
1262                    $tile_file = File::Spec->join($tile_dir, sprintf("%d.png", $y));
1263                }
1264                else {
1265                    # Construct base png directory
1266                    my $tile_dir = File::Spec->join($self->{JobDir}, sprintf("%s_%d_%d_%d.dir", $Config->get("${layer}_Prefix"), $Req->ZXY));
1267                    File::Path::mkpath($tile_dir);
1268                    $tile_file = File::Spec->join($tile_dir, sprintf("%s_%d_%d_%d.png", $Config->get("${layer}_Prefix"), $zoom, $x, $y));
1269                }
1270
1271                # libGD comparison returns true if images are different. (i.e. non-empty Land tile)
1272                # so return the opposite (false) if the tile doesn't look like an empty land tile
1273
1274                # Check for black tile output
1275                if (not ($SubImage->compare($self->{BlackTileImage}) & GD_CMP_IMAGE)) {
1276                    throw TilesetError "SplitTiles: Black Tile encountered", "inkscape";
1277                }
1278
1279                # Detect empty tile here:
1280                if (not ($SubImage->compare($self->{EmptyLandImage}) & GD_CMP_IMAGE)) { 
1281                    copy("emptyland.png", $tile_file);
1282                }
1283                # same for Sea tiles
1284                elsif (not($SubImage->compare($self->{EmptySeaImage}) & GD_CMP_IMAGE)) {
1285                    copy("emptysea.png", $tile_file);
1286                }
1287                else {
1288                    if ($Config->get($layer."_Transparent")) {
1289                        $SubImage->transparent($SubImage->colorAllocate(248, 248, 248));
1290                    }
1291                    else {
1292                        $SubImage->transparent(-1);
1293                    }
1294                    # Get the image as PNG data
1295                    my $png_data = $SubImage->png;
1296
1297                    # Store it
1298                    open (my $fp, ">$tile_file") || throw TilesetError "SplitTiles: Could not open $tile_file for writing", "fatal";
1299                    binmode $fp;
1300                    print $fp $png_data;
1301                    close $fp;
1302
1303                    $self->optimizePng($tile_file, $Config->get("${layer}_Transparent"));
1304                }
1305            }
1306        }
1307    }
1308}
1309
1310
1311#-----------------------------------------------------------------------------
1312# optimize a PNG file
1313#
1314# Parameters:
1315#   $png_file - file name of PNG file
1316#   $transparent - whether or not this is a transparent tile
1317#-----------------------------------------------------------------------------
1318sub optimizePng
1319{
1320    my $self = shift();
1321    my $png_file = shift();
1322    my $transparent = shift();
1323
1324    my $Config = $self->{Config};
1325    my $redirect = ($^O eq "MSWin32") ? "" : ">/dev/null";
1326    my $tmp_suffix = '.cut';
1327    my $tmp_file = $png_file . $tmp_suffix;
1328    my (undef, undef, $png_file_name) = File::Spec->splitpath($png_file);
1329
1330    my $cmd;
1331    if ($transparent) {
1332        # Don't quantize if it's transparent
1333        rename($png_file, $tmp_file);
1334    }
1335    elsif (($Config->get("PngQuantizer")||'') eq "pngnq") {
1336        $cmd = sprintf("\"%s\" -e .png%s -s1 -n256 %s %s",
1337                       $Config->get("pngnq"),
1338                       $tmp_suffix,
1339                       $png_file,
1340                       $redirect);
1341
1342        ::statusMessage("ColorQuantizing $png_file_name", 0, 6);
1343        if(::runCommand($cmd, $::PID)) {
1344            # Color quantizing successful
1345            unlink($png_file);
1346        }
1347        else {
1348            # Color quantizing failed
1349            ::statusMessage("ColorQuantizing $png_file_name with ".$Config->get("PngQuantizer")." failed", 1, 0);
1350            rename($png_file, $tmp_file);
1351        }
1352    }
1353    else {
1354        ::statusMessage("Not Color Quantizing $png_file_name, pngnq not installed?", 0, 6);
1355        rename($png_file, $tmp_file);
1356    }
1357
1358    if ($Config->get("PngOptimizer") eq "pngcrush") {
1359        $cmd = sprintf("\"%s\" -q %s %s %s",
1360                       $Config->get("Pngcrush"),
1361                       $tmp_file,
1362                       $png_file,
1363                       $redirect);
1364    }
1365    elsif ($Config->get("PngOptimizer") eq "optipng") {
1366           $cmd = sprintf("\"%s\" %s -out %s %s", #no quiet, because it even suppresses error output
1367                          $Config->get("Optipng"),
1368                          $tmp_file,
1369                          $png_file,
1370                          $redirect);
1371    }
1372    else {
1373        ::statusMessage("PngOptimizer not configured (should not happen, update from svn, and check config file)", 1, 0);
1374        ::talkInSleep("Install a PNG optimizer and configure it.", 15);
1375    }
1376
1377    ::statusMessage("Optimizing $png_file_name", 0, 6);
1378    if(::runCommand($cmd, $::PID)) {
1379        unlink($tmp_file);
1380    }
1381    else {
1382        ::statusMessage("Optimizing $png_file_name with " . $Config->get("PngOptimizer") . " failed", 1, 0);
1383        rename($tmp_file, $png_file);
1384    }
1385}
1386
1387
1388#-----------------------------------------------------------------------------
1389# Compress all PNG files from one directory, creating a .zip file.
1390#
1391# Parameters:
1392#   $layer - the layer for which the tileset is to be compressed
1393#-----------------------------------------------------------------------------
1394sub createZipFile
1395{
1396    my $self = shift();
1397    my $layer = shift();
1398    my $Config = $self->{Config};
1399
1400    my ($z, $x, $y) = $self->{req}->ZXY();
1401
1402    my $prefix = $Config->get("${layer}_Prefix");
1403    my $tile_dir = File::Spec->join($self->{JobDir},
1404                                    sprintf("%s_%d_%d_%d.dir",
1405                                            $prefix, $z, $x, $y));
1406
1407    my $upload_dir = File::Spec->join($Config->get("WorkingDirectory"), "uploadable");
1408    if (! -d $upload_dir) {
1409        mkpath($upload_dir) or throw TilesetError "Could not create upload directory '$upload_dir': $!", "fatal";
1410    }
1411
1412    my $zip_file = File::Spec->join($upload_dir,
1413                                    sprintf("%s_%d_%d_%d_%d.zip",
1414                                            $prefix, $z, $x, $y, ::GetClientId()));
1415   
1416    my $temp_file = File::Spec->join($self->{JobDir},
1417                                     sprintf("%s_%d_%d_%d_%d.zip",
1418                                             $prefix, $z, $x, $y, ::GetClientId()));
1419
1420    # ZIP all the tiles into a single file
1421    # First zip into "$Filename.part" and move to "$Filename" when finished
1422    my $stdout = File::Spec->join($self->{JobDir}, "zip.stdout");
1423    my $zip_cmd;
1424    if ($Config->get("7zipWin")) {
1425        $zip_cmd = sprintf('"%s" %s "%s" "%s"',
1426                           $Config->get("Zip"),
1427                           "a -tzip",
1428                           $temp_file,
1429                           File::Spec->join($tile_dir,"*.png"));
1430    }
1431    else {
1432        $zip_cmd = sprintf('"%s" -r -j "%s" "%s" > "%s"',
1433                           $Config->get("Zip"),
1434                           $temp_file,
1435                           $tile_dir,
1436                           $stdout);
1437    }
1438
1439    if (::dirEmpty($tile_dir)) {
1440        ::statusMessage("Skipping emtpy tileset directory: $tile_dir", 1, 0);
1441        return;
1442    }
1443    # Run the zip command
1444    ::runCommand($zip_cmd, $::PID) or throw TilesetError "Error running command '$zip_cmd'", "fatal";
1445
1446    # stdout is currently never used, so delete it unconditionally   
1447    unlink($stdout);
1448   
1449    # rename to final name so any uploader could pick it up now
1450    move ($temp_file, $zip_file) or throw TilesetError "Could not move ZIP file: $!", "fatal";
1451}
1452
1453
1454#-----------------------------------------------------------------------------
1455# Pack all PNG files from one directory into a tileset file.
1456#
1457# Parameters:
1458#   $layer - the layer for which the tileset file is to be created
1459#-----------------------------------------------------------------------------
1460sub createTilesetFile
1461{
1462    my $self = shift();
1463    my $layer = shift();
1464    my $Config = $self->{Config};
1465
1466    my ($z, $x, $y) = $self->{req}->ZXY();
1467
1468    my $index_start = 8;                                    # start of tile index, currently 8
1469    my $levels = $Config->get("${layer}_MaxZoom") - $z + 1; # number of layers in a tileset file, usually 6
1470    my $tiles = ((4 ** $levels) - 1) / 3;                   # number of tiles, 1365 for 6 zoom levels
1471    my $data_offset = $index_start + (4 * ($tiles + 1));    # start offset of tile data, 5472 for 6 zoom levels
1472    my $size = 1;                                           # size of base zoom level, for t@h always 1
1473
1474    my $userid = 0; # the server will fill this in
1475
1476    my $prefix = $Config->get("${layer}_Prefix");
1477    my $tile_dir = File::Spec->join($self->{JobDir},
1478                                    sprintf("%s_%d_%d_%d.dir",
1479                                            $prefix, $z, $x, $y));
1480
1481    my $upload_dir = File::Spec->join($Config->get("WorkingDirectory"), "uploadable");
1482    if (! -d $upload_dir) {
1483        mkpath($upload_dir) or throw TilesetError "Could not create upload directory '$upload_dir': $!", "fatal";
1484    }
1485
1486    my $file_name = File::Spec->join($upload_dir,
1487                                     sprintf("%s_%d_%d_%d_%d.tileset",
1488                                             $prefix, $z, $x, $y, ::GetClientId()));
1489   
1490    my $temp_file = File::Spec->join($self->{JobDir},
1491                                     sprintf("%s_%d_%d_%d_%d.tileset",
1492                                             $prefix, $z, $x, $y, ::GetClientId()));
1493
1494    my $currpos = $data_offset;
1495    open my $fh, ">$temp_file" or throw TilesetError "Couldn't open '$temp_file': $!", "fatal";
1496    seek $fh, $currpos, 0 or throw TilesetError "Couldn't seek: $!", "fatal";
1497
1498    my @offsets;
1499    for my $iz (0 .. $levels - 1) {
1500        my $width = 2**$iz;
1501        for my $iy (0 .. $width-1) {
1502            for my $ix (0 .. $width-1) {
1503                my $png_name = File::Spec->join($tile_dir,
1504                                                sprintf("%s_%d_%d_%d.png",
1505                                                        $prefix, $z + $iz, $x * $width + $ix, $y * $width + $iy));
1506                my $length = -s $png_name;
1507                if (! -e $png_name) {
1508                    push(@offsets, 0);
1509                }
1510                elsif ($length == 67) {
1511                    if ($Config->get("${layer}_Transparent")) {
1512                        #this is empty transparent
1513                        push(@offsets, 3);
1514                    }
1515                    else {
1516                        #this is empty land
1517                        push(@offsets, 2);
1518                    }
1519                }
1520                elsif ($length == 69) {
1521                    #this is empty sea
1522                    push(@offsets, 1);
1523                }
1524                else {
1525                    open my $png, "<$png_name" or throw TilesetError "Couldn't open file '$png_name': $!", "fatal";
1526                    my $buffer;
1527                    if( read($png, $buffer, $length) != $length ) {
1528                        throw TilesetError "Read failed from '$png_name': $!", "fatal";
1529                    }
1530                    close $png;
1531                    print $fh $buffer or throw TilesetError "Write failed on output to '$temp_file': $!", "fatal";
1532                    push @offsets, $currpos;
1533                    $currpos += $length;
1534                }
1535            }
1536        }
1537    }
1538
1539    if( scalar( @offsets ) != $tiles ) {
1540        throw TilesetError sprintf("Bad number of offsets: %d (should be %d)", scalar(@offsets), $tiles), "fatal";
1541    }
1542
1543    my $emptyness = 0; #what type of emptyness (land/sea)
1544    if ($currpos == $data_offset) {
1545        #tileset is empty
1546        #check the top level tile for the type of emptyness, assume that all tiles are the same
1547        my $png_name = File::Spec->join($tile_dir, sprintf("%s_%d_%d_%d.png", $prefix, $z, $x, $y));
1548        my $length = -s $png_name;
1549        if ($length == 67) {
1550            if ($Config->get("${layer}_Transparent")) {
1551                #this is empty transparent
1552                $emptyness = 3;
1553            }
1554            else {
1555                #this is empty land
1556                $emptyness = 2;
1557            }
1558        }
1559        elsif ($length == 69) {
1560            $emptyness = 1; #this is empty sea
1561        }
1562        $currpos = $index_start;
1563    }
1564    else {
1565        #tileset is not empty
1566        push @offsets, $currpos;
1567        seek $fh, $index_start, 0;
1568        print $fh pack("V*", @offsets) or throw TilesetError "Write failed to '$temp_file' $!", "fatal";
1569    }
1570
1571    seek $fh, 0, 0;
1572    print $fh pack("CCCCV", 2, $levels, $size, $emptyness, $userid) or throw TilesetError "Write failed to '$temp_file': $!", "fatal";
1573
1574    seek $fh, $currpos, 0;
1575    print $fh $self->generateMetaData($prefix);
1576    close $fh;
1577
1578    move($temp_file, $file_name) or throw TilesetError "Could not move tileset file '$temp_file' to '$file_name': $!", "fatal";
1579}
1580
1581
1582#------------------------------------------------------------------------------
1583# Assemble meta data in Tileset file format
1584# Parameters:
1585#   $layer_prefix
1586#
1587# Returns:
1588#   $meta_data - string
1589#------------------------------------------------------------------------------
1590sub generateMetaData
1591{
1592    my $self = shift();
1593    my $layer_prefix = shift();
1594    my $req = $self->{req};
1595
1596    my $meta_template = <<EOS;
1597Layer: %s
1598Zoom: %d
1599X: %d
1600Y: %d
1601Osm-Timestamp: %d
1602
1603EOS
1604
1605    my $meta_data = sprintf($meta_template, $layer_prefix, $req->ZXY(), $self->{JobTime});
1606    return $meta_data;
1607}
1608
1609
1610#------------------------------------------------------------------
1611# remove temporary files etc
1612#-------------------------------------------------------------------
1613sub cleanup
1614{
1615    my $self = shift;
1616    my $Config = $self->{Config};
1617
1618    # remove temporary job directory if 'Debug' is not set
1619    print STDERR "would be removing job dir ",$self->{JobDir},"\n\n" if $Config->get('Debug');
1620    rmtree $self->{JobDir} unless $Config->get('Debug');
1621}
1622
1623#----------------------------------------------------------------------------------------
1624# bbox->new(N,E,S,W)
1625package bbox;
1626sub new
1627{
1628    my $class = shift;
1629    my $self={};
1630    ($self->{N},$self->{E},$self->{S},$self->{W}) = @_;
1631    bless $self, $class;
1632    return $self;
1633}
1634
1635sub N { my $self = shift; return $self->{N};}
1636sub E { my $self = shift; return $self->{E};}
1637sub S { my $self = shift; return $self->{S};}
1638sub W { my $self = shift; return $self->{W};}
1639
1640sub extents
1641{
1642    my $self = shift;
1643    return ($self->{N}, $self->{E}, $self->{S}, $self->{W});
1644}
1645
1646sub center
1647{
1648    my $self = shift;
1649    return (($self->{N} + $self->{S}) / 2, ($self->{E} + $self->{W}) / 2);
1650}
1651
1652#----------------------------------------------------------------------------------------
1653# error class for Tileset
1654
1655package TilesetError;
1656use base 'Error::Simple';
1657
16581;
Note: See TracBrowser for help on using the repository browser.