source: subversion/applications/rendering/tilesAtHome/tools/lowzoom/lowzoom_composite.pl @ 8670

Revision 8670, 18.4 KB checked in by amillar, 6 years ago (diff)

Fix bug where empty land/sea tiles were not being created in output layer.

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl
2use strict;
3use LWP::Simple;
4use Image::Magick;
5use GD qw(:DEFAULT :cmp);
6#------------------------------------------------------------------------------------
7# LowZoom.pl
8# Generates low-zoom map tiles, by downloading high-zoom map tiles, and merging them
9# together.
10#
11# Part of the OpenStreetMap tiles@home project
12#
13# Copyright 2007, Oliver White.
14# Copying license: GNU general public license, v2 or later
15#-----------------------------------------------------------------------------------
16
17$|=1;
18
19## nicked from tahconfig.pm from main t@h.
20## FIXME: use the actual module instead.
21my %Config;
22open(my $fp,"<lowzoom.conf") || die("Can't open \"lowzoom.conf\" ($!)\n");
23while(my $Line = <$fp>){
24    $Line =~ s/#.*$//; # Comments
25    $Line =~ s/\s*$//; # Trailing whitespace
26    if($Line =~ m{
27        ^
28        \s*
29        ([A-Za-z0-9._-]+) # Keyword: just one single word no spaces
30        \s*            # Optional whitespace
31        =              # Equals
32        \s*            # Optional whitespace
33        (.*)           # Value
34        }x){
35
36# Store config options in a hash array
37        $Config{$1} = $2;
38        print "Found $1 ($2)\n" if(0); # debug option
39    }
40}
41close $fp;
42
43# Option: Where to move tiles, so that they get uploaded by another program
44my $uploadDir = $Config{UploadDir};
45
46die "can't find upload directory \"$uploadDir\"" unless (-d $uploadDir);
47
48# Command-line arguments
49my $X = shift();
50my $Y = shift();
51my $Z = shift();
52my $MaxZ = shift() || 8;
53my $OutputLayer = shift() || "tile";
54my $BaseLayer = shift() || "tile";
55my $CaptionLayer;
56$CaptionLayer = shift() if ($BaseLayer eq "captionless"); #only check for caption layer if a captionless layer was selected as base layer
57my $Options = shift();
58
59# Check the command-line arguments, and display usage information
60my $Usage = "Usage: lowzoom.pl x y z baseZoom outputLayer [baseLayer] [captionLayer] [keep]\n  x,y,z - the tile at the top of the tileset to be generated\n  baseZoom - the zoom level to download tiles from\n  outputLayer - which layer to produce lowzoom tiles for (tile (default) or maplint)\n  baseLayer - which layer to use as a base layer (tile (default) or base) \n  captionLayer - layer to composite over base layer (none (default) or captions)\n  'keep' - don't move tiles to an upload area afterwards\n\nOther options (URLs, upload staging area) are part of the script - change them in source code\n\nNote: For zoom level 8-12 use lowzoom.pl x y z 12 tile captionless caption\n      For zoom level 0-7 use lowzoom.pl x y z 8 tile tile\n";
61if(($MaxZ > 12)
62  || ($MaxZ <= $Z)
63  || ($Z < 0) || (!defined($Z))
64  || ($MaxZ > 17)
65  || ($X < 0) || (!defined($X))
66  || ($Y < 0) || (!defined($Y))
67  || ($X >= 2 ** $Z)
68  || ($Y >= 2 ** $Z)
69  )
70{
71    die($Usage);
72}
73
74# Timestamp to assign to generated tiles
75my $Timestamp = time();
76
77# What we intend to do
78my $Status = new status; 
79$Status->area($BaseLayer,$X,$Y,$Z,$MaxZ);
80
81#open oceantiles.dat and leave it open if UseOceantilesDat is on
82my $oceantiles;
83if ($Config{UseOceantilesDat}) {
84    open($oceantiles, "<", "../../oceantiles_12.dat") or die("../../oceantiles-z12.dat not found");
85}
86
87#If UseLatestTxt, store info from latest.txt here
88my %notBlanks ;
89if ($Config{UseLatestTxt}) {
90    parseLatestTxt();
91}
92
93#open a file for the suspicious tiles
94my $suspiciousTiles;
95if ($Config{WriteSuspicious}){
96    open($suspiciousTiles, ">>", "suspicious_tiles.txt");
97}
98
99#create a BlackTile to detect tiles generated by a buggy inkscape
100my $BlackTileImage = new GD::Image(256,256,1);
101my $BlackTileBackground = $BlackTileImage->colorAllocate(0,0,0);
102$BlackTileImage->fill(127,127,$BlackTileBackground);
103
104# Create the requested tile
105lowZoom($X,$Y,$Z, $MaxZ, $Status);
106
107# Move all low-zoom tiles to upload directory
108moveTiles(tempdir(), $uploadDir, $MaxZ) if($Options ne "keep");
109
110# Status message, saying what we did
111$Status->final();
112
113# Recursively create (including any downloads necessary) a tile
114sub lowZoom {
115  my ($X,$Y,$Z,$MaxZ,$Status) = @_;
116 
117  # Get tiles
118  if($Z >= $MaxZ){
119        downloadtile($X,$Y,$Z,$BaseLayer);
120  }
121  else{
122    # Recursively get/create the 4 subtiles
123    lowZoom($X*2,$Y*2,$Z+1,$MaxZ, $Status);
124    lowZoom($X*2+1,$Y*2,$Z+1,$MaxZ, $Status);
125    lowZoom($X*2,$Y*2+1,$Z+1,$MaxZ, $Status);
126    lowZoom($X*2+1,$Y*2+1,$Z+1,$MaxZ, $Status);
127 
128    # Create the tile from those subtiles
129    supertile($X,$Y,$Z,$OutputLayer,$BaseLayer,$CaptionLayer);
130  }
131}
132# Download a tile from the tileserver
133sub downloadtile {
134  my ($X,$Y,$Z,$BaseLayer) = @_;
135    my $f2 = localfile($X,$Y,$Z,$BaseLayer);
136    my $key = sprintf("%s,%s,%s",$X,$Y,$Z);
137
138    if(($Z < 13) || (($Z == 12) && ($notBlanks{$key} > 0))){ ## FIXME: this check doesn't make sense
139       
140        my $f1 = remotefile($X,$Y,$Z,$BaseLayer);
141
142        mirror($f1,$f2);
143
144        my $Size = -s $f2;
145       
146        my $Image = newFromPng GD::Image($f2, 1);
147        if (not($Image->compare($BlackTileImage) & GD_CMP_IMAGE)) {  ## FIXME: this only makes sense if z=12
148            unlink $f2;
149            if (askOceantiles($X,$Y) eq "land" ) {
150                link("land.png", $f2);
151            } else {
152                link("./sea.png", $f2);
153            }
154            if ($Config{WriteSuspicious}) {
155                print $suspiciousTiles "$X $Y : downloaded tile is a black tile, probably a inkscape bug \n";
156            }
157            $Status->downloadCount($BaseLayer,$X,$Y,$Z,$Size);
158
159            return;
160        }
161
162        if ($Config{UseOceantilesDat}) {
163            if ($Z eq 12) {
164                if (($Size == 103) && (askOceantiles($X,$Y) eq "land")) {
165                    if ($Config{WriteSuspicious}) {
166                        print $suspiciousTiles "$X $Y : downloaded tile is sea, oceantiles says land \n";
167                    }
168                    if ($Config{BlankSource} eq "oceantiles") {
169                        unlink $f2;
170                        link("../../emptyland.png", $f2);
171                    }
172                }elsif (($Size == 179) && (askOceantiles($X,$Y) eq "sea")) {
173                    if ($Config{WriteSuspicious}) {
174                        print $suspiciousTiles "$X $Y : downloaded tile is land, oceantiles says sea \n";
175                    }
176                    if ($Config{BlankSource} eq "oceantiles") {
177                        unlink $f2;
178                        link("../../emptysea.png", $f2);
179                    }
180                }
181            }
182        }
183        $Status->downloadCount($BaseLayer,$X,$Y,$Z,$Size);
184       
185        return;
186    } 
187 
188    unlink $f2;
189
190    if (askOceantiles($X,$Y) eq "land" ) {
191            link("land.png", $f2);
192    }else {
193            link("./sea.png", $f2);
194    }
195
196    my $Size = 0;
197  $Status->downloadCount($BaseLayer,$X,$Y,$Z,$Size);
198   
199}
200
201# Delete blank subtiles of a blank tile
202# When we notice that a tile is blank because all its subtiles are blank,
203# because of the fallback mechanism in the server we can delete those
204# subtiles. However, to avoid the fallback on the server having to work too
205# hard, we ensure that there are still real blank tiles every few zoom
206# levels.
207sub deleteBlankSubtiles
208{
209  my($X,$Y,$Z,$OutputLayer) = @_;
210    if (($Z == 7 ) && ($Options ne "keep")){
211        for my $x (0,1)
212        {
213            for my $y (0,1)
214            {
215                my $f = localfile(2*$X + $x, 2*$Y + $y, $Z+1, $OutputLayer);
216                unlink $f;
217            }
218        }
219    };
220  return if $Z <= 9;  # Not lowzoom's problem
221  # This keeps real blank tiles at zooms 3,6 and 9
222  return if ($Z+1)%3 == 0;
223 
224  for my $x (0,1)
225  {
226    for my $y (0,1)
227    {
228      my $f = localfile(2*$X + $x, 2*$Y + $y, $Z+1, $OutputLayer);
229      # Unlink prior to creating, file may be a hard link
230      unlink $f;
231      open my $fh, ">", $f;  # Make zero byte file, the marker for the server to delete the tile
232    }
233  }
234}
235# Create a supertile, by merging together 4 local image files, and creating a new local file
236sub supertile {
237  my ($X,$Y,$Z,$OutputLayer,$BaseLayer,$CaptionLayer) = @_;
238  my $CaptionFile;
239
240  # Load captions
241  if ($CaptionLayer ne undef) {
242    my $f2 = localfile($X,$Y,$Z,$CaptionLayer);
243    my $f1 = remotefile($X,$Y,$Z,$CaptionLayer);
244    mirror($f1,$f2);
245    $CaptionFile = readLocalImage($X,$Y,$Z,$CaptionLayer);
246  }
247 
248  # Load the subimages
249  my $AA = readLocalImage($X*2,$Y*2,$Z+1,$BaseLayer);
250  my $BA = readLocalImage($X*2+1,$Y*2,$Z+1,$BaseLayer);
251  my $AB = readLocalImage($X*2,$Y*2+1,$Z+1,$BaseLayer);
252  my $BB = readLocalImage($X*2+1,$Y*2+1,$Z+1,$BaseLayer);
253 
254
255    # BaseFile is a file containing a tile without captions.
256    # OutputFile is a file that is a merge of a base tile and a captions tile if there is one.
257    my $BaseFile = localfile($X,$Y,$Z,$BaseLayer);
258    my $OutputFile = localfile($X,$Y,$Z,$OutputLayer);
259
260    # Always delete files first. The use of hardlinks means we might accedently overwrite other files.
261    unlink($BaseFile);
262    unlink($OutputFile);
263
264#    print "generating $OutputFile \n";
265
266    if ($AA == undef) { $AA = Image::Magick->new; }
267    if ($AB == undef) { $AB = Image::Magick->new; }
268    if ($BA == undef) { $BA = Image::Magick->new; }
269    if ($BB == undef) { $BB = Image::Magick->new; }
270
271    # all images the same size?
272    if(($AA->Get('filesize') == 103 )  && ($AA->Get('filesize') == $BA->Get('filesize')) && ($BA->Get('filesize') == $AB->Get('filesize')) && ( $AB->Get('filesize') == $BB->Get('filesize')) ) 
273    {#if its a "404 sea" or a "sea.png" and all 4 sizes are the same, make one 69 bytes sea of it
274            my $SeaFilename = "../../emptysea.png"; 
275            link($SeaFilename,$BaseFile);
276            if ( $BaseFile ne $OutputFile ) {
277              link($SeaFilename,$OutputFile);
278            }
279            deleteBlankSubtiles($X,$Y,$Z,$OutputLayer);
280            return;
281    }
282    elsif(($AA->Get('filesize') == 179 ) && ($AA->Get('filesize') == $BA->Get('filesize')) && ($BA->Get('filesize') == $AB->Get('filesize')) && ( $AB->Get('filesize') == $BB->Get('filesize')) ) 
283    {#if its a "blank land" or a "land.png" and all 4 sizes are the same, make one 69 bytes land of it
284            my $LandFilename = "../../emptyland.png"; 
285            link($LandFilename,$BaseFile);
286            if ( $BaseFile ne $OutputFile ) {
287              link($LandFilename,$OutputFile);
288            }
289            deleteBlankSubtiles($X,$Y,$Z,$OutputLayer);
290            return;
291    }
292    else{
293        my $Image = Image::Magick->new;
294
295        # Create the supertile
296        $Image->Set(size=>'512x512');
297        $Image->ReadImage('xc:white');
298
299        # Copy the subimages into the 4 quadrants
300        foreach my $x (0, 1)
301        {
302                foreach my $y (0, 1)
303                {
304                        next unless (($Z < 9) || (($x == 0) && ($y == 0)));
305                        $Image->Composite(image => $AA, 
306                                        geometry => sprintf("512x512+%d+%d", $x, $y),
307                                        compose => "darken") if ($AA);
308
309                        $Image->Composite(image => $BA, 
310                                        geometry => sprintf("512x512+%d+%d", $x + 256, $y),
311                                        compose => "darken") if ($BA);
312
313                        $Image->Composite(image => $AB, 
314                                        geometry => sprintf("512x512+%d+%d", $x, $y + 256),
315                                        compose => "darken") if ($AB);
316
317                        $Image->Composite(image => $BB, 
318                                        geometry => sprintf("512x512+%d+%d", $x + 256, $y + 256),
319                                        compose => "darken") if ($BB);
320                }
321        }
322
323
324        $Image->Scale(width => "256", height => "256");
325        $Image->Set(type=>"Palette");
326        $Image->Set(quality => 90);  # compress image
327        $Image->Write($BaseFile);
328        utime $Timestamp, $Timestamp, $BaseFile;
329
330        # Overlay the captions onto the tiled image and then write it
331        $Image->Composite(image => $CaptionFile);
332        $Image->Write($OutputFile);
333        utime $Timestamp, $Timestamp, $OutputFile;
334
335        undef $Image; ## Destroy the ImageMagick object to save Memory
336    }
337
338     #remove tiles which will not be uploaded
339    if (($Z == 11 ) && ($Options ne "keep")){
340        for my $x (0,1)
341        {
342            for my $y (0,1)
343            {
344                my $f = localfile(2*$X + $x, 2*$Y + $y, $Z+1, $OutputLayer);
345                unlink $f;
346            }
347        }
348    };
349
350}
351
352# Open a PNG file, and return it as a Magick image (or 0 if not found)
353sub readLocalImage
354{
355    my ($X,$Y,$Z,$Layer) = @_;
356    my $Filename; 
357    $Filename = localfile($X,$Y,$Z,$Layer); 
358    if (!-f $Filename)
359    {
360        return undef;
361    }
362    my $Image = new Image::Magick;
363    if (my $err = $Image->Read($Filename))
364    {
365        print STDERR "$err\n";
366        return undef;
367    }
368        if ($Image->Get('filesize') == 69) 
369        {
370            # do not return 1x1 pixel images since we might have to put them into a lower zoom
371            @$Image=();
372            if (my $err = $Image->Read("sea.png"))
373            {
374                    print STDERR "$err\n";
375                    return undef;
376            }
377        }
378        if ($Image->Get('filesize') == 67) 
379        {
380            # do not return 1x1 pixel images since we might have to put them into a lower zoom
381            @$Image=();
382            if (my $err = $Image->Read("land.png"))
383            {
384                    print "$err\n";
385                    return undef;
386            }
387        }
388    return($Image);
389}
390
391# Take any tiles that were created (as opposed to downloaded), and move them to
392# an area ready for upload.
393# + Delete any tiles that were downloaded
394sub moveTiles {
395  my ($from, $to, $MaxZ) = @_;
396  opendir(my $dp, $from) || die($!);
397  while(my $file = readdir($dp)){
398    if(($file =~ /^${OutputLayer}_(\d+)_(\d+)_(\d+)\.png$/o) || ($file =~ /^${BaseLayer}_(\d+)_(\d+)_(\d+)\.png$/o)){
399      my ($Z,$X,$Y) = ($1,$2,$3);
400      my $f1 = "$from/$file";
401      my $f2 = "$to/$file";
402      if($Z < $MaxZ){
403        # Rename can fail if the target is on a different filesystem
404        rename($f1, $f2) or system("mv",$f1,$f2);
405      }
406      else{
407        unlink $f1;
408      }
409    }
410  } 
411  close $dp;
412}
413
414# takes x and y coordinates and returns if the corresponding tile
415# should be sea or land
416sub askOceantiles {
417
418    my ($X, $Y) = @_;
419
420    my $tileoffset = ($Y * (2**12)) + $X;
421
422    if ($Config{UseOceantilesDat}) {
423        seek $oceantiles, int($tileoffset / 4), 0; 
424        my $buffer;
425        read $oceantiles, $buffer, 1;
426        $buffer = substr( $buffer."\0", 0, 1 );
427        $buffer = unpack "B*", $buffer;
428        my $str = substr( $buffer, 2*($tileoffset % 4), 2 );
429
430#        print("lookup handler finds: $str\n") ;
431        if ($str eq "10") {    return "sea"; };
432        if ($str eq "01") {     return "land"; };
433        if ($str eq "11") {    return "land"; };
434
435        return "unknown";
436
437        # $str eq "00" => unknown (not yet checked)
438        # $str eq "01" => known land
439        # $str eq "10" => known sea
440        # $str eq "11" => known edge tile
441    }
442    else
443    {
444        print "need UseOceantilesDat config option to proceed for tile $X $Y 12\n";
445        exit (1);
446    }
447}
448
449#parses the latest.txt file and stores info about tiles
450sub parseLatestTxt {
451
452 open (fh,"<","latest_12.txt");
453
454printf("searching for zoom %d tiles where X is between %d and %d \n",$MaxZ,$X*2**($MaxZ-$Z),($X+1)*2**($MaxZ-$Z));
455printf("searching for zoom %d tiles where Y is between %d and %d \n",$MaxZ,$Y*2**($MaxZ-$Z),($Y+1)*2**($MaxZ-$Z));
456my $notBlankCount = 0;
457my $newCount = 0;
458my @tileinfo;
459my $key;
460while (my $line = <fh>){
461    if ( $line =~ /^[0-9]*\,[0-9]*,[0-9]*,/ ) {
462        @tileinfo = split(/,/,$line);
463        if ($Z <= $tileinfo[2] && $tileinfo[2] <= $MaxZ) {
464            if ($X*2**($MaxZ-$Z) <= $tileinfo[0] && $tileinfo[0] <= ($X+1)*2**($MaxZ-$Z)) {
465                if ($Y*2**($MaxZ-$Z) <= $tileinfo[1] && $tileinfo[1] <= ($Y+1)*2**($MaxZ-$Z)) {
466                    $key = sprintf("%s,%s,%s",$tileinfo[0],$tileinfo[1],$tileinfo[2]);
467                    if ($tileinfo[5] > (time()-3600*1)) {
468                        $newCount++;
469                        $notBlanks{$key} = 2;
470                    }else{
471                        $notBlanks{$key} = 1;
472                    }
473                    $notBlankCount++;
474                }
475            }
476        }
477    }
478}
479
480close(fh);
481
482printf("latest.txt parsed, found %i notBlanks (%i new)\n",$notBlankCount,$newCount);
483
484}
485
486# Option: filename for our temporary map tiles
487# (note: this should match whatever is expected by the upload scripts)
488sub localfile {
489  my ($X,$Y,$Z,$Layer) = @_;
490  return sprintf("%s/%s_%d_%d_%d.png", tempdir(), $Layer,$Z,$X,$Y);
491}
492# Option: URL for downloading tiles
493sub remotefile {
494  my ($X,$Y,$Z,$Layer) = @_;
495  return sprintf("http://tah.openstreetmap.org/Tiles/%s.php/%d/%d/%d.png", $Layer,$Z,$X,$Y);
496}
497# Option: what to use as temporary storage for tiles
498sub tempdir {
499  if ( -"temp".$Z ) {
500    return("temp".$Z)
501  } else {
502    return("temp");
503  }
504}
505
506package status;
507use Time::HiRes qw(time); # Comment-this out if you want, it's not important
508sub new {
509  my $self  = {};
510  $self->{DONE} = 0;
511  $self->{TODO} = 1;
512  $self->{SIZE} = 0;
513  bless($self);
514  return $self;
515}
516sub downloadCount(){
517  my $self = shift();
518  $self->{LAYER} = shift();
519  $self->{LAST_X} = shift();
520  $self->{LAST_Y} = shift();
521  $self->{LAST_Z} = shift();
522  $self->{LAST_SIZE} = shift();
523  $self->{DONE}++;
524  $self->{SIZE} += $self->{LAST_SIZE};
525  $self->{PERCENT} = $self->{TODO} ? (100 * ($self->{DONE} / $self->{TODO})) : 0;
526  $self->display();
527}
528sub area(){
529  my $self = shift();
530  $self->{LAYER}=shift();
531  $self->{X} = shift();
532  $self->{Y} = shift();
533  $self->{Z} = shift();
534  $self->{MAX_Z} = shift();
535  $self->{RANGE_Z} = $self->{MAX_Z} - $self->{Z};
536  $self->{TODO} = 4 ** $self->{RANGE_Z};
537  $self->display();
538  $self->{START_T} = time();
539}
540sub update(){
541  my $self = shift();
542  $self->{T} = time();
543  $self->{DT} = $self->{T} - $self->{START_T};
544  $self->{EXPECT_T} = $self->{DONE} ? ($self->{TODO} * $self->{DT} / $self->{DONE}) : 0;
545  $self->{EXPECT_FINISH} = $self->{START_T} + $self->{EXPECT_T};
546  $self->{REMAIN_T} = $self->{EXPECT_T} - $self->{EXPECT_DT};
547}
548sub display(){
549  my $self = shift();
550  $self->update();
551 
552  printf( "\rJob %s(%d,%d,%d): %03.1f%% done, %1.1f min (%d,%d,%d = %1.1f KB)", 
553    $self->{LAYER},
554    $self->{X},
555    $self->{Y},
556    $self->{Z},
557    $self->{PERCENT}, 
558    $self->{REMAIN_T} / 60,
559    $self->{LAST_X},
560    $self->{LAST_Y},
561    $self->{LAST_Z},
562    $self->{LAST_SIZE}/1024
563    );
564}
565sub final(){
566  my $self = shift();
567  $self->{END_T} = time();
568  printf("Done, %d downloads, %1.1fKB total, took %1.0f seconds\n",
569    $self->{DONE},
570    $self->{SIZE} / 1024,
571    $self->{DT});
572}
573
574
Note: See TracBrowser for help on using the repository browser.